codetether_agent/tool/
edit.rs1use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use similar::{ChangeTag, TextDiff};
8use tokio::fs;
9
10pub struct EditTool;
12
13impl EditTool {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19#[async_trait]
20impl Tool for EditTool {
21 fn id(&self) -> &str {
22 "edit"
23 }
24
25 fn name(&self) -> &str {
26 "Edit File"
27 }
28
29 fn description(&self) -> &str {
30 "Edit a file by replacing an exact string with new content. Include enough context (3+ lines before and after) to uniquely identify the location."
31 }
32
33 fn parameters(&self) -> Value {
34 json!({
35 "type": "object",
36 "properties": {
37 "path": {
38 "type": "string",
39 "description": "The path to the file to edit"
40 },
41 "old_string": {
42 "type": "string",
43 "description": "The exact string to replace (must match exactly, including whitespace)"
44 },
45 "new_string": {
46 "type": "string",
47 "description": "The string to replace old_string with"
48 }
49 },
50 "required": ["path", "old_string", "new_string"]
51 })
52 }
53
54 async fn execute(&self, args: Value) -> Result<ToolResult> {
55 let path = args["path"]
56 .as_str()
57 .ok_or_else(|| anyhow::anyhow!("path is required"))?;
58 let old_string = args["old_string"]
59 .as_str()
60 .ok_or_else(|| anyhow::anyhow!("old_string is required"))?;
61 let new_string = args["new_string"]
62 .as_str()
63 .ok_or_else(|| anyhow::anyhow!("new_string is required"))?;
64
65 let content = fs::read_to_string(path).await?;
67
68 let count = content.matches(old_string).count();
70
71 if count == 0 {
72 return Ok(ToolResult::error(
73 "old_string not found in file. Make sure it matches exactly, including whitespace.",
74 ));
75 }
76
77 if count > 1 {
78 return Ok(ToolResult::error(format!(
79 "old_string found {} times. Include more context to uniquely identify the location.",
80 count
81 )));
82 }
83
84 let new_content = content.replacen(old_string, new_string, 1);
86
87 let diff = TextDiff::from_lines(&content, &new_content);
89 let mut diff_output = String::new();
90
91 for change in diff.iter_all_changes() {
92 let sign = match change.tag() {
93 ChangeTag::Delete => "-",
94 ChangeTag::Insert => "+",
95 ChangeTag::Equal => " ",
96 };
97 diff_output.push_str(&format!("{}{}", sign, change));
98 }
99
100 fs::write(path, &new_content).await?;
102
103 Ok(ToolResult::success(format!("Successfully edited {}\n\n{}", path, diff_output)))
104 }
105}