crabtalk_runtime/os/
edit.rs1use crate::{Env, host::Host};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use wcore::{
7 agent::{AsTool, ToolDescription},
8 model::Tool,
9};
10
11use crate::os::read::MAX_FILE_SIZE;
12
13#[derive(Deserialize, JsonSchema)]
14pub struct Edit {
15 pub path: String,
17 pub old_string: String,
19 pub new_string: String,
21}
22
23impl ToolDescription for Edit {
24 const DESCRIPTION: &'static str = "Replace an exact string in a file. Fails if the string is not found or appears more than once.";
25}
26
27pub fn tools() -> Vec<Tool> {
28 vec![Edit::as_tool()]
29}
30
31impl<H: Host> Env<H> {
32 pub async fn dispatch_edit(
33 &self,
34 args: &str,
35 conversation_id: Option<u64>,
36 ) -> Result<String, String> {
37 let input: Edit =
38 serde_json::from_str(args).map_err(|e| format!("invalid arguments: {e}"))?;
39
40 if input.old_string.is_empty() {
41 return Err("old_string must not be empty".to_owned());
42 }
43 if input.old_string == input.new_string {
44 return Err("old_string and new_string are identical".to_owned());
45 }
46
47 let conversation_cwd = if let Some(id) = conversation_id {
48 self.host.conversation_cwd(id)
49 } else {
50 None
51 };
52 let cwd = conversation_cwd.as_deref().unwrap_or(&self.cwd);
53
54 let path = if std::path::Path::new(&input.path).is_absolute() {
55 std::path::PathBuf::from(&input.path)
56 } else {
57 cwd.join(&input.path)
58 };
59
60 match std::fs::metadata(&path) {
61 Ok(m) if m.len() > MAX_FILE_SIZE => {
62 return Err(format!(
63 "file is too large ({} bytes, max {})",
64 m.len(),
65 MAX_FILE_SIZE
66 ));
67 }
68 Err(e) => return Err(format!("error reading {}: {e}", path.display())),
69 _ => {}
70 }
71
72 let content = std::fs::read_to_string(&path)
73 .map_err(|e| format!("error reading {}: {e}", path.display()))?;
74
75 let count = content.matches(&input.old_string).count();
76 if count == 0 {
77 return Err("old_string not found".to_owned());
78 }
79 if count > 1 {
80 return Err(format!(
81 "old_string is not unique, found {count} occurrences"
82 ));
83 }
84
85 let new_content = content.replacen(&input.old_string, &input.new_string, 1);
86 std::fs::write(&path, &new_content)
87 .map_err(|e| format!("error writing {}: {e}", path.display()))?;
88
89 Ok("ok".to_owned())
90 }
91}