agent_engine/tools/
grep.rs1use 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 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 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 let _ = std::fs::remove_file(&test_file);
140 }
141}