Skip to main content

skill_tools/
write.rs

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(&params.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}