1use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use serde_json::{Value, json};
7use std::path::PathBuf;
8use std::time::Instant;
9use tokio::fs;
10
11use crate::telemetry::{FileChange, TOOL_EXECUTIONS, ToolExecution, record_persistent};
12
13pub struct ReadTool;
15
16impl ReadTool {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22#[async_trait]
23impl Tool for ReadTool {
24 fn id(&self) -> &str {
25 "read"
26 }
27
28 fn name(&self) -> &str {
29 "Read File"
30 }
31
32 fn description(&self) -> &str {
33 "read(path: string, offset?: int, limit?: int) - Read the contents of a file. Provide the file path to read."
34 }
35
36 fn parameters(&self) -> Value {
37 json!({
38 "type": "object",
39 "properties": {
40 "path": {
41 "type": "string",
42 "description": "The path to the file to read"
43 },
44 "offset": {
45 "type": "integer",
46 "description": "Line number to start reading from (1-indexed)"
47 },
48 "limit": {
49 "type": "integer",
50 "description": "Maximum number of lines to read"
51 }
52 },
53 "required": ["path"],
54 "example": {
55 "path": "src/main.rs",
56 "offset": 1,
57 "limit": 100
58 }
59 })
60 }
61
62 async fn execute(&self, args: Value) -> Result<ToolResult> {
63 let start = Instant::now();
64
65 let path = match args["path"].as_str() {
66 Some(p) => p,
67 None => {
68 return Ok(ToolResult::structured_error(
69 "INVALID_ARGUMENT",
70 "read",
71 "path is required",
72 Some(vec!["path"]),
73 Some(json!({"path": "src/main.rs"})),
74 ));
75 }
76 };
77 let offset = args["offset"].as_u64().map(|n| n as usize);
78 let limit = args["limit"].as_u64().map(|n| n as usize);
79
80 let content = fs::read_to_string(path).await?;
81
82 let lines: Vec<&str> = content.lines().collect();
83 let start_line = offset.map(|o| o.saturating_sub(1)).unwrap_or(0);
84 let end_line = limit
85 .map(|l| (start_line + l).min(lines.len()))
86 .unwrap_or(lines.len());
87
88 let selected: String = lines[start_line..end_line]
89 .iter()
90 .enumerate()
91 .map(|(i, line)| format!("{:4} | {}", start_line + i + 1, line))
92 .collect::<Vec<_>>()
93 .join("\n");
94
95 let duration = start.elapsed();
96
97 let file_change = FileChange::read(path, Some((start_line as u32 + 1, end_line as u32)));
99
100 let mut exec = ToolExecution::start(
101 "read",
102 json!({
103 "path": path,
104 "offset": offset,
105 "limit": limit,
106 }),
107 );
108 exec.add_file_change(file_change);
109 let exec = exec.complete_success(
110 format!("Read {} lines from {}", end_line - start_line, path),
111 duration,
112 );
113 TOOL_EXECUTIONS.record(exec.clone());
114 record_persistent(exec);
115
116 Ok(ToolResult::success(selected)
117 .with_metadata("total_lines", json!(lines.len()))
118 .with_metadata("read_lines", json!(end_line - start_line)))
119 }
120}
121
122pub struct WriteTool;
124
125impl WriteTool {
126 pub fn new() -> Self {
127 Self
128 }
129}
130
131#[async_trait]
132impl Tool for WriteTool {
133 fn id(&self) -> &str {
134 "write"
135 }
136
137 fn name(&self) -> &str {
138 "Write File"
139 }
140
141 fn description(&self) -> &str {
142 "write(path: string, content: string) - Write content to a file. Creates the file if it doesn't exist, or overwrites it."
143 }
144
145 fn parameters(&self) -> Value {
146 json!({
147 "type": "object",
148 "properties": {
149 "path": {
150 "type": "string",
151 "description": "The path to the file to write"
152 },
153 "content": {
154 "type": "string",
155 "description": "The content to write to the file"
156 }
157 },
158 "required": ["path", "content"],
159 "example": {
160 "path": "src/config.rs",
161 "content": "// Configuration module\n\npub struct Config {\n pub debug: bool,\n}\n"
162 }
163 })
164 }
165
166 async fn execute(&self, args: Value) -> Result<ToolResult> {
167 let start = Instant::now();
168
169 let path = match args["path"].as_str() {
170 Some(p) => p,
171 None => {
172 return Ok(ToolResult::structured_error(
173 "INVALID_ARGUMENT",
174 "write",
175 "path is required",
176 Some(vec!["path"]),
177 Some(json!({"path": "src/example.rs", "content": "// file content"})),
178 ));
179 }
180 };
181 let content = match args["content"].as_str() {
182 Some(c) => c,
183 None => {
184 return Ok(ToolResult::structured_error(
185 "INVALID_ARGUMENT",
186 "write",
187 "content is required",
188 Some(vec!["content"]),
189 Some(json!({"path": path, "content": "// file content"})),
190 ));
191 }
192 };
193
194 if let Some(parent) = PathBuf::from(path).parent() {
196 fs::create_dir_all(parent).await?;
197 }
198
199 let existed = fs::metadata(path).await.is_ok();
201 let old_content = if existed {
202 fs::read_to_string(path).await.ok()
203 } else {
204 None
205 };
206
207 fs::write(path, content).await?;
208
209 let duration = start.elapsed();
210
211 let file_change = if existed {
213 FileChange::modify(
214 path,
215 old_content.as_deref().unwrap_or(""),
216 content,
217 Some((1, content.lines().count() as u32)),
218 )
219 } else {
220 FileChange::create(path, content)
221 };
222
223 let mut exec = ToolExecution::start(
224 "write",
225 json!({
226 "path": path,
227 "content_length": content.len(),
228 }),
229 );
230 exec.add_file_change(file_change);
231 let exec = exec.complete_success(
232 format!("Wrote {} bytes to {}", content.len(), path),
233 duration,
234 );
235 TOOL_EXECUTIONS.record(exec.clone());
236 record_persistent(exec);
237
238 Ok(ToolResult::success(format!(
239 "Wrote {} bytes to {}",
240 content.len(),
241 path
242 )))
243 }
244}
245
246pub struct ListTool;
248
249impl ListTool {
250 pub fn new() -> Self {
251 Self
252 }
253}
254
255#[async_trait]
256impl Tool for ListTool {
257 fn id(&self) -> &str {
258 "list"
259 }
260
261 fn name(&self) -> &str {
262 "List Directory"
263 }
264
265 fn description(&self) -> &str {
266 "list(path: string) - List the contents of a directory."
267 }
268
269 fn parameters(&self) -> Value {
270 json!({
271 "type": "object",
272 "properties": {
273 "path": {
274 "type": "string",
275 "description": "The path to the directory to list"
276 }
277 },
278 "required": ["path"],
279 "example": {
280 "path": "src/"
281 }
282 })
283 }
284
285 async fn execute(&self, args: Value) -> Result<ToolResult> {
286 let path = match args["path"].as_str() {
287 Some(p) => p,
288 None => {
289 return Ok(ToolResult::structured_error(
290 "INVALID_ARGUMENT",
291 "list",
292 "path is required",
293 Some(vec!["path"]),
294 Some(json!({"path": "src/"})),
295 ));
296 }
297 };
298
299 let mut entries = fs::read_dir(path).await?;
300 let mut items = Vec::new();
301
302 while let Some(entry) = entries.next_entry().await? {
303 let name = entry.file_name().to_string_lossy().to_string();
304 let file_type = entry.file_type().await?;
305
306 let suffix = if file_type.is_dir() {
307 "/"
308 } else if file_type.is_symlink() {
309 "@"
310 } else {
311 ""
312 };
313
314 items.push(format!("{}{}", name, suffix));
315 }
316
317 items.sort();
318 Ok(ToolResult::success(items.join("\n")).with_metadata("count", json!(items.len())))
319 }
320}
321
322pub struct GlobTool;
324
325impl GlobTool {
326 pub fn new() -> Self {
327 Self
328 }
329}
330
331#[async_trait]
332impl Tool for GlobTool {
333 fn id(&self) -> &str {
334 "glob"
335 }
336
337 fn name(&self) -> &str {
338 "Glob Search"
339 }
340
341 fn description(&self) -> &str {
342 "glob(pattern: string, limit?: int) - Find files matching a glob pattern (e.g., **/*.rs, src/**/*.ts)"
343 }
344
345 fn parameters(&self) -> Value {
346 json!({
347 "type": "object",
348 "properties": {
349 "pattern": {
350 "type": "string",
351 "description": "The glob pattern to match files"
352 },
353 "limit": {
354 "type": "integer",
355 "description": "Maximum number of results to return"
356 }
357 },
358 "required": ["pattern"],
359 "example": {
360 "pattern": "src/**/*.rs",
361 "limit": 50
362 }
363 })
364 }
365
366 async fn execute(&self, args: Value) -> Result<ToolResult> {
367 let pattern = match args["pattern"].as_str() {
368 Some(p) => p,
369 None => {
370 return Ok(ToolResult::structured_error(
371 "INVALID_ARGUMENT",
372 "glob",
373 "pattern is required",
374 Some(vec!["pattern"]),
375 Some(json!({"pattern": "src/**/*.rs"})),
376 ));
377 }
378 };
379 let limit = args["limit"].as_u64().unwrap_or(100) as usize;
380
381 let mut matches = Vec::new();
382
383 for entry in glob::glob(pattern)? {
384 if matches.len() >= limit {
385 break;
386 }
387 if let Ok(path) = entry {
388 matches.push(path.display().to_string());
389 }
390 }
391
392 let truncated = matches.len() >= limit;
393 let output = matches.join("\n");
394
395 Ok(ToolResult::success(output)
396 .with_metadata("count", json!(matches.len()))
397 .with_metadata("truncated", json!(truncated)))
398 }
399}