Skip to main content

synaps_cli/tools/
grep.rs

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