// tools/glob_tool.rs — Find files by pattern // // Fast file discovery using glob patterns. Returns matching paths // sorted by modification time (newest first), which is usually // what you want when exploring a codebase. use anyhow::{Context, Result}; use serde::Deserialize; use serde_json::json; use std::path::PathBuf; use super::ToolDef; #[derive(Deserialize)] struct Args { pattern: String, #[serde(default = "default_path")] path: String, } fn default_path() -> String { ".".into() } pub fn definition() -> ToolDef { ToolDef::new( "glob", "Find files matching a glob pattern. Returns file paths sorted by \ modification time (newest first). Use patterns like '**/*.rs', \ 'src/**/*.ts', or 'Cargo.toml'.", json!({ "type": "object", "properties": { "pattern": { "type": "string", "description": "Glob pattern to match files (e.g. '**/*.rs')" }, "path": { "type": "string", "description": "Base directory to search from (default: current directory)" } }, "required": ["pattern"] }), ) } pub fn glob_search(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid glob arguments")?; let full_pattern = if a.pattern.starts_with('/') { a.pattern.clone() } else { format!("{}/{}", a.path, a.pattern) }; let mut entries: Vec<(PathBuf, std::time::SystemTime)> = Vec::new(); for entry in glob::glob(&full_pattern) .with_context(|| format!("Invalid glob pattern: {}", full_pattern))? { if let Ok(path) = entry { if path.is_file() { let mtime = path .metadata() .and_then(|m| m.modified()) .unwrap_or(std::time::SystemTime::UNIX_EPOCH); entries.push((path, mtime)); } } } // Sort by modification time, newest first entries.sort_by(|a, b| b.1.cmp(&a.1)); if entries.is_empty() { return Ok("No files matched.".to_string()); } let mut output = String::new(); for (path, _) in &entries { output.push_str(&path.display().to_string()); output.push('\n'); } output.push_str(&format!("\n({} files matched)", entries.len())); Ok(super::truncate_output(output, 30000)) }