2026-03-27 15:22:48 -04:00
// tools/glob_tool.rs — Find files by pattern
//
// Fast file discovery using glob patterns. Returns matching paths
// sorted by modification time (newest first), which is usually
// what you want when exploring a codebase.
use anyhow ::{ Context , Result } ;
use serde ::Deserialize ;
use std ::path ::PathBuf ;
#[ derive(Deserialize) ]
struct Args {
pattern : String ,
#[ serde(default = " default_path " ) ]
path : String ,
}
fn default_path ( ) -> String { " . " . into ( ) }
2026-04-04 15:34:07 -04:00
pub fn tool ( ) -> super ::Tool {
2026-04-04 15:50:14 -04:00
super ::Tool {
name : " glob " ,
description : " Find files matching a glob pattern. Returns file paths sorted by modification time (newest first). " ,
parameters_json : r #" { " type " : " object " , " properties " :{ " pattern " :{ " type " : " string " , " description " : " Glob pattern to match files ( e . g . ' * * /* .rs')"},"path":{"type":"string","description":"Directory to search in (default: current directory)"}},"required":["pattern"]}"#,
handler : | _a , v | Box ::pin ( async move { glob_search ( & v ) } ) ,
}
2026-03-27 15:22:48 -04:00
}
2026-04-04 15:34:07 -04:00
fn glob_search ( args : & serde_json ::Value ) -> Result < String > {
2026-03-27 15:22:48 -04:00
let a : Args = serde_json ::from_value ( args . clone ( ) )
. context ( " invalid glob arguments " ) ? ;
let full_pattern = if a . pattern . starts_with ( '/' ) {
a . pattern . clone ( )
} else {
format! ( " {} / {} " , a . path , a . pattern )
} ;
let mut entries : Vec < ( PathBuf , std ::time ::SystemTime ) > = Vec ::new ( ) ;
for entry in glob ::glob ( & full_pattern )
. with_context ( | | format! ( " Invalid glob pattern: {} " , full_pattern ) ) ?
{
if let Ok ( path ) = entry {
if path . is_file ( ) {
let mtime = path
. metadata ( )
. and_then ( | m | m . modified ( ) )
. unwrap_or ( std ::time ::SystemTime ::UNIX_EPOCH ) ;
entries . push ( ( path , mtime ) ) ;
}
}
}
// Sort by modification time, newest first
entries . sort_by ( | a , b | b . 1. cmp ( & a . 1 ) ) ;
if entries . is_empty ( ) {
return Ok ( " No files matched. " . to_string ( ) ) ;
}
let mut output = String ::new ( ) ;
for ( path , _ ) in & entries {
output . push_str ( & path . display ( ) . to_string ( ) ) ;
output . push ( '\n' ) ;
}
output . push_str ( & format! ( " \n ( {} files matched) " , entries . len ( ) ) ) ;
Ok ( super ::truncate_output ( output , 30000 ) )
}