synaps_cli/tools/
write.rs1use serde_json::{json, Value};
2use crate::{Result, RuntimeError};
3use super::{Tool, ToolContext, expand_path};
4
5pub struct WriteTool;
6
7#[async_trait::async_trait]
8impl Tool for WriteTool {
9 fn name(&self) -> &str { "write" }
10
11 fn description(&self) -> &str {
12 "Create or overwrite a file with the given content. Creates parent directories if needed. Use this for creating new files or completely rewriting existing ones."
13 }
14
15 fn parameters(&self) -> Value {
16 json!({
17 "type": "object",
18 "properties": {
19 "path": {
20 "type": "string",
21 "description": "Path to the file to write"
22 },
23 "content": {
24 "type": "string",
25 "description": "Content to write to the file"
26 }
27 },
28 "required": ["path", "content"]
29 })
30 }
31
32 async fn execute(&self, params: Value, _ctx: ToolContext) -> Result<String> {
33 let raw_path = params["path"].as_str()
34 .ok_or_else(|| RuntimeError::Tool("Missing path parameter".to_string()))?;
35 let content = params["content"].as_str()
36 .ok_or_else(|| RuntimeError::Tool("Missing content parameter".to_string()))?;
37
38 let path = expand_path(raw_path);
39
40 if let Some(parent) = path.parent() {
41 if !parent.as_os_str().is_empty() {
42 tokio::fs::create_dir_all(parent).await
43 .map_err(|e| RuntimeError::Tool(format!("Failed to create directories: {}", e)))?;
44 }
45 }
46
47 let original_perms = tokio::fs::metadata(&path).await
49 .map(|m| m.permissions())
50 .ok();
51
52 let tmp_path = path.with_extension("agent-tmp");
53 tokio::fs::write(&tmp_path, content).await
54 .map_err(|e| RuntimeError::Tool(format!("Failed to write file: {}", e)))?;
55
56 if let Some(perms) = original_perms {
57 let _ = tokio::fs::set_permissions(&tmp_path, perms).await;
58 }
59
60 tokio::fs::rename(&tmp_path, &path).await
61 .map_err(|e| {
62 let tmp = tmp_path.clone();
63 tokio::spawn(async move { let _ = tokio::fs::remove_file(tmp).await; });
64 RuntimeError::Tool(format!("Failed to finalize write: {}", e))
65 })?;
66
67 let line_count = content.lines().count();
68 Ok(format!("Wrote {} lines ({} bytes) to {}", line_count, content.len(), path.display()))
69 }
70}
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use super::super::test_helpers::create_tool_context;
75 use crate::tools::Tool;
76 use serde_json::json;
77
78 #[test]
79 fn test_write_tool_schema() {
80 let tool = WriteTool;
81 assert_eq!(tool.name(), "write");
82 assert!(!tool.description().is_empty());
83
84 let params = tool.parameters();
85 assert_eq!(params["type"], "object");
86 assert!(params["properties"].is_object());
87 assert!(params["required"].is_array());
88 }
89
90 #[tokio::test]
91 async fn test_write_tool_execution() {
92 let temp_dir = std::env::temp_dir();
93 let test_file = temp_dir.join("write_tool_test.txt");
94
95 let tool = WriteTool;
96 let ctx = create_tool_context();
97
98 let content = "Hello, world!\nThis is a test file.";
99 let params = json!({
100 "path": test_file.to_string_lossy(),
101 "content": content
102 });
103
104 let result = tool.execute(params, ctx).await.unwrap();
105
106 assert!(result.contains("Wrote 2 lines"));
108 assert!(result.contains("bytes"));
109
110 let written_content = std::fs::read_to_string(&test_file).unwrap();
112 assert_eq!(written_content, content);
113
114 let nested_file = temp_dir.join("nested").join("dir").join("test.txt");
116 let ctx = create_tool_context();
117 let params = json!({
118 "path": nested_file.to_string_lossy(),
119 "content": "nested content"
120 });
121
122 let result = tool.execute(params, ctx).await.unwrap();
123 assert!(result.contains("Wrote"));
124
125 let nested_content = std::fs::read_to_string(&nested_file).unwrap();
126 assert_eq!(nested_content, "nested content");
127
128 let _ = std::fs::remove_file(&test_file);
130 let _ = std::fs::remove_dir_all(temp_dir.join("nested"));
131 }
132}