Skip to main content

ararajuba_tools_coding/fs/
patch.rs

1//! `patch_file` tool — find-and-replace a unique string in a file.
2
3use ararajuba_core::tools::tool::{tool, ToolDef};
4use serde_json::json;
5
6/// Create the `patch_file` tool.
7///
8/// Finds the exact occurrence of `old_string` in the file and replaces it with
9/// `new_string`. Errors if the string is not found or is ambiguous (multiple
10/// occurrences).
11pub 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}