hello world
This commit is contained in:
commit
c99507ca1e
84 changed files with 54252 additions and 0 deletions
152
src-rust/crates/tools/src/file_edit.rs
Normal file
152
src-rust/crates/tools/src/file_edit.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
// FileEdit tool: exact string replacement with old/new strings (like sed but
|
||||
// deterministic). Mirrors the TypeScript Edit tool behaviour.
|
||||
|
||||
use crate::{PermissionLevel, Tool, ToolContext, ToolResult};
|
||||
use async_trait::async_trait;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use tracing::debug;
|
||||
|
||||
pub struct FileEditTool;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FileEditInput {
|
||||
file_path: String,
|
||||
old_string: String,
|
||||
new_string: String,
|
||||
#[serde(default)]
|
||||
replace_all: bool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for FileEditTool {
|
||||
fn name(&self) -> &str {
|
||||
cc_core::constants::TOOL_NAME_FILE_EDIT
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Performs exact string replacements in files. The edit will FAIL if \
|
||||
`old_string` is not unique in the file (unless `replace_all` is true). \
|
||||
You MUST read the file first before editing. Preserve the exact \
|
||||
indentation as it appears in the file."
|
||||
}
|
||||
|
||||
fn permission_level(&self) -> PermissionLevel {
|
||||
PermissionLevel::Write
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "The absolute path to the file to modify"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "The text to replace (must be unique in the file unless replace_all is true)"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "The text to replace it with (must be different from old_string)"
|
||||
},
|
||||
"replace_all": {
|
||||
"type": "boolean",
|
||||
"description": "Replace all occurrences of old_string (default false)"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "old_string", "new_string"]
|
||||
})
|
||||
}
|
||||
|
||||
async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
|
||||
let params: FileEditInput = match serde_json::from_value(input) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
|
||||
};
|
||||
|
||||
// Validate old != new
|
||||
if params.old_string == params.new_string {
|
||||
return ToolResult::error(
|
||||
"old_string and new_string must be different".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let path = ctx.resolve_path(¶ms.file_path);
|
||||
debug!(path = %path.display(), "Editing file");
|
||||
|
||||
// Permission check
|
||||
if let Err(e) = ctx.check_permission(
|
||||
self.name(),
|
||||
&format!("Edit {}", path.display()),
|
||||
false,
|
||||
) {
|
||||
return ToolResult::error(e.to_string());
|
||||
}
|
||||
|
||||
// Read current content
|
||||
let content = match tokio::fs::read_to_string(&path).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return ToolResult::error(format!(
|
||||
"Failed to read file {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Count occurrences
|
||||
let count = content.matches(¶ms.old_string).count();
|
||||
|
||||
if count == 0 {
|
||||
return ToolResult::error(format!(
|
||||
"old_string not found in {}. Make sure the string matches exactly, \
|
||||
including whitespace and indentation.",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
if count > 1 && !params.replace_all {
|
||||
return ToolResult::error(format!(
|
||||
"old_string appears {} times in {}. Either provide a larger string \
|
||||
with more surrounding context to make it unique, or set replace_all \
|
||||
to true to replace every occurrence.",
|
||||
count,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
|
||||
// Perform replacement
|
||||
let new_content = if params.replace_all {
|
||||
content.replace(¶ms.old_string, ¶ms.new_string)
|
||||
} else {
|
||||
// Replace only the first occurrence
|
||||
content.replacen(¶ms.old_string, ¶ms.new_string, 1)
|
||||
};
|
||||
|
||||
// Write back
|
||||
if let Err(e) = tokio::fs::write(&path, &new_content).await {
|
||||
return ToolResult::error(format!(
|
||||
"Failed to write file {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
));
|
||||
}
|
||||
|
||||
// Build a diff snippet for the response
|
||||
let replacements = if params.replace_all { count } else { 1 };
|
||||
let msg = format!(
|
||||
"Successfully edited {} ({} replacement{}).",
|
||||
path.display(),
|
||||
replacements,
|
||||
if replacements != 1 { "s" } else { "" }
|
||||
);
|
||||
|
||||
ToolResult::success(msg).with_metadata(json!({
|
||||
"file_path": path.display().to_string(),
|
||||
"replacements": replacements,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue