Skip to main content

codetether_agent/tool/
search.rs

1//! Search tools: grep
2
3use super::{Tool, ToolResult};
4use anyhow::Result;
5use async_trait::async_trait;
6use ignore::WalkBuilder;
7use regex::Regex;
8use serde_json::{Value, json};
9
10/// Search for text in files
11pub 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(&regex::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            // Check include pattern
112            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            // Read and search file
122            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}