2026-03-27 15:22:48 -04:00
// tools/edit.rs — Search-and-replace file editing
use anyhow ::{ Context , Result } ;
use serde ::Deserialize ;
2026-04-04 15:34:07 -04:00
pub fn tool ( ) -> super ::Tool {
2026-04-04 15:50:14 -04:00
super ::Tool {
name : " edit_file " ,
description : " Perform exact string replacement in a file. The old_string must appear exactly once (unless replace_all is true). Use read_file first to see current contents. " ,
parameters_json : r #" { " type " : " object " , " properties " :{ " file_path " :{ " type " : " string " , " description " : " Absolute path to the file to edit " }, " old_string " :{ " type " : " string " , " description " : " The exact text to find and replace " }, " new_string " :{ " type " : " string " , " description " : " The replacement text " }, " replace_all " :{ " type " : " boolean " , " description " : " Replace all occurrences ( default false ) " }}, " required " :[ " file_path " , " old_string " , " new_string " ]} " #,
handler : | _a , v | Box ::pin ( async move { edit_file ( & v ) } ) ,
}
2026-04-04 15:34:07 -04:00
}
2026-04-04 15:50:14 -04:00
#[ derive(Deserialize) ]
struct Args { file_path : String , old_string : String , new_string : String , #[ serde(default) ] replace_all : bool }
2026-03-27 15:22:48 -04:00
2026-04-04 15:34:07 -04:00
fn edit_file ( args : & serde_json ::Value ) -> Result < String > {
2026-04-04 15:50:14 -04:00
let a : Args = serde_json ::from_value ( args . clone ( ) ) . context ( " invalid edit_file arguments " ) ? ;
if a . old_string = = a . new_string { anyhow ::bail! ( " old_string and new_string are identical " ) ; }
2026-03-27 15:22:48 -04:00
let content = std ::fs ::read_to_string ( & a . file_path )
. with_context ( | | format! ( " Failed to read {} " , a . file_path ) ) ? ;
let count = content . matches ( & * a . old_string ) . count ( ) ;
2026-04-04 15:50:14 -04:00
if count = = 0 { anyhow ::bail! ( " old_string not found in {} " , a . file_path ) ; }
2026-03-27 15:22:48 -04:00
if a . replace_all {
let new_content = content . replace ( & * a . old_string , & a . new_string ) ;
std ::fs ::write ( & a . file_path , & new_content )
. with_context ( | | format! ( " Failed to write {} " , a . file_path ) ) ? ;
Ok ( format! ( " Replaced {} occurrences in {} " , count , a . file_path ) )
} else {
if count > 1 {
2026-04-04 15:50:14 -04:00
anyhow ::bail! ( " old_string appears {} times in {} — use replace_all or provide more context " , count , a . file_path ) ;
2026-03-27 15:22:48 -04:00
}
let new_content = content . replacen ( & * a . old_string , & a . new_string , 1 ) ;
std ::fs ::write ( & a . file_path , & new_content )
. with_context ( | | format! ( " Failed to write {} " , a . file_path ) ) ? ;
Ok ( format! ( " Edited {} " , a . file_path ) )
}
}