2025-04-19 19:45:42 +01:00
/**
* @license
2026-03-13 14:11:51 -07:00
* Copyright 2026 Google LLC
2025-04-19 19:45:42 +01:00
* SPDX-License-Identifier: Apache-2.0
*/
2025-10-21 11:45:33 -07:00
import type { MessageBus } from '../confirmation-bus/message-bus.js' ;
2025-08-25 22:11:27 +02:00
import fs from 'node:fs' ;
import fsPromises from 'node:fs/promises' ;
import path from 'node:path' ;
import { spawn } from 'node:child_process' ;
2025-06-12 19:46:00 -07:00
import { globStream } from 'glob' ;
2026-01-26 16:52:19 -05:00
import { execStreaming } from '../utils/shell-utils.js' ;
import {
DEFAULT_TOTAL_MAX_MATCHES ,
DEFAULT_SEARCH_TIMEOUT_MS ,
} from './constants.js' ;
2026-03-04 05:42:59 +05:30
import {
BaseDeclarativeTool ,
BaseToolInvocation ,
Kind ,
type ToolInvocation ,
type ToolResult ,
2026-03-10 13:01:41 -04:00
type PolicyUpdateOptions ,
type ToolConfirmationOutcome ,
2026-03-04 05:42:59 +05:30
} from './tools.js' ;
2025-04-19 19:45:42 +01:00
import { makeRelative , shortenPath } from '../utils/paths.js' ;
import { getErrorMessage , isNodeError } from '../utils/errors.js' ;
2025-06-13 12:45:07 -04:00
import { isGitRepository } from '../utils/gitUtils.js' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
import type { FileExclusions } from '../utils/ignorePatterns.js' ;
2025-08-21 14:40:18 -07:00
import { ToolErrorType } from './tool-error.js' ;
2026-03-30 16:43:29 -07:00
import { GREP_TOOL_NAME , GREP_DISPLAY_NAME } from './tool-names.js' ;
2026-03-10 13:01:41 -04:00
import { buildPatternArgsPattern } from '../policy/utils.js' ;
2025-10-19 03:08:01 +00:00
import { debugLogger } from '../utils/debugLogger.js' ;
2026-02-09 20:29:52 -05:00
import { GREP_DEFINITION } from './definitions/coreTools.js' ;
import { resolveToolDeclaration } from './definitions/resolver.js' ;
2026-02-21 00:36:10 +00:00
import { type GrepMatch , formatGrepResults } from './grep-utils.js' ;
2025-04-19 19:45:42 +01:00
// --- Interfaces ---
/**
* Parameters for the GrepTool
*/
export interface GrepToolParams {
/**
* The regular expression pattern to search for in file contents
*/
pattern : string ;
/**
* The directory to search in (optional, defaults to current directory relative to root)
*/
2025-11-06 15:03:52 -08:00
dir_path? : string ;
2025-04-19 19:45:42 +01:00
/**
* File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
*/
2026-02-25 20:16:21 -08:00
include_pattern? : string ;
2026-02-11 03:50:10 +00:00
2026-02-11 19:20:51 +00:00
/**
* Optional: A regular expression pattern to exclude from the search results.
*/
exclude_pattern? : string ;
/**
* Optional: If true, only the file paths of the matches will be returned.
*/
names_only? : boolean ;
2026-02-11 03:50:10 +00:00
/**
* Optional: Maximum number of matches to return per file. Use this to prevent being overwhelmed by repetitive matches in large files.
*/
max_matches_per_file? : number ;
/**
* Optional: Maximum number of total matches to return. Use this to limit the overall size of the response. Defaults to 100 if omitted.
*/
total_max_matches? : number ;
2025-04-19 19:45:42 +01:00
}
2025-08-07 10:05:37 -07:00
class GrepToolInvocation extends BaseToolInvocation <
GrepToolParams ,
ToolResult
> {
2025-08-23 13:35:00 +09:00
private readonly fileExclusions : FileExclusions ;
2025-08-07 10:05:37 -07:00
constructor (
private readonly config : Config ,
params : GrepToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-21 11:45:33 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-07 10:05:37 -07:00
) {
2025-10-21 11:45:33 -07:00
super ( params , messageBus , _toolName , _toolDisplayName ) ;
2025-08-23 13:35:00 +09:00
this . fileExclusions = config . getFileExclusions ( ) ;
2025-04-19 19:45:42 +01:00
}
2026-01-26 16:52:19 -05:00
/**
* Parses a single line of grep-like output (git grep, system grep).
* Expects format: filePath:lineNumber:lineContent
* @param {string} line The line to parse.
* @param {string} basePath The absolute directory for path resolution.
* @returns {GrepMatch | null} Parsed match or null if malformed.
*/
private parseGrepLine ( line : string , basePath : string ) : GrepMatch | null {
if ( ! line . trim ( ) ) return null ;
// Use regex to locate the first occurrence of :<digits>:
// This allows filenames to contain colons, as long as they don't look like :<digits>:
// Note: This regex assumes filenames do not contain colons, or at least not followed by digits.
const match = line . match ( /^(.+?):(\d+):(.*)$/ ) ;
if ( ! match ) return null ;
const [ , filePathRaw , lineNumberStr , lineContent ] = match ;
const lineNumber = parseInt ( lineNumberStr , 10 ) ;
if ( ! isNaN ( lineNumber ) ) {
const absoluteFilePath = path . resolve ( basePath , filePathRaw ) ;
const relativeCheck = path . relative ( basePath , absoluteFilePath ) ;
if (
relativeCheck === '..' ||
relativeCheck . startsWith ( ` .. ${ path . sep } ` ) ||
path . isAbsolute ( relativeCheck )
) {
return null ;
}
const relativeFilePath = path . relative ( basePath , absoluteFilePath ) ;
return {
filePath : relativeFilePath || path . basename ( absoluteFilePath ) ,
2026-02-21 00:36:10 +00:00
absolutePath : absoluteFilePath ,
2026-01-26 16:52:19 -05:00
lineNumber ,
line : lineContent ,
} ;
}
return null ;
}
2025-08-07 10:05:37 -07:00
async execute ( signal : AbortSignal ) : Promise < ToolResult > {
2025-04-19 19:45:42 +01:00
try {
2025-07-31 05:38:20 +09:00
const workspaceContext = this . config . getWorkspaceContext ( ) ;
2026-01-27 13:17:40 -08:00
const pathParam = this . params . dir_path ;
let searchDirAbs : string | null = null ;
if ( pathParam ) {
searchDirAbs = path . resolve ( this . config . getTargetDir ( ) , pathParam ) ;
2026-02-09 12:24:28 -08:00
const validationError = this . config . validatePathAccess (
searchDirAbs ,
'read' ,
) ;
2026-01-27 13:17:40 -08:00
if ( validationError ) {
return {
llmContent : validationError ,
returnDisplay : 'Error: Path not in workspace.' ,
error : {
message : validationError ,
type : ToolErrorType . PATH_NOT_IN_WORKSPACE ,
} ,
} ;
}
try {
const stats = await fsPromises . stat ( searchDirAbs ) ;
if ( ! stats . isDirectory ( ) ) {
return {
llmContent : ` Path is not a directory: ${ searchDirAbs } ` ,
returnDisplay : 'Error: Path is not a directory.' ,
error : {
message : ` Path is not a directory: ${ searchDirAbs } ` ,
type : ToolErrorType . PATH_IS_NOT_A_DIRECTORY ,
} ,
} ;
}
} catch ( error : unknown ) {
if ( isNodeError ( error ) && error . code === 'ENOENT' ) {
return {
llmContent : ` Path does not exist: ${ searchDirAbs } ` ,
returnDisplay : 'Error: Path does not exist.' ,
error : {
message : ` Path does not exist: ${ searchDirAbs } ` ,
type : ToolErrorType . FILE_NOT_FOUND ,
} ,
} ;
}
const errorMessage = getErrorMessage ( error ) ;
return {
llmContent : ` Failed to access path stats for ${ searchDirAbs } : ${ errorMessage } ` ,
returnDisplay : 'Error: Failed to access path.' ,
error : {
message : ` Failed to access path stats for ${ searchDirAbs } : ${ errorMessage } ` ,
type : ToolErrorType . GREP_EXECUTION_ERROR ,
} ,
} ;
}
}
const searchDirDisplay = pathParam || '.' ;
2025-04-19 19:45:42 +01:00
2025-07-31 05:38:20 +09:00
// Determine which directories to search
let searchDirectories : readonly string [ ] ;
if ( searchDirAbs === null ) {
// No path specified - search all workspace directories
searchDirectories = workspaceContext . getDirectories ( ) ;
} else {
// Specific path provided - search only that directory
searchDirectories = [ searchDirAbs ] ;
}
2025-04-19 19:45:42 +01:00
2025-07-31 05:38:20 +09:00
// Collect matches from all search directories
let allMatches : GrepMatch [ ] = [ ] ;
2026-02-11 03:50:10 +00:00
const totalMaxMatches =
this . params . total_max_matches ? ? DEFAULT_TOTAL_MAX_MATCHES ;
2026-01-26 16:52:19 -05:00
// Create a timeout controller to prevent indefinitely hanging searches
const timeoutController = new AbortController ( ) ;
2026-03-27 19:12:34 -04:00
const configTimeout = this . config . getFileFilteringOptions ( ) . searchTimeout ;
// If configTimeout is less than standard default, it might be too short for grep.
// We check if it's greater or if we should use DEFAULT_SEARCH_TIMEOUT_MS as a fallback.
// Let's assume the user can set it higher if they want. Using it directly if it exists, otherwise fallback.
const timeoutMs =
configTimeout && configTimeout > DEFAULT_SEARCH_TIMEOUT_MS
? configTimeout
: DEFAULT_SEARCH_TIMEOUT_MS ;
2026-01-26 16:52:19 -05:00
const timeoutId = setTimeout ( ( ) = > {
timeoutController . abort ( ) ;
2026-03-27 19:12:34 -04:00
} , timeoutMs ) ;
2026-01-26 16:52:19 -05:00
// Link the passed signal to our timeout controller
const onAbort = ( ) = > timeoutController . abort ( ) ;
if ( signal . aborted ) {
onAbort ( ) ;
} else {
signal . addEventListener ( 'abort' , onAbort , { once : true } ) ;
}
2025-07-31 05:38:20 +09:00
2026-01-26 16:52:19 -05:00
try {
for ( const searchDir of searchDirectories ) {
const remainingLimit = totalMaxMatches - allMatches . length ;
if ( remainingLimit <= 0 ) break ;
const matches = await this . performGrepSearch ( {
pattern : this.params.pattern ,
path : searchDir ,
2026-02-25 20:16:21 -08:00
include_pattern : this.params.include_pattern ,
2026-02-11 19:20:51 +00:00
exclude_pattern : this.params.exclude_pattern ,
2026-01-26 16:52:19 -05:00
maxMatches : remainingLimit ,
2026-02-11 03:50:10 +00:00
max_matches_per_file : this.params.max_matches_per_file ,
2026-01-26 16:52:19 -05:00
signal : timeoutController.signal ,
2025-07-31 05:38:20 +09:00
} ) ;
2026-01-26 16:52:19 -05:00
// Add directory prefix if searching multiple directories
if ( searchDirectories . length > 1 ) {
const dirName = path . basename ( searchDir ) ;
matches . forEach ( ( match ) = > {
match . filePath = path . join ( dirName , match . filePath ) ;
} ) ;
}
allMatches = allMatches . concat ( matches ) ;
}
2026-03-27 19:12:34 -04:00
} catch ( error ) {
if ( timeoutController . signal . aborted ) {
throw new Error (
` Operation timed out after ${ timeoutMs } ms. In large repositories, consider narrowing your search scope by specifying a 'dir_path' or an 'include_pattern'. ` ,
) ;
}
throw error ;
2026-01-26 16:52:19 -05:00
} finally {
clearTimeout ( timeoutId ) ;
signal . removeEventListener ( 'abort' , onAbort ) ;
2025-07-31 05:38:20 +09:00
}
let searchLocationDescription : string ;
if ( searchDirAbs === null ) {
const numDirs = workspaceContext . getDirectories ( ) . length ;
searchLocationDescription =
numDirs > 1
? ` across ${ numDirs } workspace directories `
: ` in the workspace directory ` ;
} else {
searchLocationDescription = ` in path " ${ searchDirDisplay } " ` ;
}
2026-02-21 00:36:10 +00:00
return await formatGrepResults (
allMatches ,
this . params ,
searchLocationDescription ,
totalMaxMatches ,
2025-04-19 19:45:42 +01:00
) ;
} catch ( error ) {
2025-10-27 11:35:16 -07:00
debugLogger . warn ( ` Error during GrepLogic execution: ${ error } ` ) ;
2025-05-18 23:13:57 -07:00
const errorMessage = getErrorMessage ( error ) ;
2025-04-19 19:45:42 +01:00
return {
llmContent : ` Error during grep search operation: ${ errorMessage } ` ,
returnDisplay : ` Error: ${ errorMessage } ` ,
2025-08-21 14:40:18 -07:00
error : {
message : errorMessage ,
type : ToolErrorType . GREP_EXECUTION_ERROR ,
} ,
2025-04-19 19:45:42 +01:00
} ;
}
}
2026-03-10 13:01:41 -04:00
override getPolicyUpdateOptions (
_outcome : ToolConfirmationOutcome ,
) : PolicyUpdateOptions | undefined {
return {
argsPattern : buildPatternArgsPattern ( this . params . pattern ) ,
} ;
}
2025-04-19 19:45:42 +01:00
/**
* Checks if a command is available in the system's PATH.
* @param {string} command The command name (e.g., 'git', 'grep').
* @returns {Promise<boolean>} True if the command is available, false otherwise.
*/
2026-03-13 14:11:51 -07:00
private async isCommandAvailable ( command : string ) : Promise < boolean > {
const checkCommand = process . platform === 'win32' ? 'where' : 'command' ;
const checkArgs =
process . platform === 'win32' ? [ command ] : [ '-v' , command ] ;
try {
const sandboxManager = this . config . sandboxManager ;
let finalCommand = checkCommand ;
let finalArgs = checkArgs ;
let finalEnv = process . env ;
if ( sandboxManager ) {
try {
const prepared = await sandboxManager . prepareCommand ( {
command : checkCommand ,
args : checkArgs ,
cwd : process.cwd ( ) ,
env : process.env ,
} ) ;
finalCommand = prepared . program ;
finalArgs = prepared . args ;
finalEnv = prepared . env ;
} catch ( err ) {
debugLogger . debug (
` [GrepTool] Sandbox preparation failed for ' ${ command } ': ` ,
err ,
) ;
}
}
return await new Promise ( ( resolve ) = > {
const child = spawn ( finalCommand , finalArgs , {
2025-04-19 19:45:42 +01:00
stdio : 'ignore' ,
2025-10-19 03:08:01 +00:00
shell : true ,
2026-03-13 14:11:51 -07:00
env : finalEnv ,
2025-04-19 19:45:42 +01:00
} ) ;
child . on ( 'close' , ( code ) = > resolve ( code === 0 ) ) ;
2025-10-19 03:08:01 +00:00
child . on ( 'error' , ( err ) = > {
debugLogger . debug (
` [GrepTool] Failed to start process for ' ${ command } ': ` ,
err . message ,
) ;
resolve ( false ) ;
} ) ;
2026-03-13 14:11:51 -07:00
} ) ;
} catch {
return false ;
}
2025-04-19 19:45:42 +01:00
}
/**
* Performs the actual search using the prioritized strategies.
* @param options Search options including pattern, absolute path, and include glob.
* @returns A promise resolving to an array of match objects.
*/
private async performGrepSearch ( options : {
pattern : string ;
path : string ; // Expects absolute path
2026-02-25 20:16:21 -08:00
include_pattern? : string ;
2026-02-11 19:20:51 +00:00
exclude_pattern? : string ;
2026-01-26 16:52:19 -05:00
maxMatches : number ;
2026-02-11 03:50:10 +00:00
max_matches_per_file? : number ;
2025-06-12 19:46:00 -07:00
signal : AbortSignal ;
2025-04-19 19:45:42 +01:00
} ) : Promise < GrepMatch [ ] > {
2026-02-11 03:50:10 +00:00
const {
pattern ,
path : absolutePath ,
2026-02-25 20:16:21 -08:00
include_pattern ,
2026-02-11 19:20:51 +00:00
exclude_pattern ,
2026-02-11 03:50:10 +00:00
maxMatches ,
max_matches_per_file ,
} = options ;
2025-04-19 19:45:42 +01:00
let strategyUsed = 'none' ;
try {
2026-02-11 19:20:51 +00:00
let excludeRegex : RegExp | null = null ;
if ( exclude_pattern ) {
excludeRegex = new RegExp ( exclude_pattern , 'i' ) ;
}
2025-04-19 19:45:42 +01:00
// --- Strategy 1: git grep ---
2025-06-13 12:45:07 -04:00
const isGit = isGitRepository ( absolutePath ) ;
2025-04-19 19:45:42 +01:00
const gitAvailable = isGit && ( await this . isCommandAvailable ( 'git' ) ) ;
if ( gitAvailable ) {
strategyUsed = 'git grep' ;
const gitArgs = [
'grep' ,
'--untracked' ,
'-n' ,
'-E' ,
'--ignore-case' ,
pattern ,
] ;
2026-02-11 03:50:10 +00:00
if ( max_matches_per_file ) {
gitArgs . push ( '--max-count' , max_matches_per_file . toString ( ) ) ;
}
2026-02-25 20:16:21 -08:00
if ( include_pattern ) {
gitArgs . push ( '--' , include_pattern ) ;
2025-04-19 19:45:42 +01:00
}
try {
2026-01-26 16:52:19 -05:00
const generator = execStreaming ( 'git' , gitArgs , {
cwd : absolutePath ,
signal : options.signal ,
allowedExitCodes : [ 0 , 1 ] ,
2026-03-13 14:11:51 -07:00
sandboxManager : this.config.sandboxManager ,
2025-04-19 19:45:42 +01:00
} ) ;
2026-01-26 16:52:19 -05:00
const results : GrepMatch [ ] = [ ] ;
for await ( const line of generator ) {
const match = this . parseGrepLine ( line , absolutePath ) ;
if ( match ) {
2026-02-11 19:20:51 +00:00
if ( excludeRegex && excludeRegex . test ( match . line ) ) {
continue ;
}
2026-01-26 16:52:19 -05:00
results . push ( match ) ;
if ( results . length >= maxMatches ) {
break ;
}
}
}
return results ;
2025-04-19 19:45:42 +01:00
} catch ( gitError : unknown ) {
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-08-07 10:05:37 -07:00
` GrepLogic: git grep failed: ${ getErrorMessage (
gitError ,
) } . Falling back... ` ,
2025-04-19 19:45:42 +01:00
) ;
}
}
// --- Strategy 2: System grep ---
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-10-19 03:08:01 +00:00
'GrepLogic: System grep is being considered as fallback strategy.' ,
) ;
2025-04-19 19:45:42 +01:00
const grepAvailable = await this . isCommandAvailable ( 'grep' ) ;
if ( grepAvailable ) {
strategyUsed = 'system grep' ;
2025-10-19 03:08:01 +00:00
const grepArgs = [ '-r' , '-n' , '-H' , '-E' , '-I' ] ;
2025-08-23 13:35:00 +09:00
// Extract directory names from exclusion patterns for grep --exclude-dir
const globExcludes = this . fileExclusions . getGlobExcludes ( ) ;
const commonExcludes = globExcludes
. map ( ( pattern ) = > {
let dir = pattern ;
if ( dir . startsWith ( '**/' ) ) {
dir = dir . substring ( 3 ) ;
}
if ( dir . endsWith ( '/**' ) ) {
dir = dir . slice ( 0 , - 3 ) ;
} else if ( dir . endsWith ( '/' ) ) {
dir = dir . slice ( 0 , - 1 ) ;
}
// Only consider patterns that are likely directories. This filters out file patterns.
if ( dir && ! dir . includes ( '/' ) && ! dir . includes ( '*' ) ) {
return dir ;
}
return null ;
} )
. filter ( ( dir ) : dir is string = > ! ! dir ) ;
2025-04-19 19:45:42 +01:00
commonExcludes . forEach ( ( dir ) = > grepArgs . push ( ` --exclude-dir= ${ dir } ` ) ) ;
2026-02-11 03:50:10 +00:00
if ( max_matches_per_file ) {
grepArgs . push ( '--max-count' , max_matches_per_file . toString ( ) ) ;
}
2026-02-25 20:16:21 -08:00
if ( include_pattern ) {
grepArgs . push ( ` --include= ${ include_pattern } ` ) ;
2025-04-19 19:45:42 +01:00
}
grepArgs . push ( pattern ) ;
grepArgs . push ( '.' ) ;
2026-01-26 16:52:19 -05:00
const results : GrepMatch [ ] = [ ] ;
2025-04-19 19:45:42 +01:00
try {
2026-01-26 16:52:19 -05:00
const generator = execStreaming ( 'grep' , grepArgs , {
cwd : absolutePath ,
signal : options.signal ,
allowedExitCodes : [ 0 , 1 ] ,
2026-03-13 14:11:51 -07:00
sandboxManager : this.config.sandboxManager ,
2025-04-19 19:45:42 +01:00
} ) ;
2026-01-26 16:52:19 -05:00
for await ( const line of generator ) {
const match = this . parseGrepLine ( line , absolutePath ) ;
if ( match ) {
2026-02-11 19:20:51 +00:00
if ( excludeRegex && excludeRegex . test ( match . line ) ) {
continue ;
}
2026-01-26 16:52:19 -05:00
results . push ( match ) ;
if ( results . length >= maxMatches ) {
break ;
}
}
}
return results ;
2025-04-19 19:45:42 +01:00
} catch ( grepError : unknown ) {
2026-01-26 16:52:19 -05:00
if (
grepError instanceof Error &&
/Permission denied|Is a directory/i . test ( grepError . message )
) {
return results ;
}
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-08-07 10:05:37 -07:00
` GrepLogic: System grep failed: ${ getErrorMessage (
grepError ,
) } . Falling back... ` ,
2025-04-19 19:45:42 +01:00
) ;
}
}
// --- Strategy 3: Pure JavaScript Fallback ---
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-04-19 19:45:42 +01:00
'GrepLogic: Falling back to JavaScript grep implementation.' ,
) ;
strategyUsed = 'javascript fallback' ;
2026-02-25 20:16:21 -08:00
const globPattern = include_pattern ? include_pattern : '**/*' ;
2025-08-23 13:35:00 +09:00
const ignorePatterns = this . fileExclusions . getGlobExcludes ( ) ;
2025-04-19 19:45:42 +01:00
2025-06-12 19:46:00 -07:00
const filesStream = globStream ( globPattern , {
2025-04-19 19:45:42 +01:00
cwd : absolutePath ,
dot : true ,
ignore : ignorePatterns ,
absolute : true ,
2025-06-12 19:46:00 -07:00
nodir : true ,
signal : options.signal ,
2025-04-19 19:45:42 +01:00
} ) ;
const regex = new RegExp ( pattern , 'i' ) ;
const allMatches : GrepMatch [ ] = [ ] ;
for await ( const filePath of filesStream ) {
2026-01-26 16:52:19 -05:00
if ( allMatches . length >= maxMatches ) break ;
2025-12-12 17:43:43 -08:00
const fileAbsolutePath = filePath ;
2026-01-26 16:52:19 -05:00
// security check
const relativePath = path . relative ( absolutePath , fileAbsolutePath ) ;
if (
relativePath === '..' ||
relativePath . startsWith ( ` .. ${ path . sep } ` ) ||
path . isAbsolute ( relativePath )
)
continue ;
2025-04-19 19:45:42 +01:00
try {
const content = await fsPromises . readFile ( fileAbsolutePath , 'utf8' ) ;
const lines = content . split ( /\r?\n/ ) ;
2026-02-11 03:50:10 +00:00
let matchesInFile = 0 ;
2026-01-26 16:52:19 -05:00
for ( let index = 0 ; index < lines . length ; index ++ ) {
const line = lines [ index ] ;
2025-04-19 19:45:42 +01:00
if ( regex . test ( line ) ) {
2026-02-11 19:20:51 +00:00
if ( excludeRegex && excludeRegex . test ( line ) ) {
continue ;
}
2025-04-19 19:45:42 +01:00
allMatches . push ( {
filePath :
path.relative ( absolutePath , fileAbsolutePath ) ||
path . basename ( fileAbsolutePath ) ,
2026-02-21 00:36:10 +00:00
absolutePath : fileAbsolutePath ,
2025-04-19 19:45:42 +01:00
lineNumber : index + 1 ,
line ,
} ) ;
2026-02-11 03:50:10 +00:00
matchesInFile ++ ;
2026-01-26 16:52:19 -05:00
if ( allMatches . length >= maxMatches ) break ;
2026-02-11 03:50:10 +00:00
if (
max_matches_per_file &&
matchesInFile >= max_matches_per_file
) {
break ;
}
2025-04-19 19:45:42 +01:00
}
2026-01-26 16:52:19 -05:00
}
2025-04-19 19:45:42 +01:00
} catch ( readError : unknown ) {
// Ignore errors like permission denied or file gone during read
if ( ! isNodeError ( readError ) || readError . code !== 'ENOENT' ) {
2025-10-21 16:35:22 -04:00
debugLogger . debug (
2025-08-07 10:05:37 -07:00
` GrepLogic: Could not read/process ${ fileAbsolutePath } : ${ getErrorMessage (
readError ,
) } ` ,
2025-04-19 19:45:42 +01:00
) ;
}
}
}
return allMatches ;
} catch ( error : unknown ) {
2025-10-27 11:35:16 -07:00
debugLogger . warn (
2025-08-07 10:05:37 -07:00
` GrepLogic: Error in performGrepSearch (Strategy: ${ strategyUsed } ): ${ getErrorMessage (
error ,
) } ` ,
2025-04-19 19:45:42 +01:00
) ;
throw error ; // Re-throw
}
}
2025-08-07 10:05:37 -07:00
2026-01-26 16:52:19 -05:00
getDescription ( ) : string {
let description = ` ' ${ this . params . pattern } ' ` ;
2026-02-25 20:16:21 -08:00
if ( this . params . include_pattern ) {
description += ` in ${ this . params . include_pattern } ` ;
2026-01-26 16:52:19 -05:00
}
if ( this . params . dir_path ) {
const resolvedPath = path . resolve (
this . config . getTargetDir ( ) ,
this . params . dir_path ,
) ;
if (
resolvedPath === this . config . getTargetDir ( ) ||
this . params . dir_path === '.'
) {
description += ` within ./ ` ;
} else {
const relativePath = makeRelative (
resolvedPath ,
this . config . getTargetDir ( ) ,
) ;
description += ` within ${ shortenPath ( relativePath ) } ` ;
}
} else {
// When no path is specified, indicate searching all workspace directories
const workspaceContext = this . config . getWorkspaceContext ( ) ;
const directories = workspaceContext . getDirectories ( ) ;
if ( directories . length > 1 ) {
description += ` across all workspace directories ` ;
}
}
return description ;
}
}
2025-08-07 10:05:37 -07:00
/**
* Implementation of the Grep tool logic (moved from CLI)
*/
export class GrepTool extends BaseDeclarativeTool < GrepToolParams , ToolResult > {
2025-10-20 22:35:35 -04:00
static readonly Name = GREP_TOOL_NAME ;
2025-10-21 11:45:33 -07:00
constructor (
private readonly config : Config ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-21 11:45:33 -07:00
) {
2025-08-07 10:05:37 -07:00
super (
2025-10-20 22:35:35 -04:00
GrepTool . Name ,
2026-03-30 16:43:29 -07:00
GREP_DISPLAY_NAME ,
2026-02-09 20:29:52 -05:00
GREP_DEFINITION . base . description ! ,
2025-08-13 12:58:26 -03:00
Kind . Search ,
2026-02-09 20:29:52 -05:00
GREP_DEFINITION . base . parametersJsonSchema ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-21 11:45:33 -07:00
true ,
false ,
2025-08-07 10:05:37 -07:00
) ;
}
/**
* Validates the parameters for the tool
* @param params Parameters to validate
* @returns An error message string if invalid, null otherwise
*/
2025-08-19 13:55:06 -07:00
protected override validateToolParamValues (
params : GrepToolParams ,
) : string | null {
2025-08-07 10:05:37 -07:00
try {
new RegExp ( params . pattern ) ;
} catch ( error ) {
return ` Invalid regular expression pattern provided: ${ params . pattern } . Error: ${ getErrorMessage ( error ) } ` ;
}
2026-02-11 19:20:51 +00:00
if ( params . exclude_pattern ) {
try {
new RegExp ( params . exclude_pattern ) ;
} catch ( error ) {
return ` Invalid exclude regular expression pattern provided: ${ params . exclude_pattern } . Error: ${ getErrorMessage ( error ) } ` ;
}
}
2026-02-11 03:50:10 +00:00
if (
params . max_matches_per_file !== undefined &&
params . max_matches_per_file < 1
) {
return 'max_matches_per_file must be at least 1.' ;
}
if (
params . total_max_matches !== undefined &&
params . total_max_matches < 1
) {
return 'total_max_matches must be at least 1.' ;
}
2025-11-06 15:03:52 -08:00
// Only validate dir_path if one is provided
if ( params . dir_path ) {
2026-01-27 13:17:40 -08:00
const resolvedPath = path . resolve (
this . config . getTargetDir ( ) ,
params . dir_path ,
) ;
2026-02-09 12:24:28 -08:00
const validationError = this . config . validatePathAccess (
resolvedPath ,
'read' ,
) ;
2026-01-27 13:17:40 -08:00
if ( validationError ) {
return validationError ;
}
// We still want to check if it's a directory
2025-08-07 10:05:37 -07:00
try {
2026-01-27 13:17:40 -08:00
const stats = fs . statSync ( resolvedPath ) ;
if ( ! stats . isDirectory ( ) ) {
return ` Path is not a directory: ${ resolvedPath } ` ;
}
} catch ( error : unknown ) {
if ( isNodeError ( error ) && error . code === 'ENOENT' ) {
return ` Path does not exist: ${ resolvedPath } ` ;
}
return ` Failed to access path stats for ${ resolvedPath } : ${ getErrorMessage ( error ) } ` ;
2025-08-07 10:05:37 -07:00
}
}
return null ; // Parameters are valid
}
protected createInvocation (
params : GrepToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-21 11:45:33 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-07 10:05:37 -07:00
) : ToolInvocation < GrepToolParams , ToolResult > {
2025-10-21 11:45:33 -07:00
return new GrepToolInvocation (
this . config ,
params ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-21 11:45:33 -07:00
_toolName ,
_toolDisplayName ,
) ;
2025-08-07 10:05:37 -07:00
}
2026-02-09 20:29:52 -05:00
override getSchema ( modelId? : string ) {
return resolveToolDeclaration ( GREP_DEFINITION , modelId ) ;
}
2025-08-07 10:05:37 -07:00
}