code_mesh_core/tool/
glob.rs1use 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
11pub 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 let search_dir = if let Some(path) = ¶ms.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 if !search_dir.exists() {
75 return Err(ToolError::ExecutionFailed(format!(
76 "Directory not found: {}",
77 search_dir.display()
78 )));
79 }
80
81 let full_pattern = search_dir.join(¶ms.pattern).to_string_lossy().to_string();
83
84 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 let mut results: Vec<PathBuf> = paths.into_iter()
93 .filter(|path| path.is_file())
94 .collect();
95
96 results.sort();
98
99 if let Some(max) = params.max_results {
101 results.truncate(max);
102 }
103
104 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 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
140pub 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 let search_dir = if let Some(path) = ¶ms.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); 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 if !params.include_dirs && path.is_dir() {
245 continue;
246 }
247
248 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 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}