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 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 = expand_path(params["path"].as_str().unwrap_or("."));
57 let include = params["include"].as_str();
58 let context = params["context"].as_u64();
59
60 let mut cmd = Command::new("grep");
61 cmd.arg("-rn");
62 cmd.arg("--color=never");
63
64 if let Some(glob) = include {
65 cmd.arg("--include").arg(glob);
66 }
67
68 if let Some(ctx) = context {
69 cmd.arg(format!("-C{}", ctx));
70 }
71
72 cmd.arg("--exclude-dir=.git");
73 cmd.arg("--exclude-dir=node_modules");
74 cmd.arg("--exclude-dir=target");
75
76 cmd.arg("--").arg(pattern).arg(&path);
77
78 let output = tokio::time::timeout(Duration::from_secs(15), cmd.output())
79 .await
80 .map_err(|_| RuntimeError::Tool("Grep timed out after 15s".to_string()))?
81 .map_err(|e| RuntimeError::Tool(format!("Failed to execute grep: {}", e)))?;
82
83 let stdout = String::from_utf8_lossy(&output.stdout);
84
85 if stdout.is_empty() {
86 Ok("No matches found.".to_string())
87 } else {
88 let result = stdout.to_string();
89 let bounded = agent_core::BoundedText::new(&result, ctx.limits.max_tool_buffer);
90 if bounded.truncated {
91 Ok(format!(
92 "{}\n\n... (output truncated, {} total bytes)",
93 bounded.text, bounded.original_bytes
94 ))
95 } else {
96 Ok(result)
97 }
98 }
99 }
100}
101#[cfg(test)]
102mod tests {
103 use super::super::test_helpers::create_tool_context;
104 use super::*;
105 use crate::tools::Tool;
106 use serde_json::json;
107
108 #[test]
109 fn test_grep_tool_schema() {
110 let tool = GrepTool;
111 assert_eq!(tool.name(), "grep");
112 assert!(!tool.description().is_empty());
113
114 let params = tool.parameters();
115 assert_eq!(params["type"], "object");
116 assert!(params["properties"].is_object());
117 assert!(params["required"].is_array());
118 }
119
120 #[tokio::test]
121 async fn test_grep_tool_execution() {
122 let temp_dir = std::env::temp_dir();
123 let test_file = temp_dir.join("test_grep_tool_execution.txt");
124
125 let content = "hello world\nfoo bar\nhello again";
127 std::fs::write(&test_file, content).unwrap();
128
129 let tool = GrepTool;
130 let ctx = create_tool_context();
131
132 let params = json!({
133 "pattern": "hello",
134 "path": test_file.to_string_lossy()
135 });
136
137 let result = tool.execute(params, ctx).await.unwrap();
138
139 assert!(result.contains("hello world"));
141 assert!(result.contains("hello again"));
142 assert!(result.contains("1:") || result.contains("hello world"));
143 assert!(result.contains("3:") || result.contains("hello again"));
144
145 let _ = std::fs::remove_file(&test_file);
147 }
148}