Skip to main content

microclaw_engine/tools/
glob.rs

1use 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}