Skip to main content

agent_engine/tools/
grep.rs

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