use async_trait::async_trait;
use regex::{Regex, RegexBuilder};
use std::path::{Path, PathBuf};
use std::collections::HashSet;
use std::ffi::OsStr;
use crate::integration::HostIntegration;
use crate::tools::{Tool, ToolResponse, ToolError, Permission, validation};
pub struct FileSearchTool;
impl FileSearchTool {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Tool for FileSearchTool {
async fn execute(
&self,
parameters: serde_json::Value,
_host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let pattern = validation::require_string(¶meters, "pattern")?;
let search_path = validation::optional_path(¶meters, "path")
.unwrap_or_else(|| PathBuf::from("."));
let max_results = parameters.get("max_results")
.and_then(|v| v.as_u64())
.unwrap_or(100) as usize;
validation::validate_safe_path(&search_path)?;
let regex = Regex::new(&pattern)
.map_err(|e| ToolError::InvalidParameters(format!("Invalid regex pattern: {}", e)))?;
let mut results = Vec::new();
fn search_recursive(
dir: &std::path::Path,
regex: &Regex,
results: &mut Vec<PathBuf>,
max_results: usize,
) -> Result<(), ToolError> {
if results.len() >= max_results {
return Ok(());
}
let entries = std::fs::read_dir(dir)
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
for entry in entries {
if results.len() >= max_results {
break;
}
let entry = entry.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
let path = entry.path();
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if regex.is_match(file_name) {
results.push(path.clone());
}
if path.is_dir() && !file_name.starts_with('.') {
search_recursive(&path, regex, results, max_results)?;
}
}
}
Ok(())
}
search_recursive(&search_path, ®ex, &mut results, max_results)?;
if results.is_empty() {
Ok(ToolResponse::success("No files found matching the pattern".to_string()))
} else {
let result_text = results
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join("\n");
let metadata = serde_json::json!({
"pattern": pattern,
"search_path": search_path,
"total_results": results.len(),
"max_results": max_results,
});
Ok(ToolResponse::with_metadata(result_text, metadata))
}
}
fn requires_permission(&self) -> Permission {
Permission::ReadFile(PathBuf::new())
}
fn description(&self) -> &str {
"Search for files by name pattern using regex"
}
fn name(&self) -> &str {
"file_search"
}
fn parameter_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for in file names"
},
"path": {
"type": "string",
"description": "Directory to search in (defaults to current directory)"
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return (default: 100)"
}
},
"required": ["pattern"]
})
}
fn clone_box(&self) -> Box<dyn Tool> {
Box::new(FileSearchTool::new())
}
}
pub struct ContentSearchTool;
impl ContentSearchTool {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Tool for ContentSearchTool {
async fn execute(
&self,
parameters: serde_json::Value,
host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let pattern = validation::require_string(¶meters, "pattern")?;
let file_path = validation::require_path(¶meters, "file")?;
let context_lines = parameters.get("context_lines")
.and_then(|v| v.as_u64())
.unwrap_or(2) as usize;
validation::validate_safe_path(&file_path)?;
let regex = Regex::new(&pattern)
.map_err(|e| ToolError::InvalidParameters(format!("Invalid regex pattern: {}", e)))?;
let content = host.get_file_content(&file_path).await
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
let lines: Vec<&str> = content.lines().collect();
let mut matches = Vec::new();
for (line_num, line) in lines.iter().enumerate() {
if regex.is_match(line) {
let start = line_num.saturating_sub(context_lines);
let end = std::cmp::min(line_num + context_lines + 1, lines.len());
let mut context = Vec::new();
for i in start..end {
let marker = if i == line_num { ">" } else { " " };
context.push(format!("{} {:4}: {}", marker, i + 1, lines[i]));
}
matches.push(format!("Match at line {}:\n{}", line_num + 1, context.join("\n")));
}
}
if matches.is_empty() {
Ok(ToolResponse::success("No matches found".to_string()))
} else {
let result_text = matches.join("\n\n");
let metadata = serde_json::json!({
"pattern": pattern,
"file": file_path,
"total_matches": matches.len(),
"context_lines": context_lines,
});
Ok(ToolResponse::with_metadata(result_text, metadata))
}
}
fn requires_permission(&self) -> Permission {
Permission::ReadFile(PathBuf::new())
}
fn description(&self) -> &str {
"Search for content within a file using regex"
}
fn name(&self) -> &str {
"content_search"
}
fn parameter_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for in file content"
},
"file": {
"type": "string",
"description": "Path to the file to search in"
},
"context_lines": {
"type": "integer",
"description": "Number of context lines to show around matches (default: 2)"
}
},
"required": ["pattern", "file"]
})
}
fn clone_box(&self) -> Box<dyn Tool> {
Box::new(ContentSearchTool::new())
}
}
pub struct GrepTool;
impl GrepTool {
pub fn new() -> Self {
Self
}
fn is_binary_file(path: &Path) -> bool {
if let Ok(mut file) = std::fs::File::open(path) {
use std::io::Read;
let mut buffer = [0; 512];
if let Ok(bytes_read) = file.read(&mut buffer) {
let null_count = buffer[..bytes_read].iter().filter(|&&b| b == 0).count();
let non_printable = buffer[..bytes_read].iter()
.filter(|&&b| b < 32 && b != 9 && b != 10 && b != 13)
.count();
return null_count > 0 || (non_printable as f64 / bytes_read as f64) > 0.3;
}
}
false
}
fn matches_file_types(path: &Path, file_types: &HashSet<String>) -> bool {
if file_types.is_empty() {
return true;
}
if let Some(extension) = path.extension().and_then(OsStr::to_str) {
file_types.contains(&extension.to_lowercase())
} else {
file_types.contains("none")
}
}
fn parse_file_types(types_str: &str) -> HashSet<String> {
types_str.split(',')
.map(|s| s.trim().to_lowercase())
.filter(|s| !s.is_empty())
.collect()
}
}
#[async_trait]
impl Tool for GrepTool {
async fn execute(
&self,
parameters: serde_json::Value,
host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let pattern = validation::require_string(¶meters, "pattern")?;
let search_path = validation::optional_path(¶meters, "path")
.unwrap_or_else(|| PathBuf::from("."));
let case_sensitive = parameters.get("case_sensitive")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let recursive = parameters.get("recursive")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let include_binary = parameters.get("include_binary")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let context_before = parameters.get("context_before")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let context_after = parameters.get("context_after")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let max_results = parameters.get("max_results")
.and_then(|v| v.as_u64())
.unwrap_or(1000) as usize;
let file_types = if let Some(types) = parameters.get("file_types").and_then(|v| v.as_str()) {
Self::parse_file_types(types)
} else {
HashSet::new()
};
validation::validate_safe_path(&search_path)?;
let regex = RegexBuilder::new(&pattern)
.case_insensitive(!case_sensitive)
.build()
.map_err(|e| ToolError::InvalidParameters(format!("Invalid regex pattern: {}", e)))?;
let mut all_matches = Vec::new();
let mut files_searched = 0;
let mut files_with_matches = 0;
fn search_directory(
dir: &Path,
regex: &Regex,
file_types: &HashSet<String>,
include_binary: bool,
context_before: usize,
context_after: usize,
recursive: bool,
matches: &mut Vec<String>,
files_searched: &mut usize,
files_with_matches: &mut usize,
max_results: usize,
) -> Result<(), ToolError> {
if matches.len() >= max_results {
return Ok(());
}
let entries = std::fs::read_dir(dir)
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to read directory {}: {}", dir.display(), e)))?;
for entry in entries {
if matches.len() >= max_results {
break;
}
let entry = entry.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
let path = entry.path();
if path.is_file() {
if !GrepTool::matches_file_types(&path, file_types) {
continue;
}
if !include_binary && GrepTool::is_binary_file(&path) {
continue;
}
*files_searched += 1;
if let Ok(content) = std::fs::read_to_string(&path) {
let lines: Vec<&str> = content.lines().collect();
let mut file_matches = Vec::new();
for (line_num, line) in lines.iter().enumerate() {
if regex.is_match(line) {
let start = line_num.saturating_sub(context_before);
let end = std::cmp::min(line_num + context_after + 1, lines.len());
let mut context = Vec::new();
for i in start..end {
let marker = if i == line_num { ">" } else { " " };
context.push(format!("{} {:4}: {}", marker, i + 1, lines[i]));
}
file_matches.push(format!(" Line {}:\n{}", line_num + 1, context.join("\n")));
}
}
if !file_matches.is_empty() {
*files_with_matches += 1;
let file_result = format!("{}:\n{}", path.display(), file_matches.join("\n\n"));
matches.push(file_result);
}
}
} else if path.is_dir() && recursive {
if let Some(dir_name) = path.file_name().and_then(OsStr::to_str) {
if !dir_name.starts_with('.') {
search_directory(
&path, regex, file_types, include_binary,
context_before, context_after, recursive,
matches, files_searched, files_with_matches, max_results
)?;
}
}
}
}
Ok(())
}
if search_path.is_file() {
if GrepTool::matches_file_types(&search_path, &file_types) &&
(include_binary || !GrepTool::is_binary_file(&search_path)) {
files_searched = 1;
let content = host.get_file_content(&search_path).await
.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
let lines: Vec<&str> = content.lines().collect();
let mut file_matches = Vec::new();
for (line_num, line) in lines.iter().enumerate() {
if regex.is_match(line) {
let start = line_num.saturating_sub(context_before);
let end = std::cmp::min(line_num + context_after + 1, lines.len());
let mut context = Vec::new();
for i in start..end {
let marker = if i == line_num { ">" } else { " " };
context.push(format!("{} {:4}: {}", marker, i + 1, lines[i]));
}
file_matches.push(format!(" Line {}:\n{}", line_num + 1, context.join("\n")));
}
}
if !file_matches.is_empty() {
files_with_matches = 1;
let file_result = format!("{}:\n{}", search_path.display(), file_matches.join("\n\n"));
all_matches.push(file_result);
}
}
} else {
search_directory(
&search_path, ®ex, &file_types, include_binary,
context_before, context_after, recursive,
&mut all_matches, &mut files_searched, &mut files_with_matches, max_results
)?;
}
let result_text = if all_matches.is_empty() {
format!("No matches found for pattern '{}' in {} files searched", pattern, files_searched)
} else {
all_matches.join("\n\n")
};
let metadata = serde_json::json!({
"pattern": pattern,
"search_path": search_path,
"case_sensitive": case_sensitive,
"recursive": recursive,
"include_binary": include_binary,
"context_before": context_before,
"context_after": context_after,
"file_types": file_types.iter().collect::<Vec<_>>(),
"files_searched": files_searched,
"files_with_matches": files_with_matches,
"total_matches": all_matches.len(),
"max_results": max_results,
});
Ok(ToolResponse::with_metadata(result_text, metadata))
}
fn requires_permission(&self) -> Permission {
Permission::ReadFile(PathBuf::new())
}
fn description(&self) -> &str {
"Advanced grep tool with regex patterns, file type filtering, and context lines"
}
fn name(&self) -> &str {
"grep"
}
fn parameter_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for"
},
"path": {
"type": "string",
"description": "File or directory to search in (defaults to current directory)"
},
"case_sensitive": {
"type": "boolean",
"description": "Whether the search should be case sensitive (default: true)"
},
"recursive": {
"type": "boolean",
"description": "Whether to search subdirectories recursively (default: true)"
},
"include_binary": {
"type": "boolean",
"description": "Whether to include binary files in search (default: false)"
},
"context_before": {
"type": "integer",
"description": "Number of lines to show before each match (default: 0)"
},
"context_after": {
"type": "integer",
"description": "Number of lines to show after each match (default: 0)"
},
"file_types": {
"type": "string",
"description": "Comma-separated list of file extensions to include (e.g., 'rs,go,py')"
},
"max_results": {
"type": "integer",
"description": "Maximum number of matches to return (default: 1000)"
}
},
"required": ["pattern"]
})
}
fn clone_box(&self) -> Box<dyn Tool> {
Box::new(GrepTool::new())
}
}