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