matrixcode_core/tools/
edit.rs1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6use crate::approval::RiskLevel;
7
8const 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 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 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}