microclaw_engine/tools/
glob.rs1use async_trait::async_trait;
2use serde_json::json;
3use std::path::PathBuf;
4use tracing::info;
5
6use crate::config::WorkingDirIsolation;
7use microclaw_core::llm_types::ToolDefinition;
8
9use super::{schema_object, Tool, ToolResult};
10
11pub struct GlobTool {
12 working_dir: PathBuf,
13 working_dir_isolation: WorkingDirIsolation,
14}
15
16impl GlobTool {
17 pub fn new(working_dir: &str) -> Self {
18 Self::new_with_isolation(working_dir, WorkingDirIsolation::Shared)
19 }
20
21 pub fn new_with_isolation(
22 working_dir: &str,
23 working_dir_isolation: WorkingDirIsolation,
24 ) -> Self {
25 Self {
26 working_dir: PathBuf::from(working_dir),
27 working_dir_isolation,
28 }
29 }
30}
31
32#[async_trait]
33impl Tool for GlobTool {
34 fn name(&self) -> &str {
35 "glob"
36 }
37
38 fn definition(&self) -> ToolDefinition {
39 ToolDefinition {
40 name: "glob".into(),
41 description: "Find files matching a glob pattern. Returns matching file paths.".into(),
42 input_schema: schema_object(
43 json!({
44 "pattern": {
45 "type": "string",
46 "description": "The glob pattern to match (e.g., '**/*.rs', 'src/**/*.ts')"
47 },
48 "path": {
49 "type": "string",
50 "description": "Base directory to search from (default: current directory)"
51 }
52 }),
53 &["pattern"],
54 ),
55 }
56 }
57
58 async fn execute(&self, input: serde_json::Value) -> ToolResult {
59 let pattern = match input.get("pattern").and_then(|v| v.as_str()) {
60 Some(p) => p,
61 None => return ToolResult::error("Missing 'pattern' parameter".into()),
62 };
63 let base = input.get("path").and_then(|v| v.as_str()).unwrap_or(".");
64 let working_dir =
65 super::resolve_tool_working_dir(&self.working_dir, self.working_dir_isolation, &input);
66 let resolved_base = super::resolve_tool_path(&working_dir, base);
67 let resolved_base_str = resolved_base.to_string_lossy().to_string();
68
69 if let Err(msg) = crate::internal::tool_runtime::path_guard::check_path(&resolved_base_str)
70 {
71 return ToolResult::error(msg);
72 }
73
74 info!("Glob: {} in {}", pattern, resolved_base.display());
75
76 let full_pattern = if pattern.starts_with('/') {
77 pattern.to_string()
78 } else {
79 format!("{}/{}", resolved_base.display(), pattern)
80 };
81
82 match glob::glob(&full_pattern) {
83 Ok(paths) => {
84 let mut matches: Vec<String> = paths
85 .filter_map(|p| p.ok())
86 .map(|p| p.display().to_string())
87 .collect();
88 matches = crate::internal::tool_runtime::path_guard::filter_paths(matches);
89 matches.sort();
90
91 if matches.is_empty() {
92 ToolResult::success("No files found matching pattern.".into())
93 } else {
94 let count = matches.len();
95 if count > 500 {
96 matches.truncate(500);
97 matches.push(format!("... and {} more files", count - 500));
98 }
99 ToolResult::success(matches.join("\n"))
100 }
101 }
102 Err(e) => ToolResult::error(format!("Invalid glob pattern: {e}")),
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use serde_json::json;
111
112 #[tokio::test]
113 async fn test_glob_finds_files() {
114 let dir = std::env::temp_dir().join(format!("microclaw_glob_{}", uuid::Uuid::new_v4()));
115 std::fs::create_dir_all(&dir).unwrap();
116 std::fs::write(dir.join("a.txt"), "").unwrap();
117 std::fs::write(dir.join("b.txt"), "").unwrap();
118 std::fs::write(dir.join("c.rs"), "").unwrap();
119
120 let tool = GlobTool::new(".");
121 let result = tool
122 .execute(json!({"pattern": "*.txt", "path": dir.to_str().unwrap()}))
123 .await;
124 assert!(!result.is_error);
125 assert!(result.content.contains("a.txt"));
126 assert!(result.content.contains("b.txt"));
127 assert!(!result.content.contains("c.rs"));
128
129 let _ = std::fs::remove_dir_all(&dir);
130 }
131
132 #[tokio::test]
133 async fn test_glob_no_matches() {
134 let dir = std::env::temp_dir().join(format!("microclaw_glob2_{}", uuid::Uuid::new_v4()));
135 std::fs::create_dir_all(&dir).unwrap();
136
137 let tool = GlobTool::new(".");
138 let result = tool
139 .execute(json!({"pattern": "*.xyz", "path": dir.to_str().unwrap()}))
140 .await;
141 assert!(!result.is_error);
142 assert!(result.content.contains("No files found"));
143
144 let _ = std::fs::remove_dir_all(&dir);
145 }
146
147 #[tokio::test]
148 async fn test_glob_missing_pattern() {
149 let tool = GlobTool::new(".");
150 let result = tool.execute(json!({})).await;
151 assert!(result.is_error);
152 assert!(result.content.contains("Missing 'pattern'"));
153 }
154
155 #[tokio::test]
156 async fn test_glob_defaults_to_working_dir() {
157 let root = std::env::temp_dir().join(format!("microclaw_glob3_{}", uuid::Uuid::new_v4()));
158 let work = root.join("workspace");
159 let shared = work.join("shared");
160 std::fs::create_dir_all(&shared).unwrap();
161 std::fs::write(shared.join("x.txt"), "").unwrap();
162
163 let tool = GlobTool::new(work.to_str().unwrap());
164 let result = tool.execute(json!({"pattern":"*.txt"})).await;
165 assert!(!result.is_error);
166 assert!(result.content.contains("x.txt"));
167
168 let _ = std::fs::remove_dir_all(&root);
169 }
170}