Skip to main content

agent_code_lib/tools/
glob.rs

1//! Glob tool: file pattern matching.
2//!
3//! Finds files matching glob patterns. Results are sorted by
4//! modification time (most recently modified first).
5
6use async_trait::async_trait;
7use serde_json::json;
8use std::path::PathBuf;
9
10use super::{Tool, ToolContext, ToolResult};
11use crate::error::ToolError;
12
13pub struct GlobTool;
14
15#[async_trait]
16impl Tool for GlobTool {
17    fn name(&self) -> &'static str {
18        "Glob"
19    }
20
21    fn description(&self) -> &'static str {
22        "Finds files matching a glob pattern. Returns paths sorted by modification time."
23    }
24
25    fn input_schema(&self) -> serde_json::Value {
26        json!({
27            "type": "object",
28            "required": ["pattern"],
29            "properties": {
30                "pattern": {
31                    "type": "string",
32                    "description": "Glob pattern (e.g., \"**/*.rs\", \"src/**/*.toml\")"
33                },
34                "path": {
35                    "type": "string",
36                    "description": "Directory to search in (defaults to cwd)"
37                }
38            }
39        })
40    }
41
42    fn is_read_only(&self) -> bool {
43        true
44    }
45
46    fn is_concurrency_safe(&self) -> bool {
47        true
48    }
49
50    async fn call(
51        &self,
52        input: serde_json::Value,
53        ctx: &ToolContext,
54    ) -> Result<ToolResult, ToolError> {
55        let pattern = input
56            .get("pattern")
57            .and_then(|v| v.as_str())
58            .ok_or_else(|| ToolError::InvalidInput("'pattern' is required".into()))?;
59
60        let base_path = input
61            .get("path")
62            .and_then(|v| v.as_str())
63            .map(PathBuf::from)
64            .unwrap_or_else(|| ctx.cwd.clone());
65
66        // Resolve the glob pattern relative to the base path.
67        let full_pattern = if pattern.starts_with('/') {
68            pattern.to_string()
69        } else {
70            format!("{}/{pattern}", base_path.display())
71        };
72
73        let entries: Vec<PathBuf> = glob::glob(&full_pattern)
74            .map_err(|e| ToolError::InvalidInput(format!("Invalid glob pattern: {e}")))?
75            .filter_map(|entry| entry.ok())
76            .filter(|p| p.is_file())
77            .collect();
78
79        // Sort by modification time (most recent first).
80        let mut entries_with_mtime: Vec<(PathBuf, std::time::SystemTime)> = entries
81            .into_iter()
82            .filter_map(|p| {
83                let mtime = std::fs::metadata(&p).ok()?.modified().ok()?;
84                Some((p, mtime))
85            })
86            .collect();
87
88        entries_with_mtime.sort_by(|a, b| b.1.cmp(&a.1));
89
90        let total = entries_with_mtime.len();
91        let max_results = 500;
92        let truncated = total > max_results;
93
94        let result: Vec<String> = entries_with_mtime
95            .iter()
96            .take(max_results)
97            .map(|(p, _)| p.display().to_string())
98            .collect();
99
100        let mut output = format!("Found {total} files:\n{}", result.join("\n"));
101        if truncated {
102            output.push_str(&format!("\n\n(Showing {max_results} of {total} files)"));
103        }
104
105        Ok(ToolResult::success(output))
106    }
107}