| 45 | } |
| 46 | |
| 47 | async fn execute(&self, args: &serde_json::Value, ctx: &ToolContext) -> Result<ToolOutput> { |
| 48 | let pattern = match args.get("pattern").and_then(|v| v.as_str()) { |
| 49 | Some(p) => p, |
| 50 | None => return Ok(ToolOutput::error("pattern parameter is required")), |
| 51 | }; |
| 52 | |
| 53 | let path_str = args.get("path").and_then(|v| v.as_str()).unwrap_or("."); |
| 54 | let base = match ctx.resolve_workspace_path(path_str) { |
| 55 | Ok(path) => path, |
| 56 | Err(e) => return Ok(ToolOutput::error(format!("Failed to resolve path: {}", e))), |
| 57 | }; |
| 58 | |
| 59 | let Some(search) = ctx.workspace_services.search() else { |
| 60 | return Ok(ToolOutput::error( |
| 61 | "glob is not available: this workspace backend did not provide search", |
| 62 | )); |
| 63 | }; |
| 64 | |
| 65 | let request = WorkspaceGlobRequest { |
| 66 | base, |
| 67 | pattern: pattern.to_string(), |
| 68 | }; |
| 69 | let result = match ctx |
| 70 | .workspace_services |
| 71 | .run_with_timeout("glob", async move { search.glob(request).await }) |
| 72 | .await |
| 73 | { |
| 74 | Ok(result) => result, |
| 75 | Err(e) => { |
| 76 | return Ok(ToolOutput::error(format!( |
| 77 | "Invalid glob pattern '{}': {}", |
| 78 | pattern, e |
| 79 | ))) |
| 80 | } |
| 81 | }; |
| 82 | let matches: Vec<String> = result |
| 83 | .matches |
| 84 | .into_iter() |
| 85 | .map(|path| path.as_str().to_string()) |
| 86 | .collect(); |
| 87 | |
| 88 | if matches.is_empty() { |
| 89 | Ok(ToolOutput::success(format!( |
| 90 | "No files found matching pattern: {}", |
| 91 | pattern |
| 92 | ))) |
| 93 | } else { |
| 94 | let count = matches.len(); |
| 95 | let mut output = matches.join("\n"); |
| 96 | output.push_str(&format!("\n\n{} file(s) found", count)); |
| 97 | Ok(ToolOutput::success(output)) |
| 98 | } |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | #[cfg(test)] |