Skip to main content

agent_engine/tools/
grep.rs

1use super::{resolve_path_in, Tool, ToolContext};
2use crate::{Result, RuntimeError};
3use serde_json::{json, Value};
4use std::time::Duration;
5use tokio::process::Command;
6
7pub struct GrepTool;
8
9#[async_trait::async_trait]
10impl Tool for GrepTool {
11    fn effect(&self) -> crate::tools::catalog::ToolEffect {
12        crate::tools::catalog::ToolEffect::ReadOnly
13    }
14
15    fn origin(&self) -> crate::tools::ToolOrigin {
16        crate::tools::ToolOrigin::Builtin
17    }
18
19    fn name(&self) -> &str {
20        "grep"
21    }
22
23    fn description(&self) -> &str {
24        "Search file contents using regex patterns. Returns matching lines with file paths and line numbers. Supports file type filtering and context lines."
25    }
26
27    fn parameters(&self) -> Value {
28        json!({
29            "type": "object",
30            "properties": {
31                "pattern": {
32                    "type": "string",
33                    "description": "Regex pattern to search for"
34                },
35                "path": {
36                    "type": "string",
37                    "description": "File or directory to search in (default: current directory)"
38                },
39                "include": {
40                    "type": "string",
41                    "description": "Glob pattern to filter files (e.g. \"*.rs\", \"*.py\")"
42                },
43                "context": {
44                    "type": "integer",
45                    "description": "Number of context lines to show before and after each match"
46                }
47            },
48            "required": ["pattern"]
49        })
50    }
51
52    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
53        let pattern = params["pattern"]
54            .as_str()
55            .ok_or_else(|| RuntimeError::Tool("Missing pattern parameter".to_string()))?;
56        let path = resolve_path_in(
57            params["path"].as_str().unwrap_or("."),
58            ctx.capabilities.cwd.as_deref(),
59        );
60        let include = params["include"].as_str();
61        let context = params["context"].as_u64();
62
63        let mut cmd = Command::new("grep");
64        if let Some(env) = &ctx.capabilities.env {
65            cmd.env_clear().envs(env.iter().map(|(k, v)| (k, v)));
66        }
67        cmd.arg("-rn");
68        cmd.arg("--color=never");
69
70        if let Some(glob) = include {
71            cmd.arg("--include").arg(glob);
72        }
73
74        if let Some(ctx) = context {
75            cmd.arg(format!("-C{}", ctx));
76        }
77
78        cmd.arg("--exclude-dir=.git");
79        cmd.arg("--exclude-dir=node_modules");
80        cmd.arg("--exclude-dir=target");
81
82        cmd.arg("--").arg(pattern).arg(&path);
83
84        let output = tokio::time::timeout(Duration::from_secs(15), cmd.output())
85            .await
86            .map_err(|_| RuntimeError::Tool("Grep timed out after 15s".to_string()))?
87            .map_err(|e| RuntimeError::Tool(format!("Failed to execute grep: {}", e)))?;
88
89        let stdout = String::from_utf8_lossy(&output.stdout);
90
91        if stdout.is_empty() {
92            Ok("No matches found.".to_string())
93        } else {
94            let result = stdout.to_string();
95            let bounded = agent_core::BoundedText::new(&result, ctx.limits.max_tool_buffer);
96            if bounded.truncated {
97                Ok(format!(
98                    "{}\n\n... (output truncated, {} total bytes)",
99                    bounded.text, bounded.original_bytes
100                ))
101            } else {
102                Ok(result)
103            }
104        }
105    }
106}
107#[cfg(test)]
108mod tests {
109    use super::super::test_helpers::create_tool_context;
110    use super::*;
111    use crate::tools::Tool;
112    use serde_json::json;
113
114    #[test]
115    fn test_grep_tool_schema() {
116        let tool = GrepTool;
117        assert_eq!(tool.name(), "grep");
118        assert!(!tool.description().is_empty());
119
120        let params = tool.parameters();
121        assert_eq!(params["type"], "object");
122        assert!(params["properties"].is_object());
123        assert!(params["required"].is_array());
124    }
125
126    #[tokio::test]
127    async fn test_grep_tool_execution() {
128        let temp_dir = std::env::temp_dir();
129        let test_file = temp_dir.join("test_grep_tool_execution.txt");
130
131        // Write test content
132        let content = "hello world\nfoo bar\nhello again";
133        std::fs::write(&test_file, content).unwrap();
134
135        let tool = GrepTool;
136        let ctx = create_tool_context();
137
138        let params = json!({
139            "pattern": "hello",
140            "path": test_file.to_string_lossy()
141        });
142
143        let result = tool.execute(params, ctx).await.unwrap();
144
145        // Should contain both matching lines with line numbers
146        assert!(result.contains("hello world"));
147        assert!(result.contains("hello again"));
148        assert!(result.contains("1:") || result.contains("hello world"));
149        assert!(result.contains("3:") || result.contains("hello again"));
150
151        // Cleanup
152        let _ = std::fs::remove_file(&test_file);
153    }
154}