Skip to main content

claude_rust_tools/infrastructure/
file_edit_tool.rs

1use 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    fn get_path(&self, input: &Value) -> Option<String> {
43        input.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
44    }
45
46    async fn execute(&self, input: Value) -> AppResult<String> {
47        let path = input
48            .get("file_path")
49            .and_then(|v| v.as_str())
50            .ok_or_else(|| AppError::Tool("missing 'file_path' field".into()))?;
51
52        let old_string = input
53            .get("old_string")
54            .and_then(|v| v.as_str())
55            .ok_or_else(|| AppError::Tool("missing 'old_string' field".into()))?;
56
57        let new_string = input
58            .get("new_string")
59            .and_then(|v| v.as_str())
60            .ok_or_else(|| AppError::Tool("missing 'new_string' field".into()))?;
61
62        tracing::info!(path, "editing file");
63
64        let content = tokio::fs::read_to_string(path)
65            .await
66            .map_err(|e| AppError::Tool(format!("cannot read '{path}': {e}")))?;
67
68        let match_count = content.matches(old_string).count();
69        if match_count == 0 {
70            return Err(AppError::Tool(format!(
71                "old_string not found in '{path}'"
72            )));
73        }
74        if match_count > 1 {
75            return Err(AppError::Tool(format!(
76                "old_string found {match_count} times in '{path}' (must be unique)"
77            )));
78        }
79
80        let new_content = content.replacen(old_string, new_string, 1);
81        tokio::fs::write(path, &new_content)
82            .await
83            .map_err(|e| AppError::Tool(format!("cannot write '{path}': {e}")))?;
84
85        Ok(format!("Edited {path}"))
86    }
87}