1use crate::{ToolDefinition, ToolError, ToolResult};
2use serde::Deserialize;
3use tokio::fs;
4use tracing::{debug, info, warn};
5
6#[derive(Debug, Deserialize)]
7pub struct WriteParams {
8 pub path: String,
9 pub content: String,
10 #[serde(default)]
11 pub append: Option<String>,
12}
13
14impl WriteParams {
15 pub fn append_bool(&self) -> Option<bool> {
16 self.append.as_ref().and_then(|s| {
17 if s == "true" || s == "false" {
18 Some(s == "true")
19 } else {
20 None
21 }
22 })
23 }
24}
25
26#[derive(Clone)]
27pub struct WriteTool {
28 base_dir: String,
29}
30
31impl WriteTool {
32 pub fn new(base_dir: String) -> Self {
33 Self { base_dir }
34 }
35
36 fn resolve_path(&self, path: &str) -> std::path::PathBuf {
37 let path = std::path::Path::new(path);
38 if path.is_absolute() {
39 path.to_path_buf()
40 } else {
41 std::path::Path::new(&self.base_dir).join(path)
42 }
43 }
44
45 pub fn definition(&self) -> ToolDefinition {
46 ToolDefinition {
47 name: "write".to_string(),
48 description: "Write content to a file".to_string(),
49 parameters: serde_json::json!({
50 "type": "object",
51 "properties": {
52 "path": {
53 "type": "string",
54 "description": "Path to the file to write"
55 },
56 "content": {
57 "type": "string",
58 "description": "Content to write to the file"
59 },
60 "append": {
61 "type": "boolean",
62 "description": "Append to file instead of overwriting (default: false)"
63 }
64 },
65 "required": ["path", "content"]
66 }),
67 }
68 }
69
70 pub async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
71 info!("Write tool executing with params: {:?}", params);
72
73 let params: WriteParams = serde_json::from_value(params).map_err(|e| {
74 warn!("Write tool failed to parse params: {}", e);
75 ToolError::InvalidParameters(e.to_string())
76 })?;
77
78 let path = self.resolve_path(¶ms.path);
79 debug!(
80 "Resolved path: {:?}, append: {:?}",
81 path,
82 params.append_bool()
83 );
84 debug!("Content length: {} chars", params.content.len());
85
86 if let Some(parent) = path.parent() {
87 if !parent.exists() {
88 debug!("Creating parent directory: {:?}", parent);
89 if let Err(e) = fs::create_dir_all(parent).await {
90 warn!("Failed to create directory: {}", e);
91 return Ok(ToolResult {
92 success: false,
93 output: String::new(),
94 error: Some(format!("Failed to create directory: {}", e)),
95 });
96 }
97 }
98 }
99
100 let result = if params.append_bool() == Some(true) {
101 debug!("Opening file for append: {:?}", path);
102 fs::OpenOptions::new()
103 .create(true)
104 .append(true)
105 .open(&path)
106 .await
107 } else {
108 debug!("Creating new file: {:?}", path);
109 fs::File::create(&path).await
110 };
111
112 match result {
113 Ok(mut file) => {
114 use tokio::io::AsyncWriteExt;
115 debug!("Writing {} bytes to file", params.content.len());
116 if let Err(e) = file.write_all(params.content.as_bytes()).await {
117 warn!("Write failed: {}", e);
118 return Ok(ToolResult {
119 success: false,
120 output: String::new(),
121 error: Some(format!("Failed to write: {}", e)),
122 });
123 }
124 info!("Successfully wrote to file: {:?}", path);
125 Ok(ToolResult {
126 success: true,
127 output: format!("Written to {}", path.display()),
128 error: None,
129 })
130 }
131 Err(e) => {
132 warn!("Failed to create/open file: {}", e);
133 Ok(ToolResult {
134 success: false,
135 output: String::new(),
136 error: Some(format!("Failed to create file: {}", e)),
137 })
138 }
139 }
140 }
141}