86 lines
2.5 KiB
Rust
86 lines
2.5 KiB
Rust
|
|
// 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_json::json;
|
||
|
|
use std::path::PathBuf;
|
||
|
|
|
||
|
|
use crate::types::ToolDef;
|
||
|
|
|
||
|
|
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<String> {
|
||
|
|
let pattern = args["pattern"].as_str().context("pattern is required")?;
|
||
|
|
let base = args["path"].as_str().unwrap_or(".");
|
||
|
|
|
||
|
|
// Build the full pattern
|
||
|
|
let full_pattern = if pattern.starts_with('/') {
|
||
|
|
pattern.to_string()
|
||
|
|
} else {
|
||
|
|
format!("{}/{}", base, 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');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Truncate if too many
|
||
|
|
const MAX_OUTPUT: usize = 30000;
|
||
|
|
if output.len() > MAX_OUTPUT {
|
||
|
|
output.truncate(MAX_OUTPUT);
|
||
|
|
output.push_str("\n... (output truncated)");
|
||
|
|
}
|
||
|
|
|
||
|
|
output.push_str(&format!("\n({} files matched)", entries.len()));
|
||
|
|
Ok(output)
|
||
|
|
}
|