Skip to main content

stynx_code_tools/infrastructure/
glob_tool.rs

1use stynx_code_errors::{AppError, AppResult};
2use stynx_code_types::{PermissionLevel, SearchReadInfo, Tool};
3use serde_json::{Value, json};
4
5pub struct GlobTool;
6
7#[async_trait::async_trait]
8impl Tool for GlobTool {
9    fn name(&self) -> &str {
10        "glob"
11    }
12
13    fn description(&self) -> &str {
14        "Find files matching a glob pattern. Returns paths sorted by modification time (newest first)."
15    }
16
17    fn input_schema(&self) -> Value {
18        json!({
19            "type": "object",
20            "properties": {
21                "pattern": {
22                    "type": "string",
23                    "description": "Glob pattern to match files (e.g. \"**/*.rs\", \"src/**/*.ts\")"
24                },
25                "path": {
26                    "type": "string",
27                    "description": "Base directory to search in (defaults to current working directory)"
28                }
29            },
30            "required": ["pattern"]
31        })
32    }
33
34    fn permission_level(&self) -> PermissionLevel {
35        PermissionLevel::ReadOnly
36    }
37
38    fn is_read_only(&self, _input: &Value) -> bool { true }
39    fn is_concurrent_safe(&self, _input: &Value) -> bool { true }
40
41    fn is_search_or_read_command(&self, _input: &Value) -> SearchReadInfo {
42        SearchReadInfo { is_search: true, is_read: false, is_list: false }
43    }
44
45    async fn execute(&self, input: Value) -> AppResult<String> {
46        let pattern = input
47            .get("pattern")
48            .and_then(|v| v.as_str())
49            .ok_or_else(|| AppError::Tool("missing 'pattern' field".into()))?;
50
51        let base = input
52            .get("path")
53            .and_then(|v| v.as_str())
54            .map(|s| s.to_string())
55            .unwrap_or_else(|| {
56                std::env::current_dir()
57                    .map(|p| p.display().to_string())
58                    .unwrap_or_else(|_| ".".into())
59            });
60
61        tracing::info!(pattern, base, "globbing files");
62
63        let full_pattern = if pattern.starts_with('/') {
64            pattern.to_string()
65        } else {
66            format!("{base}/{pattern}")
67        };
68
69        let entries = glob::glob(&full_pattern)
70            .map_err(|e| AppError::Tool(format!("invalid glob pattern: {e}")))?;
71
72        let mut files: Vec<(std::path::PathBuf, std::time::SystemTime)> = Vec::new();
73        for entry in entries {
74            if let Ok(path) = entry
75                && path.is_file() {
76                    let mtime = path
77                        .metadata()
78                        .and_then(|m| m.modified())
79                        .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
80                    files.push((path, mtime));
81                }
82        }
83
84        files.sort_by(|a, b| b.1.cmp(&a.1));
85        files.truncate(200);
86
87        if files.is_empty() {
88            return Ok("No files matched.".into());
89        }
90
91        let result: Vec<String> = files.iter().map(|(p, _)| p.display().to_string()).collect();
92        Ok(format!("{} files matched:\n{}", result.len(), result.join("\n")))
93    }
94}