ararajuba_tools_coding/fs/
patch.rs1use ararajuba_core::tools::tool::{tool, ToolDef};
4use serde_json::json;
5
6pub fn patch_file_tool() -> ToolDef {
12 tool("patch_file")
13 .description(
14 "Find and replace an exact string in a file. \
15 Errors if the old_string is not found or matches more than once.",
16 )
17 .input_schema(json!({
18 "type": "object",
19 "properties": {
20 "path": { "type": "string", "description": "File path to patch" },
21 "old_string": { "type": "string", "description": "Exact text to find" },
22 "new_string": { "type": "string", "description": "Replacement text" }
23 },
24 "required": ["path", "old_string", "new_string"]
25 }))
26 .execute(|input| async move {
27 let path = input["path"]
28 .as_str()
29 .ok_or_else(|| "missing required field: path".to_string())?;
30 let old_string = input["old_string"]
31 .as_str()
32 .ok_or_else(|| "missing required field: old_string".to_string())?;
33 let new_string = input["new_string"]
34 .as_str()
35 .ok_or_else(|| "missing required field: new_string".to_string())?;
36
37 let content = tokio::fs::read_to_string(path)
38 .await
39 .map_err(|e| format!("failed to read file: {e}"))?;
40
41 let count = content.matches(old_string).count();
42 if count == 0 {
43 return Err("old_string not found in file".to_string());
44 }
45 if count > 1 {
46 return Err(format!(
47 "old_string is ambiguous — found {count} occurrences, provide more context"
48 ));
49 }
50
51 let new_content = content.replacen(old_string, new_string, 1);
52 tokio::fs::write(path, &new_content)
53 .await
54 .map_err(|e| format!("failed to write file: {e}"))?;
55
56 Ok(json!({
57 "ok": true,
58 "replaced": 1
59 }))
60 })
61 .build()
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn tool_metadata() {
70 let t = patch_file_tool();
71 assert_eq!(t.name, "patch_file");
72 assert!(t.description.is_some());
73 assert!(t.execute.is_some());
74 }
75}