use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::instrument;
use crate::tools::context::ToolContext;
use crate::tools::error::ToolError;
use crate::tools::registry::{Tool, ToolDefinition};
const MAX_RESULTS: usize = 1000;
#[derive(Clone)]
pub struct GlobTool {
context: ToolContext,
}
impl GlobTool {
pub fn new(context: ToolContext) -> Self {
Self { context }
}
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GlobArgs {
pub pattern: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(
default,
deserialize_with = "crate::tools::serde_flexible::deserialize_flexible_optional_usize"
)]
pub max_results: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct GlobOutput {
pub matches: Vec<String>,
pub pattern: String,
pub truncated: bool,
}
impl Tool for GlobTool {
const NAME: &'static str = "glob";
type Args = GlobArgs;
type Output = GlobOutput;
type Error = ToolError;
async fn definition(&self, _prompt: String) -> ToolDefinition {
let schema = schemars::schema_for!(GlobArgs);
ToolDefinition {
name: Self::NAME.to_string(),
description: r#"Find files by matching their paths against a glob pattern.
## BEFORE CALLING THIS TOOL
Think step-by-step:
1. What files am I looking for?
2. What is the file extension or naming pattern?
3. Which directory should I search in?
## PARAMETERS
- `pattern` (REQUIRED, STRING): A glob pattern as a plain string
CORRECT: "**/*.sol"
CORRECT: "contracts/*.sol"
CORRECT: "src/**/*.rs"
WRONG: {"pattern": "..."} <-- Do NOT pass JSON objects!
WRONG: {} <-- Empty object is invalid!
WRONG: {"glob":".","sol,":"attern"} <-- This is NOT a pattern!
- `description` (optional, STRING): Brief description of what files are being searched for
- `path` (optional, STRING): Base directory to search from (default: workspace root)
- `max_results` (optional, NUMBER): Maximum files to return (default/max: 1000)
## GLOB PATTERN SYNTAX
- "*" matches any characters within a single directory (e.g., "*.sol")
- "**" matches any characters across directories recursively (e.g., "**/*.sol")
- "?" matches exactly one character (e.g., "Token?.sol")
## EXAMPLES
Find all Solidity files:
pattern: "**/*.sol"
Find files in contracts folder:
pattern: "contracts/*.sol"
Find test files:
pattern: "**/test_*.sol"
## COMMON MISTAKES TO AVOID
1. Do NOT pass JSON objects as pattern - pattern is a STRING, not an object
2. Do NOT use {} or {"key": "value"} syntax for any parameter
3. Do NOT confuse glob (file name matching) with grep (content search)
4. Use glob for finding FILES, use grep for searching TEXT inside files
NOTE: This tool finds files by NAME pattern. To search for text INSIDE files, use the 'grep' tool instead.
"#.to_string(),
parameters: serde_json::to_value(schema).unwrap_or_default(),
}
}
#[instrument(skip(self), fields(tool = "glob", pattern = %args.pattern))]
async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
let base_path = if let Some(path) = &args.path {
self.context.validate_path(path)?
} else {
self.context.workspace_directory().clone()
};
let max_results = args.max_results.unwrap_or(MAX_RESULTS).min(MAX_RESULTS);
let full_pattern = base_path.join(&args.pattern);
let pattern_str = full_pattern.display().to_string();
let paths = glob::glob(&pattern_str)
.map_err(|e| ToolError::Validation(format!("Invalid glob pattern: {}", e)))?;
let mut matches = Vec::new();
let mut truncated = false;
for entry in paths {
match entry {
Ok(path) => {
if matches.len() >= max_results {
truncated = true;
break;
}
matches.push(path.display().to_string());
}
Err(e) => {
tracing::debug!("Glob error: {}", e);
}
}
}
let matches = self.context.filter_excluded(matches);
let mut matches = matches;
matches.sort_by(|a, b| {
let a_meta = std::fs::metadata(a).and_then(|m| m.modified()).ok();
let b_meta = std::fs::metadata(b).and_then(|m| m.modified()).ok();
b_meta.cmp(&a_meta)
});
self.context.register_known_files(&matches);
Ok(GlobOutput {
matches,
pattern: args.pattern,
truncated,
})
}
}