cersei_tools/
file_edit.rs1use super::*;
4use crate::tool_primitives::fs as pfs;
5use serde::Deserialize;
6
7pub struct FileEditTool;
8
9#[async_trait]
10impl Tool for FileEditTool {
11 fn name(&self) -> &str {
12 "Edit"
13 }
14 fn description(&self) -> &str {
15 "Perform exact string replacements in files."
16 }
17 fn permission_level(&self) -> PermissionLevel {
18 PermissionLevel::Write
19 }
20 fn category(&self) -> ToolCategory {
21 ToolCategory::FileSystem
22 }
23
24 fn input_schema(&self) -> Value {
25 serde_json::json!({
26 "type": "object",
27 "properties": {
28 "file_path": { "type": "string", "description": "Absolute path to the file" },
29 "old_string": { "type": "string", "description": "The text to replace" },
30 "new_string": { "type": "string", "description": "The replacement text" },
31 "replace_all": { "type": "boolean", "description": "Replace all occurrences", "default": false }
32 },
33 "required": ["file_path", "old_string", "new_string"]
34 })
35 }
36
37 async fn execute(&self, input: Value, _ctx: &ToolContext) -> ToolResult {
38 #[derive(Deserialize)]
39 struct Input {
40 file_path: String,
41 old_string: String,
42 new_string: String,
43 #[serde(default)]
44 replace_all: bool,
45 }
46
47 let input: Input = match serde_json::from_value(input) {
48 Ok(i) => i,
49 Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
50 };
51
52 let path = std::path::Path::new(&input.file_path);
53
54 let before_content = tokio::fs::read_to_string(path).await.unwrap_or_default();
56
57 match pfs::edit_file(path, &input.old_string, &input.new_string, input.replace_all).await {
58 Ok(result) => {
59 let after_content = tokio::fs::read_to_string(path).await.unwrap_or_default();
61 let diff = crate::tool_primitives::diff::unified_diff(
62 &before_content, &after_content, 2,
63 );
64
65 let diff_preview = if diff.lines().count() > 30 {
67 let truncated: String = diff.lines().take(25).collect::<Vec<_>>().join("\n");
68 format!("{}\n... ({} more lines)", truncated, diff.lines().count() - 25)
69 } else {
70 diff
71 };
72
73 ToolResult::success(format!(
74 "The file {} has been updated. {} replacement(s) made.\n{}",
75 input.file_path, result.replacements_made, diff_preview
76 ))
77 }
78 Err(pfs::EditError::NotFound) => ToolResult::error(format!(
79 "old_string not found in {}", input.file_path
80 )),
81 Err(pfs::EditError::AmbiguousMatch { count }) => ToolResult::error(format!(
82 "old_string is not unique ({} occurrences). Use replace_all or provide more context.",
83 count
84 )),
85 Err(pfs::EditError::Io(e)) => ToolResult::error(format!(
86 "Failed to edit file: {}", e
87 )),
88 }
89 }
90}