Skip to main content

robit_agent/tool/
write.rs

1//! `write` tool — creates or overwrites files.
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::Value;
6
7use super::{resolve_path, Tool, ToolContext, ToolResult};
8use crate::error::Result;
9
10#[derive(Debug, Deserialize)]
11struct WriteArgs {
12    file_path: String,
13    content: String,
14}
15
16pub struct WriteTool;
17
18impl WriteTool {
19    pub fn new() -> Self {
20        Self
21    }
22}
23
24impl Default for WriteTool {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30#[async_trait]
31impl Tool for WriteTool {
32    fn name(&self) -> &str {
33        "write"
34    }
35
36    fn description(&self) -> &str {
37        "Create or overwrite a file. Automatically creates parent directories. Overwrites if the file already exists."
38    }
39
40    fn parameters_schema(&self) -> Value {
41        serde_json::json!({
42            "type": "object",
43            "properties": {
44                "file_path": {
45                    "type": "string",
46                    "description": "Target file path (relative or absolute)"
47                },
48                "content": {
49                    "type": "string",
50                    "description": "File content to write"
51                }
52            },
53            "required": ["file_path", "content"]
54        })
55    }
56
57    fn requires_confirmation(&self) -> bool {
58        true
59    }
60
61    async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
62        let parsed: WriteArgs = match serde_json::from_value(args) {
63            Ok(a) => a,
64            Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
65        };
66
67        // Validate file_path
68        if parsed.file_path.trim().is_empty() {
69            return Ok(ToolResult::error("File path cannot be empty".to_string()));
70        }
71
72        let path = resolve_path(&parsed.file_path, &ctx.working_dir);
73
74        // Check if path is an existing directory
75        if path.is_dir() {
76            return Ok(ToolResult::error(format!(
77                "Path is a directory: {}",
78                path.display()
79            )));
80        }
81
82        // Check if file exists (for message differentiation)
83        let existed = path.exists();
84
85        // Create parent directories if needed
86        if let Some(parent) = path.parent() {
87            if !parent.exists() {
88                if let Err(e) = tokio::fs::create_dir_all(parent).await {
89                    return Ok(ToolResult::error(format!(
90                        "Failed to create directory '{}': {}",
91                        parent.display(),
92                        e
93                    )));
94                }
95            }
96        }
97
98        // Write file
99        let content_bytes = parsed.content.as_bytes();
100        match tokio::fs::write(&path, &parsed.content).await {
101            Ok(()) => {
102                let msg = if existed {
103                    format!(
104                        "Overwritten file: {} ({} bytes)",
105                        path.display(),
106                        content_bytes.len()
107                    )
108                } else {
109                    format!(
110                        "Created file: {} ({} bytes)",
111                        path.display(),
112                        content_bytes.len()
113                    )
114                };
115                Ok(ToolResult::success(msg))
116            }
117            Err(e) => Ok(ToolResult::error(format!(
118                "Failed to write file '{}': {}",
119                path.display(),
120                e
121            ))),
122        }
123    }
124}