use std::future::Future;
use std::pin::Pin;
use serde_json::Value;
use crate::error::Result;
use crate::tools::tool::{ToolContext, ToolResult, Toolable};
pub struct WriteFileTool;
const DESCRIPTION: &str = "\
Write content to a file, creating parent directories if needed.
- If this is an existing file, you MUST use read_file first to read its contents.
- Prefer edit_file for modifying existing files — it only sends the diff. Use write_file for new files or complete rewrites.";
impl Toolable for WriteFileTool {
fn name(&self) -> &str {
"write_file"
}
fn description(&self) -> &str {
DESCRIPTION
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write"
},
"content": {
"type": "string",
"description": "Content to write to the file"
}
},
"required": ["path", "content"]
})
}
fn call<'a>(
&'a self,
input: Value,
ctx: &'a ToolContext,
) -> Pin<Box<dyn Future<Output = Result<ToolResult>> + Send + 'a>> {
Box::pin(async move {
let path = match input["path"].as_str() {
Some(p) => p,
None => {
return Ok(ToolResult::error("Missing required parameter: path"));
}
};
let content = match input["content"].as_str() {
Some(c) => c,
None => {
return Ok(ToolResult::error("Missing required parameter: content"));
}
};
let resolved = ctx.working_directory.join(path);
if let Some(parent) = resolved.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
return Ok(ToolResult::error(format!(
"Failed to create parent directories: {e}"
)));
}
}
match std::fs::write(&resolved, content) {
Ok(()) => Ok(ToolResult::success(format!("File written: {path}"))),
Err(e) => Ok(ToolResult::error(format!("Failed to write file: {e}"))),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_ctx(dir: &std::path::Path) -> ToolContext {
ToolContext::new(PathBuf::from(dir))
}
#[tokio::test]
async fn create_new_file() {
let dir = tempfile::tempdir().unwrap();
let tool = WriteFileTool;
let ctx = test_ctx(dir.path());
let result = tool
.call(
serde_json::json!({ "path": "new.txt", "content": "hello world" }),
&ctx,
)
.await
.unwrap();
assert!(!result.is_err());
assert!(result.content().contains("File written: new.txt"));
let written = std::fs::read_to_string(dir.path().join("new.txt")).unwrap();
assert_eq!(written, "hello world");
}
#[tokio::test]
async fn overwrite_existing_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("existing.txt"), "old content").unwrap();
let tool = WriteFileTool;
let ctx = test_ctx(dir.path());
let result = tool
.call(
serde_json::json!({ "path": "existing.txt", "content": "new content" }),
&ctx,
)
.await
.unwrap();
assert!(!result.is_err());
let written = std::fs::read_to_string(dir.path().join("existing.txt")).unwrap();
assert_eq!(written, "new content");
}
#[tokio::test]
async fn creates_parent_dirs() {
let dir = tempfile::tempdir().unwrap();
let tool = WriteFileTool;
let ctx = test_ctx(dir.path());
let result = tool
.call(
serde_json::json!({ "path": "a/b/c/deep.txt", "content": "nested" }),
&ctx,
)
.await
.unwrap();
assert!(!result.is_err());
let written = std::fs::read_to_string(dir.path().join("a/b/c/deep.txt")).unwrap();
assert_eq!(written, "nested");
}
}