codetether_agent/tool/
file.rs1use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{json, Value};
7use std::path::PathBuf;
8use tokio::fs;
9
10pub 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
80pub 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 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
139pub 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
204pub 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}