use std::fs; use serde_json::{json, Value}; use anyhow::{Result, anyhow}; use ignore::Walk; use globset::{GlobBuilder, GlobSetBuilder}; use super::Tool; use super::ToolCtx; use super::resolve_path; pub struct Grep; impl Tool for Grep { fn name(&self) -> &'static str { "grep" } fn description(&self) -> &'static str { "Search for a pattern in files using recursive text search" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "pattern": { "type": "string", "description": "Text pattern to search for" }, "path": { "type": "string", "description": "Path to search in (relative to workspace root)" } }, "required": ["pattern", "path"] }) } fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let pattern = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: pattern"))? .to_string(); let rel = args.get("path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))? .to_string(); let path = resolve_path(&ctx.workspaces, &rel)?; if !path.exists() { anyhow::bail!("path '{}' does not exist", rel); } if !path.is_dir() { anyhow::bail!("path '{}' is not a directory", rel); } let mut results: Vec<(String, usize, String)> = Vec::new(); for entry in Walk::new(&path).flatten() { let file_path = entry.path(); if !file_path.is_file() { continue; } if let Ok(content) = fs::read_to_string(file_path) { for (i, line) in content.lines().enumerate() { if line.contains(&pattern) { let rel_path = file_path.strip_prefix(&path) .unwrap_or(file_path) .display() .to_string(); results.push((rel_path, i + 1, line.to_string())); } } } } if results.is_empty() { return Ok(format!("no matches found for '{}' in {}", pattern, rel)); } let output = results.iter() .map(|(f, line, text)| format!("{}:{}:{}", f, line, text)) .collect::>() .join("\n"); Ok(format!("found {} matches:\n{}", results.len(), output)) } } pub struct Glob; impl Tool for Glob { fn name(&self) -> &'static str { "glob" } fn description(&self) -> &'static str { "List files matching a glob pattern" } fn parameters(&self) -> Value { json!({ "type": "object", "properties": { "pattern": { "type": "string", "description": "Glob pattern to match files (e.g. '**/*.rs')" }, "path": { "type": "string", "description": "Root path to search from (relative to workspace root)" } }, "required": ["pattern", "path"] }) } fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let pat_str = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: pattern"))? .to_string(); let rel = args.get("path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow!("missing required argument: path"))? .to_string(); let root = resolve_path(&ctx.workspaces, &rel)?; if !root.exists() || !root.is_dir() { anyhow::bail!("path '{}' is not a valid directory", rel); } let mut builder = GlobSetBuilder::new(); let full_pattern = root.join(&pat_str).display().to_string(); builder.add(GlobBuilder::new(&full_pattern).build() .map_err(|e| anyhow!("invalid glob pattern '{}': {}", pat_str, e))?); let glob_set = builder.build() .map_err(|e| anyhow!("failed to build glob set: {}", e))?; let mut matches: Vec = Vec::new(); for entry in Walk::new(&root).flatten() { let p = entry.path(); if glob_set.is_match(p) { let rel_path = p.strip_prefix(&root) .unwrap_or(p) .display() .to_string(); matches.push(format!("{}{}", rel_path, if p.is_dir() { "/" } else { "" })); } } matches.sort(); if matches.is_empty() { return Ok(format!("no files match '{}' in {}", pat_str, rel)); } Ok(matches.join("\n")) } }