Skip to main content

codetether_agent/tool/
edit.rs

1//! Edit tool: replace strings in files
2
3use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use similar::{ChangeTag, TextDiff};
8use tokio::fs;
9
10/// Edit files by replacing strings
11pub 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        // Read the file
66        let content = fs::read_to_string(path).await?;
67
68        // Count occurrences
69        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        // Perform the replacement
85        let new_content = content.replacen(old_string, new_string, 1);
86
87        // Generate diff for output
88        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        // Write the file
101        fs::write(path, &new_content).await?;
102
103        Ok(ToolResult::success(format!("Successfully edited {}\n\n{}", path, diff_output)))
104    }
105}