claude_rust_tools/infrastructure/
grep_tool.rs1use claude_rust_errors::{AppError, AppResult};
2use claude_rust_types::{PermissionLevel, Tool};
3use serde_json::{Value, json};
4use tokio::process::Command;
5
6pub struct GrepTool;
7
8#[async_trait::async_trait]
9impl Tool for GrepTool {
10 fn name(&self) -> &str {
11 "grep"
12 }
13
14 fn description(&self) -> &str {
15 "Search file contents using regex. Uses ripgrep (rg) if available, falls back to grep."
16 }
17
18 fn input_schema(&self) -> Value {
19 json!({
20 "type": "object",
21 "properties": {
22 "pattern": {
23 "type": "string",
24 "description": "Regular expression pattern to search for"
25 },
26 "path": {
27 "type": "string",
28 "description": "File or directory to search in (defaults to current working directory)"
29 },
30 "glob": {
31 "type": "string",
32 "description": "Glob pattern to filter files (e.g. \"*.rs\", \"*.{ts,tsx}\")"
33 }
34 },
35 "required": ["pattern"]
36 })
37 }
38
39 fn permission_level(&self) -> PermissionLevel {
40 PermissionLevel::ReadOnly
41 }
42
43 async fn execute(&self, input: Value) -> AppResult<String> {
44 let pattern = input
45 .get("pattern")
46 .and_then(|v| v.as_str())
47 .ok_or_else(|| AppError::Tool("missing 'pattern' field".into()))?;
48
49 let search_path = input
50 .get("path")
51 .and_then(|v| v.as_str())
52 .unwrap_or(".");
53
54 let file_glob = input.get("glob").and_then(|v| v.as_str());
55
56 tracing::info!(pattern, search_path, "grepping");
57
58 let output = match try_ripgrep(pattern, search_path, file_glob).await {
60 Ok(out) => out,
61 Err(_) => try_grep(pattern, search_path).await?,
62 };
63
64 if output.is_empty() {
65 return Ok("No matches found.".into());
66 }
67
68 let mut result = output;
69 if result.len() > 100_000 {
70 result.truncate(100_000);
71 result.push_str("\n... (truncated)");
72 }
73
74 Ok(result)
75 }
76}
77
78async fn try_ripgrep(
79 pattern: &str,
80 path: &str,
81 file_glob: Option<&str>,
82) -> AppResult<String> {
83 let mut cmd = Command::new("rg");
84 cmd.arg("-n").arg("--no-heading").arg(pattern);
85
86 if let Some(g) = file_glob {
87 cmd.arg("--glob").arg(g);
88 }
89
90 cmd.arg(path);
91
92 let output = cmd
93 .output()
94 .await
95 .map_err(|e| AppError::Tool(format!("rg not available: {e}")))?;
96
97 if output.status.code() == Some(2) {
99 return Err(AppError::Tool("ripgrep error".into()));
100 }
101
102 Ok(String::from_utf8_lossy(&output.stdout).to_string())
103}
104
105async fn try_grep(pattern: &str, path: &str) -> AppResult<String> {
106 let output = Command::new("grep")
107 .arg("-rn")
108 .arg(pattern)
109 .arg(path)
110 .output()
111 .await
112 .map_err(|e| AppError::Tool(format!("grep failed: {e}")))?;
113
114 Ok(String::from_utf8_lossy(&output.stdout).to_string())
115}