//! Edit a file by replacing a text block. use crate::tools::{resolve_path, Tool, ToolCtx}; use anyhow::Result; use serde_json::{json, Value}; use std::fs; use tracing::{info, instrument, warn}; /// Edit a file by replacing `old` text with `new` text. /// /// Flow: resolve path → read file → ensure `old` exists → perform single /// replacement → write file. pub struct Edit; impl Tool for Edit { fn name(&self) -> &'static str { "edit" } fn description(&self) -> &'static str { "Edit a file by replacing 'old' text with 'new' text" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "path": { "type": "string", "description": "Path to the file to edit (relative to workspace root)" }, "old": { "type": "string", "description": "Text to replace (must exist in the file)" }, "new": { "type": "string", "description": "Replacement text" }, "reason": { "type": "string", "description": "Reason for this change" } }, "required": ["path", "old", "new"] }) } #[instrument(skip(self, ctx, args))] fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = crate::tools::arg_str(args, "path")?; let old = crate::tools::arg_str(args, "old")?; let new = crate::tools::arg_str(args, "new")?; let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { warn!(rel = %rel, "edit target does not exist"); anyhow::bail!("file '{rel}' does not exist"); } let content = fs::read_to_string(&path)?; if !content.contains(&old) { warn!(rel = %rel, old_len = old.len(), "old text not found in file"); anyhow::bail!("old text not found in '{}'", rel); } let new_content = content.replacen(&old, &new, 1); fs::write(&path, &new_content)?; info!(rel = %rel, old_len = old.len(), new_len = new.len(), "file edited"); Ok(format!( "Edited '{}': replaced {} bytes with {} bytes", rel, old.len(), new.len() )) } }