Skip to main content

codetether_agent/tool/
file.rs

1//! File tools: read, write, list, glob
2
3use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use std::path::PathBuf;
8use tokio::fs;
9
10/// Read file contents
11pub struct ReadTool;
12
13impl ReadTool {
14    pub fn new() -> Self {
15        Self
16    }
17}
18
19#[async_trait]
20impl Tool for ReadTool {
21    fn id(&self) -> &str {
22        "read"
23    }
24
25    fn name(&self) -> &str {
26        "Read File"
27    }
28
29    fn description(&self) -> &str {
30        "Read the contents of a file. Provide the file path to read."
31    }
32
33    fn parameters(&self) -> Value {
34        json!({
35            "type": "object",
36            "properties": {
37                "path": {
38                    "type": "string",
39                    "description": "The path to the file to read"
40                },
41                "offset": {
42                    "type": "integer",
43                    "description": "Line number to start reading from (1-indexed)"
44                },
45                "limit": {
46                    "type": "integer",
47                    "description": "Maximum number of lines to read"
48                }
49            },
50            "required": ["path"]
51        })
52    }
53
54    async fn execute(&self, args: Value) -> Result<ToolResult> {
55        let path = args["path"]
56            .as_str()
57            .ok_or_else(|| anyhow::anyhow!("path is required"))?;
58        let offset = args["offset"].as_u64().map(|n| n as usize);
59        let limit = args["limit"].as_u64().map(|n| n as usize);
60
61        let content = fs::read_to_string(path).await?;
62        
63        let lines: Vec<&str> = content.lines().collect();
64        let start = offset.map(|o| o.saturating_sub(1)).unwrap_or(0);
65        let end = limit.map(|l| (start + l).min(lines.len())).unwrap_or(lines.len());
66        
67        let selected: String = lines[start..end]
68            .iter()
69            .enumerate()
70            .map(|(i, line)| format!("{:4} | {}", start + i + 1, line))
71            .collect::<Vec<_>>()
72            .join("\n");
73
74        Ok(ToolResult::success(selected)
75            .with_metadata("total_lines", json!(lines.len()))
76            .with_metadata("read_lines", json!(end - start)))
77    }
78}
79
80/// Write file contents
81pub struct WriteTool;
82
83impl WriteTool {
84    pub fn new() -> Self {
85        Self
86    }
87}
88
89#[async_trait]
90impl Tool for WriteTool {
91    fn id(&self) -> &str {
92        "write"
93    }
94
95    fn name(&self) -> &str {
96        "Write File"
97    }
98
99    fn description(&self) -> &str {
100        "Write content to a file. Creates the file if it doesn't exist, or overwrites it."
101    }
102
103    fn parameters(&self) -> Value {
104        json!({
105            "type": "object",
106            "properties": {
107                "path": {
108                    "type": "string",
109                    "description": "The path to the file to write"
110                },
111                "content": {
112                    "type": "string",
113                    "description": "The content to write to the file"
114                }
115            },
116            "required": ["path", "content"]
117        })
118    }
119
120    async fn execute(&self, args: Value) -> Result<ToolResult> {
121        let path = args["path"]
122            .as_str()
123            .ok_or_else(|| anyhow::anyhow!("path is required"))?;
124        let content = args["content"]
125            .as_str()
126            .ok_or_else(|| anyhow::anyhow!("content is required"))?;
127
128        // Create parent directories if needed
129        if let Some(parent) = PathBuf::from(path).parent() {
130            fs::create_dir_all(parent).await?;
131        }
132
133        fs::write(path, content).await?;
134
135        Ok(ToolResult::success(format!("Wrote {} bytes to {}", content.len(), path)))
136    }
137}
138
139/// List directory contents
140pub struct ListTool;
141
142impl ListTool {
143    pub fn new() -> Self {
144        Self
145    }
146}
147
148#[async_trait]
149impl Tool for ListTool {
150    fn id(&self) -> &str {
151        "list"
152    }
153
154    fn name(&self) -> &str {
155        "List Directory"
156    }
157
158    fn description(&self) -> &str {
159        "List the contents of a directory."
160    }
161
162    fn parameters(&self) -> Value {
163        json!({
164            "type": "object",
165            "properties": {
166                "path": {
167                    "type": "string",
168                    "description": "The path to the directory to list"
169                }
170            },
171            "required": ["path"]
172        })
173    }
174
175    async fn execute(&self, args: Value) -> Result<ToolResult> {
176        let path = args["path"]
177            .as_str()
178            .ok_or_else(|| anyhow::anyhow!("path is required"))?;
179
180        let mut entries = fs::read_dir(path).await?;
181        let mut items = Vec::new();
182
183        while let Some(entry) = entries.next_entry().await? {
184            let name = entry.file_name().to_string_lossy().to_string();
185            let file_type = entry.file_type().await?;
186            
187            let suffix = if file_type.is_dir() {
188                "/"
189            } else if file_type.is_symlink() {
190                "@"
191            } else {
192                ""
193            };
194            
195            items.push(format!("{}{}", name, suffix));
196        }
197
198        items.sort();
199        Ok(ToolResult::success(items.join("\n"))
200            .with_metadata("count", json!(items.len())))
201    }
202}
203
204/// Find files using glob patterns
205pub struct GlobTool;
206
207impl GlobTool {
208    pub fn new() -> Self {
209        Self
210    }
211}
212
213#[async_trait]
214impl Tool for GlobTool {
215    fn id(&self) -> &str {
216        "glob"
217    }
218
219    fn name(&self) -> &str {
220        "Glob Search"
221    }
222
223    fn description(&self) -> &str {
224        "Find files matching a glob pattern (e.g., **/*.rs, src/**/*.ts)"
225    }
226
227    fn parameters(&self) -> Value {
228        json!({
229            "type": "object",
230            "properties": {
231                "pattern": {
232                    "type": "string",
233                    "description": "The glob pattern to match files"
234                },
235                "limit": {
236                    "type": "integer",
237                    "description": "Maximum number of results to return"
238                }
239            },
240            "required": ["pattern"]
241        })
242    }
243
244    async fn execute(&self, args: Value) -> Result<ToolResult> {
245        let pattern = args["pattern"]
246            .as_str()
247            .ok_or_else(|| anyhow::anyhow!("pattern is required"))?;
248        let limit = args["limit"].as_u64().unwrap_or(100) as usize;
249
250        let mut matches = Vec::new();
251        
252        for entry in glob::glob(pattern)? {
253            if matches.len() >= limit {
254                break;
255            }
256            if let Ok(path) = entry {
257                matches.push(path.display().to_string());
258            }
259        }
260
261        let truncated = matches.len() >= limit;
262        let output = matches.join("\n");
263
264        Ok(ToolResult::success(output)
265            .with_metadata("count", json!(matches.len()))
266            .with_metadata("truncated", json!(truncated)))
267    }
268}