1use async_trait::async_trait;
2use serde_json::{json, Value};
3use tokio::fs;
4
5use crate::types::{AgentTool, AgentToolResult};
6
7pub struct EditTool;
8
9#[async_trait]
10impl AgentTool for EditTool {
11 fn name(&self) -> &str {
12 "edit"
13 }
14 fn requires_permission(&self) -> bool {
15 true
16 }
17 fn description(&self) -> &str {
18 "Replace one occurrence (or all, with replace_all) of old_string with new_string in the file at path."
19 }
20 fn parameters(&self) -> Value {
21 json!({
22 "type": "object",
23 "properties": {
24 "path": {"type": "string"},
25 "old_string": {"type": "string"},
26 "new_string": {"type": "string"},
27 "replace_all": {"type": "boolean", "default": false}
28 },
29 "required": ["path", "old_string", "new_string"]
30 })
31 }
32 async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
33 let path = args
34 .get("path")
35 .and_then(|v| v.as_str())
36 .ok_or("missing 'path'")?;
37 let old_s = args
38 .get("old_string")
39 .and_then(|v| v.as_str())
40 .ok_or("missing 'old_string'")?;
41 let new_s = args
42 .get("new_string")
43 .and_then(|v| v.as_str())
44 .ok_or("missing 'new_string'")?;
45 let replace_all = args
46 .get("replace_all")
47 .and_then(|v| v.as_bool())
48 .unwrap_or(false);
49
50 let text = fs::read_to_string(path)
51 .await
52 .map_err(|e| format!("read {path}: {e}"))?;
53 let count = text.matches(old_s).count();
54 if count == 0 {
55 return Err(format!("old_string not found in {path}"));
56 }
57 if count > 1 && !replace_all {
58 return Err(format!(
59 "old_string occurs {count} times in {path}; pass replace_all=true or expand the match"
60 ));
61 }
62 let updated = if replace_all {
63 text.replace(old_s, new_s)
64 } else {
65 text.replacen(old_s, new_s, 1)
66 };
67 fs::write(path, updated)
68 .await
69 .map_err(|e| format!("write {path}: {e}"))?;
70 let n = if replace_all { count } else { 1 };
71 Ok(AgentToolResult::text(format!(
72 "edited {path}: {n} replacement(s)"
73 )))
74 }
75}