Skip to main content

dot/tools/
glob.rs

1use anyhow::{Context, Result};
2use serde_json::Value;
3use std::path::Path;
4
5use super::Tool;
6
7pub struct GlobTool;
8
9impl Tool for GlobTool {
10    fn name(&self) -> &str {
11        "glob"
12    }
13
14    fn description(&self) -> &str {
15        "Find files matching a glob pattern. Returns file paths sorted by modification time (newest first). Use for locating files by name."
16    }
17
18    fn input_schema(&self) -> Value {
19        serde_json::json!({
20            "type": "object",
21            "properties": {
22                "pattern": {
23                    "type": "string",
24                    "description": "Glob pattern (e.g. '**/*.rs', 'src/**/*.ts', '*.json')"
25                },
26                "path": {
27                    "type": "string",
28                    "description": "Base directory to search from (defaults to current directory)"
29                }
30            },
31            "required": ["pattern"]
32        })
33    }
34
35    fn execute(&self, input: Value) -> Result<String> {
36        let pattern = input["pattern"]
37            .as_str()
38            .context("Missing required parameter 'pattern'")?;
39        let base = input["path"].as_str().unwrap_or(".");
40        tracing::debug!("glob: {} in {}", pattern, base);
41
42        let full = if Path::new(pattern).is_absolute() {
43            pattern.to_string()
44        } else {
45            format!("{}/{}", base, pattern)
46        };
47
48        let mut entries: Vec<(String, std::time::SystemTime)> = Vec::new();
49        for path in glob::glob(&full).with_context(|| format!("invalid glob: {}", pattern))? {
50            if entries.len() >= 200 {
51                break;
52            }
53            if let Ok(p) = path {
54                let mtime = p
55                    .metadata()
56                    .and_then(|m| m.modified())
57                    .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
58                entries.push((p.display().to_string(), mtime));
59            }
60        }
61
62        entries.sort_by(|a, b| b.1.cmp(&a.1));
63
64        if entries.is_empty() {
65            Ok(format!("No files matching '{}'", pattern))
66        } else {
67            let count = entries.len();
68            let paths: Vec<String> = entries.into_iter().map(|(p, _)| p).collect();
69            let mut output = paths.join("\n");
70            if count >= 200 {
71                output.push_str("\n... (truncated at 200 results)");
72            }
73            Ok(output)
74        }
75    }
76}