llm_coding_tools_rig/absolute/
glob.rs

1//! Glob pattern file finding tool using [`AbsolutePathResolver`].
2
3use llm_coding_tools_core::operations::glob_files;
4use llm_coding_tools_core::path::AbsolutePathResolver;
5use llm_coding_tools_core::tool_names;
6use llm_coding_tools_core::{GlobOutput, ToolContext, ToolError};
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use schemars::{schema_for, JsonSchema};
10use serde::Deserialize;
11
12/// Arguments for the glob tool.
13#[derive(Debug, Deserialize, JsonSchema)]
14pub struct GlobArgs {
15    /// Glob pattern to match files against (e.g., "**/*.rs", "src/**/*.ts").
16    pub pattern: String,
17    /// Absolute directory path to search in.
18    pub path: String,
19}
20
21/// Tool for finding files matching glob patterns.
22#[derive(Debug, Default, Clone, Copy)]
23pub struct GlobTool;
24
25impl GlobTool {
26    /// Creates a new glob tool instance.
27    #[inline]
28    pub fn new() -> Self {
29        Self
30    }
31}
32
33impl Tool for GlobTool {
34    const NAME: &'static str = tool_names::GLOB;
35
36    type Error = ToolError;
37    type Args = GlobArgs;
38    type Output = GlobOutput;
39
40    async fn definition(&self, _prompt: String) -> ToolDefinition {
41        ToolDefinition {
42            name: <Self as Tool>::NAME.to_string(),
43            description: "Find files matching a glob pattern. Respects .gitignore and \
44                returns paths sorted by modification time (newest first)."
45                .to_string(),
46            parameters: serde_json::to_value(schema_for!(GlobArgs))
47                .expect("schema serialization should not fail"),
48        }
49    }
50
51    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
52        let resolver = AbsolutePathResolver;
53        glob_files(&resolver, &args.pattern, &args.path)
54    }
55}
56
57impl ToolContext for GlobTool {
58    const NAME: &'static str = tool_names::GLOB;
59
60    fn context(&self) -> &'static str {
61        llm_coding_tools_core::context::GLOB_ABSOLUTE
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::fs::{self, File};
69    use tempfile::TempDir;
70
71    #[tokio::test]
72    async fn finds_matching_files() {
73        let dir = TempDir::new().unwrap();
74        fs::create_dir_all(dir.path().join("src")).unwrap();
75        File::create(dir.path().join("src/lib.rs")).unwrap();
76        let tool = GlobTool::new();
77        let result = tool
78            .call(GlobArgs {
79                pattern: "**/*.rs".to_string(),
80                path: dir.path().to_string_lossy().to_string(),
81            })
82            .await
83            .unwrap();
84        assert!(result.files.iter().any(|f| f.ends_with("lib.rs")));
85    }
86
87    #[tokio::test]
88    async fn rejects_relative_path() {
89        let tool = GlobTool::new();
90        let result = tool
91            .call(GlobArgs {
92                pattern: "*.rs".to_string(),
93                path: "relative/path".to_string(),
94            })
95            .await;
96        assert!(matches!(result, Err(ToolError::InvalidPath(_))));
97    }
98}