refactor(fctool): apply rustfmt formatting
This commit is contained in:
parent
02a1020f75
commit
2dcb5b3028
@ -64,7 +64,8 @@ pub async fn retract_message_exec(
|
||||
if !ctx.is_sent_in_turn(message_id) {
|
||||
return Err(ToolError::ExecutionError(
|
||||
"can only retract messages sent in the current turn — \
|
||||
cross-turn retraction is not allowed".into(),
|
||||
cross-turn retraction is not allowed"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -106,16 +107,25 @@ async fn is_room_admin(
|
||||
) -> bool {
|
||||
use models::rooms::room;
|
||||
let room_model = room::Entity::find_by_id(room_id)
|
||||
.one(db.reader()).await.ok().flatten();
|
||||
.one(db.reader())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
match room_model {
|
||||
Some(r) if r.created_by == user_id => true,
|
||||
Some(_) => {
|
||||
let member = project_members::Entity::find()
|
||||
.filter(project_members::Column::Project.eq(project_id))
|
||||
.filter(project_members::Column::User.eq(user_id))
|
||||
.one(db.reader()).await.ok().flatten();
|
||||
.one(db.reader())
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
match member {
|
||||
Some(m) => matches!(m.scope_role(), Ok(models::projects::MemberRole::Admin | models::projects::MemberRole::Owner)),
|
||||
Some(m) => matches!(
|
||||
m.scope_role(),
|
||||
Ok(models::projects::MemberRole::Admin | models::projects::MemberRole::Owner)
|
||||
),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,15 +9,13 @@
|
||||
|
||||
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
||||
use chrono::Utc;
|
||||
use models::rooms::{
|
||||
room_message, MessageContentType, MessageSenderType,
|
||||
};
|
||||
use models::rooms::{MessageContentType, MessageSenderType, room_message};
|
||||
use queue::{ProjectRoomEvent, RoomMessageEnvelope};
|
||||
use sea_orm::*;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
const MAX_CONTENT_LEN: usize = 10000;
|
||||
const MAX_CONTENT_LEN: usize = 600;
|
||||
|
||||
/// Send a brief message to the specified room.
|
||||
/// The message content may include mentions in `@[type:id:label]` format.
|
||||
@ -135,7 +133,9 @@ pub async fn send_message_exec(
|
||||
seq: Some(seq),
|
||||
timestamp: now,
|
||||
};
|
||||
producer.publish_project_room_event(ctx.project_id(), project_event).await;
|
||||
producer
|
||||
.publish_project_room_event(ctx.project_id(), project_event)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
||||
use chrono::Utc;
|
||||
use models::ai::{ai_conversation, ai_message, AiMessage};
|
||||
use models::ai::{AiMessage, ai_conversation, ai_message};
|
||||
use sea_orm::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@ -8,18 +8,71 @@ use std::collections::HashMap;
|
||||
|
||||
/// Text file extensions to search (skip binary files).
|
||||
const TEXT_EXTS: &[&str] = &[
|
||||
"rs", "toml", "yaml", "yml", "json", "jsonc", "js", "jsx", "ts", "tsx",
|
||||
"css", "scss", "less", "html", "htm", "xml", "svg", "vue", "svelte",
|
||||
"py", "rb", "go", "java", "kt", "swift", "c", "cpp", "h", "hpp",
|
||||
"cs", "php", "pl", "sh", "bash", "zsh", "fish", "ps1", "bat", "cmd",
|
||||
"sql", "md", "markdown", "rst", "txt", "log", "ini", "cfg", "conf",
|
||||
"dockerfile", "makefile", "cmake", "gradle", "properties", "env",
|
||||
"proto", "graphql", "vue", "lock",
|
||||
"rs",
|
||||
"toml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"json",
|
||||
"jsonc",
|
||||
"js",
|
||||
"jsx",
|
||||
"ts",
|
||||
"tsx",
|
||||
"css",
|
||||
"scss",
|
||||
"less",
|
||||
"html",
|
||||
"htm",
|
||||
"xml",
|
||||
"svg",
|
||||
"vue",
|
||||
"svelte",
|
||||
"py",
|
||||
"rb",
|
||||
"go",
|
||||
"java",
|
||||
"kt",
|
||||
"swift",
|
||||
"c",
|
||||
"cpp",
|
||||
"h",
|
||||
"hpp",
|
||||
"cs",
|
||||
"php",
|
||||
"pl",
|
||||
"sh",
|
||||
"bash",
|
||||
"zsh",
|
||||
"fish",
|
||||
"ps1",
|
||||
"bat",
|
||||
"cmd",
|
||||
"sql",
|
||||
"md",
|
||||
"markdown",
|
||||
"rst",
|
||||
"txt",
|
||||
"log",
|
||||
"ini",
|
||||
"cfg",
|
||||
"conf",
|
||||
"dockerfile",
|
||||
"makefile",
|
||||
"cmake",
|
||||
"gradle",
|
||||
"properties",
|
||||
"env",
|
||||
"proto",
|
||||
"graphql",
|
||||
"vue",
|
||||
"lock",
|
||||
];
|
||||
|
||||
fn is_text_ext(path: &str) -> bool {
|
||||
let lower = path.to_lowercase();
|
||||
TEXT_EXTS.iter().any(|&e| lower.ends_with(&format!(".{}", e)))
|
||||
TEXT_EXTS
|
||||
.iter()
|
||||
.any(|&e| lower.ends_with(&format!(".{}", e)))
|
||||
}
|
||||
|
||||
fn is_binary_content(data: &[u8]) -> bool {
|
||||
@ -51,18 +104,9 @@ async fn git_grep_exec(
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing pattern")?;
|
||||
let glob = p.get("glob").and_then(|v| v.as_str()).map(String::from);
|
||||
let is_regex = p
|
||||
.get("is_regex")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true);
|
||||
let context_lines = p
|
||||
.get("context_lines")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
let max_results = p
|
||||
.get("max_results")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(100) as usize;
|
||||
let is_regex = p.get("is_regex").and_then(|v| v.as_bool()).unwrap_or(true);
|
||||
let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
let max_results = p.get("max_results").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
|
||||
@ -115,7 +159,8 @@ async fn git_grep_exec(
|
||||
};
|
||||
|
||||
if entry.kind() == Some(git2::ObjectType::Tree) {
|
||||
if let Some(subtree) = entry.to_object(&repo).ok().and_then(|o| o.into_tree().ok()) {
|
||||
if let Some(subtree) = entry.to_object(&repo).ok().and_then(|o| o.into_tree().ok())
|
||||
{
|
||||
stack.push((subtree, path));
|
||||
}
|
||||
continue;
|
||||
@ -217,7 +262,11 @@ fn glob_match(path: &str, pattern: &str) -> bool {
|
||||
}
|
||||
if let Some(star) = pattern_part.find('*') {
|
||||
let (prefix, suffix) = pattern_part.split_at(star);
|
||||
let suffix = if suffix.starts_with('*') { &suffix[1..] } else { suffix };
|
||||
let suffix = if suffix.starts_with('*') {
|
||||
&suffix[1..]
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
if !prefix.is_empty() && !path_part.starts_with(prefix) {
|
||||
return false;
|
||||
}
|
||||
@ -246,7 +295,11 @@ fn glob_match(path: &str, pattern: &str) -> bool {
|
||||
break;
|
||||
}
|
||||
// Try skipping segments until the next pattern part matches
|
||||
let next_part = parts.iter().skip_while(|p| **p == "**").next().unwrap_or(&"*");
|
||||
let next_part = parts
|
||||
.iter()
|
||||
.skip_while(|p| **p == "**")
|
||||
.next()
|
||||
.unwrap_or(&"*");
|
||||
while pi < path_parts.len() && !matches_part(path_parts[pi], next_part) {
|
||||
pi += 1;
|
||||
}
|
||||
@ -263,76 +316,110 @@ fn glob_match(path: &str, pattern: &str) -> bool {
|
||||
|
||||
pub fn register_grep_tools(registry: &mut ToolRegistry) {
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("pattern".into(), ToolParam {
|
||||
name: "pattern".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Search pattern (regex or literal string)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("rev".into(), ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Git revision to search in (branch, tag, commit). Default: HEAD".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("glob".into(), ToolParam {
|
||||
name: "glob".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File glob pattern to filter (e.g. *.rs, src/**/*.ts)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("is_regex".into(), ToolParam {
|
||||
name: "is_regex".into(),
|
||||
param_type: "boolean".into(),
|
||||
description: Some("If true, pattern is a regex. If false, literal string. Default: true".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("context_lines".into(), ToolParam {
|
||||
name: "context_lines".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Number of surrounding lines to include for each match. Default: 0".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("max_results".into(), ToolParam {
|
||||
name: "max_results".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of matches to return. Default: 100".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pattern".into(),
|
||||
ToolParam {
|
||||
name: "pattern".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Search pattern (regex or literal string)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Git revision to search in (branch, tag, commit). Default: HEAD".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"glob".into(),
|
||||
ToolParam {
|
||||
name: "glob".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File glob pattern to filter (e.g. *.rs, src/**/*.ts)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"is_regex".into(),
|
||||
ToolParam {
|
||||
name: "is_regex".into(),
|
||||
param_type: "boolean".into(),
|
||||
description: Some(
|
||||
"If true, pattern is a regex. If false, literal string. Default: true".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"context_lines".into(),
|
||||
ToolParam {
|
||||
name: "context_lines".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some(
|
||||
"Number of surrounding lines to include for each match. Default: 0".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"max_results".into(),
|
||||
ToolParam {
|
||||
name: "max_results".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of matches to return. Default: 100".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "pattern".into()]),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"pattern".into(),
|
||||
]),
|
||||
};
|
||||
|
||||
registry.register(
|
||||
|
||||
@ -118,7 +118,10 @@ async fn read_json_exec(
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
@ -213,7 +216,11 @@ fn query_json(value: &JsonValue, query: &str) -> Result<JsonValue, String> {
|
||||
let mut current = value.clone();
|
||||
|
||||
// Parse into access segments: Key("name"), Index(0), BracketKey("key.with.dots")
|
||||
enum Segment { Key(String), Index(usize), BracketKey(String) }
|
||||
enum Segment {
|
||||
Key(String),
|
||||
Index(usize),
|
||||
BracketKey(String),
|
||||
}
|
||||
let mut segments: Vec<Segment> = Vec::new();
|
||||
let mut i = 0;
|
||||
let q_chars: Vec<char> = query.chars().collect();
|
||||
@ -230,10 +237,10 @@ fn query_json(value: &JsonValue, query: &str) -> Result<JsonValue, String> {
|
||||
let content = bracket_content.trim();
|
||||
// Check if it's a quoted string key or a numeric index
|
||||
if content.starts_with('"') && content.ends_with('"') {
|
||||
let key = content[1..content.len()-1].to_string();
|
||||
let key = content[1..content.len() - 1].to_string();
|
||||
segments.push(Segment::BracketKey(key));
|
||||
} else if content.starts_with("'") && content.ends_with("'") {
|
||||
let key = content[1..content.len()-1].to_string();
|
||||
let key = content[1..content.len() - 1].to_string();
|
||||
segments.push(Segment::BracketKey(key));
|
||||
} else if let Ok(idx) = content.parse::<usize>() {
|
||||
segments.push(Segment::Index(idx));
|
||||
@ -297,7 +304,15 @@ pub fn register_json_tools(registry: &mut ToolRegistry) {
|
||||
("schema_depth".into(), ToolParam { name: "schema_depth".into(), param_type: "integer".into(), description: Some("How deep to infer the JSON schema (default: 4)".into()), required: false, properties: None, items: None }),
|
||||
("pretty".into(), ToolParam { name: "pretty".into(), param_type: "boolean".into(), description: Some("Pretty-print the output (default: false)".into()), required: false, properties: None, items: None }),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("read_json")
|
||||
.description("Parse, validate, and query JSON and JSONC files. Supports JSONPath-like queries ($.key, $.arr[0]), schema inference, and pretty-printing. Automatically detects JSONC (with // comments).")
|
||||
|
||||
@ -24,4 +24,4 @@ pub fn register_all(registry: &mut ToolRegistry) {
|
||||
markdown::register_markdown_tools(registry);
|
||||
sql::register_sql_tools(registry);
|
||||
json::register_json_tools(registry);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
use crate::file_tools::MAX_FILE_SIZE;
|
||||
use crate::git_tools::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use sqlparser::ast::{Statement, ColumnDef};
|
||||
use sqlparser::ast::{ColumnDef, Statement};
|
||||
use sqlparser::dialect::{GenericDialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect};
|
||||
use sqlparser::parser::Parser;
|
||||
use std::collections::HashMap;
|
||||
@ -23,13 +23,19 @@ async fn read_sql_exec(
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
let dialect = p.get("dialect").and_then(|v| v.as_str()).unwrap_or("generic");
|
||||
let dialect = p
|
||||
.get("dialect")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("generic");
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
|
||||
@ -74,10 +80,15 @@ async fn read_sql_exec(
|
||||
let mut views: Vec<serde_json::Value> = Vec::new();
|
||||
let mut functions: Vec<serde_json::Value> = Vec::new();
|
||||
let mut indexes: Vec<serde_json::Value> = Vec::new();
|
||||
let mut statement_kinds: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
let mut statement_kinds: std::collections::HashMap<String, usize> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for statement in &statements {
|
||||
let kind = format!("{:?}", statement).split('{').next().unwrap_or("unknown").to_string();
|
||||
let kind = format!("{:?}", statement)
|
||||
.split('{')
|
||||
.next()
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
*statement_kinds.entry(kind).or_insert(0) += 1;
|
||||
|
||||
match statement {
|
||||
@ -135,13 +146,73 @@ fn format_column_def(col: &ColumnDef) -> String {
|
||||
|
||||
pub fn register_sql_tools(registry: &mut ToolRegistry) {
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to the SQL file".into()), required: true, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Git revision (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
("dialect".into(), ToolParam { name: "dialect".into(), param_type: "string".into(), description: Some("SQL dialect: generic, mysql, postgresql, sqlite. Default: generic".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path to the SQL file".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Git revision (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"dialect".into(),
|
||||
ToolParam {
|
||||
name: "dialect".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"SQL dialect: generic, mysql, postgresql, sqlite. Default: generic".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("read_sql")
|
||||
.description("Parse and analyze a SQL file. Extracts CREATE TABLE statements (with columns and types), CREATE VIEW, CREATE INDEX, CREATE FUNCTION, and counts all statement types.")
|
||||
|
||||
@ -11,8 +11,14 @@ async fn git_blob_info_exec(
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -32,8 +38,14 @@ async fn git_blob_exists_exec(
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -49,10 +61,19 @@ async fn git_blob_content_exec(
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
||||
let max_size = p.get("max_size").and_then(|v| v.as_u64()).unwrap_or(1_048_576) as usize; // 1MB default
|
||||
let max_size = p
|
||||
.get("max_size")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(1_048_576) as usize; // 1MB default
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let blob_oid = git::commit::types::CommitOid::new(oid);
|
||||
@ -66,7 +87,10 @@ async fn git_blob_content_exec(
|
||||
}
|
||||
|
||||
let (content, is_binary) = if blob.is_binary {
|
||||
(base64::engine::general_purpose::STANDARD.encode(&blob.content), true)
|
||||
(
|
||||
base64::engine::general_purpose::STANDARD.encode(&blob.content),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
(String::from_utf8_lossy(&blob.content).to_string(), false)
|
||||
};
|
||||
@ -85,17 +109,34 @@ async fn git_blob_create_exec(
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let content = p.get("content").and_then(|v| v.as_str()).ok_or("missing content")?;
|
||||
let encoding = p.get("encoding").and_then(|v| v.as_str()).unwrap_or("utf-8");
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let content = p
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing content")?;
|
||||
let encoding = p
|
||||
.get("encoding")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("utf-8");
|
||||
|
||||
let data = match encoding {
|
||||
"base64" => base64::engine::general_purpose::STANDARD
|
||||
.decode(content)
|
||||
.map_err(|e| format!("invalid base64: {}", e))?,
|
||||
"utf-8" => content.as_bytes().to_vec(),
|
||||
other => return Err(format!("unsupported encoding '{}'. Use 'utf-8' or 'base64'.", other)),
|
||||
other => {
|
||||
return Err(format!(
|
||||
"unsupported encoding '{}'. Use 'utf-8' or 'base64'.",
|
||||
other
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -109,30 +150,51 @@ async fn git_blob_create_exec(
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_blob_info
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(), param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("oid".into(), ToolParam {
|
||||
name: "oid".into(), param_type: "string".into(),
|
||||
description: Some("Blob OID (full 40-char hex or short prefix)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"oid".into(),
|
||||
ToolParam {
|
||||
name: "oid".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Blob OID (full 40-char hex or short prefix)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"oid".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blob_info")
|
||||
@ -148,26 +210,48 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_blob_exists
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(), param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("oid".into(), ToolParam {
|
||||
name: "oid".into(), param_type: "string".into(),
|
||||
description: Some("Blob OID to check".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"oid".into(),
|
||||
ToolParam {
|
||||
name: "oid".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Blob OID to check".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"oid".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blob_exists")
|
||||
@ -176,38 +260,68 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_blob_exists_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_blob_exists_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_blob_content
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(), param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("oid".into(), ToolParam {
|
||||
name: "oid".into(), param_type: "string".into(),
|
||||
description: Some("Blob OID to retrieve content for".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("max_size".into(), ToolParam {
|
||||
name: "max_size".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum blob size in bytes (default: 1MB)".into()),
|
||||
required: false, properties: None, items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"oid".into(),
|
||||
ToolParam {
|
||||
name: "oid".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Blob OID to retrieve content for".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"max_size".into(),
|
||||
ToolParam {
|
||||
name: "max_size".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum blob size in bytes (default: 1MB)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"oid".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blob_content")
|
||||
@ -223,31 +337,59 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_blob_create
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(), param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("content".into(), ToolParam {
|
||||
name: "content".into(), param_type: "string".into(),
|
||||
description: Some("Blob content (utf-8 string or base64-encoded bytes)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("encoding".into(), ToolParam {
|
||||
name: "encoding".into(), param_type: "string".into(),
|
||||
description: Some("Encoding of content: 'utf-8' (default) or 'base64'".into()),
|
||||
required: false, properties: None, items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"content".into(),
|
||||
ToolParam {
|
||||
name: "content".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Blob content (utf-8 string or base64-encoded bytes)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"encoding".into(),
|
||||
ToolParam {
|
||||
name: "encoding".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Encoding of content: 'utf-8' (default) or 'base64'".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into(), "content".into()]),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"content".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blob_create")
|
||||
|
||||
@ -4,11 +4,24 @@ use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_branch_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let remote_only = p.get("remote_only").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
async fn git_branch_list_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let remote_only = p
|
||||
.get("remote_only")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let branches = domain.branch_list(remote_only).map_err(|e| e.to_string())?;
|
||||
@ -25,11 +38,24 @@ async fn git_branch_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
async fn git_branch_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let name = p.get("name").and_then(|v| v.as_str()).ok_or("missing name")?;
|
||||
async fn git_branch_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let name = p
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let info = domain.branch_get(name).map_err(|e| e.to_string())?;
|
||||
@ -39,7 +65,9 @@ async fn git_branch_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
Ok((ahead, behind)) => Some(serde_json::json!({ "ahead": ahead, "behind": behind })),
|
||||
Err(e) => Some(serde_json::json!({ "error": e.to_string() })),
|
||||
}
|
||||
} else { None };
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"branch": { "name": info.name, "oid": info.oid.to_string(), "is_head": info.is_head,
|
||||
@ -48,84 +76,188 @@ async fn git_branch_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_branches_merged_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let branch = p.get("branch").and_then(|v| v.as_str()).ok_or("missing branch")?;
|
||||
let into = p.get("into").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "main".to_string());
|
||||
async fn git_branches_merged_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let branch = p
|
||||
.get("branch")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing branch")?;
|
||||
let into = p
|
||||
.get("into")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "main".to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let is_merged = domain.branch_is_merged(branch, &into).map_err(|e| e.to_string())?;
|
||||
let is_merged = domain
|
||||
.branch_is_merged(branch, &into)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Resolve branch names to commit OIDs before calling merge_base
|
||||
let branch_oid = domain.branch_target(branch)
|
||||
let branch_oid = domain
|
||||
.branch_target(branch)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("branch '{}' not found or has no target", branch))?;
|
||||
let into_oid = domain.branch_target(&into)
|
||||
let into_oid = domain
|
||||
.branch_target(&into)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("branch '{}' not found or has no target", into))?;
|
||||
let merge_base = domain.merge_base(&branch_oid, &into_oid)
|
||||
.map(|oid| oid.to_string()).ok();
|
||||
let merge_base = domain
|
||||
.merge_base(&branch_oid, &into_oid)
|
||||
.map(|oid| oid.to_string())
|
||||
.ok();
|
||||
|
||||
Ok(serde_json::json!({ "branch": branch, "into": into, "is_merged": is_merged, "merge_base": merge_base }))
|
||||
Ok(
|
||||
serde_json::json!({ "branch": branch, "into": into, "is_merged": is_merged, "merge_base": merge_base }),
|
||||
)
|
||||
}
|
||||
|
||||
async fn git_branch_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let local = p.get("local").and_then(|v| v.as_str()).ok_or("missing local")?;
|
||||
let remote = p.get("remote").and_then(|v| v.as_str()).unwrap_or(local).to_string();
|
||||
async fn git_branch_diff_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let local = p
|
||||
.get("local")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing local")?;
|
||||
let remote = p
|
||||
.get("remote")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(local)
|
||||
.to_string();
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let diff = domain.branch_diff(local, &remote).map_err(|e| e.to_string())?;
|
||||
let diff = domain
|
||||
.branch_diff(local, &remote)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(serde_json::json!({ "ahead": diff.ahead, "behind": diff.behind, "diverged": diff.diverged }))
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
let mut p = HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(), param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true, properties: None, items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
// git_branch_list
|
||||
p.insert("remote_only".into(), ToolParam {
|
||||
name: "remote_only".into(), param_type: "boolean".into(),
|
||||
description: Some("If true, list only remote-tracking branches".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(std::mem::take(&mut p)), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
p.insert(
|
||||
"remote_only".into(),
|
||||
ToolParam {
|
||||
name: "remote_only".into(),
|
||||
param_type: "boolean".into(),
|
||||
description: Some("If true, list only remote-tracking branches".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(std::mem::take(&mut p)),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
registry.register(
|
||||
ToolDefinition::new("git_branch_list").description("List all local or remote branches with their current HEAD commit.").parameters(schema),
|
||||
ToolDefinition::new("git_branch_list")
|
||||
.description("List all local or remote branches with their current HEAD commit.")
|
||||
.parameters(schema),
|
||||
ToolHandler::new(move |ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_branch_list_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_branch_list_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_branch_info
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Branch name (e.g. main, feature/my-branch)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(std::mem::take(&mut p)), required: Some(vec!["project_name".into(), "repo_name".into(), "name".into()]) };
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Branch name (e.g. main, feature/my-branch)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(std::mem::take(&mut p)),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"name".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_branch_info").description("Get detailed information about a specific branch including ahead/behind status vs upstream.").parameters(schema),
|
||||
ToolHandler::new(move |ctx, args| {
|
||||
@ -138,12 +270,60 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_branches_merged
|
||||
p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("branch".into(), ToolParam { name: "branch".into(), param_type: "string".into(), description: Some("Branch to check (source)".into()), required: true, properties: None, items: None }),
|
||||
("into".into(), ToolParam { name: "into".into(), param_type: "string".into(), description: Some("Branch to check against (target, default: main)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"branch".into(),
|
||||
ToolParam {
|
||||
name: "branch".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Branch to check (source)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"into".into(),
|
||||
ToolParam {
|
||||
name: "into".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Branch to check against (target, default: main)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "branch".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"branch".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_branches_merged").description("Check whether a branch has been merged into another branch, and find the merge base.").parameters(schema),
|
||||
ToolHandler::new(move |ctx, args| {
|
||||
@ -156,12 +336,60 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_branch_diff
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("local".into(), ToolParam { name: "local".into(), param_type: "string".into(), description: Some("Local branch name".into()), required: true, properties: None, items: None }),
|
||||
("remote".into(), ToolParam { name: "remote".into(), param_type: "string".into(), description: Some("Remote branch name (defaults to local)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"local".into(),
|
||||
ToolParam {
|
||||
name: "local".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Local branch name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"remote".into(),
|
||||
ToolParam {
|
||||
name: "remote".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Remote branch name (defaults to local)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "local".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"local".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_branch_diff").description("Compare a local branch against its remote counterpart to see how many commits ahead/behind they are.").parameters(schema),
|
||||
ToolHandler::new(move |ctx, args| {
|
||||
@ -171,4 +399,4 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,57 +7,87 @@ use std::collections::HashMap;
|
||||
|
||||
// --- Execution functions for each tool ---
|
||||
|
||||
async fn git_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn git_log_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
let skip = p.get("skip").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commits = domain.commit_log(rev.as_deref(), skip, limit)
|
||||
let commits = domain
|
||||
.commit_log(rev.as_deref(), skip, limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Flatten to simple JSON
|
||||
let result: Vec<_> = commits.iter().map(|c| {
|
||||
use chrono::TimeZone;
|
||||
let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
|
||||
let time_str = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
let result: Vec<_> = commits
|
||||
.iter()
|
||||
.map(|c| {
|
||||
use chrono::TimeZone;
|
||||
let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
|
||||
let time_str = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
|
||||
let oid = c.oid.to_string();
|
||||
let short_oid = oid.get(..7).unwrap_or(&oid).to_string();
|
||||
let oid = c.oid.to_string();
|
||||
let short_oid = oid.get(..7).unwrap_or(&oid).to_string();
|
||||
|
||||
serde_json::json!({
|
||||
"oid": oid,
|
||||
"short_oid": short_oid,
|
||||
"message": c.message,
|
||||
"summary": c.summary,
|
||||
"author_name": c.author.name,
|
||||
"author_email": c.author.email,
|
||||
"author_time": time_str,
|
||||
"committer_name": c.committer.name,
|
||||
"committer_email": c.committer.email,
|
||||
"parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>(),
|
||||
"tree_oid": c.tree_id.to_string()
|
||||
serde_json::json!({
|
||||
"oid": oid,
|
||||
"short_oid": short_oid,
|
||||
"message": c.message,
|
||||
"summary": c.summary,
|
||||
"author_name": c.author.name,
|
||||
"author_email": c.author.email,
|
||||
"author_time": time_str,
|
||||
"committer_name": c.committer.name,
|
||||
"committer_email": c.committer.email,
|
||||
"parent_oids": c.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>(),
|
||||
"tree_oid": c.tree_id.to_string()
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
/// Resolve a rev string to commit metadata using the full rev-parse machinery
|
||||
/// (branch names, tags, HEAD, hex prefixes, etc.).
|
||||
fn resolve_commit(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitMeta, String> {
|
||||
fn resolve_commit(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitMeta, String> {
|
||||
let oid = domain.resolve_rev(rev).map_err(|e| e.to_string())?;
|
||||
domain.commit_get(&oid).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn git_show_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -67,8 +97,11 @@ async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
|
||||
use chrono::TimeZone;
|
||||
let ts = meta.author.time_secs - (meta.author.offset_minutes as i64 * 60);
|
||||
let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", meta.author.time_secs));
|
||||
let author_time = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", meta.author.time_secs));
|
||||
|
||||
let oid = meta.oid.to_string();
|
||||
let short_oid = oid.get(..7).unwrap_or(&oid).to_string();
|
||||
@ -85,20 +118,36 @@ async fn git_show_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_search_commits_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let query = p.get("query").and_then(|v| v.as_str()).ok_or("missing query")?;
|
||||
async fn git_search_commits_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let query = p
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing query")?;
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
// Fetch extra commits to have enough candidates after filtering
|
||||
let walk_limit = limit.saturating_mul(2).max(100);
|
||||
let commits = domain.commit_log(Some("HEAD"), 0, walk_limit).map_err(|e| e.to_string())?;
|
||||
let commits = domain
|
||||
.commit_log(Some("HEAD"), 0, walk_limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let q = query.to_lowercase();
|
||||
|
||||
let result: Vec<_> = commits.iter()
|
||||
let result: Vec<_> = commits
|
||||
.iter()
|
||||
.filter(|c| c.message.to_lowercase().contains(&q))
|
||||
.take(limit)
|
||||
.map(|c| flatten_commit(c))
|
||||
@ -110,8 +159,11 @@ async fn git_search_commits_exec(ctx: GitToolCtx, args: serde_json::Value) -> Re
|
||||
fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value {
|
||||
use chrono::TimeZone;
|
||||
let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
|
||||
let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
let author_time = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
let oid = c.oid.to_string();
|
||||
serde_json::json!({
|
||||
"oid": oid.clone(),
|
||||
@ -124,10 +176,20 @@ fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
async fn git_commit_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn git_commit_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).ok_or("missing rev")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -136,69 +198,118 @@ async fn git_commit_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
Ok(flatten_commit(&meta))
|
||||
}
|
||||
|
||||
async fn git_graph_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn git_graph_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commits = domain.commit_log(rev.as_deref(), 0, limit).map_err(|e| e.to_string())?;
|
||||
let commits = domain
|
||||
.commit_log(rev.as_deref(), 0, limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let mut col_map: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
let lines: Vec<_> = commits.iter().map(|m| {
|
||||
let lane_index = *col_map.get(m.oid.as_str()).unwrap_or(&0);
|
||||
let oid = m.oid.to_string();
|
||||
let refs = match domain.commit_refs(&m.oid) {
|
||||
Ok(refs) => refs.iter().map(|r| {
|
||||
if r.is_tag { format!("tag: {}", r.name.trim_start_matches("refs/tags/")) }
|
||||
else if r.is_remote { r.name.trim_start_matches("refs/remotes/").to_string() }
|
||||
else { r.name.trim_start_matches("refs/heads/").to_string() }
|
||||
}).collect::<Vec<_>>().join(", "),
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
for (i, p) in m.parent_ids.iter().enumerate() {
|
||||
if i == 0 { col_map.insert(p.to_string(), lane_index); } else { col_map.remove(p.as_str()); }
|
||||
}
|
||||
let ts = m.author.time_secs - (m.author.offset_minutes as i64 * 60);
|
||||
let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", m.author.time_secs));
|
||||
let lines: Vec<_> = commits
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let lane_index = *col_map.get(m.oid.as_str()).unwrap_or(&0);
|
||||
let oid = m.oid.to_string();
|
||||
let refs = match domain.commit_refs(&m.oid) {
|
||||
Ok(refs) => refs
|
||||
.iter()
|
||||
.map(|r| {
|
||||
if r.is_tag {
|
||||
format!("tag: {}", r.name.trim_start_matches("refs/tags/"))
|
||||
} else if r.is_remote {
|
||||
r.name.trim_start_matches("refs/remotes/").to_string()
|
||||
} else {
|
||||
r.name.trim_start_matches("refs/heads/").to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
Err(_) => String::new(),
|
||||
};
|
||||
for (i, p) in m.parent_ids.iter().enumerate() {
|
||||
if i == 0 {
|
||||
col_map.insert(p.to_string(), lane_index);
|
||||
} else {
|
||||
col_map.remove(p.as_str());
|
||||
}
|
||||
}
|
||||
let ts = m.author.time_secs - (m.author.offset_minutes as i64 * 60);
|
||||
let author_time = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", m.author.time_secs));
|
||||
|
||||
serde_json::json!({
|
||||
"oid": oid.clone(),
|
||||
"short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
|
||||
"refs": refs,
|
||||
"short_message": m.summary,
|
||||
"lane_index": lane_index,
|
||||
"author_name": m.author.name,
|
||||
"author_email": m.author.email,
|
||||
"author_time": author_time,
|
||||
"parent_oids": m.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>()
|
||||
serde_json::json!({
|
||||
"oid": oid.clone(),
|
||||
"short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
|
||||
"refs": refs,
|
||||
"short_message": m.summary,
|
||||
"lane_index": lane_index,
|
||||
"author_name": m.author.name,
|
||||
"author_email": m.author.email,
|
||||
"author_time": author_time,
|
||||
"parent_oids": m.parent_ids.iter().map(|p| p.to_string()).collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(lines).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
async fn git_reflog_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let ref_name = p.get("ref_name").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
async fn git_reflog_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let ref_name = p
|
||||
.get("ref_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let entries = domain.reflog_entries(ref_name.as_deref()).map_err(|e| e.to_string())?;
|
||||
let entries = domain
|
||||
.reflog_entries(ref_name.as_deref())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result: Vec<_> = entries.iter()
|
||||
let result: Vec<_> = entries
|
||||
.iter()
|
||||
.take(limit)
|
||||
.map(|e| {
|
||||
// Convert to UTC by subtracting the timezone offset, consistent
|
||||
// with all other timestamp conversions in this module.
|
||||
let ts = e.time_secs - (e.offset_minutes as i64 * 60);
|
||||
let time_str = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", e.time_secs));
|
||||
let time_str = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", e.time_secs));
|
||||
serde_json::json!({
|
||||
"oid_new": e.oid_new.to_string(), "oid_old": e.oid_old.to_string(),
|
||||
"committer_name": e.committer_name, "committer_email": e.committer_email,
|
||||
@ -213,155 +324,297 @@ async fn git_reflog_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<ser
|
||||
/// Common required params used across all git tools.
|
||||
fn common_params() -> HashMap<String, ToolParam> {
|
||||
HashMap::from([
|
||||
("project_name".into(), ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
}),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_log
|
||||
let mut p = common_params();
|
||||
p.insert("rev".into(), ToolParam {
|
||||
name: "rev".into(), param_type: "string".into(),
|
||||
description: Some("Revision/range specifier (branch name, commit hash, etc.)".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("limit".into(), ToolParam {
|
||||
name: "limit".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum number of commits to return".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("skip".into(), ToolParam {
|
||||
name: "skip".into(), param_type: "integer".into(),
|
||||
description: Some("Number of commits to skip".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
p.insert(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision/range specifier (branch name, commit hash, etc.)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of commits to return".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"skip".into(),
|
||||
ToolParam {
|
||||
name: "skip".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Number of commits to skip".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_log").description("List commits in a repository, optionally filtered by revision range.").parameters(schema),
|
||||
ToolDefinition::new("git_log")
|
||||
.description("List commits in a repository, optionally filtered by revision range.")
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_log_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_log_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_show
|
||||
let mut p = common_params();
|
||||
p.insert("rev".into(), ToolParam {
|
||||
name: "rev".into(), param_type: "string".into(),
|
||||
description: Some("Revision to show (commit hash, branch, tag)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) };
|
||||
p.insert(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to show (commit hash, branch, tag)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"rev".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_show").description("Show detailed commit information including message, author, refs, and diff stats.").parameters(schema),
|
||||
ToolDefinition::new("git_show")
|
||||
.description(
|
||||
"Show detailed commit information including message, author, refs, and diff stats.",
|
||||
)
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_show_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_show_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_search_commits
|
||||
let mut p = common_params();
|
||||
p.insert("query".into(), ToolParam {
|
||||
name: "query".into(), param_type: "string".into(),
|
||||
description: Some("Keyword to search in commit messages".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("limit".into(), ToolParam {
|
||||
name: "limit".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum results to return".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "query".into()]) };
|
||||
p.insert(
|
||||
"query".into(),
|
||||
ToolParam {
|
||||
name: "query".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Keyword to search in commit messages".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum results to return".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"query".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_search_commits").description("Search commit messages for a keyword and return matching commits.").parameters(schema),
|
||||
ToolDefinition::new("git_search_commits")
|
||||
.description("Search commit messages for a keyword and return matching commits.")
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_search_commits_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_search_commits_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_commit_info
|
||||
let mut p = common_params();
|
||||
p.insert("rev".into(), ToolParam {
|
||||
name: "rev".into(), param_type: "string".into(),
|
||||
description: Some("Revision to look up (full or short commit hash, branch, tag)".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "rev".into()]) };
|
||||
p.insert(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Revision to look up (full or short commit hash, branch, tag)".into(),
|
||||
),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"rev".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_commit_info").description("Get detailed metadata for a specific commit (author, committer, parents, tree)." ).parameters(schema),
|
||||
ToolDefinition::new("git_commit_info")
|
||||
.description(
|
||||
"Get detailed metadata for a specific commit (author, committer, parents, tree).",
|
||||
)
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_commit_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_commit_info_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_graph
|
||||
let mut p = common_params();
|
||||
p.insert("rev".into(), ToolParam {
|
||||
name: "rev".into(), param_type: "string".into(),
|
||||
description: Some("Starting revision (default: HEAD)".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("limit".into(), ToolParam {
|
||||
name: "limit".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum number of commits (default: 20)".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
p.insert(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Starting revision (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of commits (default: 20)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_graph").description("Show an ASCII commit graph with branch lanes and refs.").parameters(schema),
|
||||
ToolDefinition::new("git_graph")
|
||||
.description("Show an ASCII commit graph with branch lanes and refs.")
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_graph_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_graph_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_reflog
|
||||
let mut p = common_params();
|
||||
p.insert("ref_name".into(), ToolParam {
|
||||
name: "ref_name".into(), param_type: "string".into(),
|
||||
description: Some("Reference name (e.g. refs/heads/main). Defaults to all refs.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("limit".into(), ToolParam {
|
||||
name: "limit".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum number of entries (default: 50)".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
p.insert(
|
||||
"ref_name".into(),
|
||||
ToolParam {
|
||||
name: "ref_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Reference name (e.g. refs/heads/main). Defaults to all refs.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of entries (default: 50)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_reflog").description("Show the reference log (reflog) recording when branch tips and refs were updated.").parameters(schema),
|
||||
ToolDefinition::new("git_reflog")
|
||||
.description(
|
||||
"Show the reference log (reflog) recording when branch tips and refs were updated.",
|
||||
)
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_reflog_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_reflog_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
@ -20,7 +20,11 @@ impl GitToolCtx {
|
||||
}
|
||||
|
||||
/// Opens a git repository by project name and repo name.
|
||||
pub async fn open_repo(&self, project_name: &str, repo_name: &str) -> Result<GitDomain, String> {
|
||||
pub async fn open_repo(
|
||||
&self,
|
||||
project_name: &str,
|
||||
repo_name: &str,
|
||||
) -> Result<GitDomain, String> {
|
||||
let db = self.ctx.db();
|
||||
resolve_project_and_repo(db, project_name, repo_name)
|
||||
.await
|
||||
@ -46,7 +50,12 @@ async fn resolve_project_and_repo(
|
||||
.filter(repo::Column::RepoName.eq(repo_name))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| format!("DB error looking up repo '{}/{}': {}", project_name, repo_name, e))?
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"DB error looking up repo '{}/{}': {}",
|
||||
project_name, repo_name, e
|
||||
)
|
||||
})?
|
||||
.ok_or_else(|| format!("repo '{}/{}' not found", project_name, repo_name))?;
|
||||
|
||||
Ok((project.id, repo_model.storage_path))
|
||||
|
||||
@ -4,14 +4,32 @@ use super::ctx::GitToolCtx;
|
||||
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let base = p.get("base").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let head = p.get("head").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
async fn git_diff_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let base = p
|
||||
.get("base")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let head = p
|
||||
.get("head")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let paths = p.get("paths").and_then(|v| v.as_array()).map(|a| {
|
||||
a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect::<Vec<_>>()
|
||||
a.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -22,7 +40,10 @@ async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
Ok(oid)
|
||||
} else {
|
||||
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
||||
domain
|
||||
.commit_get_prefix(rev)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|m| m.oid)
|
||||
}
|
||||
};
|
||||
|
||||
@ -35,11 +56,15 @@ async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
None => None,
|
||||
};
|
||||
|
||||
let opts = paths.map(|ps| {
|
||||
let mut o = git::diff::types::DiffOptions::new();
|
||||
for p in ps { o = o.pathspec(&p); }
|
||||
Some(o)
|
||||
}).flatten();
|
||||
let opts = paths
|
||||
.map(|ps| {
|
||||
let mut o = git::diff::types::DiffOptions::new();
|
||||
for p in ps {
|
||||
o = o.pathspec(&p);
|
||||
}
|
||||
Some(o)
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let result = match (&base_oid, &head_oid) {
|
||||
(None, None) => {
|
||||
@ -47,25 +72,35 @@ async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
if domain.repo().head().is_err() {
|
||||
return Err("No commits found in repository".into());
|
||||
}
|
||||
let head_oid = domain.ref_target("HEAD").map_err(|e| e.to_string())?
|
||||
let head_oid = domain
|
||||
.ref_target("HEAD")
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| "HEAD reference not found".to_string())?;
|
||||
// Bare repos have no working tree — use tree-to-tree diff instead
|
||||
if domain.repo().is_bare() {
|
||||
domain.diff_tree_to_tree(None, Some(&head_oid), opts).map_err(|e| e.to_string())?
|
||||
domain
|
||||
.diff_tree_to_tree(None, Some(&head_oid), opts)
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
domain.diff_commit_to_workdir(&head_oid, opts).map_err(|e| e.to_string())?
|
||||
domain
|
||||
.diff_commit_to_workdir(&head_oid, opts)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
(Some(base), None) => {
|
||||
if domain.repo().is_bare() {
|
||||
domain.diff_tree_to_tree(Some(base), None, opts).map_err(|e| e.to_string())?
|
||||
domain
|
||||
.diff_tree_to_tree(Some(base), None, opts)
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
domain.diff_commit_to_workdir(base, opts).map_err(|e| e.to_string())?
|
||||
domain
|
||||
.diff_commit_to_workdir(base, opts)
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
}
|
||||
(Some(base), Some(head_oid_val)) => {
|
||||
domain.diff_tree_to_tree(Some(base), Some(head_oid_val), opts).map_err(|e| e.to_string())?
|
||||
}
|
||||
(Some(base), Some(head_oid_val)) => domain
|
||||
.diff_tree_to_tree(Some(base), Some(head_oid_val), opts)
|
||||
.map_err(|e| e.to_string())?,
|
||||
(None, Some(_)) => {
|
||||
return Err("base revision required when head is specified".into());
|
||||
}
|
||||
@ -87,12 +122,28 @@ async fn git_diff_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_diff_stats_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let base = p.get("base").and_then(|v| v.as_str()).ok_or("missing base")?;
|
||||
let head = p.get("head").and_then(|v| v.as_str()).ok_or("missing head")?;
|
||||
async fn git_diff_stats_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let base = p
|
||||
.get("base")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing base")?;
|
||||
let head = p
|
||||
.get("head")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing head")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
|
||||
@ -102,7 +153,10 @@ async fn git_diff_stats_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(rev) {
|
||||
Ok(oid)
|
||||
} else {
|
||||
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
||||
domain
|
||||
.commit_get_prefix(rev)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|m| m.oid)
|
||||
}
|
||||
};
|
||||
let b = resolve(base).map_err(|e| e.to_string())?;
|
||||
@ -116,13 +170,32 @@ async fn git_diff_stats_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_blame_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "HEAD".to_string());
|
||||
let from_line = p.get("from_line").and_then(|v| v.as_u64().map(|n| n as u32));
|
||||
async fn git_blame_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
let from_line = p
|
||||
.get("from_line")
|
||||
.and_then(|v| v.as_u64().map(|n| n as u32));
|
||||
let to_line = p.get("to_line").and_then(|v| v.as_u64().map(|n| n as u32));
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -131,28 +204,40 @@ async fn git_blame_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serd
|
||||
} else if let Ok(Some(oid)) = domain.ref_target(&rev) {
|
||||
Ok(oid)
|
||||
} else {
|
||||
domain.commit_get_prefix(&rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
||||
domain
|
||||
.commit_get_prefix(&rev)
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|m| m.oid)
|
||||
}?;
|
||||
|
||||
use git::blame::ops::BlameOptions;
|
||||
let mut bopts = BlameOptions::new();
|
||||
if let Some(fl) = from_line { bopts = bopts.min_line(fl as usize); }
|
||||
if let Some(tl) = to_line { bopts = bopts.max_line(tl as usize); }
|
||||
if let Some(fl) = from_line {
|
||||
bopts = bopts.min_line(fl as usize);
|
||||
}
|
||||
if let Some(tl) = to_line {
|
||||
bopts = bopts.max_line(tl as usize);
|
||||
}
|
||||
|
||||
let hunks = domain.blame_file(&oid, path, Some(bopts)).map_err(|e| e.to_string())?;
|
||||
let hunks = domain
|
||||
.blame_file(&oid, path, Some(bopts))
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result: Vec<_> = hunks.iter().map(|h| {
|
||||
let oid = h.commit_oid.to_string();
|
||||
serde_json::json!({
|
||||
"commit_oid": oid.clone(),
|
||||
"short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
|
||||
"final_start_line": h.final_start_line,
|
||||
"final_lines": h.final_lines,
|
||||
"orig_start_line": h.orig_start_line,
|
||||
"orig_path": h.orig_path,
|
||||
"boundary": h.boundary
|
||||
let result: Vec<_> = hunks
|
||||
.iter()
|
||||
.map(|h| {
|
||||
let oid = h.commit_oid.to_string();
|
||||
serde_json::json!({
|
||||
"commit_oid": oid.clone(),
|
||||
"short_oid": oid.get(..7).unwrap_or(&oid).to_string(),
|
||||
"final_start_line": h.final_start_line,
|
||||
"final_lines": h.final_lines,
|
||||
"orig_start_line": h.orig_start_line,
|
||||
"orig_path": h.orig_path,
|
||||
"boundary": h.boundary
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
@ -160,13 +245,78 @@ async fn git_blame_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serd
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_diff
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("base".into(), ToolParam { name: "base".into(), param_type: "string".into(), description: Some("Base revision (commit hash or branch). Defaults to HEAD.".into()), required: false, properties: None, items: None }),
|
||||
("head".into(), ToolParam { name: "head".into(), param_type: "string".into(), description: Some("Head revision to diff against base. Requires base to be set.".into()), required: false, properties: None, items: None }),
|
||||
("paths".into(), ToolParam { name: "paths".into(), param_type: "array".into(), description: Some("Filter diff to specific file paths".into()), required: false, properties: None, items: Some(Box::new(ToolParam { name: "".into(), param_type: "string".into(), description: None, required: false, properties: None, items: None })) }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"base".into(),
|
||||
ToolParam {
|
||||
name: "base".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Base revision (commit hash or branch). Defaults to HEAD.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"head".into(),
|
||||
ToolParam {
|
||||
name: "head".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Head revision to diff against base. Requires base to be set.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"paths".into(),
|
||||
ToolParam {
|
||||
name: "paths".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Filter diff to specific file paths".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "string".into(),
|
||||
description: None,
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_diff").description("Show file changes between two commits, or between a commit and the working directory.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -179,12 +329,61 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_diff_stats
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("base".into(), ToolParam { name: "base".into(), param_type: "string".into(), description: Some("Base revision".into()), required: true, properties: None, items: None }),
|
||||
("head".into(), ToolParam { name: "head".into(), param_type: "string".into(), description: Some("Head revision".into()), required: true, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"base".into(),
|
||||
ToolParam {
|
||||
name: "base".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Base revision".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"head".into(),
|
||||
ToolParam {
|
||||
name: "head".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Head revision".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "base".into(), "head".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"base".into(),
|
||||
"head".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_diff_stats").description("Get aggregated diff statistics (files changed, insertions, deletions) between two revisions.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -197,21 +396,95 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_blame
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to blame".into()), required: true, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to blame from (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
("from_line".into(), ToolParam { name: "from_line".into(), param_type: "integer".into(), description: Some("Start line number for blame range".into()), required: false, properties: None, items: None }),
|
||||
("to_line".into(), ToolParam { name: "to_line".into(), param_type: "integer".into(), description: Some("End line number for blame range".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path to blame".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to blame from (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"from_line".into(),
|
||||
ToolParam {
|
||||
name: "from_line".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Start line number for blame range".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"to_line".into(),
|
||||
ToolParam {
|
||||
name: "to_line".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("End line number for blame range".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blame").description("Show what revision and author last modified each line of a file (git blame).").parameters(schema),
|
||||
ToolDefinition::new("git_blame")
|
||||
.description(
|
||||
"Show what revision and author last modified each line of a file (git blame).",
|
||||
)
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_blame_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_blame_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,10 +54,20 @@ fn head_tree(domain: &git::GitDomain) -> Result<git2::Tree<'_>, String> {
|
||||
// ── Tool executors ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tool: repo_doc_index — list all markdown docs with frontmatter
|
||||
async fn repo_doc_index_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_doc_index_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
@ -79,14 +89,22 @@ async fn repo_doc_index_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
};
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
if !name.starts_with('.') && !matches!(name, "node_modules" | "target" | ".git" | ".github" | ".next" | "dist") {
|
||||
if !name.starts_with('.')
|
||||
&& !matches!(
|
||||
name,
|
||||
"node_modules" | "target" | ".git" | ".github" | ".next" | "dist"
|
||||
)
|
||||
{
|
||||
if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) {
|
||||
stack.push((subtree, entry_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(git2::ObjectType::Blob) => {
|
||||
if name.ends_with(".md") || name.ends_with(".mdx") || name.ends_with(".markdown") {
|
||||
if name.ends_with(".md")
|
||||
|| name.ends_with(".mdx")
|
||||
|| name.ends_with(".markdown")
|
||||
{
|
||||
if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) {
|
||||
let raw = String::from_utf8_lossy(blob.content());
|
||||
let (fm_raw, body) = extract_frontmatter(&raw);
|
||||
@ -122,7 +140,11 @@ async fn repo_doc_index_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
let tags: Vec<String> = metadata
|
||||
.get("tags")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let headings = extract_headings(body);
|
||||
@ -145,7 +167,10 @@ async fn repo_doc_index_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
|
||||
// Sort by path for consistent ordering
|
||||
docs.sort_by(|a, b| {
|
||||
a["path"].as_str().unwrap_or("").cmp(b["path"].as_str().unwrap_or(""))
|
||||
a["path"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.cmp(b["path"].as_str().unwrap_or(""))
|
||||
});
|
||||
|
||||
Ok(serde_json::json!({
|
||||
@ -155,20 +180,36 @@ async fn repo_doc_index_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
}
|
||||
|
||||
/// Tool: repo_doc_read — read a specific document with structure
|
||||
async fn repo_doc_read_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
async fn repo_doc_read_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
let tree = head_tree(&domain)?;
|
||||
|
||||
// Navigate to the file using git2 path lookup
|
||||
let entry = tree.get_path(std::path::Path::new(path))
|
||||
let entry = tree
|
||||
.get_path(std::path::Path::new(path))
|
||||
.map_err(|e| format!("file '{}' not found: {e}", path))?;
|
||||
let blob = entry.to_object(repo).and_then(|o| o.peel_to_blob())
|
||||
let blob = entry
|
||||
.to_object(repo)
|
||||
.and_then(|o| o.peel_to_blob())
|
||||
.map_err(|e| format!("not a blob: {e}"))?;
|
||||
|
||||
let raw = String::from_utf8_lossy(blob.content());
|
||||
@ -200,11 +241,24 @@ async fn repo_doc_read_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<
|
||||
}
|
||||
|
||||
/// Tool: repo_doc_search — search through docs content
|
||||
async fn repo_doc_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let keyword = p.get("keyword").and_then(|v| v.as_str()).ok_or("missing keyword")?;
|
||||
async fn repo_doc_search_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let keyword = p
|
||||
.get("keyword")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing keyword")?;
|
||||
let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(2) as usize;
|
||||
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
@ -230,14 +284,23 @@ async fn repo_doc_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
};
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
if !name.starts_with('.') && !matches!(name, "node_modules" | "target" | ".git" | ".github" | ".next" | "dist") {
|
||||
if !name.starts_with('.')
|
||||
&& !matches!(
|
||||
name,
|
||||
"node_modules" | "target" | ".git" | ".github" | ".next" | "dist"
|
||||
)
|
||||
{
|
||||
if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) {
|
||||
stack.push((subtree, entry_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(git2::ObjectType::Blob) => {
|
||||
if name.ends_with(".md") || name.ends_with(".mdx") || name.ends_with(".markdown") || name.ends_with(".txt") {
|
||||
if name.ends_with(".md")
|
||||
|| name.ends_with(".mdx")
|
||||
|| name.ends_with(".markdown")
|
||||
|| name.ends_with(".txt")
|
||||
{
|
||||
if let Ok(blob) = entry.to_object(repo).and_then(|o| o.peel_to_blob()) {
|
||||
let content = String::from_utf8_lossy(blob.content());
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
@ -267,10 +330,8 @@ async fn repo_doc_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
}
|
||||
|
||||
for (start, end) in windows {
|
||||
let snippet: Vec<String> = lines[start..end]
|
||||
.iter()
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
let snippet: Vec<String> =
|
||||
lines[start..end].iter().map(|l| l.to_string()).collect();
|
||||
file_hits.push(serde_json::json!({
|
||||
"line_start": start + 1,
|
||||
"line_end": end,
|
||||
|
||||
@ -77,12 +77,34 @@ fn ext_to_language(ext: &str) -> Option<&'static str> {
|
||||
fn is_ignored_dir(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
".git" | "node_modules" | "target" | "dist" | "build" | ".next"
|
||||
| ".nuxt" | ".output" | ".cache" | "__pycache__" | ".tox"
|
||||
| "vendor" | ".bundle" | ".gradle" | "bin" | "obj"
|
||||
| ".svn" | ".hg" | ".idea" | ".vscode" | "coverage"
|
||||
| ".terraform" | ".serverless" | "deps" | "_build"
|
||||
| "elm-stuff" | ".stack-work" | ".pytest_cache"
|
||||
".git"
|
||||
| "node_modules"
|
||||
| "target"
|
||||
| "dist"
|
||||
| "build"
|
||||
| ".next"
|
||||
| ".nuxt"
|
||||
| ".output"
|
||||
| ".cache"
|
||||
| "__pycache__"
|
||||
| ".tox"
|
||||
| "vendor"
|
||||
| ".bundle"
|
||||
| ".gradle"
|
||||
| "bin"
|
||||
| "obj"
|
||||
| ".svn"
|
||||
| ".hg"
|
||||
| ".idea"
|
||||
| ".vscode"
|
||||
| "coverage"
|
||||
| ".terraform"
|
||||
| ".serverless"
|
||||
| "deps"
|
||||
| "_build"
|
||||
| "elm-stuff"
|
||||
| ".stack-work"
|
||||
| ".pytest_cache"
|
||||
)
|
||||
}
|
||||
|
||||
@ -114,9 +136,7 @@ fn collect_languages(
|
||||
match entry.kind() {
|
||||
Some(git2::ObjectType::Tree) => {
|
||||
if !is_ignored_dir(name) && !name.starts_with('.') {
|
||||
if let Ok(subtree) =
|
||||
entry.to_object(repo).and_then(|o| o.peel_to_tree())
|
||||
{
|
||||
if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) {
|
||||
stack.push((subtree, entry_path));
|
||||
}
|
||||
}
|
||||
@ -126,9 +146,7 @@ fn collect_languages(
|
||||
if let Some(ext) = name.rsplit('.').next() {
|
||||
let ext = ext.to_lowercase();
|
||||
if let Some(lang) = ext_to_language(&ext) {
|
||||
let entry = stats
|
||||
.entry(lang.to_string())
|
||||
.or_insert_with(|| (ext, 0));
|
||||
let entry = stats.entry(lang.to_string()).or_insert_with(|| (ext, 0));
|
||||
entry.1 += 1;
|
||||
}
|
||||
}
|
||||
@ -173,7 +191,15 @@ fn collect_file_tree(
|
||||
"kind": "dir"
|
||||
}));
|
||||
if let Ok(subtree) = entry.to_object(repo).and_then(|o| o.peel_to_tree()) {
|
||||
collect_file_tree(repo, &subtree, &entry_path, depth + 1, max_depth, max_files, files);
|
||||
collect_file_tree(
|
||||
repo,
|
||||
&subtree,
|
||||
&entry_path,
|
||||
depth + 1,
|
||||
max_depth,
|
||||
max_files,
|
||||
files,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -192,22 +218,55 @@ fn collect_file_tree(
|
||||
fn detect_config_files(tree: &git2::Tree) -> Vec<String> {
|
||||
let mut configs = Vec::new();
|
||||
let known_configs = [
|
||||
"Cargo.toml", "package.json", "go.mod", "Gemfile", "README.md",
|
||||
"Dockerfile", "docker-compose.yml", "docker-compose.yaml",
|
||||
".github/workflows", ".gitignore", ".gitattributes",
|
||||
"Makefile", "CMakeLists.txt", "composer.json", "pyproject.toml",
|
||||
"requirements.txt", "Pipfile", "pom.xml", "build.gradle",
|
||||
"build.gradle.kts", "settings.gradle", "settings.gradle.kts",
|
||||
"tsconfig.json", ".eslintrc.js", ".eslintrc.json",
|
||||
"prettier.config.js", "prettierrc", "webpack.config.js",
|
||||
"vite.config.ts", "vite.config.js", "next.config.js",
|
||||
"nuxt.config.ts", "svelte.config.js",
|
||||
"rust-toolchain", "rust-toolchain.toml",
|
||||
"clippy.toml", ".rustfmt.toml", "rustfmt.toml",
|
||||
"renovate.json", ".renovaterc", ".mergify.yml",
|
||||
"docker-bake.hcl", ".dockerignore",
|
||||
"Cargo.lock", "yarn.lock", "package-lock.json", "pnpm-lock.yaml",
|
||||
"Gemfile.lock", "Cargo.lock",
|
||||
"Cargo.toml",
|
||||
"package.json",
|
||||
"go.mod",
|
||||
"Gemfile",
|
||||
"README.md",
|
||||
"Dockerfile",
|
||||
"docker-compose.yml",
|
||||
"docker-compose.yaml",
|
||||
".github/workflows",
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
"Makefile",
|
||||
"CMakeLists.txt",
|
||||
"composer.json",
|
||||
"pyproject.toml",
|
||||
"requirements.txt",
|
||||
"Pipfile",
|
||||
"pom.xml",
|
||||
"build.gradle",
|
||||
"build.gradle.kts",
|
||||
"settings.gradle",
|
||||
"settings.gradle.kts",
|
||||
"tsconfig.json",
|
||||
".eslintrc.js",
|
||||
".eslintrc.json",
|
||||
"prettier.config.js",
|
||||
"prettierrc",
|
||||
"webpack.config.js",
|
||||
"vite.config.ts",
|
||||
"vite.config.js",
|
||||
"next.config.js",
|
||||
"nuxt.config.ts",
|
||||
"svelte.config.js",
|
||||
"rust-toolchain",
|
||||
"rust-toolchain.toml",
|
||||
"clippy.toml",
|
||||
".rustfmt.toml",
|
||||
"rustfmt.toml",
|
||||
"renovate.json",
|
||||
".renovaterc",
|
||||
".mergify.yml",
|
||||
"docker-bake.hcl",
|
||||
".dockerignore",
|
||||
"Cargo.lock",
|
||||
"yarn.lock",
|
||||
"package-lock.json",
|
||||
"pnpm-lock.yaml",
|
||||
"Gemfile.lock",
|
||||
"Cargo.lock",
|
||||
];
|
||||
for entry in tree.iter() {
|
||||
let name = match entry.name() {
|
||||
@ -243,7 +302,11 @@ fn parse_dependencies(content: &str, manifest_name: &str) -> serde_json::Value {
|
||||
if in_deps {
|
||||
if let Some(eq_pos) = trimmed.find('=') {
|
||||
let name = trimmed[..eq_pos].trim().to_string();
|
||||
let version = trimmed[eq_pos + 1..].trim().trim_matches('"').trim_matches('\'').to_string();
|
||||
let version = trimmed[eq_pos + 1..]
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'')
|
||||
.to_string();
|
||||
if !name.is_empty() && !name.starts_with('#') {
|
||||
deps.push(serde_json::json!({ "name": name, "version": version }));
|
||||
}
|
||||
@ -300,8 +363,17 @@ fn parse_dependencies(content: &str, manifest_name: &str) -> serde_json::Value {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("gem ") {
|
||||
let rest = trimmed.trim_start_matches("gem ");
|
||||
let name = rest.split(',').next().unwrap_or(rest).trim().trim_matches('"').trim_matches('\'');
|
||||
let version = rest.split(',').nth(1).map(|v| v.trim().trim_matches('"').trim_matches('\''));
|
||||
let name = rest
|
||||
.split(',')
|
||||
.next()
|
||||
.unwrap_or(rest)
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'');
|
||||
let version = rest
|
||||
.split(',')
|
||||
.nth(1)
|
||||
.map(|v| v.trim().trim_matches('"').trim_matches('\''));
|
||||
deps.push(serde_json::json!({ "name": name, "version": version }));
|
||||
}
|
||||
}
|
||||
@ -311,7 +383,11 @@ fn parse_dependencies(content: &str, manifest_name: &str) -> serde_json::Value {
|
||||
let mut deps = Vec::new();
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() && !trimmed.starts_with('#') && !trimmed.starts_with("-r") && !trimmed.starts_with("--") {
|
||||
if !trimmed.is_empty()
|
||||
&& !trimmed.starts_with('#')
|
||||
&& !trimmed.starts_with("-r")
|
||||
&& !trimmed.starts_with("--")
|
||||
{
|
||||
if let Some(eq_eq) = trimmed.find("==") {
|
||||
let name = trimmed[..eq_eq].trim().to_string();
|
||||
let version = trimmed[eq_eq + 2..].trim().to_string();
|
||||
@ -323,7 +399,9 @@ fn parse_dependencies(content: &str, manifest_name: &str) -> serde_json::Value {
|
||||
}
|
||||
serde_json::json!({ "manifest": "requirements.txt", "ecosystem": "python", "dependencies": deps })
|
||||
}
|
||||
_ => serde_json::json!({ "manifest": manifest_name, "ecosystem": "unknown", "dependencies": [] }),
|
||||
_ => {
|
||||
serde_json::json!({ "manifest": manifest_name, "ecosystem": "unknown", "dependencies": [] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,10 +424,20 @@ fn head_oid(domain: &git::GitDomain) -> Result<String, String> {
|
||||
}
|
||||
|
||||
/// Tool: repo_overview — quick project overview
|
||||
async fn repo_overview_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_overview_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
@ -373,7 +461,9 @@ async fn repo_overview_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<
|
||||
.map(|(lang, (_ext, count))| serde_json::json!({ "language": lang, "file_count": count }))
|
||||
.collect();
|
||||
languages.sort_by(|a, b| {
|
||||
b["file_count"].as_u64().unwrap_or(0)
|
||||
b["file_count"]
|
||||
.as_u64()
|
||||
.unwrap_or(0)
|
||||
.cmp(&a["file_count"].as_u64().unwrap_or(0))
|
||||
});
|
||||
|
||||
@ -413,10 +503,20 @@ async fn repo_overview_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<
|
||||
}
|
||||
|
||||
/// Tool: repo_file_tree — recursive file tree with depth/ignore
|
||||
async fn repo_file_tree_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_file_tree_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let max_depth = p.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
|
||||
let max_files = p.get("max_files").and_then(|v| v.as_u64()).unwrap_or(200);
|
||||
|
||||
@ -434,10 +534,20 @@ async fn repo_file_tree_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
}
|
||||
|
||||
/// Tool: repo_languages — detailed language breakdown
|
||||
async fn repo_languages_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_languages_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
@ -451,7 +561,9 @@ async fn repo_languages_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
.map(|(lang, (_ext, count))| serde_json::json!({ "language": lang, "file_count": count }))
|
||||
.collect();
|
||||
languages.sort_by(|a, b| {
|
||||
b["file_count"].as_u64().unwrap_or(0)
|
||||
b["file_count"]
|
||||
.as_u64()
|
||||
.unwrap_or(0)
|
||||
.cmp(&a["file_count"].as_u64().unwrap_or(0))
|
||||
});
|
||||
|
||||
@ -462,10 +574,20 @@ async fn repo_languages_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
}
|
||||
|
||||
/// Tool: repo_dependencies — parse dependency manifests
|
||||
async fn repo_dependencies_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_dependencies_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let tree = head_tree(&domain)?;
|
||||
|
||||
@ -25,42 +25,77 @@ fn head_oid(domain: &git::GitDomain) -> Result<String, String> {
|
||||
fn is_ignored_dir(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
".git" | "node_modules" | "target" | "dist" | "build" | ".next"
|
||||
| ".nuxt" | ".output" | ".cache" | "__pycache__" | ".tox"
|
||||
| "vendor" | ".bundle" | ".gradle" | "bin" | "obj"
|
||||
| ".svn" | ".hg" | ".idea" | ".vscode" | "coverage"
|
||||
| ".terraform" | ".serverless" | "deps" | "_build"
|
||||
| "elm-stuff" | ".stack-work" | ".pytest_cache"
|
||||
".git"
|
||||
| "node_modules"
|
||||
| "target"
|
||||
| "dist"
|
||||
| "build"
|
||||
| ".next"
|
||||
| ".nuxt"
|
||||
| ".output"
|
||||
| ".cache"
|
||||
| "__pycache__"
|
||||
| ".tox"
|
||||
| "vendor"
|
||||
| ".bundle"
|
||||
| ".gradle"
|
||||
| "bin"
|
||||
| "obj"
|
||||
| ".svn"
|
||||
| ".hg"
|
||||
| ".idea"
|
||||
| ".vscode"
|
||||
| "coverage"
|
||||
| ".terraform"
|
||||
| ".serverless"
|
||||
| "deps"
|
||||
| "_build"
|
||||
| "elm-stuff"
|
||||
| ".stack-work"
|
||||
| ".pytest_cache"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_binary_ext(name: &str) -> bool {
|
||||
match name.rsplit('.').next().unwrap_or("") {
|
||||
"png" | "jpg" | "jpeg" | "gif" | "webp" | "ico" | "svg" | "bmp"
|
||||
| "mp3" | "mp4" | "wav" | "avi" | "mov" | "mkv" | "webm"
|
||||
| "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar"
|
||||
| "exe" | "dll" | "so" | "dylib" | "o" | "a" | "lib"
|
||||
| "woff" | "woff2" | "ttf" | "otf" | "eot"
|
||||
| "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx"
|
||||
| "sqlite" | "db" | "bin" | "dat" | "pyc" | "class"
|
||||
| "wasm" | "node" => true,
|
||||
"png" | "jpg" | "jpeg" | "gif" | "webp" | "ico" | "svg" | "bmp" | "mp3" | "mp4" | "wav"
|
||||
| "avi" | "mov" | "mkv" | "webm" | "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar"
|
||||
| "exe" | "dll" | "so" | "dylib" | "o" | "a" | "lib" | "woff" | "woff2" | "ttf" | "otf"
|
||||
| "eot" | "pdf" | "doc" | "docx" | "xls" | "xlsx" | "ppt" | "pptx" | "sqlite" | "db"
|
||||
| "bin" | "dat" | "pyc" | "class" | "wasm" | "node" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a rev string to a commit OID.
|
||||
fn resolve_commit_oid(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitOid, String> {
|
||||
fn resolve_commit_oid(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitOid, String> {
|
||||
domain.resolve_rev(rev).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ── Tool executors ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Tool: repo_search — search code content across the repo
|
||||
async fn repo_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let keyword = p.get("keyword").and_then(|v| v.as_str()).ok_or("missing keyword")?;
|
||||
async fn repo_search_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let keyword = p
|
||||
.get("keyword")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing keyword")?;
|
||||
let context_lines = p.get("context_lines").and_then(|v| v.as_u64()).unwrap_or(2) as usize;
|
||||
let max_results = p.get("max_results").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
|
||||
|
||||
@ -153,10 +188,8 @@ async fn repo_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<se
|
||||
let snippets: Vec<serde_json::Value> = windows
|
||||
.iter()
|
||||
.map(|(start, end)| {
|
||||
let snippet: Vec<String> = lines[*start..*end]
|
||||
.iter()
|
||||
.map(|l| l.to_string())
|
||||
.collect();
|
||||
let snippet: Vec<String> =
|
||||
lines[*start..*end].iter().map(|l| l.to_string()).collect();
|
||||
serde_json::json!({
|
||||
"line_start": start + 1,
|
||||
"line_end": end,
|
||||
@ -187,17 +220,34 @@ async fn repo_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<se
|
||||
}
|
||||
|
||||
/// Tool: repo_readme — get README content
|
||||
async fn repo_readme_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_readme_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let repo = domain.repo();
|
||||
let tree = head_tree(&domain)?;
|
||||
|
||||
// Try common README filenames
|
||||
let candidates = ["README.md", "README.MD", "README.markdown", "README.rst", "README.txt", "README"];
|
||||
let candidates = [
|
||||
"README.md",
|
||||
"README.MD",
|
||||
"README.markdown",
|
||||
"README.rst",
|
||||
"README.txt",
|
||||
"README",
|
||||
];
|
||||
let mut found = None;
|
||||
|
||||
for candidate in &candidates {
|
||||
@ -225,10 +275,20 @@ async fn repo_readme_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<se
|
||||
}
|
||||
|
||||
/// Tool: repo_commit_log — filtered commit history
|
||||
async fn repo_commit_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_commit_log_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let author = p.get("author").and_then(|v| v.as_str());
|
||||
let keyword = p.get("keyword").and_then(|v| v.as_str());
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
@ -243,7 +303,9 @@ async fn repo_commit_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
limit
|
||||
};
|
||||
|
||||
let commits = domain.commit_log(Some(&head_oid), 0, fetch_limit).map_err(|e| e.to_string())?;
|
||||
let commits = domain
|
||||
.commit_log(Some(&head_oid), 0, fetch_limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let keyword_lower = keyword.map(|k| k.to_lowercase());
|
||||
let author_lower = author.map(|a| a.to_lowercase());
|
||||
@ -284,17 +346,29 @@ async fn repo_commit_log_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resul
|
||||
}
|
||||
|
||||
/// Tool: repo_contributors — contributor statistics
|
||||
async fn repo_contributors_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
async fn repo_contributors_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let head_oid = head_oid(&domain)?;
|
||||
|
||||
// Walk all commits (up to 10000)
|
||||
let commits = domain.commit_log(Some(&head_oid), 0, 10000).map_err(|e| e.to_string())?;
|
||||
let commits = domain
|
||||
.commit_log(Some(&head_oid), 0, 10000)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Aggregate by author email (more reliable than name)
|
||||
let mut authors: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
@ -309,9 +383,7 @@ async fn repo_contributors_exec(ctx: GitToolCtx, args: serde_json::Value) -> Res
|
||||
"last_commit_time": c.author.time_secs,
|
||||
})
|
||||
});
|
||||
entry["commit_count"] = serde_json::json!(
|
||||
entry["commit_count"].as_u64().unwrap_or(0) + 1
|
||||
);
|
||||
entry["commit_count"] = serde_json::json!(entry["commit_count"].as_u64().unwrap_or(0) + 1);
|
||||
let t = c.author.time_secs;
|
||||
if t < entry["first_commit_time"].as_i64().unwrap_or(i64::MAX) {
|
||||
entry["first_commit_time"] = serde_json::json!(t);
|
||||
@ -323,7 +395,9 @@ async fn repo_contributors_exec(ctx: GitToolCtx, args: serde_json::Value) -> Res
|
||||
|
||||
let mut contributors: Vec<serde_json::Value> = authors.into_values().collect();
|
||||
contributors.sort_by(|a, b| {
|
||||
b["commit_count"].as_u64().unwrap_or(0)
|
||||
b["commit_count"]
|
||||
.as_u64()
|
||||
.unwrap_or(0)
|
||||
.cmp(&a["commit_count"].as_u64().unwrap_or(0))
|
||||
});
|
||||
|
||||
@ -338,11 +412,24 @@ async fn repo_contributors_exec(ctx: GitToolCtx, args: serde_json::Value) -> Res
|
||||
}
|
||||
|
||||
/// Tool: repo_diff_summary — change summary between two revisions
|
||||
async fn repo_diff_summary_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let from_rev = p.get("from_rev").and_then(|v| v.as_str()).ok_or("missing from_rev")?;
|
||||
async fn repo_diff_summary_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let from_rev = p
|
||||
.get("from_rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing from_rev")?;
|
||||
let to_rev = p.get("to_rev").and_then(|v| v.as_str()).unwrap_or("HEAD");
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
@ -351,15 +438,18 @@ async fn repo_diff_summary_exec(ctx: GitToolCtx, args: serde_json::Value) -> Res
|
||||
let from_oid = resolve_commit_oid(&domain, from_rev)?;
|
||||
let to_oid = resolve_commit_oid(&domain, to_rev)?;
|
||||
|
||||
let from_commit = repo.find_commit(from_oid.to_oid().map_err(|e| e.to_string())?)
|
||||
let from_commit = repo
|
||||
.find_commit(from_oid.to_oid().map_err(|e| e.to_string())?)
|
||||
.map_err(|e| format!("from_rev not found: {e}"))?;
|
||||
let to_commit = repo.find_commit(to_oid.to_oid().map_err(|e| e.to_string())?)
|
||||
let to_commit = repo
|
||||
.find_commit(to_oid.to_oid().map_err(|e| e.to_string())?)
|
||||
.map_err(|e| format!("to_rev not found: {e}"))?;
|
||||
|
||||
let from_tree = from_commit.tree().map_err(|e| e.to_string())?;
|
||||
let to_tree = to_commit.tree().map_err(|e| e.to_string())?;
|
||||
|
||||
let diff = repo.diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)
|
||||
let diff = repo
|
||||
.diff_tree_to_tree(Some(&from_tree), Some(&to_tree), None)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let stats = diff.stats().map_err(|e| e.to_string())?;
|
||||
|
||||
@ -5,11 +5,24 @@ use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
||||
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||
use std::collections::HashMap;
|
||||
|
||||
async fn git_tag_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let pattern = p.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
async fn git_tag_list_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let pattern = p
|
||||
.get("pattern")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let all_tags = domain.tag_list().map_err(|e| e.to_string())?;
|
||||
@ -24,9 +37,9 @@ async fn git_tag_list_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<s
|
||||
.map(|s| regex::escape(s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(".*");
|
||||
let re = regex::Regex::new(&format!("^{}$", regex_pat))
|
||||
.ok();
|
||||
all_tags.iter()
|
||||
let re = regex::Regex::new(&format!("^{}$", regex_pat)).ok();
|
||||
all_tags
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
let n = t.name.to_lowercase();
|
||||
re.as_ref().map(|r| r.is_match(&n)).unwrap_or(false)
|
||||
@ -52,11 +65,24 @@ fn tag_to_json(t: &git::tags::types::TagInfo) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
async fn git_tag_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let name = p.get("name").and_then(|v| v.as_str()).ok_or("missing name")?;
|
||||
async fn git_tag_info_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let name = p
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing name")?;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let info = domain.tag_get(name).map_err(|e| e.to_string())?;
|
||||
@ -64,10 +90,20 @@ async fn git_tag_info_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<s
|
||||
Ok(tag_to_json(&info))
|
||||
}
|
||||
|
||||
async fn git_tag_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let query = p.get("query").and_then(|v| v.as_str()).ok_or("missing query")?;
|
||||
async fn git_tag_search_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let query = p
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing query")?;
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
|
||||
|
||||
// Resolve project_id from project_name for project isolation
|
||||
@ -81,35 +117,50 @@ async fn git_tag_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
let project_id = project.id.to_string();
|
||||
|
||||
// Get embed_service from context
|
||||
let embed = ctx.ctx.embed_service()
|
||||
.ok_or_else(|| "EmbedService not available — Qdrant vector search is disabled".to_string())?;
|
||||
let embed = ctx.ctx.embed_service().ok_or_else(|| {
|
||||
"EmbedService not available — Qdrant vector search is disabled".to_string()
|
||||
})?;
|
||||
|
||||
let results = embed.search_tags(query, &project_id, limit).await
|
||||
let results = embed
|
||||
.search_tags(query, &project_id, limit)
|
||||
.await
|
||||
.map_err(|e| format!("tag search failed: {}", e))?;
|
||||
|
||||
let json_results: Vec<serde_json::Value> = results.into_iter().map(|r| {
|
||||
let repo_name = r.payload.extra.as_ref()
|
||||
.and_then(|e| e.get("repo_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let tag_name = r.payload.extra.as_ref()
|
||||
.and_then(|e| e.get("tag_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let description = r.payload.extra.as_ref()
|
||||
.and_then(|e| e.get("description"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
serde_json::json!({
|
||||
"tag_name": tag_name,
|
||||
"repo_name": repo_name,
|
||||
"description": description,
|
||||
"score": r.score,
|
||||
let json_results: Vec<serde_json::Value> = results
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
let repo_name = r
|
||||
.payload
|
||||
.extra
|
||||
.as_ref()
|
||||
.and_then(|e| e.get("repo_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let tag_name = r
|
||||
.payload
|
||||
.extra
|
||||
.as_ref()
|
||||
.and_then(|e| e.get("tag_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let description = r
|
||||
.payload
|
||||
.extra
|
||||
.as_ref()
|
||||
.and_then(|e| e.get("description"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
serde_json::json!({
|
||||
"tag_name": tag_name,
|
||||
"repo_name": repo_name,
|
||||
"description": description,
|
||||
"score": r.score,
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(json_results).map_err(|e| e.to_string())?)
|
||||
}
|
||||
@ -117,28 +168,104 @@ async fn git_tag_search_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_tag_list
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("pattern".into(), ToolParam { name: "pattern".into(), param_type: "string".into(), description: Some("Filter tags by name pattern (supports * wildcard)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"pattern".into(),
|
||||
ToolParam {
|
||||
name: "pattern".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Filter tags by name pattern (supports * wildcard)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_tag_list").description("List all tags in a repository, optionally filtered by a name pattern.").parameters(schema),
|
||||
ToolDefinition::new("git_tag_list")
|
||||
.description("List all tags in a repository, optionally filtered by a name pattern.")
|
||||
.parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
let gctx = super::ctx::GitToolCtx::new(ctx);
|
||||
Box::pin(async move {
|
||||
git_tag_list_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
||||
git_tag_list_exec(gctx, args)
|
||||
.await
|
||||
.map_err(agent::ToolError::ExecutionError)
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// git_tag_info
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("name".into(), ToolParam { name: "name".into(), param_type: "string".into(), description: Some("Tag name to look up".into()), required: true, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Tag name to look up".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "name".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"name".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_tag_info").description("Get detailed information about a specific tag including target commit, annotation, and tagger.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -151,11 +278,45 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_tag_search
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("query".into(), ToolParam { name: "query".into(), param_type: "string".into(), description: Some("Semantic search query to find relevant tags".into()), required: true, properties: None, items: None }),
|
||||
("limit".into(), ToolParam { name: "limit".into(), param_type: "integer".into(), description: Some("Maximum number of results (default 5)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"query".into(),
|
||||
ToolParam {
|
||||
name: "query".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Semantic search query to find relevant tags".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of results (default 5)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "query".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "query".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_tag_search").description("Semantically search tags across repositories in a project. Uses vector search to find tags relevant to the query, with project-level isolation.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
|
||||
@ -7,26 +7,51 @@ use std::collections::HashMap;
|
||||
|
||||
/// Resolve a rev string to a commit OID using the full rev-parse machinery
|
||||
/// (branch names, tags, HEAD, hex prefixes, etc.).
|
||||
fn resolve_commit_oid(domain: &git::GitDomain, rev: &str) -> Result<git::commit::types::CommitOid, String> {
|
||||
fn resolve_commit_oid(
|
||||
domain: &git::GitDomain,
|
||||
rev: &str,
|
||||
) -> Result<git::commit::types::CommitOid, String> {
|
||||
domain.resolve_rev(rev).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn git_file_content_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "HEAD".to_string());
|
||||
async fn git_file_content_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let oid = resolve_commit_oid(&domain, &rev)?;
|
||||
|
||||
let entry = domain.tree_entry_by_path_from_commit(&oid, path).map_err(|e| e.to_string())?;
|
||||
let entry = domain
|
||||
.tree_entry_by_path_from_commit(&oid, path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let blob_info = domain.blob_get(&entry.oid).map_err(|e| e.to_string())?;
|
||||
|
||||
let content = domain.blob_content(&entry.oid).map_err(|e| e.to_string())?;
|
||||
let (display_content, is_binary) = if blob_info.is_binary {
|
||||
(base64::engine::general_purpose::STANDARD.encode(&content.content), true)
|
||||
(
|
||||
base64::engine::general_purpose::STANDARD.encode(&content.content),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
(String::from_utf8_lossy(&content.content).to_string(), false)
|
||||
};
|
||||
@ -40,12 +65,29 @@ async fn git_file_content_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resu
|
||||
}))
|
||||
}
|
||||
|
||||
async fn git_tree_ls_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let dir_path = p.get("path").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(|s| s.to_string()).unwrap_or_else(|| "HEAD".to_string());
|
||||
async fn git_tree_ls_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let dir_path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let commit_oid = resolve_commit_oid(&domain, &rev)?;
|
||||
@ -56,38 +98,63 @@ async fn git_tree_ls_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<se
|
||||
|
||||
let entries = match dir_path {
|
||||
Some(ref dp) => {
|
||||
let entry = domain.tree_entry_by_path(tree_oid, dp).map_err(|e| e.to_string())?;
|
||||
let entry = domain
|
||||
.tree_entry_by_path(tree_oid, dp)
|
||||
.map_err(|e| e.to_string())?;
|
||||
domain.tree_list(&entry.oid).map_err(|e| e.to_string())?
|
||||
}
|
||||
None => domain.tree_list(tree_oid).map_err(|e| e.to_string())?,
|
||||
};
|
||||
|
||||
let result: Vec<_> = entries.iter().map(|e| {
|
||||
serde_json::json!({
|
||||
"name": e.name,
|
||||
"oid": e.oid.to_string(),
|
||||
"kind": e.kind,
|
||||
"is_binary": e.is_binary
|
||||
let result: Vec<_> = entries
|
||||
.iter()
|
||||
.map(|e| {
|
||||
serde_json::json!({
|
||||
"name": e.name,
|
||||
"oid": e.oid.to_string(),
|
||||
"kind": e.kind,
|
||||
"is_binary": e.is_binary
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
async fn git_file_history_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(String::from).unwrap_or_else(|| "HEAD".to_string());
|
||||
async fn git_file_history_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
let limit = p.get("limit").and_then(|v| v.as_u64()).unwrap_or(20) as usize;
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
// Fetch extra commits to have enough candidates after filtering
|
||||
let walk_limit = limit.saturating_mul(2).max(200);
|
||||
let commits = domain.commit_log(Some(&rev), 0, walk_limit).map_err(|e| e.to_string())?;
|
||||
let commits = domain
|
||||
.commit_log(Some(&rev), 0, walk_limit)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let result: Vec<_> = commits.iter()
|
||||
let result: Vec<_> = commits
|
||||
.iter()
|
||||
.filter(|c| domain.tree_entry_by_path(&c.tree_id, path).is_ok())
|
||||
.take(limit)
|
||||
.map(|c| flatten_commit(c))
|
||||
@ -96,17 +163,36 @@ async fn git_file_history_exec(ctx: GitToolCtx, args: serde_json::Value) -> Resu
|
||||
Ok(serde_json::to_value(result).map_err(|e| e.to_string())?)
|
||||
}
|
||||
|
||||
async fn git_blob_get_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p.get("project_name").and_then(|v| v.as_str()).ok_or("missing project_name")?;
|
||||
let repo_name = p.get("repo_name").and_then(|v| v.as_str()).ok_or("missing repo_name")?;
|
||||
let path = p.get("path").and_then(|v| v.as_str()).ok_or("missing path")?;
|
||||
let rev = p.get("rev").and_then(|v| v.as_str()).map(String::from).unwrap_or_else(|| "HEAD".to_string());
|
||||
async fn git_blob_get_exec(
|
||||
ctx: GitToolCtx,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let p: serde_json::Map<String, serde_json::Value> =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let project_name = p
|
||||
.get("project_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing project_name")?;
|
||||
let repo_name = p
|
||||
.get("repo_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing repo_name")?;
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or("missing path")?;
|
||||
let rev = p
|
||||
.get("rev")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| "HEAD".to_string());
|
||||
|
||||
let domain = ctx.open_repo(project_name, repo_name).await?;
|
||||
let oid = resolve_commit_oid(&domain, &rev).map_err(|e| e.to_string())?;
|
||||
|
||||
let entry = domain.tree_entry_by_path_from_commit(&oid, path).map_err(|e| e.to_string())?;
|
||||
let entry = domain
|
||||
.tree_entry_by_path_from_commit(&oid, path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let blob_info = domain.blob_get(&entry.oid).map_err(|e| e.to_string())?;
|
||||
|
||||
if blob_info.is_binary {
|
||||
@ -127,8 +213,11 @@ async fn git_blob_get_exec(ctx: GitToolCtx, args: serde_json::Value) -> Result<s
|
||||
fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value {
|
||||
use chrono::TimeZone;
|
||||
let ts = c.author.time_secs - (c.author.offset_minutes as i64 * 60);
|
||||
let author_time = chrono::Utc.timestamp_opt(ts, 0).single()
|
||||
.map(|dt| dt.to_rfc3339()).unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
let author_time = chrono::Utc
|
||||
.timestamp_opt(ts, 0)
|
||||
.single()
|
||||
.map(|dt| dt.to_rfc3339())
|
||||
.unwrap_or_else(|| format!("{}", c.author.time_secs));
|
||||
let oid = c.oid.to_string();
|
||||
serde_json::json!({
|
||||
"oid": oid.clone(),
|
||||
@ -144,12 +233,60 @@ fn flatten_commit(c: &git::commit::types::CommitMeta) -> serde_json::Value {
|
||||
pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
// git_file_content
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path within the repository".into()), required: true, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to read file from (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path within the repository".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to read file from (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_file_content").description("Read the full content of a file at a given revision. Handles both text and binary files.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -162,12 +299,56 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_tree_ls
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("Directory path to list (root if omitted)".into()), required: false, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to list tree from (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Directory path to list (root if omitted)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to list tree from (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec!["project_name".into(), "repo_name".into()]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_tree_ls").description("List the contents of a directory (tree) at a given revision, showing files and subdirectories.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -180,13 +361,71 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_file_history
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path to trace history for".into()), required: true, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to start history from (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
("limit".into(), ToolParam { name: "limit".into(), param_type: "integer".into(), description: Some("Maximum number of commits to return (default: 20)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path to trace history for".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to start history from (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"limit".into(),
|
||||
ToolParam {
|
||||
name: "limit".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Maximum number of commits to return (default: 20)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_file_history").description("Show the commit history for a specific file, listing all commits that modified it.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -199,12 +438,60 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
|
||||
// git_blob_get
|
||||
let p = HashMap::from([
|
||||
("project_name".into(), ToolParam { name: "project_name".into(), param_type: "string".into(), description: Some("Project name (slug)".into()), required: true, properties: None, items: None }),
|
||||
("repo_name".into(), ToolParam { name: "repo_name".into(), param_type: "string".into(), description: Some("Repository name".into()), required: true, properties: None, items: None }),
|
||||
("path".into(), ToolParam { name: "path".into(), param_type: "string".into(), description: Some("File path within the repository".into()), required: true, properties: None, items: None }),
|
||||
("rev".into(), ToolParam { name: "rev".into(), param_type: "string".into(), description: Some("Revision to read file from (default: HEAD)".into()), required: false, properties: None, items: None }),
|
||||
(
|
||||
"project_name".into(),
|
||||
ToolParam {
|
||||
name: "project_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Project name (slug)".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path within the repository".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
(
|
||||
"rev".into(),
|
||||
ToolParam {
|
||||
name: "rev".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Revision to read file from (default: HEAD)".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
),
|
||||
]);
|
||||
let schema = ToolSchema { schema_type: "object".into(), properties: Some(p), required: Some(vec!["project_name".into(), "repo_name".into(), "path".into()]) };
|
||||
let schema = ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
required: Some(vec![
|
||||
"project_name".into(),
|
||||
"repo_name".into(),
|
||||
"path".into(),
|
||||
]),
|
||||
};
|
||||
registry.register(
|
||||
ToolDefinition::new("git_blob_get").description("Retrieve the raw content of a single file (blob) at a given revision. Returns error if the file is binary.").parameters(schema),
|
||||
ToolHandler::new(|ctx, args| {
|
||||
@ -214,4 +501,4 @@ pub fn register_git_tools(registry: &mut ToolRegistry) {
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,54 +11,127 @@ use git::tree::types::TreeEntry;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct RevQuery { pub rev: String }
|
||||
pub struct RevQuery {
|
||||
pub rev: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SearchCommits { pub query: String, #[serde(default = "dl")] pub limit: u32 }
|
||||
fn dl() -> u32 { 20 }
|
||||
pub struct SearchCommits {
|
||||
pub query: String,
|
||||
#[serde(default = "dl")]
|
||||
pub limit: u32,
|
||||
}
|
||||
fn dl() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct GraphParams { #[serde(default)] pub rev: Option<String>, #[serde(default = "dl")] pub limit: u32 }
|
||||
pub struct GraphParams {
|
||||
#[serde(default)]
|
||||
pub rev: Option<String>,
|
||||
#[serde(default = "dl")]
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct ReflogParams { #[serde(default)] pub ref_name: Option<String>, #[serde(default = "reflog_def")] pub limit: u32 }
|
||||
fn reflog_def() -> u32 { 50 }
|
||||
pub struct ReflogParams {
|
||||
#[serde(default)]
|
||||
pub ref_name: Option<String>,
|
||||
#[serde(default = "reflog_def")]
|
||||
pub limit: u32,
|
||||
}
|
||||
fn reflog_def() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SingleBranch { pub name: String }
|
||||
pub struct SingleBranch {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct BranchesMerged { pub branch: String, #[serde(default)] pub into: Option<String> }
|
||||
pub struct BranchesMerged {
|
||||
pub branch: String,
|
||||
#[serde(default)]
|
||||
pub into: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct BranchDiffP { pub local: String, #[serde(default)] pub remote: Option<String> }
|
||||
pub struct BranchDiffP {
|
||||
pub local: String,
|
||||
#[serde(default)]
|
||||
pub remote: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct DiffP { #[serde(default)] pub base: Option<String>, #[serde(default)] pub head: Option<String>, #[serde(default)] pub paths: Option<Vec<String>> }
|
||||
pub struct DiffP {
|
||||
#[serde(default)]
|
||||
pub base: Option<String>,
|
||||
#[serde(default)]
|
||||
pub head: Option<String>,
|
||||
#[serde(default)]
|
||||
pub paths: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct DiffStatsP { pub base: String, pub head: String }
|
||||
pub struct DiffStatsP {
|
||||
pub base: String,
|
||||
pub head: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct BlameP { pub path: String, #[serde(default)] pub rev: Option<String>, #[serde(default)] pub from_line: Option<u32>, #[serde(default)] pub to_line: Option<u32> }
|
||||
pub struct BlameP {
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub rev: Option<String>,
|
||||
#[serde(default)]
|
||||
pub from_line: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub to_line: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct FileContentP { pub path: String, #[serde(default)] pub rev: Option<String> }
|
||||
pub struct FileContentP {
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub rev: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TreeLsP { #[serde(default)] pub path: Option<String>, #[serde(default)] pub rev: Option<String> }
|
||||
pub struct TreeLsP {
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
#[serde(default)]
|
||||
pub rev: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct FileHistoryP { pub path: String, #[serde(default = "dl")] pub limit: u32 }
|
||||
pub struct FileHistoryP {
|
||||
pub path: String,
|
||||
#[serde(default = "dl")]
|
||||
pub limit: u32,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct TagListP { #[serde(default)] pub pattern: Option<String> }
|
||||
pub struct TagListP {
|
||||
#[serde(default)]
|
||||
pub pattern: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SingleTagP { pub name: String }
|
||||
pub struct SingleTagP {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct GitLogP { #[serde(default)] pub rev: Option<String>, #[serde(default = "dl")] pub limit: u32, #[serde(default)] pub skip: u32 }
|
||||
pub struct GitLogP {
|
||||
#[serde(default)]
|
||||
pub rev: Option<String>,
|
||||
#[serde(default = "dl")]
|
||||
pub limit: u32,
|
||||
#[serde(default)]
|
||||
pub skip: u32,
|
||||
}
|
||||
|
||||
/// Flat commit information for tool responses.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -84,7 +157,12 @@ impl CommitInfo {
|
||||
|
||||
Self {
|
||||
oid: meta.oid.to_string(),
|
||||
short_oid: meta.oid.to_string().get(..7).unwrap_or(&meta.oid.to_string()).to_string(),
|
||||
short_oid: meta
|
||||
.oid
|
||||
.to_string()
|
||||
.get(..7)
|
||||
.unwrap_or(&meta.oid.to_string())
|
||||
.to_string(),
|
||||
message: meta.message.clone(),
|
||||
summary: meta.summary.clone(),
|
||||
author_name: meta.author.name.clone(),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
//! AI agent function-call tools: git operations, file parsing/search, project management, and chat tools.
|
||||
|
||||
pub mod git_tools;
|
||||
pub mod file_tools;
|
||||
pub mod project_tools;
|
||||
pub mod chat_tools;
|
||||
pub mod file_tools;
|
||||
pub mod git_tools;
|
||||
pub mod project_tools;
|
||||
|
||||
@ -72,10 +72,7 @@ pub async fn arxiv_search_exec(
|
||||
.unwrap_or(DEFAULT_MAX_RESULTS as u64)
|
||||
.min(MAX_MAX_RESULTS as u64) as usize;
|
||||
|
||||
let start = args
|
||||
.get("start")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as usize;
|
||||
let start = args.get("start").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
|
||||
|
||||
// Build arXiv API query URL
|
||||
// Encode query for URL
|
||||
@ -138,7 +135,12 @@ pub async fn arxiv_search_exec(
|
||||
let author_str = if entry.author.is_empty() {
|
||||
"Unknown".to_string()
|
||||
} else {
|
||||
entry.author.iter().map(|a| a.name.as_str()).collect::<Vec<_>>().join(", ")
|
||||
entry
|
||||
.author
|
||||
.iter()
|
||||
.map(|a| a.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
@ -204,16 +206,30 @@ pub fn tool_definition() -> ToolDefinition {
|
||||
description: Some("Search query (required). Supports arXiv search syntax, e.g. 'ti:transformer AND au:bengio'.".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("max_results".into(), ToolParam {
|
||||
name: "max_results".into(), param_type: "integer".into(),
|
||||
description: Some("Maximum number of results to return (default 10, max 50). Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("start".into(), ToolParam {
|
||||
name: "start".into(), param_type: "integer".into(),
|
||||
description: Some("Offset for pagination. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"max_results".into(),
|
||||
ToolParam {
|
||||
name: "max_results".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some(
|
||||
"Maximum number of results to return (default 10, max 50). Optional.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"start".into(),
|
||||
ToolParam {
|
||||
name: "start".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Offset for pagination. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_arxiv_search")
|
||||
.description(
|
||||
"Search arXiv papers by keyword or phrase. \
|
||||
|
||||
@ -3,10 +3,8 @@
|
||||
|
||||
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
||||
use chrono::Utc;
|
||||
use models::projects::{
|
||||
project_board, project_board_card, project_board_column, project_members,
|
||||
};
|
||||
use models::projects::{MemberRole, ProjectBoard, ProjectBoardCard, ProjectBoardColumn};
|
||||
use models::projects::{project_board, project_board_card, project_board_column, project_members};
|
||||
use models::users::user::Model as UserModel;
|
||||
use sea_orm::*;
|
||||
use std::collections::HashMap;
|
||||
@ -305,9 +303,7 @@ pub async fn create_board_card_exec(
|
||||
.get("assignee_id")
|
||||
.and_then(|v| Uuid::parse_str(v.as_str()?).ok());
|
||||
|
||||
let issue_id = args
|
||||
.get("issue_id")
|
||||
.and_then(|v| v.as_i64());
|
||||
let issue_id = args.get("issue_id").and_then(|v| v.as_i64());
|
||||
|
||||
// Verify board belongs to project
|
||||
let board = ProjectBoard::find_by_id(board_id)
|
||||
@ -575,16 +571,28 @@ pub fn list_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn create_board_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Board name (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("Board description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Board name (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Board description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_board")
|
||||
.description(
|
||||
"Create a new Kanban board in the current project. Requires admin or owner role.",
|
||||
@ -598,25 +606,41 @@ pub fn create_board_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn update_board_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("board_id".into(), ToolParam {
|
||||
name: "board_id".into(), param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("New board name. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("New board description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"board_id".into(),
|
||||
ToolParam {
|
||||
name: "board_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New board name. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New board description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_update_board")
|
||||
.description(
|
||||
"Update a Kanban board (name or description). Requires admin or owner role.",
|
||||
)
|
||||
.description("Update a Kanban board (name or description). Requires admin or owner role.")
|
||||
.parameters(ToolSchema {
|
||||
schema_type: "object".into(),
|
||||
properties: Some(p),
|
||||
@ -626,41 +650,83 @@ pub fn update_board_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn create_card_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("board_id".into(), ToolParam {
|
||||
name: "board_id".into(), param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("column_id".into(), ToolParam {
|
||||
name: "column_id".into(), param_type: "string".into(),
|
||||
description: Some("Column UUID. Optional — defaults to first column.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("title".into(), ToolParam {
|
||||
name: "title".into(), param_type: "string".into(),
|
||||
description: Some("Card title (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("Card description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("priority".into(), ToolParam {
|
||||
name: "priority".into(), param_type: "string".into(),
|
||||
description: Some("Card priority (e.g. 'low', 'medium', 'high'). Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("assignee_id".into(), ToolParam {
|
||||
name: "assignee_id".into(), param_type: "string".into(),
|
||||
description: Some("Card assignee user UUID. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("issue_id".into(), ToolParam {
|
||||
name: "issue_id".into(), param_type: "integer".into(),
|
||||
description: Some("Link a project issue NUMBER to this card. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"board_id".into(),
|
||||
ToolParam {
|
||||
name: "board_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"column_id".into(),
|
||||
ToolParam {
|
||||
name: "column_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Column UUID. Optional — defaults to first column.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"title".into(),
|
||||
ToolParam {
|
||||
name: "title".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card title (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"priority".into(),
|
||||
ToolParam {
|
||||
name: "priority".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card priority (e.g. 'low', 'medium', 'high'). Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"assignee_id".into(),
|
||||
ToolParam {
|
||||
name: "assignee_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card assignee user UUID. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"issue_id".into(),
|
||||
ToolParam {
|
||||
name: "issue_id".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Link a project issue NUMBER to this card. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_board_card")
|
||||
.description(
|
||||
"Create a card on a Kanban board. If column_id is not provided, \
|
||||
@ -675,46 +741,96 @@ pub fn create_card_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn update_card_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("card_id".into(), ToolParam {
|
||||
name: "card_id".into(), param_type: "string".into(),
|
||||
description: Some("Card UUID (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("title".into(), ToolParam {
|
||||
name: "title".into(), param_type: "string".into(),
|
||||
description: Some("New card title. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("New card description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("column_id".into(), ToolParam {
|
||||
name: "column_id".into(), param_type: "string".into(),
|
||||
description: Some("Move card to a different column. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("position".into(), ToolParam {
|
||||
name: "position".into(), param_type: "integer".into(),
|
||||
description: Some("New position within column. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("priority".into(), ToolParam {
|
||||
name: "priority".into(), param_type: "string".into(),
|
||||
description: Some("New priority. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("assignee_id".into(), ToolParam {
|
||||
name: "assignee_id".into(), param_type: "string".into(),
|
||||
description: Some("New assignee UUID. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("issue_id".into(), ToolParam {
|
||||
name: "issue_id".into(), param_type: "integer".into(),
|
||||
description: Some("Link to a project issue number. Set to 0 to unlink. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"card_id".into(),
|
||||
ToolParam {
|
||||
name: "card_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card UUID (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"title".into(),
|
||||
ToolParam {
|
||||
name: "title".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New card title. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New card description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"column_id".into(),
|
||||
ToolParam {
|
||||
name: "column_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Move card to a different column. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"position".into(),
|
||||
ToolParam {
|
||||
name: "position".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("New position within column. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"priority".into(),
|
||||
ToolParam {
|
||||
name: "priority".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New priority. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"assignee_id".into(),
|
||||
ToolParam {
|
||||
name: "assignee_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New assignee UUID. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"issue_id".into(),
|
||||
ToolParam {
|
||||
name: "issue_id".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some(
|
||||
"Link to a project issue number. Set to 0 to unlink. Optional.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_update_board_card")
|
||||
.description(
|
||||
"Update a board card (title, description, column, position, assignee, priority). \
|
||||
@ -729,11 +845,17 @@ pub fn update_card_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn delete_card_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("card_id".into(), ToolParam {
|
||||
name: "card_id".into(), param_type: "string".into(),
|
||||
description: Some("Card UUID (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"card_id".into(),
|
||||
ToolParam {
|
||||
name: "card_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Card UUID (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_delete_board_card")
|
||||
.description("Delete a board card. Requires admin or owner role.")
|
||||
.parameters(ToolSchema {
|
||||
@ -750,20 +872,28 @@ pub async fn create_board_column_exec(
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ToolError> {
|
||||
let project_id = ctx.project_id();
|
||||
let sender_id = ctx.sender_id().ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let sender_id = ctx
|
||||
.sender_id()
|
||||
.ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let db = ctx.db();
|
||||
|
||||
require_admin(db, project_id, sender_id).await?;
|
||||
|
||||
let board_id = args.get("board_id")
|
||||
let board_id = args
|
||||
.get("board_id")
|
||||
.and_then(|v| Uuid::parse_str(v.as_str()?).ok())
|
||||
.ok_or_else(|| ToolError::ExecutionError("board_id is required".into()))?;
|
||||
|
||||
let name = args.get("name").and_then(|v| v.as_str())
|
||||
let name = args
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::ExecutionError("name is required".into()))?
|
||||
.to_string();
|
||||
|
||||
let color = args.get("color").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let color = args
|
||||
.get("color")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let board = ProjectBoard::find_by_id(board_id)
|
||||
.one(db)
|
||||
@ -771,7 +901,9 @@ pub async fn create_board_column_exec(
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
||||
.ok_or_else(|| ToolError::ExecutionError("Board not found".into()))?;
|
||||
if board.project != project_id {
|
||||
return Err(ToolError::ExecutionError("Board does not belong to this project".into()));
|
||||
return Err(ToolError::ExecutionError(
|
||||
"Board does not belong to this project".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let max_pos: Option<Option<i32>> = ProjectBoardColumn::find()
|
||||
@ -794,7 +926,8 @@ pub async fn create_board_column_exec(
|
||||
color: Set(color.clone()),
|
||||
};
|
||||
|
||||
let model = active.insert(db)
|
||||
let model = active
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
|
||||
@ -810,21 +943,39 @@ pub async fn create_board_column_exec(
|
||||
|
||||
pub fn create_column_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("board_id".into(), ToolParam {
|
||||
name: "board_id".into(), param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Column name (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("color".into(), ToolParam {
|
||||
name: "color".into(), param_type: "string".into(),
|
||||
description: Some("Column color (e.g. '#ff0000'). Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"board_id".into(),
|
||||
ToolParam {
|
||||
name: "board_id".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Board UUID (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Column name (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"color".into(),
|
||||
ToolParam {
|
||||
name: "color".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Column color (e.g. '#ff0000'). Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_board_column")
|
||||
.description(
|
||||
"Create a new column on a Kanban board. \
|
||||
|
||||
@ -14,8 +14,12 @@ const MAX_BODY_BYTES: usize = 1 << 20;
|
||||
|
||||
/// Headers that are blocked from user-supplied values to prevent injection attacks.
|
||||
const BLOCKED_HEADERS: &[&str] = &[
|
||||
"host", "authorization", "cookie", "proxy-authorization",
|
||||
"proxy-connection", "proxy-authenticate",
|
||||
"host",
|
||||
"authorization",
|
||||
"cookie",
|
||||
"proxy-authorization",
|
||||
"proxy-connection",
|
||||
"proxy-authenticate",
|
||||
];
|
||||
|
||||
/// Shared reqwest::Client for connection pooling.
|
||||
@ -111,9 +115,10 @@ pub async fn curl_exec(
|
||||
// Block sensitive headers that could be used for injection attacks
|
||||
for (key, _) in &headers {
|
||||
if BLOCKED_HEADERS.contains(&key.to_lowercase().as_str()) {
|
||||
return Err(ToolError::ExecutionError(
|
||||
format!("Header '{}' is not allowed for security reasons", key),
|
||||
));
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Header '{}' is not allowed for security reasons",
|
||||
key
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +150,7 @@ pub async fn curl_exec(
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Unsupported HTTP method: {}. Use GET, POST, PUT, DELETE, PATCH, or HEAD.",
|
||||
method
|
||||
)))
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
@ -156,7 +161,11 @@ pub async fn curl_exec(
|
||||
}
|
||||
|
||||
// Set default Content-Type for POST/PUT/PATCH if not provided and body exists
|
||||
if body.is_some() && !headers.iter().any(|(k, _)| k.to_lowercase() == "content-type") {
|
||||
if body.is_some()
|
||||
&& !headers
|
||||
.iter()
|
||||
.any(|(k, _)| k.to_lowercase() == "content-type")
|
||||
{
|
||||
request = request.header("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
@ -175,22 +184,29 @@ pub async fn curl_exec(
|
||||
if status >= 300 && status < 400 {
|
||||
redirect_count += 1;
|
||||
if redirect_count > MAX_REDIRECTS {
|
||||
return Err(ToolError::ExecutionError(
|
||||
format!("Too many redirects (max {})", MAX_REDIRECTS),
|
||||
));
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Too many redirects (max {})",
|
||||
MAX_REDIRECTS
|
||||
)));
|
||||
}
|
||||
let location = response.headers()
|
||||
let location = response
|
||||
.headers()
|
||||
.get("location")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
let location = match location {
|
||||
Some(l) => l,
|
||||
None => return Err(ToolError::ExecutionError("Redirect with no Location header".into())),
|
||||
None => {
|
||||
return Err(ToolError::ExecutionError(
|
||||
"Redirect with no Location header".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
// Resolve relative redirect against current URL
|
||||
let base = reqwest::Url::parse(¤t_url)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Invalid current URL: {}", e)))?;
|
||||
let next_url = base.join(&location)
|
||||
let next_url = base
|
||||
.join(&location)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Invalid redirect URL: {}", e)))?;
|
||||
// Validate redirect target against SSRF rules
|
||||
if let Some(host) = next_url.host_str() {
|
||||
@ -209,12 +225,7 @@ pub async fn curl_exec(
|
||||
let response_headers: std::collections::HashMap<String, String> = response
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k.to_string(),
|
||||
v.to_str().unwrap_or("<binary>").to_string(),
|
||||
)
|
||||
})
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("<binary>").to_string()))
|
||||
.collect();
|
||||
|
||||
let content_type = response
|
||||
@ -229,10 +240,9 @@ pub async fn curl_exec(
|
||||
|| content_type.contains("xml")
|
||||
|| content_type.contains("javascript");
|
||||
|
||||
let body_bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to read response body: {}", e)))?;
|
||||
let body_bytes = response.bytes().await.map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to read response body: {}", e))
|
||||
})?;
|
||||
|
||||
let body_len = body_bytes.len();
|
||||
let truncated = body_len > MAX_BODY_BYTES;
|
||||
@ -264,31 +274,64 @@ pub async fn curl_exec(
|
||||
|
||||
pub fn tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("url".into(), ToolParam {
|
||||
name: "url".into(), param_type: "string".into(),
|
||||
description: Some("Full URL to request (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("method".into(), ToolParam {
|
||||
name: "method".into(), param_type: "string".into(),
|
||||
description: Some("HTTP method: GET (default), POST, PUT, DELETE, PATCH, HEAD.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("body".into(), ToolParam {
|
||||
name: "body".into(), param_type: "string".into(),
|
||||
description: Some("Request body. Defaults to 'application/json' Content-Type if provided. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("headers".into(), ToolParam {
|
||||
name: "headers".into(), param_type: "object".into(),
|
||||
description: Some("HTTP headers as key-value pairs. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("timeout".into(), ToolParam {
|
||||
name: "timeout".into(), param_type: "integer".into(),
|
||||
description: Some("Request timeout in seconds (default 30, max 120). Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"url".into(),
|
||||
ToolParam {
|
||||
name: "url".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Full URL to request (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"method".into(),
|
||||
ToolParam {
|
||||
name: "method".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("HTTP method: GET (default), POST, PUT, DELETE, PATCH, HEAD.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"body".into(),
|
||||
ToolParam {
|
||||
name: "body".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Request body. Defaults to 'application/json' Content-Type if provided. Optional."
|
||||
.into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"headers".into(),
|
||||
ToolParam {
|
||||
name: "headers".into(),
|
||||
param_type: "object".into(),
|
||||
description: Some("HTTP headers as key-value pairs. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"timeout".into(),
|
||||
ToolParam {
|
||||
name: "timeout".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Request timeout in seconds (default 30, max 120). Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_curl")
|
||||
.description(
|
||||
"Perform an HTTP request to any URL. Supports GET, POST, PUT, DELETE, PATCH, HEAD. \
|
||||
|
||||
@ -2,9 +2,11 @@
|
||||
|
||||
use agent::{ToolContext, ToolDefinition, ToolError, ToolParam, ToolSchema};
|
||||
use chrono::Utc;
|
||||
use models::issues::{issue, issue_assignee, issue_label, Issue, IssueAssignee, IssueLabel, IssueState};
|
||||
use models::projects::{MemberRole, ProjectMember};
|
||||
use models::issues::{
|
||||
Issue, IssueAssignee, IssueLabel, IssueState, issue, issue_assignee, issue_label,
|
||||
};
|
||||
use models::projects::project_members;
|
||||
use models::projects::{MemberRole, ProjectMember};
|
||||
use models::system::{Label, label};
|
||||
use models::users::User;
|
||||
use sea_orm::*;
|
||||
@ -232,7 +234,9 @@ pub async fn create_issue_exec(
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
if member.is_none() {
|
||||
return Err(ToolError::ExecutionError("You are not a member of this project".into()));
|
||||
return Err(ToolError::ExecutionError(
|
||||
"You are not a member of this project".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let number = next_issue_number(db, project_id).await?;
|
||||
@ -452,11 +456,17 @@ pub async fn update_issue_exec(
|
||||
|
||||
pub fn list_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("state".into(), ToolParam {
|
||||
name: "state".into(), param_type: "string".into(),
|
||||
description: Some("Filter by issue state: 'open' or 'closed'. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"state".into(),
|
||||
ToolParam {
|
||||
name: "state".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Filter by issue state: 'open' or 'closed'. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_list_issues")
|
||||
.description(
|
||||
"List all issues in the current project. \
|
||||
@ -471,39 +481,75 @@ pub fn list_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn create_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("title".into(), ToolParam {
|
||||
name: "title".into(), param_type: "string".into(),
|
||||
description: Some("Issue title (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("body".into(), ToolParam {
|
||||
name: "body".into(), param_type: "string".into(),
|
||||
description: Some("Issue body / description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("milestone".into(), ToolParam {
|
||||
name: "milestone".into(), param_type: "string".into(),
|
||||
description: Some("Milestone name. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("assignee_ids".into(), ToolParam {
|
||||
name: "assignee_ids".into(), param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to assign. Optional.".into()),
|
||||
required: false, properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(), param_type: "string".into(),
|
||||
description: None, required: false, properties: None, items: None,
|
||||
})),
|
||||
});
|
||||
p.insert("label_ids".into(), ToolParam {
|
||||
name: "label_ids".into(), param_type: "array".into(),
|
||||
description: Some("Array of label IDs to apply. Optional.".into()),
|
||||
required: false, properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(), param_type: "integer".into(),
|
||||
description: None, required: false, properties: None, items: None,
|
||||
})),
|
||||
});
|
||||
p.insert(
|
||||
"title".into(),
|
||||
ToolParam {
|
||||
name: "title".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Issue title (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"body".into(),
|
||||
ToolParam {
|
||||
name: "body".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Issue body / description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"milestone".into(),
|
||||
ToolParam {
|
||||
name: "milestone".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Milestone name. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"assignee_ids".into(),
|
||||
ToolParam {
|
||||
name: "assignee_ids".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to assign. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "string".into(),
|
||||
description: None,
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"label_ids".into(),
|
||||
ToolParam {
|
||||
name: "label_ids".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Array of label IDs to apply. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "integer".into(),
|
||||
description: None,
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_issue")
|
||||
.description(
|
||||
"Create a new issue in the current project. \
|
||||
@ -518,31 +564,61 @@ pub fn create_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn update_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("number".into(), ToolParam {
|
||||
name: "number".into(), param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("title".into(), ToolParam {
|
||||
name: "title".into(), param_type: "string".into(),
|
||||
description: Some("New issue title. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("body".into(), ToolParam {
|
||||
name: "body".into(), param_type: "string".into(),
|
||||
description: Some("New issue body. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("state".into(), ToolParam {
|
||||
name: "state".into(), param_type: "string".into(),
|
||||
description: Some("New issue state: 'open' or 'closed'. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("milestone".into(), ToolParam {
|
||||
name: "milestone".into(), param_type: "string".into(),
|
||||
description: Some("New milestone name. Set to null to remove. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"number".into(),
|
||||
ToolParam {
|
||||
name: "number".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"title".into(),
|
||||
ToolParam {
|
||||
name: "title".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New issue title. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"body".into(),
|
||||
ToolParam {
|
||||
name: "body".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New issue body. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"state".into(),
|
||||
ToolParam {
|
||||
name: "state".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New issue state: 'open' or 'closed'. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"milestone".into(),
|
||||
ToolParam {
|
||||
name: "milestone".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New milestone name. Set to null to remove. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_update_issue")
|
||||
.description(
|
||||
"Update an existing issue in the current project by its number. \
|
||||
@ -564,10 +640,14 @@ pub async fn assign_issue_exec(
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ToolError> {
|
||||
let project_id = ctx.project_id();
|
||||
let sender_id = ctx.sender_id().ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let sender_id = ctx
|
||||
.sender_id()
|
||||
.ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let db = ctx.db();
|
||||
|
||||
let number = args.get("number").and_then(|v| v.as_i64())
|
||||
let number = args
|
||||
.get("number")
|
||||
.and_then(|v| v.as_i64())
|
||||
.ok_or_else(|| ToolError::ExecutionError("number is required".into()))?;
|
||||
|
||||
let issue = Issue::find()
|
||||
@ -580,14 +660,24 @@ pub async fn assign_issue_exec(
|
||||
|
||||
require_issue_modifier(db, project_id, sender_id, issue.author).await?;
|
||||
|
||||
let add_ids: Vec<Uuid> = args.get("add_user_ids")
|
||||
let add_ids: Vec<Uuid> = args
|
||||
.get("add_user_ids")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|v| Uuid::parse_str(v.as_str()?).ok()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| Uuid::parse_str(v.as_str()?).ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let remove_ids: Vec<Uuid> = args.get("remove_user_ids")
|
||||
let remove_ids: Vec<Uuid> = args
|
||||
.get("remove_user_ids")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|a| a.iter().filter_map(|v| Uuid::parse_str(v.as_str()?).ok()).collect())
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(|v| Uuid::parse_str(v.as_str()?).ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let now = Utc::now();
|
||||
@ -608,7 +698,9 @@ pub async fn assign_issue_exec(
|
||||
assigned_at: Set(now),
|
||||
..Default::default()
|
||||
};
|
||||
am.insert(db).await.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
am.insert(db)
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
}
|
||||
|
||||
for uid in &remove_ids {
|
||||
@ -653,29 +745,53 @@ pub async fn assign_issue_exec(
|
||||
|
||||
pub fn assign_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("number".into(), ToolParam {
|
||||
name: "number".into(), param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("add_user_ids".into(), ToolParam {
|
||||
name: "add_user_ids".into(), param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to add as assignees. Optional.".into()),
|
||||
required: false, properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(), param_type: "string".into(),
|
||||
description: Some("User UUID".into()), required: false, properties: None, items: None,
|
||||
})),
|
||||
});
|
||||
p.insert("remove_user_ids".into(), ToolParam {
|
||||
name: "remove_user_ids".into(), param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to remove from assignees. Optional.".into()),
|
||||
required: false, properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(), param_type: "string".into(),
|
||||
description: Some("User UUID".into()), required: false, properties: None, items: None,
|
||||
})),
|
||||
});
|
||||
p.insert(
|
||||
"number".into(),
|
||||
ToolParam {
|
||||
name: "number".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"add_user_ids".into(),
|
||||
ToolParam {
|
||||
name: "add_user_ids".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to add as assignees. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("User UUID".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"remove_user_ids".into(),
|
||||
ToolParam {
|
||||
name: "remove_user_ids".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Array of user UUIDs to remove from assignees. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("User UUID".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_assign_issue")
|
||||
.description(
|
||||
"Add or remove assignees on an issue by its number. \
|
||||
@ -696,13 +812,19 @@ pub async fn add_comment_exec(
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, ToolError> {
|
||||
let project_id = ctx.project_id();
|
||||
let sender_id = ctx.sender_id().ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let sender_id = ctx
|
||||
.sender_id()
|
||||
.ok_or_else(|| ToolError::ExecutionError("No sender".into()))?;
|
||||
let db = ctx.db();
|
||||
|
||||
let number = args.get("number").and_then(|v| v.as_i64())
|
||||
let number = args
|
||||
.get("number")
|
||||
.and_then(|v| v.as_i64())
|
||||
.ok_or_else(|| ToolError::ExecutionError("number is required".into()))?;
|
||||
|
||||
let body = args.get("body").and_then(|v| v.as_str())
|
||||
let body = args
|
||||
.get("body")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| ToolError::ExecutionError("body is required".into()))?
|
||||
.to_string();
|
||||
|
||||
@ -722,7 +844,9 @@ pub async fn add_comment_exec(
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
if member.is_none() {
|
||||
return Err(ToolError::ExecutionError("You are not a member of this project".into()));
|
||||
return Err(ToolError::ExecutionError(
|
||||
"You are not a member of this project".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
@ -734,16 +858,23 @@ pub async fn add_comment_exec(
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
let model = comment.insert(db).await
|
||||
let model = comment
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
|
||||
// Update issue updated_at
|
||||
let mut i_active: issue::ActiveModel = issue.into();
|
||||
i_active.updated_at = Set(now);
|
||||
i_active.update(db).await.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
i_active
|
||||
.update(db)
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
|
||||
// Look up author name
|
||||
let author_name = User::find_by_id(sender_id).one(db).await
|
||||
let author_name = User::find_by_id(sender_id)
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?
|
||||
.map(|u| u.display_name.unwrap_or(u.username));
|
||||
|
||||
@ -759,16 +890,28 @@ pub async fn add_comment_exec(
|
||||
|
||||
pub fn add_comment_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("number".into(), ToolParam {
|
||||
name: "number".into(), param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("body".into(), ToolParam {
|
||||
name: "body".into(), param_type: "string".into(),
|
||||
description: Some("Comment body text (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"number".into(),
|
||||
ToolParam {
|
||||
name: "number".into(),
|
||||
param_type: "integer".into(),
|
||||
description: Some("Issue number (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"body".into(),
|
||||
ToolParam {
|
||||
name: "body".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Comment body text (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_add_comment")
|
||||
.description(
|
||||
"Add a comment to an issue in the current project by its number. \
|
||||
@ -797,22 +940,24 @@ pub async fn list_labels_exec(
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
|
||||
let result: Vec<serde_json::Value> = labels.into_iter().map(|l| {
|
||||
serde_json::json!({
|
||||
"id": l.id,
|
||||
"name": l.name,
|
||||
"color": l.color,
|
||||
let result: Vec<serde_json::Value> = labels
|
||||
.into_iter()
|
||||
.map(|l| {
|
||||
serde_json::json!({
|
||||
"id": l.id,
|
||||
"name": l.name,
|
||||
"color": l.color,
|
||||
})
|
||||
})
|
||||
}).collect();
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::to_value(result).map_err(|e| ToolError::ExecutionError(e.to_string()))?)
|
||||
}
|
||||
|
||||
pub fn list_labels_tool_definition() -> ToolDefinition {
|
||||
ToolDefinition::new("project_list_labels")
|
||||
.description(
|
||||
"List all labels available in the current project. \
|
||||
ToolDefinition::new("project_list_labels").description(
|
||||
"List all labels available in the current project. \
|
||||
Returns label id, name, color, and description. \
|
||||
Use label IDs when creating or updating issues.",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -25,8 +25,7 @@ pub async fn list_members_exec(
|
||||
.await
|
||||
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
|
||||
|
||||
let user_map: std::collections::HashMap<_, _> =
|
||||
users.into_iter().map(|u| (u.uid, u)).collect();
|
||||
let user_map: std::collections::HashMap<_, _> = users.into_iter().map(|u| (u.uid, u)).collect();
|
||||
|
||||
let result: Vec<_> = members
|
||||
.into_iter()
|
||||
|
||||
@ -17,20 +17,18 @@ use agent::{ToolHandler, ToolRegistry};
|
||||
|
||||
pub use arxiv::arxiv_search_exec;
|
||||
pub use boards::{
|
||||
create_board_card_exec, create_board_exec, create_board_column_exec,
|
||||
delete_board_card_exec, list_boards_exec,
|
||||
update_board_card_exec, update_board_exec,
|
||||
create_board_card_exec, create_board_column_exec, create_board_exec, delete_board_card_exec,
|
||||
list_boards_exec, update_board_card_exec, update_board_exec,
|
||||
};
|
||||
pub use curl::curl_exec;
|
||||
pub use issues::{
|
||||
add_comment_exec, assign_issue_exec, create_issue_exec, list_issues_exec,
|
||||
list_labels_exec, update_issue_exec,
|
||||
add_comment_exec, assign_issue_exec, create_issue_exec, list_issues_exec, list_labels_exec,
|
||||
update_issue_exec,
|
||||
};
|
||||
pub use members::list_members_exec;
|
||||
pub use repos::{create_commit_exec, create_repo_exec, list_repos_exec, update_repo_exec};
|
||||
|
||||
pub fn register_all(registry: &mut ToolRegistry) {
|
||||
|
||||
registry.register(
|
||||
arxiv::tool_definition(),
|
||||
ToolHandler::new(|ctx, args| Box::pin(arxiv_search_exec(ctx, args))),
|
||||
|
||||
@ -5,8 +5,8 @@ use chrono::Utc;
|
||||
use git::commit::types::CommitOid;
|
||||
use git::commit::types::CommitSignature;
|
||||
use git2;
|
||||
use models::projects::{MemberRole, ProjectMember};
|
||||
use models::projects::project_members;
|
||||
use models::projects::{MemberRole, ProjectMember};
|
||||
use models::repos::repo;
|
||||
use models::users::user_email;
|
||||
use sea_orm::*;
|
||||
@ -96,9 +96,15 @@ pub async fn create_repo_exec(
|
||||
.to_string();
|
||||
|
||||
// Validate repo name: no path traversal, no special chars
|
||||
if repo_name.contains("..") || repo_name.contains('/') || repo_name.contains('\\')
|
||||
|| repo_name.is_empty() || repo_name.len() > 100
|
||||
|| !repo_name.chars().next().map_or(false, |c| c.is_alphanumeric())
|
||||
if repo_name.contains("..")
|
||||
|| repo_name.contains('/')
|
||||
|| repo_name.contains('\\')
|
||||
|| repo_name.is_empty()
|
||||
|| repo_name.len() > 100
|
||||
|| !repo_name
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(false, |c| c.is_alphanumeric())
|
||||
{
|
||||
return Err(ToolError::ExecutionError(
|
||||
"Invalid repository name: must start with alphanumeric, contain no path separators or '..', max 100 chars".into(),
|
||||
@ -176,7 +182,10 @@ pub async fn create_repo_exec(
|
||||
let repo_name = model.repo_name.clone();
|
||||
let repo_desc = model.description.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = es.embed_repo(&repo_id, &repo_name, repo_desc.as_deref()).await {
|
||||
if let Err(e) = es
|
||||
.embed_repo(&repo_id, &repo_name, repo_desc.as_deref())
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, repo_id = %repo_id, "failed to embed repo");
|
||||
}
|
||||
});
|
||||
@ -276,7 +285,10 @@ pub async fn update_repo_exec(
|
||||
let repo_name = model.repo_name.clone();
|
||||
let repo_desc = model.description.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = es.embed_repo(&repo_id, &repo_name, repo_desc.as_deref()).await {
|
||||
if let Err(e) = es
|
||||
.embed_repo(&repo_id, &repo_name, repo_desc.as_deref())
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, repo_id = %repo_id, "failed to re-embed repo on update");
|
||||
}
|
||||
});
|
||||
@ -345,11 +357,15 @@ pub async fn create_commit_exec(
|
||||
.to_string();
|
||||
|
||||
// Validate branch: no path traversal, no backslashes, not empty, no lock files
|
||||
if branch.contains("..") || branch.contains('\\') || branch.is_empty()
|
||||
|| branch.ends_with(".lock") || branch.starts_with('-')
|
||||
if branch.contains("..")
|
||||
|| branch.contains('\\')
|
||||
|| branch.is_empty()
|
||||
|| branch.ends_with(".lock")
|
||||
|| branch.starts_with('-')
|
||||
{
|
||||
return Err(ToolError::ExecutionError(
|
||||
"Invalid branch name: must not contain '..' or backslashes, and must not be empty".into(),
|
||||
"Invalid branch name: must not contain '..' or backslashes, and must not be empty"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
@ -415,9 +431,10 @@ pub async fn create_commit_exec(
|
||||
let parent_oid = repo.refname_to_id(&format!("refs/heads/{}", branch)).ok();
|
||||
|
||||
if has_head && parent_oid.is_none() {
|
||||
return Err(ToolError::ExecutionError(
|
||||
format!("Branch '{}' does not exist in this repository", branch),
|
||||
));
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Branch '{}' does not exist in this repository",
|
||||
branch
|
||||
)));
|
||||
}
|
||||
|
||||
let parent_ids: Vec<CommitOid> = parent_oid
|
||||
@ -434,12 +451,15 @@ pub async fn create_commit_exec(
|
||||
// If repo has a parent commit, read its tree into the index so we don't
|
||||
// lose existing files when write_tree() is called.
|
||||
if let Some(oid) = &parent_oid {
|
||||
let parent_commit = repo.find_commit(*oid)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to find parent commit: {}", e)))?;
|
||||
let parent_tree = parent_commit.tree()
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to get parent tree: {}", e)))?;
|
||||
index.read_tree(&parent_tree)
|
||||
.map_err(|e| ToolError::ExecutionError(format!("Failed to read parent tree into index: {}", e)))?;
|
||||
let parent_commit = repo.find_commit(*oid).map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to find parent commit: {}", e))
|
||||
})?;
|
||||
let parent_tree = parent_commit.tree().map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to get parent tree: {}", e))
|
||||
})?;
|
||||
index.read_tree(&parent_tree).map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to read parent tree into index: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
for file in files_data {
|
||||
@ -449,12 +469,17 @@ pub async fn create_commit_exec(
|
||||
.ok_or_else(|| ToolError::ExecutionError("Each file must have a 'path'".into()))?;
|
||||
|
||||
// Validate path: no traversal, no absolute paths, no .git/ prefix
|
||||
if path.contains("..") || path.starts_with('/') || path.starts_with('\\')
|
||||
|| path.is_empty() || path.starts_with(".git/") || path == ".git"
|
||||
if path.contains("..")
|
||||
|| path.starts_with('/')
|
||||
|| path.starts_with('\\')
|
||||
|| path.is_empty()
|
||||
|| path.starts_with(".git/")
|
||||
|| path == ".git"
|
||||
{
|
||||
return Err(ToolError::ExecutionError(
|
||||
format!("Invalid file path '{}': must be relative, no '..' or absolute path components", path)
|
||||
));
|
||||
return Err(ToolError::ExecutionError(format!(
|
||||
"Invalid file path '{}': must be relative, no '..' or absolute path components",
|
||||
path
|
||||
)));
|
||||
}
|
||||
let content = file
|
||||
.get("content")
|
||||
@ -477,9 +502,11 @@ pub async fn create_commit_exec(
|
||||
flags_extended: 0,
|
||||
path: path.as_bytes().to_vec(),
|
||||
};
|
||||
index.add_frombuffer(&mut entry, content.as_bytes()).map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to add '{}' to index: {}", path, e))
|
||||
})?;
|
||||
index
|
||||
.add_frombuffer(&mut entry, content.as_bytes())
|
||||
.map_err(|e| {
|
||||
ToolError::ExecutionError(format!("Failed to add '{}' to index: {}", path, e))
|
||||
})?;
|
||||
}
|
||||
|
||||
let tree_oid = index
|
||||
@ -546,21 +573,41 @@ pub fn list_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn create_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name (required). Must be unique within the project.".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("Repository description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("is_private".into(), ToolParam {
|
||||
name: "is_private".into(), param_type: "boolean".into(),
|
||||
description: Some("Whether the repo is private. Defaults to false. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Repository name (required). Must be unique within the project.".into(),
|
||||
),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"is_private".into(),
|
||||
ToolParam {
|
||||
name: "is_private".into(),
|
||||
param_type: "boolean".into(),
|
||||
description: Some("Whether the repo is private. Defaults to false. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_repo")
|
||||
.description(
|
||||
"Create a new repository in the current project. \
|
||||
@ -576,26 +623,50 @@ pub fn create_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn update_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("description".into(), ToolParam {
|
||||
name: "description".into(), param_type: "string".into(),
|
||||
description: Some("New repository description. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("is_private".into(), ToolParam {
|
||||
name: "is_private".into(), param_type: "boolean".into(),
|
||||
description: Some("New privacy setting. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("default_branch".into(), ToolParam {
|
||||
name: "default_branch".into(), param_type: "string".into(),
|
||||
description: Some("New default branch name. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"description".into(),
|
||||
ToolParam {
|
||||
name: "description".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New repository description. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"is_private".into(),
|
||||
ToolParam {
|
||||
name: "is_private".into(),
|
||||
param_type: "boolean".into(),
|
||||
description: Some("New privacy setting. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"default_branch".into(),
|
||||
ToolParam {
|
||||
name: "default_branch".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("New default branch name. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_update_repo")
|
||||
.description(
|
||||
"Update a repository's description, privacy, or default branch. \
|
||||
@ -610,47 +681,94 @@ pub fn update_tool_definition() -> ToolDefinition {
|
||||
|
||||
pub fn create_commit_tool_definition() -> ToolDefinition {
|
||||
let mut p = HashMap::new();
|
||||
p.insert("repo_name".into(), ToolParam {
|
||||
name: "repo_name".into(), param_type: "string".into(),
|
||||
description: Some("Repository name. Can also use 'name' as alias. Required.".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("name".into(), ToolParam {
|
||||
name: "name".into(), param_type: "string".into(),
|
||||
description: Some("Alias for repo_name. Use the same value as returned by project_list_repos.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("branch".into(), ToolParam {
|
||||
name: "branch".into(), param_type: "string".into(),
|
||||
description: Some("Branch to commit to. Defaults to 'main'. Optional.".into()),
|
||||
required: false, properties: None, items: None,
|
||||
});
|
||||
p.insert("message".into(), ToolParam {
|
||||
name: "message".into(), param_type: "string".into(),
|
||||
description: Some("Commit message (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert(
|
||||
"repo_name".into(),
|
||||
ToolParam {
|
||||
name: "repo_name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Repository name. Can also use 'name' as alias. Required.".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"name".into(),
|
||||
ToolParam {
|
||||
name: "name".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some(
|
||||
"Alias for repo_name. Use the same value as returned by project_list_repos.".into(),
|
||||
),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"branch".into(),
|
||||
ToolParam {
|
||||
name: "branch".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Branch to commit to. Defaults to 'main'. Optional.".into()),
|
||||
required: false,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"message".into(),
|
||||
ToolParam {
|
||||
name: "message".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Commit message (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
// files items
|
||||
let mut file_item = HashMap::new();
|
||||
file_item.insert("path".into(), ToolParam {
|
||||
name: "path".into(), param_type: "string".into(),
|
||||
description: Some("File path in the repo (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
file_item.insert("content".into(), ToolParam {
|
||||
name: "content".into(), param_type: "string".into(),
|
||||
description: Some("Full file content as string (required).".into()),
|
||||
required: true, properties: None, items: None,
|
||||
});
|
||||
p.insert("files".into(), ToolParam {
|
||||
name: "files".into(), param_type: "array".into(),
|
||||
description: Some("Array of files to commit (required, non-empty).".into()),
|
||||
required: true, properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(), param_type: "object".into(),
|
||||
description: None, required: true, properties: Some(file_item), items: None,
|
||||
})),
|
||||
});
|
||||
file_item.insert(
|
||||
"path".into(),
|
||||
ToolParam {
|
||||
name: "path".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("File path in the repo (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
file_item.insert(
|
||||
"content".into(),
|
||||
ToolParam {
|
||||
name: "content".into(),
|
||||
param_type: "string".into(),
|
||||
description: Some("Full file content as string (required).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: None,
|
||||
},
|
||||
);
|
||||
p.insert(
|
||||
"files".into(),
|
||||
ToolParam {
|
||||
name: "files".into(),
|
||||
param_type: "array".into(),
|
||||
description: Some("Array of files to commit (required, non-empty).".into()),
|
||||
required: true,
|
||||
properties: None,
|
||||
items: Some(Box::new(ToolParam {
|
||||
name: "".into(),
|
||||
param_type: "object".into(),
|
||||
description: None,
|
||||
required: true,
|
||||
properties: Some(file_item),
|
||||
items: None,
|
||||
})),
|
||||
},
|
||||
);
|
||||
ToolDefinition::new("project_create_commit")
|
||||
.description(
|
||||
"Create a new commit in a repository. Commits the given files to the specified branch. \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user