2025-04-19 19:45:42 +01:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
import fs from 'fs' ;
import path from 'path' ;
import fg from 'fast-glob' ;
import { SchemaValidator } from '../utils/schemaValidator.js' ;
import { BaseTool , ToolResult } from './tools.js' ;
import { shortenPath , makeRelative } from '../utils/paths.js' ;
/ * *
* Parameters for the GlobTool
* /
export interface GlobToolParams {
/ * *
* The glob pattern to match files against
* /
pattern : string ;
/ * *
* The directory to search in ( optional , defaults to current directory )
* /
path? : string ;
}
/ * *
2025-05-02 14:39:39 -07:00
* Implementation of the Glob tool logic
2025-04-19 19:45:42 +01:00
* /
2025-04-21 10:53:11 -04:00
export class GlobTool extends BaseTool < GlobToolParams , ToolResult > {
2025-05-02 14:39:39 -07:00
static readonly Name = 'glob' ;
2025-04-19 19:45:42 +01:00
/ * *
* Creates a new instance of the GlobLogic
* @param rootDirectory Root directory to ground this tool in .
* /
2025-05-02 09:31:18 -07:00
constructor ( private rootDirectory : string ) {
2025-04-19 19:45:42 +01:00
super (
2025-04-21 10:53:11 -04:00
GlobTool . Name ,
2025-05-02 14:39:39 -07:00
'FindFiles' ,
'Efficiently finds files matching specific glob patterns (e.g., `src/**/*.ts`, `**/*.md`), returning absolute paths sorted by modification time (newest first). Ideal for quickly locating files based on their name or path structure, especially in large codebases.' ,
2025-04-19 19:45:42 +01:00
{
properties : {
pattern : {
description :
"The glob pattern to match against (e.g., '*.py', 'src/**/*.js', 'docs/*.md')." ,
type : 'string' ,
} ,
path : {
description :
'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.' ,
type : 'string' ,
} ,
} ,
required : [ 'pattern' ] ,
type : 'object' ,
} ,
) ;
this . rootDirectory = path . resolve ( rootDirectory ) ;
}
/ * *
* Checks if a path is within the root directory .
* /
private isWithinRoot ( pathToCheck : string ) : boolean {
const absolutePathToCheck = path . resolve ( pathToCheck ) ;
const normalizedPath = path . normalize ( absolutePathToCheck ) ;
const normalizedRoot = path . normalize ( this . rootDirectory ) ;
const rootWithSep = normalizedRoot . endsWith ( path . sep )
? normalizedRoot
: normalizedRoot + path . sep ;
return (
normalizedPath === normalizedRoot ||
normalizedPath . startsWith ( rootWithSep )
) ;
}
/ * *
* Validates the parameters for the tool .
* /
validateToolParams ( params : GlobToolParams ) : string | null {
if (
this . schema . parameters &&
! SchemaValidator . validate (
this . schema . parameters as Record < string , unknown > ,
params ,
)
) {
return "Parameters failed schema validation. Ensure 'pattern' is a string and 'path' (if provided) is a string." ;
}
const searchDirAbsolute = path . resolve (
this . rootDirectory ,
params . path || '.' ,
) ;
if ( ! this . isWithinRoot ( searchDirAbsolute ) ) {
return ` Search path (" ${ searchDirAbsolute } ") resolves outside the tool's root directory (" ${ this . rootDirectory } "). ` ;
}
try {
if ( ! fs . existsSync ( searchDirAbsolute ) ) {
return ` Search path does not exist: ${ shortenPath ( makeRelative ( searchDirAbsolute , this . rootDirectory ) ) } (absolute: ${ searchDirAbsolute } ) ` ;
}
if ( ! fs . statSync ( searchDirAbsolute ) . isDirectory ( ) ) {
return ` Search path is not a directory: ${ shortenPath ( makeRelative ( searchDirAbsolute , this . rootDirectory ) ) } (absolute: ${ searchDirAbsolute } ) ` ;
}
} catch ( e : unknown ) {
return ` Error accessing search path: ${ e } ` ;
}
if (
! params . pattern ||
typeof params . pattern !== 'string' ||
params . pattern . trim ( ) === ''
) {
return "The 'pattern' parameter cannot be empty." ;
}
return null ;
}
/ * *
* Gets a description of the glob operation .
* /
getDescription ( params : GlobToolParams ) : string {
let description = ` ' ${ params . pattern } ' ` ;
if ( params . path ) {
const searchDir = path . resolve ( this . rootDirectory , params . path || '.' ) ;
const relativePath = makeRelative ( searchDir , this . rootDirectory ) ;
description += ` within ${ shortenPath ( relativePath ) } ` ;
}
return description ;
}
/ * *
* Executes the glob search with the given parameters
* /
2025-05-09 23:29:02 -07:00
async execute (
params : GlobToolParams ,
_signal : AbortSignal ,
) : Promise < ToolResult > {
2025-04-19 19:45:42 +01:00
const validationError = this . validateToolParams ( params ) ;
if ( validationError ) {
return {
llmContent : ` Error: Invalid parameters provided. Reason: ${ validationError } ` ,
2025-04-20 22:10:23 -04:00
returnDisplay : validationError ,
2025-04-19 19:45:42 +01:00
} ;
}
try {
const searchDirAbsolute = path . resolve (
this . rootDirectory ,
params . path || '.' ,
) ;
const entries = await fg ( params . pattern , {
cwd : searchDirAbsolute ,
absolute : true ,
onlyFiles : true ,
stats : true ,
dot : true ,
ignore : [ '**/node_modules/**' , '**/.git/**' ] ,
followSymbolicLinks : false ,
suppressErrors : true ,
} ) ;
if ( ! entries || entries . length === 0 ) {
const displayPath = makeRelative ( searchDirAbsolute , this . rootDirectory ) ;
return {
llmContent : ` No files found matching pattern " ${ params . pattern } " within ${ displayPath || '.' } . ` ,
returnDisplay : ` No files found ` ,
} ;
}
entries . sort ( ( a , b ) = > {
const mtimeA = a . stats ? . mtime ? . getTime ( ) ? ? 0 ;
const mtimeB = b . stats ? . mtime ? . getTime ( ) ? ? 0 ;
return mtimeB - mtimeA ;
} ) ;
const sortedAbsolutePaths = entries . map ( ( entry ) = > entry . path ) ;
const sortedRelativePaths = sortedAbsolutePaths . map ( ( absPath ) = >
makeRelative ( absPath , this . rootDirectory ) ,
) ;
const fileListDescription = sortedRelativePaths . join ( '\n' ) ;
const fileCount = sortedRelativePaths . length ;
const relativeSearchDir = makeRelative (
searchDirAbsolute ,
this . rootDirectory ,
) ;
const displayPath = shortenPath (
relativeSearchDir === '.' ? 'root directory' : relativeSearchDir ,
) ;
return {
llmContent : ` Found ${ fileCount } file(s) matching " ${ params . pattern } " within ${ displayPath } , sorted by modification time (newest first): \ n ${ fileListDescription } ` ,
returnDisplay : ` Found ${ fileCount } matching file(s) ` ,
} ;
} catch ( error ) {
const errorMessage =
error instanceof Error ? error.message : String ( error ) ;
console . error ( ` GlobLogic execute Error: ${ errorMessage } ` , error ) ;
return {
llmContent : ` Error during glob search operation: ${ errorMessage } ` ,
returnDisplay : ` Error: An unexpected error occurred. ` ,
} ;
}
}
}