2026-07-20 09:04:57 +07:00
|
|
|
//! 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;
|
2026-07-20 15:53:20 +07:00
|
|
|
use tracing::{info, instrument, warn};
|
2026-07-20 09:04:57 +07:00
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
/// Edit a file by replacing `old` text with `new` text.
|
|
|
|
|
///
|
|
|
|
|
/// Flow: resolve path → read file → ensure `old` exists → perform single
|
|
|
|
|
/// replacement → write file.
|
2026-07-20 09:04:57 +07:00
|
|
|
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"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
#[instrument(skip(self, ctx, args))]
|
2026-07-20 09:04:57 +07:00
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
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() {
|
2026-07-20 15:53:20 +07:00
|
|
|
warn!(rel = %rel, "edit target does not exist");
|
2026-07-20 09:04:57 +07:00
|
|
|
anyhow::bail!("file '{rel}' does not exist");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let content = fs::read_to_string(&path)?;
|
|
|
|
|
if !content.contains(&old) {
|
2026-07-20 15:53:20 +07:00
|
|
|
warn!(rel = %rel, old_len = old.len(), "old text not found in file");
|
2026-07-20 09:04:57 +07:00
|
|
|
anyhow::bail!("old text not found in '{}'", rel);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 12:26:08 +07:00
|
|
|
let new_content = content.replacen(&old, &new, 1);
|
2026-07-20 09:04:57 +07:00
|
|
|
fs::write(&path, &new_content)?;
|
|
|
|
|
|
2026-07-20 15:53:20 +07:00
|
|
|
info!(rel = %rel, old_len = old.len(), new_len = new.len(), "file edited");
|
2026-07-20 09:04:57 +07:00
|
|
|
Ok(format!(
|
|
|
|
|
"Edited '{}': replaced {} bytes with {} bytes",
|
|
|
|
|
rel,
|
|
|
|
|
old.len(),
|
|
|
|
|
new.len()
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|