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
84 .map(|o| o.saturating_sub(1))
85 .unwrap_or(0)
86 .min(lines.len());
87 let end_line = limit
88 .map(|l| (start_line + l).min(lines.len()))
89 .unwrap_or(lines.len());
90
91 let selected: String = lines[start_line..end_line]
92 .iter()
93 .enumerate()
94 .map(|(i, line)| format!("{:4} | {}", start_line + i + 1, line))
95 .collect::<Vec<_>>()
96 .join("\n");
97
98 let duration = start.elapsed();
99
100 let file_change = FileChange::read(path, Some((start_line as u32 + 1, end_line as u32)));
102
103 let mut exec = ToolExecution::start(
104 "read",
105 json!({
106 "path": path,
107 "offset": offset,
108 "limit": limit,
109 }),
110 );
111 exec.add_file_change(file_change);
112 let exec = exec.complete_success(
113 format!("Read {} lines from {}", end_line - start_line, path),
114 duration,
115 );
116 TOOL_EXECUTIONS.record(exec.clone());
117 record_persistent(exec);
118
119 Ok(ToolResult::success(selected)
120 .with_metadata("total_lines", json!(lines.len()))
121 .with_metadata("read_lines", json!(end_line - start_line)))
122 }
123}
124
125pub struct WriteTool;
127
128impl WriteTool {
129 pub fn new() -> Self {
130 Self
131 }
132}
133
134#[async_trait]
135impl Tool for WriteTool {
136 fn id(&self) -> &str {
137 "write"
138 }
139
140 fn name(&self) -> &str {
141 "Write File"
142 }
143
144 fn description(&self) -> &str {
145 "write(path: string, content: string) - Write content to a file. Creates the file if it doesn't exist, or overwrites it."
146 }
147
148 fn parameters(&self) -> Value {
149 json!({
150 "type": "object",
151 "properties": {
152 "path": {
153 "type": "string",
154 "description": "The path to the file to write"
155 },
156 "content": {
157 "type": "string",
158 "description": "The content to write to the file"
159 }
160 },
161 "required": ["path", "content"],
162 "example": {
163 "path": "src/config.rs",
164 "content": "// Configuration module\n\npub struct Config {\n pub debug: bool,\n}\n"
165 }
166 })
167 }
168
169 async fn execute(&self, args: Value) -> Result<ToolResult> {
170 let start = Instant::now();
171
172 let path = match args["path"].as_str() {
173 Some(p) => p,
174 None => {
175 return Ok(ToolResult::structured_error(
176 "INVALID_ARGUMENT",
177 "write",
178 "path is required",
179 Some(vec!["path"]),
180 Some(json!({"path": "src/example.rs", "content": "// file content"})),
181 ));
182 }
183 };
184 let content = match args["content"].as_str() {
185 Some(c) => c,
186 None => {
187 return Ok(ToolResult::structured_error(
188 "INVALID_ARGUMENT",
189 "write",
190 "content is required",
191 Some(vec!["content"]),
192 Some(json!({"path": path, "content": "// file content"})),
193 ));
194 }
195 };
196
197 if let Some(parent) = PathBuf::from(path).parent() {
199 fs::create_dir_all(parent).await?;
200 }
201
202 let existed = fs::metadata(path).await.is_ok();
204 let old_content = if existed {
205 fs::read_to_string(path).await.ok()
206 } else {
207 None
208 };
209
210 fs::write(path, content).await?;
211
212 let duration = start.elapsed();
213
214 let file_change = if existed {
216 FileChange::modify(
217 path,
218 old_content.as_deref().unwrap_or(""),
219 content,
220 Some((1, content.lines().count() as u32)),
221 )
222 } else {
223 FileChange::create(path, content)
224 };
225
226 let mut exec = ToolExecution::start(
227 "write",
228 json!({
229 "path": path,
230 "content_length": content.len(),
231 }),
232 );
233 exec.add_file_change(file_change);
234 let exec = exec.complete_success(
235 format!("Wrote {} bytes to {}", content.len(), path),
236 duration,
237 );
238 TOOL_EXECUTIONS.record(exec.clone());
239 record_persistent(exec);
240
241 Ok(ToolResult::success(format!(
242 "Wrote {} bytes to {}",
243 content.len(),
244 path
245 )))
246 }
247}
248
249pub struct ListTool;
251
252impl ListTool {
253 pub fn new() -> Self {
254 Self
255 }
256}
257
258#[async_trait]
259impl Tool for ListTool {
260 fn id(&self) -> &str {
261 "list"
262 }
263
264 fn name(&self) -> &str {
265 "List Directory"
266 }
267
268 fn description(&self) -> &str {
269 "list(path: string) - List the contents of a directory."
270 }
271
272 fn parameters(&self) -> Value {
273 json!({
274 "type": "object",
275 "properties": {
276 "path": {
277 "type": "string",
278 "description": "The path to the directory to list"
279 }
280 },
281 "required": ["path"],
282 "example": {
283 "path": "src/"
284 }
285 })
286 }
287
288 async fn execute(&self, args: Value) -> Result<ToolResult> {
289 let path = match args["path"].as_str() {
290 Some(p) => p,
291 None => {
292 return Ok(ToolResult::structured_error(
293 "INVALID_ARGUMENT",
294 "list",
295 "path is required",
296 Some(vec!["path"]),
297 Some(json!({"path": "src/"})),
298 ));
299 }
300 };
301
302 let mut entries = fs::read_dir(path).await?;
303 let mut items = Vec::new();
304
305 while let Some(entry) = entries.next_entry().await? {
306 let name = entry.file_name().to_string_lossy().to_string();
307 let file_type = entry.file_type().await?;
308
309 let suffix = if file_type.is_dir() {
310 "/"
311 } else if file_type.is_symlink() {
312 "@"
313 } else {
314 ""
315 };
316
317 items.push(format!("{}{}", name, suffix));
318 }
319
320 items.sort();
321 Ok(ToolResult::success(items.join("\n")).with_metadata("count", json!(items.len())))
322 }
323}
324
325pub struct GlobTool;
327
328impl GlobTool {
329 pub fn new() -> Self {
330 Self
331 }
332}
333
334#[async_trait]
335impl Tool for GlobTool {
336 fn id(&self) -> &str {
337 "glob"
338 }
339
340 fn name(&self) -> &str {
341 "Glob Search"
342 }
343
344 fn description(&self) -> &str {
345 "glob(pattern: string, limit?: int) - Find files matching a glob pattern (e.g., **/*.rs, src/**/*.ts)"
346 }
347
348 fn parameters(&self) -> Value {
349 json!({
350 "type": "object",
351 "properties": {
352 "pattern": {
353 "type": "string",
354 "description": "The glob pattern to match files"
355 },
356 "limit": {
357 "type": "integer",
358 "description": "Maximum number of results to return"
359 }
360 },
361 "required": ["pattern"],
362 "example": {
363 "pattern": "src/**/*.rs",
364 "limit": 50
365 }
366 })
367 }
368
369 async fn execute(&self, args: Value) -> Result<ToolResult> {
370 let pattern = match args["pattern"].as_str() {
371 Some(p) => p,
372 None => {
373 return Ok(ToolResult::structured_error(
374 "INVALID_ARGUMENT",
375 "glob",
376 "pattern is required",
377 Some(vec!["pattern"]),
378 Some(json!({"pattern": "src/**/*.rs"})),
379 ));
380 }
381 };
382 let limit = args["limit"].as_u64().unwrap_or(100) as usize;
383
384 let mut matches = Vec::new();
385
386 let (base_dir, match_pattern) = {
389 let p = std::path::Path::new(pattern);
390 let mut prefix = std::path::PathBuf::new();
391 let mut found_meta = false;
392 for component in p.components() {
393 let s = component.as_os_str().to_string_lossy();
394 if !found_meta && glob::Pattern::escape(&s) == *s {
395 prefix.push(component);
396 } else {
397 found_meta = true;
398 }
399 }
400 let base = if prefix.as_os_str().is_empty() {
401 ".".to_string()
402 } else {
403 prefix.display().to_string()
404 };
405 (base, pattern.to_string())
406 };
407
408 let compiled = glob::Pattern::new(&match_pattern)?;
409
410 let walker = ignore::WalkBuilder::new(&base_dir)
411 .hidden(false)
412 .git_ignore(true)
413 .follow_links(false) .max_depth(Some(30)) .build();
416
417 for entry in walker {
418 if matches.len() >= limit {
419 break;
420 }
421 let entry = match entry {
422 Ok(e) => e,
423 Err(_) => continue,
424 };
425 let path = entry.path();
426 if compiled.matches_path(path) || compiled.matches(path.to_string_lossy().as_ref()) {
427 matches.push(path.display().to_string());
428 }
429 }
430
431 let truncated = matches.len() >= limit;
432 let output = matches.join("\n");
433
434 Ok(ToolResult::success(output)
435 .with_metadata("count", json!(matches.len()))
436 .with_metadata("truncated", json!(truncated)))
437 }
438}