use std::fs; use std::path::PathBuf; use serde_json::{json, Value}; use anyhow::{Result, anyhow}; use super::super::Tool; use super::super::ToolCtx; use super::super::resolve_path; use super::helpers::arg_str; pub struct Edit; impl Tool for Edit { fn name(&self) -> &'static str { "edit" } fn description(&self) -> &'static str { "Replace a string in a file with a new string. The old string must be unique unless replace_all is true." } 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": "The exact text to replace" }, "new": { "type": "string", "description": "The replacement text" }, "replace_all": { "type": "boolean", "description": "Replace all occurrences instead of requiring uniqueness" }, "reason": { "type": "string", "description": "Reason for the change (must be non-empty)" } }, "required": ["path", "old", "new", "reason"] }) } fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let rel = arg_str(args, "path")?; let old = arg_str(args, "old")?; let new_str = arg_str(args, "new")?; let reason = arg_str(args, "reason")?; if reason.trim().is_empty() { anyhow::bail!("reason must be a non-empty string"); } let replace_all = args.get("replace_all").and_then(|v| v.as_bool()).unwrap_or(false); let path: PathBuf = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { anyhow::bail!("file '{}' does not exist at resolved path {}", rel, path.display()); } if path.is_dir() { anyhow::bail!("'{}' is a directory, not a file", rel); } let content = fs::read_to_string(&path) .map_err(|e| anyhow!("failed to read '{}': {}", rel, e))?; if !content.contains(&old) { anyhow::bail!("old string not found in '{}'", rel); } if !replace_all { let count = content.matches(&old).count(); if count > 1 { anyhow::bail!( "old string appears {} times in '{}'. Set replace_all=true to replace all occurrences, or provide a more specific match.", count, rel ); } } let new_content = if replace_all { content.replace(&old, &new_str) } else { content.replacen(&old, &new_str, 1) }; fs::write(&path, &new_content) .map_err(|e| anyhow!("failed to write '{}': {}", rel, e))?; let bytes_diff = if new_content.len() > content.len() { new_content.len() - content.len() } else { content.len() - new_content.len() }; Ok(format!("edited {} ({} byte delta)", rel, bytes_diff as isize)) } }