2025-04-19 19:45:42 +01:00
/ * *
* @license
* Copyright 2025 Google LLC
* 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 { EOL } from 'node:os' ;
import { spawn } from 'node:child_process' ;
2025-06-12 19:46:00 -07:00
import { globStream } from 'glob' ;
2025-08-26 00:04:53 +02:00
import type { ToolInvocation , ToolResult } from './tools.js' ;
import { BaseDeclarativeTool , BaseToolInvocation , Kind } 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' ;
2025-10-19 19:21:47 -04:00
import { GREP_TOOL_NAME } from './tool-names.js' ;
2025-10-19 03:08:01 +00:00
import { debugLogger } from '../utils/debugLogger.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}" )
* /
include? : string ;
}
/ * *
* Result object for a single grep match
* /
interface GrepMatch {
filePath : string ;
lineNumber : number ;
line : string ;
}
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 ,
2025-10-21 11:45:33 -07:00
messageBus? : MessageBus ,
_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
}
/ * *
* Checks if a path is within the root directory and resolves it .
* @param relativePath Path relative to the root directory ( or undefined for root ) .
2025-07-31 05:38:20 +09:00
* @returns The absolute path if valid and exists , or null if no path specified ( to search all directories ) .
2025-04-19 19:45:42 +01:00
* @throws { Error } If path is outside root , doesn 't exist, or isn' t a directory .
* /
2025-07-31 05:38:20 +09:00
private resolveAndValidatePath ( relativePath? : string ) : string | null {
// If no path specified, return null to indicate searching all workspace directories
if ( ! relativePath ) {
return null ;
}
const targetPath = path . resolve ( this . config . getTargetDir ( ) , relativePath ) ;
2025-04-19 19:45:42 +01:00
2025-07-31 05:38:20 +09:00
// Security Check: Ensure the resolved path is within workspace boundaries
const workspaceContext = this . config . getWorkspaceContext ( ) ;
if ( ! workspaceContext . isPathWithinWorkspace ( targetPath ) ) {
const directories = workspaceContext . getDirectories ( ) ;
2025-04-19 19:45:42 +01:00
throw new Error (
2025-07-31 05:38:20 +09:00
` Path validation failed: Attempted path " ${ relativePath } " resolves outside the allowed workspace directories: ${ directories . join ( ', ' ) } ` ,
2025-04-19 19:45:42 +01:00
) ;
}
// Check existence and type after resolving
try {
const stats = fs . statSync ( targetPath ) ;
if ( ! stats . isDirectory ( ) ) {
throw new Error ( ` Path is not a directory: ${ targetPath } ` ) ;
}
} catch ( error : unknown ) {
if ( isNodeError ( error ) && error . code !== 'ENOENT' ) {
throw new Error ( ` Path does not exist: ${ targetPath } ` ) ;
}
throw new Error (
` Failed to access path stats for ${ targetPath } : ${ error } ` ,
) ;
}
return targetPath ;
}
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 ( ) ;
2025-11-06 15:03:52 -08:00
const searchDirAbs = this . resolveAndValidatePath ( this . params . dir_path ) ;
const searchDirDisplay = this . params . dir_path || '.' ;
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 [ ] = [ ] ;
for ( const searchDir of searchDirectories ) {
const matches = await this . performGrepSearch ( {
2025-08-07 10:05:37 -07:00
pattern : this.params.pattern ,
2025-07-31 05:38:20 +09:00
path : searchDir ,
2025-08-07 10:05:37 -07:00
include : this.params.include ,
2025-07-31 05:38:20 +09:00
signal ,
} ) ;
// 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 ) ;
}
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 } " ` ;
}
if ( allMatches . length === 0 ) {
2025-08-07 10:05:37 -07:00
const noMatchMsg = ` No matches found for pattern " ${ this . params . pattern } " ${ searchLocationDescription } ${ this . params . include ? ` (filter: " ${ this . params . include } ") ` : '' } . ` ;
2025-04-19 19:45:42 +01:00
return { llmContent : noMatchMsg , returnDisplay : ` No matches found ` } ;
}
2025-07-31 05:38:20 +09:00
// Group matches by file
const matchesByFile = allMatches . reduce (
2025-04-19 19:45:42 +01:00
( acc , match ) = > {
2025-07-31 05:38:20 +09:00
const fileKey = match . filePath ;
if ( ! acc [ fileKey ] ) {
acc [ fileKey ] = [ ] ;
2025-04-19 19:45:42 +01:00
}
2025-07-31 05:38:20 +09:00
acc [ fileKey ] . push ( match ) ;
acc [ fileKey ] . sort ( ( a , b ) = > a . lineNumber - b . lineNumber ) ;
2025-04-19 19:45:42 +01:00
return acc ;
} ,
{ } as Record < string , GrepMatch [ ] > ,
) ;
2025-07-31 05:38:20 +09:00
const matchCount = allMatches . length ;
2025-06-28 17:41:25 +03:00
const matchTerm = matchCount === 1 ? 'match' : 'matches' ;
2025-08-07 10:05:37 -07:00
let llmContent = ` Found ${ matchCount } ${ matchTerm } for pattern " ${ this . params . pattern } " ${ searchLocationDescription } ${ this . params . include ? ` (filter: " ${ this . params . include } ") ` : '' } :
2025-07-31 05:38:20 +09:00
-- -
` ;
2025-04-19 19:45:42 +01:00
for ( const filePath in matchesByFile ) {
llmContent += ` File: ${ filePath } \ n ` ;
matchesByFile [ filePath ] . forEach ( ( match ) = > {
const trimmedLine = match . line . trim ( ) ;
llmContent += ` L ${ match . lineNumber } : ${ trimmedLine } \ n ` ;
} ) ;
llmContent += '---\n' ;
}
return {
llmContent : llmContent.trim ( ) ,
2025-06-28 17:41:25 +03:00
returnDisplay : ` Found ${ matchCount } ${ matchTerm } ` ,
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
} ;
}
}
/ * *
* 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 .
* /
private isCommandAvailable ( command : string ) : Promise < boolean > {
return new Promise ( ( resolve ) = > {
const checkCommand = process . platform === 'win32' ? 'where' : 'command' ;
const checkArgs =
process . platform === 'win32' ? [ command ] : [ '-v' , command ] ;
try {
const child = spawn ( checkCommand , checkArgs , {
stdio : 'ignore' ,
2025-10-19 03:08:01 +00:00
shell : true ,
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 ) ;
} ) ;
2025-04-19 19:45:42 +01:00
} catch {
resolve ( false ) ;
}
} ) ;
}
/ * *
* Parses the standard output of grep - like commands ( git grep , system grep ) .
* Expects format : filePath : lineNumber :lineContent
* Handles colons within file paths and line content correctly .
* @param { string } output The raw stdout string .
* @param { string } basePath The absolute directory the search was run from , for relative paths .
* @returns { GrepMatch [ ] } Array of match objects .
* /
private parseGrepOutput ( output : string , basePath : string ) : GrepMatch [ ] {
const results : GrepMatch [ ] = [ ] ;
if ( ! output ) return results ;
const lines = output . split ( EOL ) ; // Use OS-specific end-of-line
for ( const line of lines ) {
if ( ! line . trim ( ) ) continue ;
// Find the index of the first colon.
const firstColonIndex = line . indexOf ( ':' ) ;
if ( firstColonIndex === - 1 ) continue ; // Malformed
// Find the index of the second colon, searching *after* the first one.
const secondColonIndex = line . indexOf ( ':' , firstColonIndex + 1 ) ;
if ( secondColonIndex === - 1 ) continue ; // Malformed
// Extract parts based on the found colon indices
const filePathRaw = line . substring ( 0 , firstColonIndex ) ;
const lineNumberStr = line . substring (
firstColonIndex + 1 ,
secondColonIndex ,
) ;
const lineContent = line . substring ( secondColonIndex + 1 ) ;
const lineNumber = parseInt ( lineNumberStr , 10 ) ;
if ( ! isNaN ( lineNumber ) ) {
const absoluteFilePath = path . resolve ( basePath , filePathRaw ) ;
const relativeFilePath = path . relative ( basePath , absoluteFilePath ) ;
results . push ( {
filePath : relativeFilePath || path . basename ( absoluteFilePath ) ,
lineNumber ,
line : lineContent ,
} ) ;
}
}
return results ;
}
/ * *
* Gets a description of the grep operation
* @returns A string describing the grep
* /
2025-08-07 10:05:37 -07:00
getDescription ( ) : string {
let description = ` ' ${ this . params . pattern } ' ` ;
if ( this . params . include ) {
description += ` in ${ this . params . include } ` ;
2025-04-19 19:45:42 +01:00
}
2025-11-06 15:03:52 -08:00
if ( this . params . dir_path ) {
2025-07-14 22:55:49 -07:00
const resolvedPath = path . resolve (
this . config . getTargetDir ( ) ,
2025-11-06 15:03:52 -08:00
this . params . dir_path ,
2025-07-14 22:55:49 -07:00
) ;
2025-08-07 10:05:37 -07:00
if (
resolvedPath === this . config . getTargetDir ( ) ||
2025-11-06 15:03:52 -08:00
this . params . dir_path === '.'
2025-08-07 10:05:37 -07:00
) {
2025-05-18 23:13:57 -07:00
description += ` within ./ ` ;
} else {
2025-07-14 22:55:49 -07:00
const relativePath = makeRelative (
resolvedPath ,
this . config . getTargetDir ( ) ,
) ;
2025-05-18 23:13:57 -07:00
description += ` within ${ shortenPath ( relativePath ) } ` ;
}
2025-07-31 05:38:20 +09:00
} 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 ` ;
}
2025-04-19 19:45:42 +01:00
}
return description ;
}
/ * *
* 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
include? : string ;
2025-06-12 19:46:00 -07:00
signal : AbortSignal ;
2025-04-19 19:45:42 +01:00
} ) : Promise < GrepMatch [ ] > {
const { pattern , path : absolutePath , include } = options ;
let strategyUsed = 'none' ;
try {
// --- 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 ,
] ;
if ( include ) {
gitArgs . push ( '--' , include ) ;
}
try {
const output = await new Promise < string > ( ( resolve , reject ) = > {
const child = spawn ( 'git' , gitArgs , {
cwd : absolutePath ,
windowsHide : true ,
} ) ;
const stdoutChunks : Buffer [ ] = [ ] ;
const stderrChunks : Buffer [ ] = [ ] ;
child . stdout . on ( 'data' , ( chunk ) = > stdoutChunks . push ( chunk ) ) ;
child . stderr . on ( 'data' , ( chunk ) = > stderrChunks . push ( chunk ) ) ;
child . on ( 'error' , ( err ) = >
reject ( new Error ( ` Failed to start git grep: ${ err . message } ` ) ) ,
) ;
child . on ( 'close' , ( code ) = > {
const stdoutData = Buffer . concat ( stdoutChunks ) . toString ( 'utf8' ) ;
const stderrData = Buffer . concat ( stderrChunks ) . toString ( 'utf8' ) ;
if ( code === 0 ) resolve ( stdoutData ) ;
else if ( code === 1 )
resolve ( '' ) ; // No matches
else
reject (
new Error ( ` git grep exited with code ${ code } : ${ stderrData } ` ) ,
) ;
} ) ;
} ) ;
return this . parseGrepOutput ( output , absolutePath ) ;
} 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 } ` ) ) ;
if ( include ) {
grepArgs . push ( ` --include= ${ include } ` ) ;
}
grepArgs . push ( pattern ) ;
grepArgs . push ( '.' ) ;
try {
const output = await new Promise < string > ( ( resolve , reject ) = > {
const child = spawn ( 'grep' , grepArgs , {
cwd : absolutePath ,
windowsHide : true ,
} ) ;
const stdoutChunks : Buffer [ ] = [ ] ;
const stderrChunks : Buffer [ ] = [ ] ;
2025-06-05 06:40:33 -07:00
const onData = ( chunk : Buffer ) = > stdoutChunks . push ( chunk ) ;
const onStderr = ( chunk : Buffer ) = > {
2025-04-19 19:45:42 +01:00
const stderrStr = chunk . toString ( ) ;
// Suppress common harmless stderr messages
if (
! stderrStr . includes ( 'Permission denied' ) &&
! /grep:.*: Is a directory/i . test ( stderrStr )
) {
stderrChunks . push ( chunk ) ;
}
2025-06-05 06:40:33 -07:00
} ;
const onError = ( err : Error ) = > {
cleanup ( ) ;
reject ( new Error ( ` Failed to start system grep: ${ err . message } ` ) ) ;
} ;
const onClose = ( code : number | null ) = > {
2025-04-19 19:45:42 +01:00
const stdoutData = Buffer . concat ( stdoutChunks ) . toString ( 'utf8' ) ;
const stderrData = Buffer . concat ( stderrChunks )
. toString ( 'utf8' )
. trim ( ) ;
2025-06-05 06:40:33 -07:00
cleanup ( ) ;
2025-04-19 19:45:42 +01:00
if ( code === 0 ) resolve ( stdoutData ) ;
else if ( code === 1 )
resolve ( '' ) ; // No matches
else {
if ( stderrData )
reject (
new Error (
` System grep exited with code ${ code } : ${ stderrData } ` ,
) ,
) ;
else resolve ( '' ) ; // Exit code > 1 but no stderr, likely just suppressed errors
}
2025-06-05 06:40:33 -07:00
} ;
const cleanup = ( ) = > {
child . stdout . removeListener ( 'data' , onData ) ;
child . stderr . removeListener ( 'data' , onStderr ) ;
child . removeListener ( 'error' , onError ) ;
child . removeListener ( 'close' , onClose ) ;
if ( child . connected ) {
child . disconnect ( ) ;
}
} ;
child . stdout . on ( 'data' , onData ) ;
child . stderr . on ( 'data' , onStderr ) ;
child . on ( 'error' , onError ) ;
child . on ( 'close' , onClose ) ;
2025-04-19 19:45:42 +01:00
} ) ;
return this . parseGrepOutput ( output , absolutePath ) ;
} catch ( grepError : unknown ) {
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' ;
const globPattern = include ? include : '**/*' ;
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 ) {
const fileAbsolutePath = filePath as string ;
try {
const content = await fsPromises . readFile ( fileAbsolutePath , 'utf8' ) ;
const lines = content . split ( /\r?\n/ ) ;
lines . forEach ( ( line , index ) = > {
if ( regex . test ( line ) ) {
allMatches . push ( {
filePath :
path . relative ( absolutePath , fileAbsolutePath ) ||
path . basename ( fileAbsolutePath ) ,
lineNumber : index + 1 ,
line ,
} ) ;
}
} ) ;
} 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
// --- GrepLogic Class ---
/ * *
* 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 ,
messageBus? : MessageBus ,
) {
2025-08-07 10:05:37 -07:00
super (
2025-10-20 22:35:35 -04:00
GrepTool . Name ,
2025-08-07 10:05:37 -07:00
'SearchText' ,
'Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.' ,
2025-08-13 12:58:26 -03:00
Kind . Search ,
2025-08-07 10:05:37 -07:00
{
properties : {
pattern : {
description :
"The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*')." ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
2025-11-06 15:03:52 -08:00
dir_path : {
2025-08-07 10:05:37 -07:00
description :
'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.' ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
include : {
description :
"Optional: A glob pattern to filter which files are searched (e.g., '*.js', '*.{ts,tsx}', 'src/**'). If omitted, searches all files (respecting potential global ignores)." ,
2025-08-11 16:12:41 -07:00
type : 'string' ,
2025-08-07 10:05:37 -07:00
} ,
} ,
required : [ 'pattern' ] ,
2025-08-11 16:12:41 -07:00
type : 'object' ,
2025-08-07 10:05:37 -07:00
} ,
2025-10-21 11:45:33 -07:00
true ,
false ,
messageBus ,
2025-08-07 10:05:37 -07:00
) ;
}
/ * *
* Checks if a path is within the root directory and resolves it .
* @param relativePath Path relative to the root directory ( or undefined for root ) .
* @returns The absolute path if valid and exists , or null if no path specified ( to search all directories ) .
* @throws { Error } If path is outside root , doesn 't exist, or isn' t a directory .
* /
private resolveAndValidatePath ( relativePath? : string ) : string | null {
// If no path specified, return null to indicate searching all workspace directories
if ( ! relativePath ) {
return null ;
}
const targetPath = path . resolve ( this . config . getTargetDir ( ) , relativePath ) ;
// Security Check: Ensure the resolved path is within workspace boundaries
const workspaceContext = this . config . getWorkspaceContext ( ) ;
if ( ! workspaceContext . isPathWithinWorkspace ( targetPath ) ) {
const directories = workspaceContext . getDirectories ( ) ;
throw new Error (
` Path validation failed: Attempted path " ${ relativePath } " resolves outside the allowed workspace directories: ${ directories . join ( ', ' ) } ` ,
) ;
}
// Check existence and type after resolving
try {
const stats = fs . statSync ( targetPath ) ;
if ( ! stats . isDirectory ( ) ) {
throw new Error ( ` Path is not a directory: ${ targetPath } ` ) ;
}
} catch ( error : unknown ) {
if ( isNodeError ( error ) && error . code !== 'ENOENT' ) {
throw new Error ( ` Path does not exist: ${ targetPath } ` ) ;
}
throw new Error (
` Failed to access path stats for ${ targetPath } : ${ error } ` ,
) ;
}
return targetPath ;
}
/ * *
* 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 ) } ` ;
}
2025-11-06 15:03:52 -08:00
// Only validate dir_path if one is provided
if ( params . dir_path ) {
2025-08-07 10:05:37 -07:00
try {
2025-11-06 15:03:52 -08:00
this . resolveAndValidatePath ( params . dir_path ) ;
2025-08-07 10:05:37 -07:00
} catch ( error ) {
return getErrorMessage ( error ) ;
}
}
return null ; // Parameters are valid
}
protected createInvocation (
params : GrepToolParams ,
2025-10-21 11:45:33 -07:00
messageBus? : MessageBus ,
_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 ,
messageBus ,
_toolName ,
_toolDisplayName ,
) ;
2025-08-07 10:05:37 -07:00
}
}