use async_trait::async_trait;
use serde_json::{json, Value};
use super::paths::resolve_in_workspace;
use super::Tool;
pub struct ReadFileTool;
#[async_trait]
impl Tool for ReadFileTool {
fn name(&self) -> &str {
"read_file"
}
fn capability(&self) -> crate::risk::Capability {
crate::risk::Capability::ReadOnly
}
fn description(&self) -> &str {
"Read the contents of a file"
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
}
},
"required": ["path"]
})
}
async fn execute(&self, input: Value) -> Result<String, String> {
let path = input
.get("path")
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;
let resolved = resolve_in_workspace(path)?;
if !tokio::fs::try_exists(&resolved).await.unwrap_or(false) {
return Err(format!("File does not exist: {}", path));
}
tokio::fs::read_to_string(&resolved)
.await
.map_err(|e| format!("Failed to read file: {}", e))
}
}
pub struct WriteFileTool;
#[async_trait]
impl Tool for WriteFileTool {
fn name(&self) -> &str {
"write_file"
}
fn capability(&self) -> crate::risk::Capability {
crate::risk::Capability::Write
}
fn description(&self) -> &str {
"Write content to a file"
}
fn input_schema(&self) -> Value {
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"]
})
}
async fn execute(&self, input: Value) -> Result<String, String> {
let path = input
.get("path")
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;
let content = input
.get("content")
.and_then(|v| v.as_str())
.ok_or("Missing 'content' parameter")?;
let resolved = resolve_in_workspace(path)?;
if let Some(parent) = resolved.parent() {
if !tokio::fs::try_exists(parent).await.unwrap_or(false) {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("Failed to create parent directories: {}", e))?;
}
}
tokio::fs::write(&resolved, content)
.await
.map_err(|e| format!("Failed to write file: {}", e))?;
Ok(format!("File written successfully: {}", path))
}
}
pub struct EditFileTool;
#[async_trait]
impl Tool for EditFileTool {
fn name(&self) -> &str {
"edit_file"
}
fn capability(&self) -> crate::risk::Capability {
crate::risk::Capability::Write
}
fn description(&self) -> &str {
"Edit a file by replacing specific text"
}
fn input_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit"
},
"old_text": {
"type": "string",
"description": "Text to find and replace"
},
"new_text": {
"type": "string",
"description": "Text to replace with"
}
},
"required": ["path", "old_text", "new_text"]
})
}
async fn execute(&self, input: Value) -> Result<String, String> {
let path = input
.get("path")
.and_then(|v| v.as_str())
.ok_or("Missing 'path' parameter")?;
let old_text = input
.get("old_text")
.and_then(|v| v.as_str())
.ok_or("Missing 'old_text' parameter")?;
let new_text = input
.get("new_text")
.and_then(|v| v.as_str())
.ok_or("Missing 'new_text' parameter")?;
let resolved = resolve_in_workspace(path)?;
if !tokio::fs::try_exists(&resolved).await.unwrap_or(false) {
return Err(format!("File does not exist: {}", path));
}
let content = tokio::fs::read_to_string(&resolved)
.await
.map_err(|e| format!("Failed to read file: {}", e))?;
if !content.contains(old_text) {
return Err("Text not found in file".to_string());
}
let new_content = content.replace(old_text, new_text);
tokio::fs::write(&resolved, new_content)
.await
.map_err(|e| format!("Failed to write file: {}", e))?;
Ok(format!("File edited successfully: {}", path))
}
}