claude_rust_tools/infrastructure/
file_edit_tool.rs1use claude_rust_errors::{AppError, AppResult};
2use claude_rust_types::{PermissionLevel, Tool};
3use serde_json::{Value, json};
4
5pub struct FileEditTool;
6
7#[async_trait::async_trait]
8impl Tool for FileEditTool {
9 fn name(&self) -> &str {
10 "file_edit"
11 }
12
13 fn description(&self) -> &str {
14 "Edit a file by replacing an exact string match. The old_string must appear exactly once."
15 }
16
17 fn input_schema(&self) -> Value {
18 json!({
19 "type": "object",
20 "properties": {
21 "file_path": {
22 "type": "string",
23 "description": "Absolute path to the file to edit"
24 },
25 "old_string": {
26 "type": "string",
27 "description": "The exact string to find and replace (must be unique in the file)"
28 },
29 "new_string": {
30 "type": "string",
31 "description": "The replacement string"
32 }
33 },
34 "required": ["file_path", "old_string", "new_string"]
35 })
36 }
37
38 fn permission_level(&self) -> PermissionLevel {
39 PermissionLevel::Dangerous
40 }
41
42 async fn execute(&self, input: Value) -> AppResult<String> {
43 let path = input
44 .get("file_path")
45 .and_then(|v| v.as_str())
46 .ok_or_else(|| AppError::Tool("missing 'file_path' field".into()))?;
47
48 let old_string = input
49 .get("old_string")
50 .and_then(|v| v.as_str())
51 .ok_or_else(|| AppError::Tool("missing 'old_string' field".into()))?;
52
53 let new_string = input
54 .get("new_string")
55 .and_then(|v| v.as_str())
56 .ok_or_else(|| AppError::Tool("missing 'new_string' field".into()))?;
57
58 tracing::info!(path, "editing file");
59
60 let content = tokio::fs::read_to_string(path)
61 .await
62 .map_err(|e| AppError::Tool(format!("cannot read '{path}': {e}")))?;
63
64 let match_count = content.matches(old_string).count();
65 if match_count == 0 {
66 return Err(AppError::Tool(format!(
67 "old_string not found in '{path}'"
68 )));
69 }
70 if match_count > 1 {
71 return Err(AppError::Tool(format!(
72 "old_string found {match_count} times in '{path}' (must be unique)"
73 )));
74 }
75
76 let new_content = content.replacen(old_string, new_string, 1);
77 tokio::fs::write(path, &new_content)
78 .await
79 .map_err(|e| AppError::Tool(format!("cannot write '{path}': {e}")))?;
80
81 Ok(format!("Edited {path}"))
82 }
83}