codetether_agent/tool/
search.rs1use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use ignore::WalkBuilder;
7use regex::Regex;
8use serde_json::{Value, json};
9
10pub struct GrepTool;
12
13impl GrepTool {
14 pub fn new() -> Self {
15 Self
16 }
17}
18
19#[async_trait]
20impl Tool for GrepTool {
21 fn id(&self) -> &str {
22 "grep"
23 }
24
25 fn name(&self) -> &str {
26 "Grep Search"
27 }
28
29 fn description(&self) -> &str {
30 "grep(pattern: string, path?: string, is_regex?: bool, include?: string, limit?: int) - Search for text or regex patterns in files. Respects .gitignore by default."
31 }
32
33 fn parameters(&self) -> Value {
34 json!({
35 "type": "object",
36 "properties": {
37 "pattern": {
38 "type": "string",
39 "description": "The text or regex pattern to search for"
40 },
41 "path": {
42 "type": "string",
43 "description": "Directory or file to search in (default: current directory)"
44 },
45 "is_regex": {
46 "type": "boolean",
47 "description": "Whether the pattern is a regex (default: false)"
48 },
49 "include": {
50 "type": "string",
51 "description": "Glob pattern to include files (e.g., *.rs)"
52 },
53 "limit": {
54 "type": "integer",
55 "description": "Maximum number of matches to return"
56 }
57 },
58 "required": ["pattern"],
59 "example": {
60 "pattern": "fn main",
61 "path": "src/",
62 "include": "*.rs"
63 }
64 })
65 }
66
67 async fn execute(&self, args: Value) -> Result<ToolResult> {
68 let pattern = match args["pattern"].as_str() {
69 Some(p) => p,
70 None => {
71 return Ok(ToolResult::structured_error(
72 "INVALID_ARGUMENT",
73 "grep",
74 "pattern is required",
75 Some(vec!["pattern"]),
76 Some(json!({"pattern": "search text", "path": "src/"})),
77 ));
78 }
79 };
80 let search_path = args["path"].as_str().unwrap_or(".");
81 let is_regex = args["is_regex"].as_bool().unwrap_or(false);
82 let include = args["include"].as_str();
83 let limit = args["limit"].as_u64().unwrap_or(50) as usize;
84
85 let regex = if is_regex {
86 Regex::new(pattern)?
87 } else {
88 Regex::new(®ex::escape(pattern))?
89 };
90
91 let mut results = Vec::new();
92 let mut walker = WalkBuilder::new(search_path);
93 walker.hidden(false).git_ignore(true);
94
95 for entry in walker.build() {
96 if results.len() >= limit {
97 break;
98 }
99
100 let entry = match entry {
101 Ok(e) => e,
102 Err(_) => continue,
103 };
104
105 if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
106 continue;
107 }
108
109 let path = entry.path();
110
111 if let Some(include_pattern) = include {
113 if !glob::Pattern::new(include_pattern)
114 .map(|p| p.matches_path(path))
115 .unwrap_or(false)
116 {
117 continue;
118 }
119 }
120
121 if let Ok(content) = tokio::fs::read_to_string(path).await {
123 for (line_num, line) in content.lines().enumerate() {
124 if results.len() >= limit {
125 break;
126 }
127
128 if regex.is_match(line) {
129 results.push(format!(
130 "{}:{}: {}",
131 path.display(),
132 line_num + 1,
133 line.trim()
134 ));
135 }
136 }
137 }
138 }
139
140 let truncated = results.len() >= limit;
141 let output = results.join("\n");
142
143 Ok(ToolResult::success(output)
144 .with_metadata("count", json!(results.len()))
145 .with_metadata("truncated", json!(truncated)))
146 }
147}