Skip to main content

matrixcode_core/tools/
edit.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6use crate::approval::RiskLevel;
7
8/// Maximum file size for safe editing (1MB)
9const MAX_EDIT_FILE_SIZE: u64 = 1_000_000;
10
11pub struct EditTool;
12
13#[async_trait]
14impl Tool for EditTool {
15    fn definition(&self) -> ToolDefinition {
16        ToolDefinition {
17            name: "edit".to_string(),
18            description: "在文件中查找精确匹配的字符串并替换为新内容".to_string(),
19            parameters: json!({
20                "type": "object",
21                "properties": {
22                    "path": {
23                        "type": "string",
24                        "description": "要编辑的文件路径"
25                    },
26                    "old_string": {
27                        "type": "string",
28                        "description": "要查找并替换的原始字符串(必须精确匹配)"
29                    },
30                    "new_string": {
31                        "type": "string",
32                        "description": "替换后的新字符串"
33                    }
34                },
35                "required": ["path", "old_string", "new_string"]
36            }),
37        }
38    }
39
40    async fn execute(&self, params: Value) -> Result<String> {
41        let path = params["path"]
42            .as_str()
43            .ok_or_else(|| anyhow::anyhow!("missing 'path'"))?;
44        let old_string = params["old_string"]
45            .as_str()
46            .ok_or_else(|| anyhow::anyhow!("missing 'old_string'"))?;
47        let new_string = params["new_string"]
48            .as_str()
49            .ok_or_else(|| anyhow::anyhow!("missing 'new_string'"))?;
50
51        // Check file size first
52        let metadata = tokio::fs::metadata(path).await?;
53        let file_size = metadata.len();
54
55        if file_size > MAX_EDIT_FILE_SIZE {
56            return Ok(format!(
57                "⚠️ File is too large ({:.1}MB) for safe editing.\n\
58                 Large file edits may cause memory issues.\n\
59                 Consider using other methods:\n\
60                 - Use `bash` with sed/awk for large files\n\
61                 - Split the file into smaller sections first",
62                file_size as f64 / 1_000_000.0
63            ));
64        }
65
66        let content = tokio::fs::read_to_string(path).await?;
67
68        let count = content.matches(old_string).count();
69        if count == 0 {
70            anyhow::bail!("old_string not found in {}", path);
71        }
72        if count > 1 {
73            anyhow::bail!(
74                "old_string found {} times in {} — must be unique",
75                count,
76                path
77            );
78        }
79
80        let new_content = content.replacen(old_string, new_string, 1);
81        tokio::fs::write(path, &new_content).await?;
82
83        // Return diff-style output
84        let old_lines: Vec<&str> = old_string.lines().collect();
85        let new_lines: Vec<&str> = new_string.lines().collect();
86        let mut diff = format!("Successfully edited {}\n", path);
87        for line in &old_lines {
88            diff.push_str(&format!("- {}\n", line));
89        }
90        for line in &new_lines {
91            diff.push_str(&format!("+ {}\n", line));
92        }
93        Ok(diff)
94    }
95
96    fn risk_level(&self) -> RiskLevel {
97        RiskLevel::Mutating
98    }
99}