use crate::types::*;
use tokio::process::Command;
use std::path::Path;
pub struct GrepTool;
impl GrepTool {
pub fn new() -> Self {
Self
}
pub fn name(&self) -> &str {
"Grep"
}
pub fn description(&self) -> &str {
"Search file contents using regex patterns. Uses ripgrep (rg) if available, falls back to grep."
}
pub fn input_schema(&self) -> ToolInputSchema {
ToolInputSchema {
schema_type: "object".to_string(),
properties: serde_json::json!({
"pattern": {
"type": "string",
"description": "The regex pattern to search for"
},
"path": {
"type": "string",
"description": "File or directory to search in (defaults to cwd)"
},
"glob": {
"type": "string",
"description": "Glob pattern to filter files (e.g., \"*.ts\", \"*.{js,jsx}\")"
},
"type": {
"type": "string",
"description": "File type filter (e.g., \"ts\", \"py\", \"js\")"
},
"output_mode": {
"type": "string",
"enum": ["content", "files_with_matches", "count"],
"description": "Output mode (default: files_with_matches)"
},
"-i": {
"type": "boolean",
"description": "Case insensitive search"
},
"-n": {
"type": "boolean",
"description": "Show line numbers (default: true)"
},
"-A": {
"type": "number",
"description": "Lines after match"
},
"-B": {
"type": "number",
"description": "Lines before match"
},
"-C": {
"type": "number",
"description": "Context lines"
},
"context": {
"type": "number",
"description": "Context lines (alias for -C)"
},
"head_limit": {
"type": "number",
"description": "Limit output entries (default: 250)"
}
}),
required: Some(vec!["pattern".to_string()]),
}
}
pub async fn execute(
&self,
input: serde_json::Value,
context: &ToolContext,
) -> Result<ToolResult, crate::error::AgentError> {
let pattern = input["pattern"]
.as_str()
.ok_or_else(|| crate::error::AgentError::Tool("pattern is required".to_string()))?;
let search_path = input["path"]
.as_str()
.map(|p| {
if Path::new(p).is_absolute() {
p.to_string()
} else {
Path::new(&context.cwd).join(p).to_string_lossy().to_string()
}
})
.unwrap_or_else(|| context.cwd.clone());
let output_mode = input["output_mode"]
.as_str()
.unwrap_or("files_with_matches");
let head_limit = input["head_limit"]
.as_u64()
.unwrap_or(250) as usize;
let result = self.run_rg(input.clone(), pattern, &search_path, output_mode, head_limit).await;
match result {
Ok(output) => Ok(output),
Err(_) => {
self.run_grep(input.clone(), pattern, &search_path, output_mode, head_limit).await
}
}
}
async fn run_rg(
&self,
input: serde_json::Value,
pattern: &str,
search_path: &str,
output_mode: &str,
head_limit: usize,
) -> Result<ToolResult, crate::error::AgentError> {
let mut args: Vec<String> = vec![];
if output_mode == "files_with_matches" {
args.push("--files-with-matches".to_string());
} else if output_mode == "count" {
args.push("--count".to_string());
} else if output_mode == "content" && input["-n"].as_bool().unwrap_or(true) {
args.push("--line-number".to_string());
}
if input["-i"].as_bool().unwrap_or(false) {
args.push("--ignore-case".to_string());
}
if let Some(n) = input["-A"].as_u64() {
args.push("-A".to_string());
args.push(n.to_string());
}
if let Some(n) = input["-B"].as_u64() {
args.push("-B".to_string());
args.push(n.to_string());
}
let ctx = input["-C"].as_u64().or_else(|| input["context"].as_u64());
if let Some(n) = ctx {
args.push("-C".to_string());
args.push(n.to_string());
}
if let Some(glob) = input["glob"].as_str() {
args.push("--glob".to_string());
args.push(glob.to_string());
}
if let Some(t) = input["type"].as_str() {
args.push("--type".to_string());
args.push(t.to_string());
}
args.push("--".to_string());
args.push(pattern.to_string());
args.push(search_path.to_string());
let output = Command::new("rg")
.args(&args)
.output()
.await
.map_err(|e| crate::error::AgentError::Tool(e.to_string()))?;
if !output.status.success() {
return Err(crate::error::AgentError::Tool("rg failed".to_string()));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let result = stdout.trim();
if result.is_empty() {
return Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "".to_string(),
content: format!("No matches found for pattern \"{}\"", pattern),
is_error: None,
});
}
let content = self.apply_head_limit(result, head_limit);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "".to_string(),
content,
is_error: None,
})
}
async fn run_grep(
&self,
input: serde_json::Value,
pattern: &str,
search_path: &str,
output_mode: &str,
head_limit: usize,
) -> Result<ToolResult, crate::error::AgentError> {
let mut args: Vec<String> = vec!["-r".to_string()];
if input["-i"].as_bool().unwrap_or(false) {
args.push("-i".to_string());
}
if output_mode == "files_with_matches" {
args.push("-l".to_string());
} else if output_mode == "count" {
args.push("-c".to_string());
} else if output_mode == "content" && input["-n"].as_bool().unwrap_or(true) {
args.push("-n".to_string());
}
if let Some(glob) = input["glob"].as_str() {
args.push("--include".to_string());
args.push(glob.to_string());
}
args.push("--".to_string());
args.push(pattern.to_string());
args.push(search_path.to_string());
let output = Command::new("grep")
.args(&args)
.output()
.await
.map_err(|e| crate::error::AgentError::Tool(e.to_string()))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let result = stdout.trim();
if result.is_empty() {
return Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "".to_string(),
content: format!("No matches found for pattern \"{}\"", pattern),
is_error: None,
});
}
let content = self.apply_head_limit(result, head_limit);
Ok(ToolResult {
result_type: "text".to_string(),
tool_use_id: "".to_string(),
content,
is_error: None,
})
}
fn apply_head_limit(&self, result: &str, head_limit: usize) -> String {
if head_limit > 0 {
let lines: Vec<&str> = result.lines().collect();
if lines.len() > head_limit {
let limited: Vec<&str> = lines.iter().take(head_limit).cloned().collect();
let remaining = lines.len() - head_limit;
return format!("{}\n... ({} more)", limited.join("\n"), remaining);
}
}
result.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_grep_tool() {
tokio::fs::write("/tmp/test_grep.txt", "hello world\nfoo bar\ntest line").await.unwrap();
let tool = GrepTool::new();
let result = tool.execute(
serde_json::json!({"pattern": "hello", "path": "/tmp/test_grep.txt", "output_mode": "content"}),
&ToolContext { cwd: "/tmp".to_string(), abort_signal: None },
).await;
assert!(result.is_ok());
let content = result.unwrap().content;
assert!(content.contains("hello"));
}
}