Move git_tools, file_tools, and project_tools from libs/service into a new libs/fctool crate with correct workspace dependencies. Fixes the rev.len() >= 40 bug in all git tool resolve functions (OID check needs exact 40-char hex, not just >= 40). Adds 4 new git blob tools (blob_get, blob_exists, blob_content, blob_create). Fixes parameter naming inconsistency in repos.rs and adds project_name to list_repos output. Removes unused excel/pdf/ppt/word file tools.
274 lines
11 KiB
Rust
274 lines
11 KiB
Rust
//! Git blob tools — raw object-level operations on blob OIDs.
|
|
|
|
use super::ctx::GitToolCtx;
|
|
use agent::{ToolDefinition, ToolHandler, ToolParam, ToolRegistry, ToolSchema};
|
|
use base64::Engine;
|
|
use std::collections::HashMap;
|
|
|
|
async fn git_blob_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 oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
let commit_oid = resolve_oid(&domain, oid)?;
|
|
let info = domain.blob_get(&commit_oid).map_err(|e| e.to_string())?;
|
|
|
|
Ok(serde_json::json!({
|
|
"oid": info.oid.to_string(),
|
|
"size": info.size,
|
|
"is_binary": info.is_binary,
|
|
}))
|
|
}
|
|
|
|
async fn git_blob_exists_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 oid = p.get("oid").and_then(|v| v.as_str()).ok_or("missing oid")?;
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
let commit_oid = resolve_oid(&domain, oid)?;
|
|
let exists = domain.blob_exists(&commit_oid);
|
|
|
|
Ok(serde_json::json!({ "oid": commit_oid.to_string(), "exists": exists }))
|
|
}
|
|
|
|
async fn git_blob_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 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 domain = ctx.open_repo(project_name, repo_name).await?;
|
|
let commit_oid = resolve_oid(&domain, oid)?;
|
|
let blob = domain.blob_content(&commit_oid).map_err(|e| e.to_string())?;
|
|
|
|
if blob.size > max_size {
|
|
return Err(format!(
|
|
"blob too large ({} bytes), max {} bytes. Use a smaller max_size or retrieve the raw OID.",
|
|
blob.size, max_size
|
|
));
|
|
}
|
|
|
|
let (content, is_binary) = if blob.is_binary {
|
|
(base64::engine::general_purpose::STANDARD.encode(&blob.content), true)
|
|
} else {
|
|
(String::from_utf8_lossy(&blob.content).to_string(), false)
|
|
};
|
|
|
|
Ok(serde_json::json!({
|
|
"oid": blob.oid.to_string(),
|
|
"size": blob.size,
|
|
"is_binary": is_binary,
|
|
"content": content,
|
|
}))
|
|
}
|
|
|
|
async fn git_blob_create_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 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)),
|
|
};
|
|
|
|
let domain = ctx.open_repo(project_name, repo_name).await?;
|
|
let oid = domain.blob_create(&data).map_err(|e| e.to_string())?;
|
|
let info = domain.blob_get(&oid).map_err(|e| e.to_string())?;
|
|
|
|
Ok(serde_json::json!({
|
|
"oid": info.oid.to_string(),
|
|
"size": info.size,
|
|
"is_binary": info.is_binary,
|
|
}))
|
|
}
|
|
|
|
fn resolve_oid(
|
|
domain: &git::GitDomain,
|
|
rev: &str,
|
|
) -> Result<git::commit::types::CommitOid, String> {
|
|
if rev.len() == 40 && rev.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
Ok(git::commit::types::CommitOid::new(rev))
|
|
} else {
|
|
domain.commit_get_prefix(rev).map_err(|e| e.to_string()).map(|m| m.oid)
|
|
}
|
|
}
|
|
|
|
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,
|
|
}),
|
|
]);
|
|
let schema = ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
|
};
|
|
registry.register(
|
|
ToolDefinition::new("git_blob_info")
|
|
.description("Get metadata about a git blob by its OID. Returns size and whether the blob is binary.")
|
|
.parameters(schema),
|
|
ToolHandler::new(|ctx, args| {
|
|
let gctx = GitToolCtx::new(ctx);
|
|
Box::pin(async move {
|
|
git_blob_info_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
|
})
|
|
}),
|
|
);
|
|
|
|
// 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,
|
|
}),
|
|
]);
|
|
let schema = ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
|
};
|
|
registry.register(
|
|
ToolDefinition::new("git_blob_exists")
|
|
.description("Check whether a git blob exists in the repository by its OID.")
|
|
.parameters(schema),
|
|
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_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,
|
|
}),
|
|
]);
|
|
let schema = ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["project_name".into(), "repo_name".into(), "oid".into()]),
|
|
};
|
|
registry.register(
|
|
ToolDefinition::new("git_blob_content")
|
|
.description("Retrieve the raw content of a git blob by its OID. Binary content is base64-encoded. Limits to 1MB by default.")
|
|
.parameters(schema),
|
|
ToolHandler::new(|ctx, args| {
|
|
let gctx = GitToolCtx::new(ctx);
|
|
Box::pin(async move {
|
|
git_blob_content_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
|
})
|
|
}),
|
|
);
|
|
|
|
// 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,
|
|
}),
|
|
]);
|
|
let schema = ToolSchema {
|
|
schema_type: "object".into(),
|
|
properties: Some(p),
|
|
required: Some(vec!["project_name".into(), "repo_name".into(), "content".into()]),
|
|
};
|
|
registry.register(
|
|
ToolDefinition::new("git_blob_create")
|
|
.description("Create a new git blob in the repository. Writes the raw content to the object database and returns the new blob OID. Supports both utf-8 text and base64-encoded binary content.")
|
|
.parameters(schema),
|
|
ToolHandler::new(|ctx, args| {
|
|
let gctx = GitToolCtx::new(ctx);
|
|
Box::pin(async move {
|
|
git_blob_create_exec(gctx, args).await.map_err(agent::ToolError::ExecutionError)
|
|
})
|
|
}),
|
|
);
|
|
}
|