consciousness/poc-agent/src/tools/edit.rs

93 lines
3.2 KiB
Rust
Raw Normal View History

// tools/edit.rs — Search-and-replace file editing
//
// The edit tool performs exact string replacement in files. This is the
// same pattern used by Claude Code and aider — it's more reliable than
// line-number-based editing because the model specifies what it sees,
// not where it thinks it is.
//
// Supports replace_all for bulk renaming (e.g. variable renames).
use anyhow::{Context, Result};
use serde_json::json;
use crate::types::ToolDef;
pub fn definition() -> ToolDef {
ToolDef::new(
"edit_file",
"Perform exact string replacement in a file. The old_string must appear \
exactly once in the file (unless replace_all is true). Use read_file first \
to see the current contents.",
json!({
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path to the file to edit"
},
"old_string": {
"type": "string",
"description": "The exact text to find and replace"
},
"new_string": {
"type": "string",
"description": "The replacement text"
},
"replace_all": {
"type": "boolean",
"description": "Replace all occurrences (default false)"
}
},
"required": ["file_path", "old_string", "new_string"]
}),
)
}
pub fn edit_file(args: &serde_json::Value) -> Result<String> {
let path = args["file_path"]
.as_str()
.context("file_path is required")?;
let old_string = args["old_string"]
.as_str()
.context("old_string is required")?;
let new_string = args["new_string"]
.as_str()
.context("new_string is required")?;
let replace_all = args["replace_all"].as_bool().unwrap_or(false);
if old_string == new_string {
anyhow::bail!("old_string and new_string are identical");
}
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read {}", path))?;
if replace_all {
let count = content.matches(old_string).count();
if count == 0 {
anyhow::bail!("old_string not found in {}", path);
}
let new_content = content.replace(old_string, new_string);
std::fs::write(path, &new_content)
.with_context(|| format!("Failed to write {}", path))?;
Ok(format!("Replaced {} occurrences in {}", count, path))
} else {
let count = content.matches(old_string).count();
if count == 0 {
anyhow::bail!("old_string not found in {}", path);
}
if count > 1 {
anyhow::bail!(
"old_string appears {} times in {} — use replace_all or provide more context \
to make it unique",
count,
path
);
}
let new_content = content.replacen(old_string, new_string, 1);
std::fs::write(path, &new_content)
.with_context(|| format!("Failed to write {}", path))?;
Ok(format!("Edited {}", path))
}
}