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' ;
2025-06-12 19:46:00 -07:00
import { glob } from 'glob' ;
2025-04-19 19:45:42 +01:00
import { SchemaValidator } from '../utils/schemaValidator.js' ;
import { BaseTool , ToolResult } from './tools.js' ;
2025-07-07 23:48:44 -07:00
import { Type } from '@google/genai' ;
2025-04-19 19:45:42 +01:00
import { shortenPath , makeRelative } from '../utils/paths.js' ;
2025-07-14 22:55:49 -07:00
import { isWithinRoot } from '../utils/fileUtils.js' ;
2025-06-03 21:40:46 -07:00
import { Config } from '../config/config.js' ;
2025-04-19 19:45:42 +01:00
2025-06-12 19:46:00 -07:00
// Subset of 'Path' interface provided by 'glob' that we can implement for testing
export interface GlobPath {
fullpath ( ) : string ;
mtimeMs? : number ;
2025-06-09 08:07:24 -07:00
}
/**
* Sorts file entries based on recency and then alphabetically.
* Recent files (modified within recencyThresholdMs) are listed first, newest to oldest.
* Older files are listed after recent ones, sorted alphabetically by path.
*/
export function sortFileEntries (
2025-06-12 19:46:00 -07:00
entries : GlobPath [ ] ,
2025-06-09 08:07:24 -07:00
nowTimestamp : number ,
recencyThresholdMs : number ,
2025-06-12 19:46:00 -07:00
) : GlobPath [ ] {
2025-06-09 08:07:24 -07:00
const sortedEntries = [ . . . entries ] ;
sortedEntries . sort ( ( a , b ) = > {
2025-06-12 19:46:00 -07:00
const mtimeA = a . mtimeMs ? ? 0 ;
const mtimeB = b . mtimeMs ? ? 0 ;
2025-06-09 08:07:24 -07:00
const aIsRecent = nowTimestamp - mtimeA < recencyThresholdMs ;
const bIsRecent = nowTimestamp - mtimeB < recencyThresholdMs ;
if ( aIsRecent && bIsRecent ) {
return mtimeB - mtimeA ;
} else if ( aIsRecent ) {
return - 1 ;
} else if ( bIsRecent ) {
return 1 ;
} else {
2025-06-12 19:46:00 -07:00
return a . fullpath ( ) . localeCompare ( b . fullpath ( ) ) ;
2025-06-09 08:07:24 -07:00
}
} ) ;
return sortedEntries ;
}
2025-04-19 19:45:42 +01:00
/**
* 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-18 00:04:32 -07:00
/**
* Whether the search should be case-sensitive (optional, defaults to false)
*/
case_sensitive? : boolean ;
2025-06-03 21:40:46 -07:00
/**
* Whether to respect .gitignore patterns (optional, defaults to true)
*/
respect_git_ignore? : boolean ;
2025-04-19 19:45:42 +01:00
}
/**
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-07-14 22:55:49 -07:00
constructor ( private config : Config ) {
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 :
2025-05-18 00:04:32 -07:00
"The glob pattern to match against (e.g., '**/*.py', 'docs/*.md')." ,
2025-07-07 23:48:44 -07:00
type : Type . STRING ,
2025-04-19 19:45:42 +01:00
} ,
path : {
description :
'Optional: The absolute path to the directory to search within. If omitted, searches the root directory.' ,
2025-07-07 23:48:44 -07:00
type : Type . STRING ,
2025-04-19 19:45:42 +01:00
} ,
2025-05-18 00:04:32 -07:00
case_sensitive : {
description :
'Optional: Whether the search should be case-sensitive. Defaults to false.' ,
2025-07-07 23:48:44 -07:00
type : Type . BOOLEAN ,
2025-05-18 00:04:32 -07:00
} ,
2025-06-03 21:40:46 -07:00
respect_git_ignore : {
description :
'Optional: Whether to respect .gitignore patterns when finding files. Only available in git repositories. Defaults to true.' ,
2025-07-07 23:48:44 -07:00
type : Type . BOOLEAN ,
2025-06-03 21:40:46 -07:00
} ,
2025-04-19 19:45:42 +01:00
} ,
required : [ 'pattern' ] ,
2025-07-07 23:48:44 -07:00
type : Type . OBJECT ,
2025-04-19 19:45:42 +01:00
} ,
) ;
}
/**
* Validates the parameters for the tool.
*/
validateToolParams ( params : GlobToolParams ) : string | null {
2025-07-07 23:48:44 -07:00
const errors = SchemaValidator . validate ( this . schema . parameters , params ) ;
if ( errors ) {
return errors ;
2025-04-19 19:45:42 +01:00
}
const searchDirAbsolute = path . resolve (
2025-07-14 22:55:49 -07:00
this . config . getTargetDir ( ) ,
2025-04-19 19:45:42 +01:00
params . path || '.' ,
) ;
2025-07-14 22:55:49 -07:00
if ( ! isWithinRoot ( searchDirAbsolute , this . config . getTargetDir ( ) ) ) {
return ` Search path (" ${ searchDirAbsolute } ") resolves outside the tool's root directory (" ${ this . config . getTargetDir ( ) } "). ` ;
2025-04-19 19:45:42 +01:00
}
2025-07-14 22:55:49 -07:00
const targetDir = searchDirAbsolute || this . config . getTargetDir ( ) ;
2025-04-19 19:45:42 +01:00
try {
2025-05-18 00:04:32 -07:00
if ( ! fs . existsSync ( targetDir ) ) {
return ` Search path does not exist ${ targetDir } ` ;
2025-04-19 19:45:42 +01:00
}
2025-05-18 00:04:32 -07:00
if ( ! fs . statSync ( targetDir ) . isDirectory ( ) ) {
return ` Search path is not a directory: ${ targetDir } ` ;
2025-04-19 19:45:42 +01:00
}
} 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 ) {
2025-07-14 22:55:49 -07:00
const searchDir = path . resolve (
this . config . getTargetDir ( ) ,
params . path || '.' ,
) ;
const relativePath = makeRelative ( searchDir , this . config . getTargetDir ( ) ) ;
2025-04-19 19:45:42 +01:00
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 ,
2025-06-12 19:46:00 -07:00
signal : AbortSignal ,
2025-05-09 23:29:02 -07:00
) : 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 (
2025-07-14 22:55:49 -07:00
this . config . getTargetDir ( ) ,
2025-04-19 19:45:42 +01:00
params . path || '.' ,
) ;
2025-06-03 21:40:46 -07:00
// Get centralized file discovery service
const respectGitIgnore =
params . respect_git_ignore ? ?
this . config . getFileFilteringRespectGitIgnore ( ) ;
2025-06-14 10:25:34 -04:00
const fileDiscovery = this . config . getFileService ( ) ;
2025-06-03 21:40:46 -07:00
2025-06-12 19:46:00 -07:00
const entries = ( await glob ( params . pattern , {
2025-04-19 19:45:42 +01:00
cwd : searchDirAbsolute ,
2025-06-12 19:46:00 -07:00
withFileTypes : true ,
nodir : true ,
stat : true ,
nocase : ! params . case_sensitive ,
2025-04-19 19:45:42 +01:00
dot : true ,
ignore : [ '**/node_modules/**' , '**/.git/**' ] ,
2025-06-12 19:46:00 -07:00
follow : false ,
signal ,
} ) ) as GlobPath [ ] ;
2025-04-19 19:45:42 +01:00
2025-06-03 21:40:46 -07:00
// Apply git-aware filtering if enabled and in git repository
let filteredEntries = entries ;
let gitIgnoredCount = 0 ;
2025-06-14 10:25:34 -04:00
if ( respectGitIgnore ) {
2025-06-12 19:46:00 -07:00
const relativePaths = entries . map ( ( p ) = >
2025-07-14 22:55:49 -07:00
path . relative ( this . config . getTargetDir ( ) , p . fullpath ( ) ) ,
2025-06-03 21:40:46 -07:00
) ;
const filteredRelativePaths = fileDiscovery . filterFiles ( relativePaths , {
respectGitIgnore ,
} ) ;
const filteredAbsolutePaths = new Set (
2025-07-14 22:55:49 -07:00
filteredRelativePaths . map ( ( p ) = >
path . resolve ( this . config . getTargetDir ( ) , p ) ,
) ,
2025-06-03 21:40:46 -07:00
) ;
filteredEntries = entries . filter ( ( entry ) = >
2025-06-12 19:46:00 -07:00
filteredAbsolutePaths . has ( entry . fullpath ( ) ) ,
2025-06-03 21:40:46 -07:00
) ;
gitIgnoredCount = entries . length - filteredEntries . length ;
}
if ( ! filteredEntries || filteredEntries . length === 0 ) {
let message = ` No files found matching pattern " ${ params . pattern } " within ${ searchDirAbsolute } . ` ;
if ( gitIgnoredCount > 0 ) {
message += ` ( ${ gitIgnoredCount } files were git-ignored) ` ;
}
2025-04-19 19:45:42 +01:00
return {
2025-06-03 21:40:46 -07:00
llmContent : message ,
2025-04-19 19:45:42 +01:00
returnDisplay : ` No files found ` ,
} ;
}
2025-06-09 08:07:24 -07:00
// Set filtering such that we first show the most recent files
const oneDayInMs = 24 * 60 * 60 * 1000 ;
const nowTimestamp = new Date ( ) . getTime ( ) ;
// Sort the filtered entries using the new helper function
const sortedEntries = sortFileEntries (
2025-06-12 19:46:00 -07:00
filteredEntries ,
2025-06-09 08:07:24 -07:00
nowTimestamp ,
oneDayInMs ,
) ;
2025-04-19 19:45:42 +01:00
2025-06-12 19:46:00 -07:00
const sortedAbsolutePaths = sortedEntries . map ( ( entry ) = >
entry . fullpath ( ) ,
) ;
2025-05-18 00:04:32 -07:00
const fileListDescription = sortedAbsolutePaths . join ( '\n' ) ;
const fileCount = sortedAbsolutePaths . length ;
2025-04-19 19:45:42 +01:00
2025-06-03 21:40:46 -07:00
let resultMessage = ` Found ${ fileCount } file(s) matching " ${ params . pattern } " within ${ searchDirAbsolute } ` ;
if ( gitIgnoredCount > 0 ) {
resultMessage += ` ( ${ gitIgnoredCount } additional files were git-ignored) ` ;
}
resultMessage += ` , sorted by modification time (newest first): \ n ${ fileListDescription } ` ;
2025-04-19 19:45:42 +01:00
return {
2025-06-03 21:40:46 -07:00
llmContent : resultMessage ,
2025-04-19 19:45:42 +01:00
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. ` ,
} ;
}
}
}