code_mesh_core/tool/
glob.rs

1//! Glob tool implementation for file pattern matching
2
3use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::{json, Value};
6use std::path::PathBuf;
7use tokio::fs;
8
9use super::{Tool, ToolContext, ToolResult, ToolError};
10
11/// Tool for finding files using glob patterns
12pub struct GlobTool;
13
14#[derive(Debug, Deserialize)]
15struct GlobParams {
16    pattern: String,
17    #[serde(default)]
18    path: Option<String>,
19    #[serde(default)]
20    max_results: Option<usize>,
21}
22
23#[async_trait]
24impl Tool for GlobTool {
25    fn id(&self) -> &str {
26        "glob"
27    }
28    
29    fn description(&self) -> &str {
30        "Find files matching glob patterns"
31    }
32    
33    fn parameters_schema(&self) -> Value {
34        json!({
35            "type": "object",
36            "properties": {
37                "pattern": {
38                    "type": "string",
39                    "description": "Glob pattern to match files (e.g., '*.rs', '**/*.js', 'src/**/*.{ts,tsx}')"
40                },
41                "path": {
42                    "type": "string",
43                    "description": "Directory to search in (default: current directory)"
44                },
45                "max_results": {
46                    "type": "integer",
47                    "description": "Maximum number of results to return"
48                }
49            },
50            "required": ["pattern"]
51        })
52    }
53    
54    async fn execute(
55        &self,
56        args: Value,
57        ctx: ToolContext,
58    ) -> Result<ToolResult, ToolError> {
59        let params: GlobParams = serde_json::from_value(args)
60            .map_err(|e| ToolError::InvalidParameters(e.to_string()))?;
61        
62        // Determine search directory
63        let search_dir = if let Some(path) = &params.path {
64            if PathBuf::from(path).is_absolute() {
65                PathBuf::from(path)
66            } else {
67                ctx.working_directory.join(path)
68            }
69        } else {
70            ctx.working_directory.clone()
71        };
72        
73        // Validate directory exists
74        if !search_dir.exists() {
75            return Err(ToolError::ExecutionFailed(format!(
76                "Directory not found: {}",
77                search_dir.display()
78            )));
79        }
80        
81        // Build full pattern
82        let full_pattern = search_dir.join(&params.pattern).to_string_lossy().to_string();
83        
84        // Use glob crate to find matches
85        let matches: Result<Vec<_>, _> = glob::glob(&full_pattern)
86            .map_err(|e| ToolError::InvalidParameters(format!("Invalid glob pattern: {}", e)))?
87            .collect();
88        
89        let paths = matches.map_err(|e| ToolError::ExecutionFailed(format!("Glob error: {}", e)))?;
90        
91        // Filter and limit results
92        let mut results: Vec<PathBuf> = paths.into_iter()
93            .filter(|path| path.is_file())
94            .collect();
95        
96        // Sort results for consistent output
97        results.sort();
98        
99        // Apply limit
100        if let Some(max) = params.max_results {
101            results.truncate(max);
102        }
103        
104        // Convert to relative paths for cleaner output
105        let relative_results: Vec<String> = results.iter()
106            .filter_map(|path| {
107                path.strip_prefix(&ctx.working_directory)
108                    .ok()
109                    .map(|p| p.to_string_lossy().to_string())
110                    .or_else(|| Some(path.to_string_lossy().to_string()))
111            })
112            .collect();
113        
114        // Create output
115        let output = if relative_results.is_empty() {
116            "No files found matching pattern".to_string()
117        } else {
118            relative_results.join("\n")
119        };
120        
121        let metadata = json!({
122            "pattern": params.pattern,
123            "search_directory": search_dir.to_string_lossy(),
124            "matches": relative_results.len(),
125            "files": relative_results,
126        });
127        
128        Ok(ToolResult {
129            title: format!("Found {} file{} matching '{}'", 
130                relative_results.len(),
131                if relative_results.len() == 1 { "" } else { "s" },
132                params.pattern
133            ),
134            metadata,
135            output,
136        })
137    }
138}
139
140/// Enhanced glob tool with more sophisticated patterns
141pub struct GlobAdvancedTool;
142
143#[derive(Debug, Deserialize)]
144struct AdvancedGlobParams {
145    pattern: String,
146    #[serde(default)]
147    path: Option<String>,
148    #[serde(default)]
149    max_results: Option<usize>,
150    #[serde(default)]
151    include_dirs: bool,
152    #[serde(default)]
153    case_insensitive: bool,
154    #[serde(default)]
155    follow_symlinks: bool,
156}
157
158#[async_trait]
159impl Tool for GlobAdvancedTool {
160    fn id(&self) -> &str {
161        "glob_advanced"
162    }
163    
164    fn description(&self) -> &str {
165        "Advanced file pattern matching with additional options"
166    }
167    
168    fn parameters_schema(&self) -> Value {
169        json!({
170            "type": "object",
171            "properties": {
172                "pattern": {
173                    "type": "string",
174                    "description": "Glob pattern to match files"
175                },
176                "path": {
177                    "type": "string",
178                    "description": "Directory to search in"
179                },
180                "max_results": {
181                    "type": "integer",
182                    "description": "Maximum number of results"
183                },
184                "include_dirs": {
185                    "type": "boolean",
186                    "description": "Include directories in results",
187                    "default": false
188                },
189                "case_insensitive": {
190                    "type": "boolean",
191                    "description": "Case insensitive matching",
192                    "default": false
193                },
194                "follow_symlinks": {
195                    "type": "boolean",
196                    "description": "Follow symbolic links",
197                    "default": false
198                }
199            },
200            "required": ["pattern"]
201        })
202    }
203    
204    async fn execute(
205        &self,
206        args: Value,
207        ctx: ToolContext,
208    ) -> Result<ToolResult, ToolError> {
209        let params: AdvancedGlobParams = serde_json::from_value(args)
210            .map_err(|e| ToolError::InvalidParameters(e.to_string()))?;
211        
212        // Use walkdir for more advanced traversal
213        let search_dir = if let Some(path) = &params.path {
214            if PathBuf::from(path).is_absolute() {
215                PathBuf::from(path)
216            } else {
217                ctx.working_directory.join(path)
218            }
219        } else {
220            ctx.working_directory.clone()
221        };
222        
223        let walker = walkdir::WalkDir::new(&search_dir)
224            .follow_links(params.follow_symlinks)
225            .max_depth(100); // Reasonable limit
226        
227        let pattern = if params.case_insensitive {
228            params.pattern.to_lowercase()
229        } else {
230            params.pattern.clone()
231        };
232        
233        let mut matches = Vec::new();
234        
235        for entry in walker {
236            if *ctx.abort_signal.borrow() {
237                return Err(ToolError::Aborted);
238            }
239            
240            let entry = entry.map_err(|e| ToolError::ExecutionFailed(e.to_string()))?;
241            let path = entry.path();
242            
243            // Filter by type
244            if !params.include_dirs && path.is_dir() {
245                continue;
246            }
247            
248            // Check pattern match
249            let path_str = path.to_string_lossy();
250            let check_str = if params.case_insensitive {
251                path_str.to_lowercase()
252            } else {
253                path_str.to_string()
254            };
255            
256            if glob::Pattern::new(&pattern)
257                .map_err(|e| ToolError::InvalidParameters(e.to_string()))?
258                .matches(&check_str) 
259            {
260                matches.push(path.to_path_buf());
261                
262                if let Some(max) = params.max_results {
263                    if matches.len() >= max {
264                        break;
265                    }
266                }
267            }
268        }
269        
270        // Convert to relative paths
271        let relative_matches: Vec<String> = matches.iter()
272            .filter_map(|path| {
273                path.strip_prefix(&ctx.working_directory)
274                    .ok()
275                    .map(|p| p.to_string_lossy().to_string())
276                    .or_else(|| Some(path.to_string_lossy().to_string()))
277            })
278            .collect();
279        
280        let output = if relative_matches.is_empty() {
281            "No matches found".to_string()
282        } else {
283            relative_matches.join("\n")
284        };
285        
286        let metadata = json!({
287            "pattern": params.pattern,
288            "search_directory": search_dir.to_string_lossy(),
289            "matches": relative_matches.len(),
290            "include_dirs": params.include_dirs,
291            "case_insensitive": params.case_insensitive,
292        });
293        
294        Ok(ToolResult {
295            title: format!("Found {} match{}", 
296                relative_matches.len(),
297                if relative_matches.len() == 1 { "" } else { "es" }
298            ),
299            metadata,
300            output,
301        })
302    }
303}