1use async_trait::async_trait;
2use ignore::WalkBuilder;
3use serde_json::{json, Value};
4use std::fs;
5
6use crate::types::{AgentTool, AgentToolResult};
7
8pub struct GrepTool;
9
10#[async_trait]
11impl AgentTool for GrepTool {
12 fn name(&self) -> &str {
13 "grep"
14 }
15 fn description(&self) -> &str {
16 "Search file contents under a directory for a fixed substring. Honors .gitignore by default."
17 }
18 fn parameters(&self) -> Value {
19 json!({
20 "type": "object",
21 "properties": {
22 "pattern": {"type": "string", "description": "Substring to search for"},
23 "path": {"type": "string", "description": "Directory to search (default: cwd)"},
24 "max_matches": {"type": "integer", "default": 200}
25 },
26 "required": ["pattern"]
27 })
28 }
29 async fn execute(&self, _id: &str, args: Value) -> Result<AgentToolResult, String> {
30 let pattern = args
31 .get("pattern")
32 .and_then(|v| v.as_str())
33 .ok_or("missing 'pattern'")?
34 .to_string();
35 let path = args
36 .get("path")
37 .and_then(|v| v.as_str())
38 .unwrap_or(".")
39 .to_string();
40 let max = args
41 .get("max_matches")
42 .and_then(|v| v.as_u64())
43 .unwrap_or(200) as usize;
44
45 let result = tokio::task::spawn_blocking(move || -> Result<String, String> {
46 let mut buf = String::new();
47 let mut hits = 0usize;
48 let walker = WalkBuilder::new(&path).follow_links(false).build();
49 for entry in walker.flatten() {
50 if hits >= max {
51 buf.push_str(&format!("... (truncated at {max} matches)\n"));
52 break;
53 }
54 let p = entry.path();
55 if !p.is_file() {
56 continue;
57 }
58 let text = match fs::read_to_string(p) {
59 Ok(t) => t,
60 Err(_) => continue, };
62 for (i, line) in text.lines().enumerate() {
63 if line.contains(&pattern) {
64 buf.push_str(&format!("{}:{}:{}\n", p.display(), i + 1, line));
65 hits += 1;
66 if hits >= max {
67 break;
68 }
69 }
70 }
71 }
72 Ok(buf)
73 })
74 .await
75 .map_err(|e| e.to_string())??;
76
77 Ok(AgentToolResult::text(if result.is_empty() {
78 "(no matches)".to_string()
79 } else {
80 result
81 }))
82 }
83}