2025-08-22 14:10:45 +08: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' ;
2026-01-27 13:17:40 -08:00
import fsPromises from 'node:fs/promises' ;
2025-08-25 22:11:27 +02:00
import path from 'node:path' ;
2025-09-19 11:08:41 -07:00
import { downloadRipGrep } from '@joshua.litt/get-ripgrep' ;
2026-03-04 05:42:59 +05:30
import {
BaseDeclarativeTool ,
BaseToolInvocation ,
Kind ,
type ToolInvocation ,
type ToolResult ,
} from './tools.js' ;
2026-01-27 13:17:40 -08:00
import { ToolErrorType } from './tool-error.js' ;
2025-08-22 14:10:45 +08:00
import { makeRelative , shortenPath } from '../utils/paths.js' ;
import { getErrorMessage , isNodeError } from '../utils/errors.js' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-09-08 14:44:56 -07:00
import { fileExists } from '../utils/fileUtils.js' ;
import { Storage } from '../config/storage.js' ;
2025-10-19 19:21:47 -04:00
import { GREP_TOOL_NAME } from './tool-names.js' ;
2025-10-21 16:35:22 -04:00
import { debugLogger } from '../utils/debugLogger.js' ;
2025-11-12 00:11:19 -05:00
import {
FileExclusions ,
COMMON_DIRECTORY_EXCLUDES ,
} from '../utils/ignorePatterns.js' ;
2026-01-27 17:19:13 -08:00
import { FileDiscoveryService } from '../services/fileDiscoveryService.js' ;
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-02-12 21:05:33 -05:00
import { RIP_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-08-22 14:10:45 +08:00
2025-10-20 19:17:44 -04:00
function getRgCandidateFilenames ( ) : readonly string [ ] {
return process . platform === 'win32' ? [ 'rg.exe' , 'rg' ] : [ 'rg' ] ;
}
async function resolveExistingRgPath ( ) : Promise < string | null > {
const binDir = Storage . getGlobalBinDir ( ) ;
for ( const fileName of getRgCandidateFilenames ( ) ) {
const candidatePath = path . join ( binDir , fileName ) ;
if ( await fileExists ( candidatePath ) ) {
return candidatePath ;
}
}
return null ;
}
let ripgrepAcquisitionPromise : Promise < string | null > | null = null ;
2026-03-06 11:40:12 +05:30
/ * *
* Ensures a ripgrep binary is available .
*
* NOTE :
* - The Gemini CLI currently prefers a managed ripgrep binary downloaded
* into its global bin directory .
* - Even if ripgrep is available on the system PATH , it is intentionally
* not used at this time .
*
* Preference for system - installed ripgrep is blocked on :
* - checksum verification of external binaries
* - internalization of the get - ripgrep dependency
*
* See :
* - feat ( core ) : Prefer rg in system path ( # 11847 )
* - Move get - ripgrep to third_party ( # 12099 )
* /
2025-10-20 19:17:44 -04:00
async function ensureRipgrepAvailable ( ) : Promise < string | null > {
const existingPath = await resolveExistingRgPath ( ) ;
if ( existingPath ) {
return existingPath ;
}
if ( ! ripgrepAcquisitionPromise ) {
ripgrepAcquisitionPromise = ( async ( ) = > {
try {
await downloadRipGrep ( Storage . getGlobalBinDir ( ) ) ;
return await resolveExistingRgPath ( ) ;
} finally {
ripgrepAcquisitionPromise = null ;
}
} ) ( ) ;
}
return ripgrepAcquisitionPromise ;
2025-09-08 14:44:56 -07:00
}
/ * *
* Checks if ` rg ` exists , if not then attempt to download it .
* /
export async function canUseRipgrep ( ) : Promise < boolean > {
2025-10-20 19:17:44 -04:00
return ( await ensureRipgrepAvailable ( ) ) !== null ;
2025-09-08 14:44:56 -07:00
}
2025-09-11 15:18:29 -07:00
/ * *
* Ensures ` rg ` is downloaded , or throws .
* /
export async function ensureRgPath ( ) : Promise < string > {
2025-10-20 19:17:44 -04:00
const downloadedPath = await ensureRipgrepAvailable ( ) ;
if ( downloadedPath ) {
return downloadedPath ;
2025-09-11 15:18:29 -07:00
}
throw new Error ( 'Cannot use ripgrep.' ) ;
}
2025-08-22 14:10:45 +08:00
/ * *
* Parameters for the GrepTool
* /
export interface RipGrepToolParams {
/ * *
* 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-08-22 14:10:45 +08:00
/ * *
* File pattern to include in the search ( e . g . "*.js" , "*.{ts,tsx}" )
* /
2026-02-25 20:16:21 -08:00
include_pattern? : string ;
2025-11-12 00:11:19 -05: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 ;
2025-11-12 00:11:19 -05:00
/ * *
* If true , searches case - sensitively . Defaults to false .
* /
case_sensitive? : boolean ;
/ * *
* If true , treats pattern as a literal string . Defaults to false .
* /
fixed_strings? : boolean ;
/ * *
* Show num lines of context around each match .
* /
context? : number ;
/ * *
* Show num lines after each match .
* /
after? : number ;
/ * *
* Show num lines before each match .
* /
before? : number ;
/ * *
* If true , does not respect . gitignore or default ignores ( like build / dist ) .
* /
no_ignore? : 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-08-22 14:10:45 +08:00
}
class GrepToolInvocation extends BaseToolInvocation <
RipGrepToolParams ,
ToolResult
> {
constructor (
private readonly config : Config ,
2026-01-27 17:19:13 -08:00
private readonly fileDiscoveryService : FileDiscoveryService ,
2025-08-22 14:10:45 +08:00
params : RipGrepToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-21 11:45:33 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-22 14:10:45 +08:00
) {
2025-10-21 11:45:33 -07:00
super ( params , messageBus , _toolName , _toolDisplayName ) ;
2025-08-22 14:10:45 +08:00
}
async execute ( signal : AbortSignal ) : Promise < ToolResult > {
try {
2025-11-12 00:11:19 -05:00
// Default to '.' if path is explicitly undefined/null.
// This forces CWD search instead of 'all workspaces' search by default.
const pathParam = this . params . dir_path || '.' ;
2025-08-22 14:10:45 +08:00
2026-01-27 13:17:40 -08:00
const 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 ,
} ,
} ;
}
// Check existence and type asynchronously
try {
const stats = await fsPromises . stat ( searchDirAbs ) ;
if ( ! stats . isDirectory ( ) && ! stats . isFile ( ) ) {
return {
llmContent : ` Path is not a valid directory or file: ${ searchDirAbs } ` ,
returnDisplay : 'Error: Path is not a valid directory or file.' ,
} ;
}
} 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 ,
} ,
} ;
}
return {
llmContent : ` Failed to access path stats for ${ searchDirAbs } : ${ getErrorMessage ( error ) } ` ,
returnDisplay : 'Error: Failed to access path.' ,
} ;
}
2025-11-12 00:11:19 -05:00
const searchDirDisplay = pathParam ;
2025-08-22 14:10:45 +08:00
2026-02-11 03:50:10 +00:00
const totalMaxMatches =
this . params . total_max_matches ? ? DEFAULT_TOTAL_MAX_MATCHES ;
2025-08-22 14:10:45 +08:00
if ( this . config . getDebugMode ( ) ) {
2025-10-21 16:35:22 -04:00
debugLogger . log ( ` [GrepTool] Total result limit: ${ totalMaxMatches } ` ) ;
2025-08-22 14:10:45 +08:00
}
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-08-22 14:10:45 +08:00
2026-01-26 16:52:19 -05:00
let allMatches : GrepMatch [ ] ;
try {
allMatches = await this . performRipgrepSearch ( {
pattern : this.params.pattern ,
2026-01-27 13:17:40 -08:00
path : searchDirAbs ,
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
case_sensitive : this.params.case_sensitive ,
fixed_strings : this.params.fixed_strings ,
context : this.params.context ,
after : this.params.after ,
before : this.params.before ,
no_ignore : this.params.no_ignore ,
maxMatches : totalMaxMatches ,
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 ,
} ) ;
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-08-22 14:10:45 +08:00
}
2026-01-27 17:19:13 -08:00
if ( ! this . params . no_ignore ) {
const uniqueFiles = Array . from (
new Set ( allMatches . map ( ( m ) = > m . filePath ) ) ,
) ;
const absoluteFilePaths = uniqueFiles . map ( ( f ) = >
path . resolve ( searchDirAbs , f ) ,
) ;
const allowedFiles =
this . fileDiscoveryService . filterFiles ( absoluteFilePaths ) ;
const allowedSet = new Set ( allowedFiles ) ;
allMatches = allMatches . filter ( ( m ) = >
allowedSet . has ( path . resolve ( searchDirAbs , m . filePath ) ) ,
) ;
}
2026-02-21 00:36:10 +00:00
const matchCount = allMatches . filter ( ( m ) = > ! m . isContext ) . length ;
allMatches = await this . enrichWithRipgrepAutoContext (
allMatches ,
matchCount ,
totalMaxMatches ,
searchDirAbs ,
timeoutController . signal ,
2025-08-22 14:10:45 +08:00
) ;
2026-02-21 00:36:10 +00:00
const searchLocationDescription = ` in path " ${ searchDirDisplay } " ` ;
2025-08-22 14:10:45 +08:00
2026-02-21 00:36:10 +00:00
return await formatGrepResults (
allMatches ,
this . params ,
searchLocationDescription ,
totalMaxMatches ,
) ;
2025-08-22 14:10:45 +08:00
} catch ( error ) {
2025-12-17 12:40:28 -05:00
debugLogger . warn ( ` Error during GrepLogic execution: ${ error } ` ) ;
2025-08-22 14:10:45 +08:00
const errorMessage = getErrorMessage ( error ) ;
return {
llmContent : ` Error during grep search operation: ${ errorMessage } ` ,
returnDisplay : ` Error: ${ errorMessage } ` ,
} ;
}
}
2026-02-21 00:36:10 +00:00
private async enrichWithRipgrepAutoContext (
allMatches : GrepMatch [ ] ,
matchCount : number ,
totalMaxMatches : number ,
searchDirAbs : string ,
signal : AbortSignal ,
) : Promise < GrepMatch [ ] > {
if (
matchCount >= 1 &&
matchCount <= 3 &&
! this . params . names_only &&
this . params . context === undefined &&
this . params . before === undefined &&
this . params . after === undefined
) {
const contextLines = matchCount === 1 ? 50 : 15 ;
const uniqueFiles = Array . from (
new Set ( allMatches . map ( ( m ) = > m . absolutePath ) ) ,
) ;
let enrichedMatches = await this . performRipgrepSearch ( {
pattern : this.params.pattern ,
path : uniqueFiles ,
basePath : searchDirAbs ,
2026-02-25 20:16:21 -08:00
include_pattern : this.params.include_pattern ,
2026-02-21 00:36:10 +00:00
exclude_pattern : this.params.exclude_pattern ,
case_sensitive : this.params.case_sensitive ,
fixed_strings : this.params.fixed_strings ,
context : contextLines ,
no_ignore : this.params.no_ignore ,
maxMatches : totalMaxMatches ,
max_matches_per_file : this.params.max_matches_per_file ,
signal ,
} ) ;
if ( ! this . params . no_ignore ) {
const allowedFiles = this . fileDiscoveryService . filterFiles ( uniqueFiles ) ;
const allowedSet = new Set ( allowedFiles ) ;
enrichedMatches = enrichedMatches . filter ( ( m ) = >
allowedSet . has ( m . absolutePath ) ,
) ;
}
// Set context to prevent grep-utils from doing the JS fallback auto-context
this . params . context = contextLines ;
return enrichedMatches ;
}
return allMatches ;
}
2025-08-22 14:10:45 +08:00
private async performRipgrepSearch ( options : {
pattern : string ;
2026-02-21 00:36:10 +00:00
path : string | string [ ] ;
basePath? : string ;
2026-02-25 20:16:21 -08:00
include_pattern? : string ;
2026-02-11 19:20:51 +00:00
exclude_pattern? : string ;
2025-11-12 00:11:19 -05:00
case_sensitive? : boolean ;
fixed_strings? : boolean ;
context? : number ;
after? : number ;
before? : number ;
no_ignore? : boolean ;
2026-01-26 16:52:19 -05:00
maxMatches : number ;
2026-02-11 03:50:10 +00:00
max_matches_per_file? : number ;
2025-08-22 14:10:45 +08:00
signal : AbortSignal ;
} ) : Promise < GrepMatch [ ] > {
2025-11-12 00:11:19 -05:00
const {
pattern ,
2026-02-21 00:36:10 +00:00
path ,
basePath ,
2026-02-25 20:16:21 -08:00
include_pattern ,
2026-02-11 19:20:51 +00:00
exclude_pattern ,
2025-11-12 00:11:19 -05:00
case_sensitive ,
fixed_strings ,
context ,
after ,
before ,
no_ignore ,
2026-01-26 16:52:19 -05:00
maxMatches ,
2026-02-11 03:50:10 +00:00
max_matches_per_file ,
2025-11-12 00:11:19 -05:00
} = options ;
2026-02-21 00:36:10 +00:00
const searchPaths = Array . isArray ( path ) ? path : [ path ] ;
2025-11-12 00:11:19 -05:00
const rgArgs = [ '--json' ] ;
if ( ! case_sensitive ) {
rgArgs . push ( '--ignore-case' ) ;
}
2025-08-22 14:10:45 +08:00
2025-11-12 00:11:19 -05:00
if ( fixed_strings ) {
rgArgs . push ( '--fixed-strings' ) ;
}
2026-02-18 17:11:24 -05:00
rgArgs . push ( '--regexp' , pattern ) ;
2025-11-12 00:11:19 -05:00
if ( context ) {
rgArgs . push ( '--context' , context . toString ( ) ) ;
}
if ( after ) {
rgArgs . push ( '--after-context' , after . toString ( ) ) ;
}
if ( before ) {
rgArgs . push ( '--before-context' , before . toString ( ) ) ;
}
if ( no_ignore ) {
rgArgs . push ( '--no-ignore' ) ;
}
2025-08-22 14:10:45 +08:00
2026-02-11 03:50:10 +00:00
if ( max_matches_per_file ) {
rgArgs . push ( '--max-count' , max_matches_per_file . toString ( ) ) ;
}
2026-02-25 20:16:21 -08:00
if ( include_pattern ) {
rgArgs . push ( '--glob' , include_pattern ) ;
2025-08-22 14:10:45 +08:00
}
2025-11-12 00:11:19 -05:00
if ( ! no_ignore ) {
2026-02-03 21:37:21 +02:00
if ( ! this . config . getFileFilteringRespectGitIgnore ( ) ) {
rgArgs . push ( '--no-ignore-vcs' , '--no-ignore-exclude' ) ;
}
2025-11-12 00:11:19 -05:00
const fileExclusions = new FileExclusions ( this . config ) ;
const excludes = fileExclusions . getGlobExcludes ( [
. . . COMMON_DIRECTORY_EXCLUDES ,
'*.log' ,
'*.tmp' ,
] ) ;
excludes . forEach ( ( exclude ) = > {
rgArgs . push ( '--glob' , ` ! ${ exclude } ` ) ;
} ) ;
2025-12-22 06:25:26 +02:00
2026-01-27 17:19:13 -08:00
// Add .geminiignore and custom ignore files support (if provided/mandated)
// (ripgrep natively handles .gitignore)
const geminiIgnorePaths = this . fileDiscoveryService . getIgnoreFilePaths ( ) ;
for ( const ignorePath of geminiIgnorePaths ) {
rgArgs . push ( '--ignore-file' , ignorePath ) ;
2025-12-22 06:25:26 +02:00
}
2025-11-12 00:11:19 -05:00
}
2025-08-22 14:10:45 +08:00
rgArgs . push ( '--threads' , '4' ) ;
2026-02-21 00:36:10 +00:00
rgArgs . push ( . . . searchPaths ) ;
2025-08-22 14:10:45 +08:00
2026-01-26 16:52:19 -05:00
const results : GrepMatch [ ] = [ ] ;
2025-08-22 14:10:45 +08:00
try {
2025-09-11 15:18:29 -07:00
const rgPath = await ensureRgPath ( ) ;
2026-01-26 16:52:19 -05:00
const generator = execStreaming ( rgPath , rgArgs , {
signal : options.signal ,
allowedExitCodes : [ 0 , 1 ] ,
2026-03-13 14:11:51 -07:00
sandboxManager : this.config.sandboxManager ,
2026-01-26 16:52:19 -05:00
} ) ;
2025-08-22 14:10:45 +08:00
2026-02-10 20:48:56 +00:00
let matchesFound = 0 ;
2026-02-11 19:20:51 +00:00
let excludeRegex : RegExp | null = null ;
if ( exclude_pattern ) {
excludeRegex = new RegExp ( exclude_pattern , case_sensitive ? '' : 'i' ) ;
}
2026-02-21 00:36:10 +00:00
const parseBasePath = basePath || searchPaths [ 0 ] ;
2026-01-26 16:52:19 -05:00
for await ( const line of generator ) {
2026-02-21 00:36:10 +00:00
const match = this . parseRipgrepJsonLine ( line , parseBasePath ) ;
2026-01-26 16:52:19 -05:00
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 ) ;
2026-02-10 20:48:56 +00:00
if ( ! match . isContext ) {
matchesFound ++ ;
}
if ( matchesFound >= maxMatches ) {
2026-01-26 16:52:19 -05:00
break ;
2025-08-22 14:10:45 +08:00
}
2026-01-26 16:52:19 -05:00
}
}
2025-08-22 14:10:45 +08:00
2026-01-26 16:52:19 -05:00
return results ;
2025-08-22 14:10:45 +08:00
} catch ( error : unknown ) {
2025-12-17 12:40:28 -05:00
debugLogger . debug ( ` GrepLogic: ripgrep failed: ${ getErrorMessage ( error ) } ` ) ;
2025-08-22 14:10:45 +08:00
throw error ;
}
}
2026-01-26 16:52:19 -05:00
private parseRipgrepJsonLine (
line : string ,
basePath : string ,
) : GrepMatch | null {
try {
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2026-01-26 16:52:19 -05:00
const json = JSON . parse ( line ) ;
2026-02-10 20:48:56 +00:00
if ( json . type === 'match' || json . type === 'context' ) {
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2026-02-10 20:48:56 +00:00
const data = json . data ;
2026-01-26 16:52:19 -05:00
// Defensive check: ensure text properties exist (skips binary/invalid encoding)
2026-02-10 20:48:56 +00:00
if ( data . path ? . text && data . lines ? . text ) {
const absoluteFilePath = path . resolve ( basePath , data . path . text ) ;
2026-01-26 16:52:19 -05:00
const relativeCheck = path . relative ( basePath , absoluteFilePath ) ;
if (
relativeCheck === '..' ||
relativeCheck . startsWith ( ` .. ${ path . sep } ` ) ||
path . isAbsolute ( relativeCheck )
) {
return null ;
}
const relativeFilePath = path . relative ( basePath , absoluteFilePath ) ;
return {
2026-02-21 00:36:10 +00:00
absolutePath : absoluteFilePath ,
2026-01-26 16:52:19 -05:00
filePath : relativeFilePath || path . basename ( absoluteFilePath ) ,
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2026-02-10 20:48:56 +00:00
lineNumber : data.line_number ,
2026-02-20 22:28:55 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
2026-02-10 20:48:56 +00:00
line : data.lines.text.trimEnd ( ) ,
isContext : json.type === 'context' ,
2026-01-26 16:52:19 -05:00
} ;
}
}
} catch ( error ) {
// Only log if it's not a simple empty line or widely invalid
if ( line . trim ( ) . length > 0 ) {
debugLogger . warn (
` Failed to parse ripgrep JSON line: ${ line . substring ( 0 , 100 ) } ... ` ,
error ,
) ;
}
}
return null ;
}
2025-08-22 14:10:45 +08:00
/ * *
* Gets a description of the grep operation
* @param params Parameters for the grep operation
* @returns A string describing the grep
* /
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 } ` ;
2025-08-22 14:10:45 +08:00
}
2025-11-12 00:11:19 -05:00
const pathParam = this . params . dir_path || '.' ;
const resolvedPath = path . resolve ( this . config . getTargetDir ( ) , pathParam ) ;
if ( resolvedPath === this . config . getTargetDir ( ) || pathParam === '.' ) {
description += ` within ./ ` ;
} else {
const relativePath = makeRelative (
resolvedPath ,
2025-08-22 14:10:45 +08:00
this . config . getTargetDir ( ) ,
) ;
2025-11-12 00:11:19 -05:00
description += ` within ${ shortenPath ( relativePath ) } ` ;
2025-08-22 14:10:45 +08:00
}
return description ;
}
}
/ * *
* Implementation of the Grep tool logic ( moved from CLI )
* /
export class RipGrepTool extends BaseDeclarativeTool <
RipGrepToolParams ,
ToolResult
> {
2025-10-20 22:35:35 -04:00
static readonly Name = GREP_TOOL_NAME ;
2026-01-27 17:19:13 -08:00
private readonly fileDiscoveryService : FileDiscoveryService ;
2025-10-20 22:35:35 -04:00
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-22 14:10:45 +08:00
super (
2025-10-20 22:35:35 -04:00
RipGrepTool . Name ,
2025-08-22 14:10:45 +08:00
'SearchText' ,
2026-02-12 21:05:33 -05:00
RIP_GREP_DEFINITION . base . description ! ,
2025-08-22 14:10:45 +08:00
Kind . Search ,
2026-02-12 21:05:33 -05:00
RIP_GREP_DEFINITION . base . parametersJsonSchema ,
2026-01-04 17:11:43 -05:00
messageBus ,
2025-10-21 11:45:33 -07:00
true , // isOutputMarkdown
false , // canUpdateOutput
2025-08-22 14:10:45 +08:00
) ;
2026-01-27 17:19:13 -08:00
this . fileDiscoveryService = new FileDiscoveryService (
config . getTargetDir ( ) ,
config . getFileFilteringOptions ( ) ,
) ;
2025-08-22 14:10:45 +08:00
}
/ * *
* Validates the parameters for the tool
* @param params Parameters to validate
* @returns An error message string if invalid , null otherwise
* /
2026-01-27 13:17:40 -08:00
protected override validateToolParamValues (
params : RipGrepToolParams ,
) : string | null {
2026-02-10 20:48:56 +00:00
if ( ! params . fixed_strings ) {
try {
new RegExp ( params . pattern ) ;
} catch ( error ) {
return ` Invalid regular expression pattern provided: ${ params . pattern } . Error: ${ getErrorMessage ( error ) } ` ;
}
2025-08-22 14:10:45 +08:00
}
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-08-22 14:10:45 +08:00
// Only validate path if one is provided
2025-11-06 15:03:52 -08:00
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 ;
}
// Check existence and type
2025-08-22 14:10:45 +08:00
try {
2026-01-27 13:17:40 -08:00
const stats = fs . statSync ( resolvedPath ) ;
if ( ! stats . isDirectory ( ) && ! stats . isFile ( ) ) {
return ` Path is not a valid directory or file: ${ 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-22 14:10:45 +08:00
}
}
return null ; // Parameters are valid
}
protected createInvocation (
params : RipGrepToolParams ,
2026-01-04 17:11:43 -05:00
messageBus : MessageBus ,
2025-10-21 11:45:33 -07:00
_toolName? : string ,
_toolDisplayName? : string ,
2025-08-22 14:10:45 +08:00
) : ToolInvocation < RipGrepToolParams , ToolResult > {
2025-10-21 11:45:33 -07:00
return new GrepToolInvocation (
this . config ,
2026-01-27 17:19:13 -08:00
this . fileDiscoveryService ,
2025-10-21 11:45:33 -07:00
params ,
2026-01-04 15:51:23 -05:00
messageBus ? ? this . messageBus ,
2025-10-21 11:45:33 -07:00
_toolName ,
_toolDisplayName ,
) ;
2025-08-22 14:10:45 +08:00
}
2026-02-12 21:05:33 -05:00
override getSchema ( modelId? : string ) {
return resolveToolDeclaration ( RIP_GREP_DEFINITION , modelId ) ;
}
2025-08-22 14:10:45 +08:00
}