stynx_code_tools/infrastructure/
glob_tool.rs1use 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}