use super::{Tool, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use ignore::WalkBuilder;
use regex::Regex;
use serde_json::{Value, json};
pub struct GrepTool;
impl GrepTool {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Tool for GrepTool {
fn id(&self) -> &str {
"grep"
}
fn name(&self) -> &str {
"Grep Search"
}
fn description(&self) -> &str {
"grep(pattern: string, path?: string, is_regex?: bool, include?: string, limit?: int) - Search for text or regex patterns in files. Respects .gitignore by default."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "The text or regex pattern to search for"
},
"path": {
"type": "string",
"description": "Directory or file to search in (default: current directory)"
},
"is_regex": {
"type": "boolean",
"description": "Whether the pattern is a regex (default: false)"
},
"include": {
"type": "string",
"description": "Glob pattern to include files (e.g., *.rs)"
},
"limit": {
"type": "integer",
"description": "Maximum number of matches to return"
}
},
"required": ["pattern"],
"example": {
"pattern": "fn main",
"path": "src/",
"include": "*.rs"
}
})
}
async fn execute(&self, args: Value) -> Result<ToolResult> {
let pattern = match args["pattern"].as_str() {
Some(p) => p,
None => {
return Ok(ToolResult::structured_error(
"INVALID_ARGUMENT",
"grep",
"pattern is required",
Some(vec!["pattern"]),
Some(json!({"pattern": "search text", "path": "src/"})),
));
}
};
let search_path = args["path"].as_str().unwrap_or(".");
let is_regex = args["is_regex"].as_bool().unwrap_or(false);
let include = args["include"].as_str();
let limit = args["limit"].as_u64().unwrap_or(50) as usize;
let regex = if is_regex {
Regex::new(pattern)?
} else {
Regex::new(®ex::escape(pattern))?
};
let mut results = Vec::new();
let mut walker = WalkBuilder::new(search_path);
walker.hidden(false).git_ignore(true);
for entry in walker.build() {
if results.len() >= limit {
break;
}
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
continue;
}
let path = entry.path();
if let Some(include_pattern) = include {
if !glob::Pattern::new(include_pattern)
.map(|p| p.matches_path(path))
.unwrap_or(false)
{
continue;
}
}
if let Ok(content) = tokio::fs::read_to_string(path).await {
for (line_num, line) in content.lines().enumerate() {
if results.len() >= limit {
break;
}
if regex.is_match(line) {
results.push(format!(
"{}:{}: {}",
path.display(),
line_num + 1,
line.trim()
));
}
}
}
}
let truncated = results.len() >= limit;
let output = results.join("\n");
Ok(ToolResult::success(output)
.with_metadata("count", json!(results.len()))
.with_metadata("truncated", json!(truncated)))
}
}