dot-ai 0.6.1

A minimal AI agent that lives in your terminal
Documentation
use anyhow::{Context, Result};
use serde_json::Value;
use std::fs;
use std::path::Path;

use super::Tool;

pub struct ApplyPatchTool;

impl Tool for ApplyPatchTool {
    fn name(&self) -> &str {
        "apply_patch"
    }

    fn description(&self) -> &str {
        "Apply search-and-replace patches to one or more files. Each patch specifies a file path, the exact text to find (old), and the replacement text (new). Use for precise multi-file edits."
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "patches": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "File path to modify"
                            },
                            "old": {
                                "type": "string",
                                "description": "Exact text to find in the file"
                            },
                            "new": {
                                "type": "string",
                                "description": "Replacement text"
                            }
                        },
                        "required": ["path", "old", "new"]
                    },
                    "description": "Array of patches to apply"
                }
            },
            "required": ["patches"]
        })
    }

    fn execute(&self, input: Value) -> Result<String> {
        let patches = input["patches"]
            .as_array()
            .context("Missing required parameter 'patches'")?;

        if patches.is_empty() {
            return Ok("No patches to apply.".to_string());
        }

        tracing::debug!("apply_patch: {} patches", patches.len());

        let mut results: Vec<String> = Vec::new();
        let mut errors: Vec<String> = Vec::new();

        for patch in patches {
            let path = match patch["path"].as_str() {
                Some(p) => p,
                None => {
                    errors.push("patch missing 'path' field".to_string());
                    continue;
                }
            };
            let old = match patch["old"].as_str() {
                Some(o) => o,
                None => {
                    errors.push(format!("{}: missing 'old' field", path));
                    continue;
                }
            };
            let new = match patch["new"].as_str() {
                Some(n) => n,
                None => {
                    errors.push(format!("{}: missing 'new' field", path));
                    continue;
                }
            };

            if old.is_empty() && new.is_empty() {
                continue;
            }

            if old.is_empty() {
                if let Some(parent) = Path::new(path).parent()
                    && !parent.as_os_str().is_empty()
                {
                    let _ = fs::create_dir_all(parent);
                }
                match fs::write(path, new) {
                    Ok(()) => results.push(format!("created {}", path)),
                    Err(e) => errors.push(format!("{}: {}", path, e)),
                }
                continue;
            }

            let content = match fs::read_to_string(path) {
                Ok(c) => c,
                Err(e) => {
                    errors.push(format!("{}: {}", path, e));
                    continue;
                }
            };

            if !content.contains(old) {
                errors.push(format!("{}: 'old' text not found in file", path));
                continue;
            }

            let updated = content.replacen(old, new, 1);
            match fs::write(path, &updated) {
                Ok(()) => results.push(format!("patched {}", path)),
                Err(e) => errors.push(format!("{}: write failed: {}", path, e)),
            }
        }

        let mut output = String::new();
        if !results.is_empty() {
            output.push_str(&results.join("\n"));
        }
        if !errors.is_empty() {
            if !output.is_empty() {
                output.push('\n');
            }
            output.push_str("Errors:\n");
            output.push_str(&errors.join("\n"));
        }

        Ok(output)
    }
}