diff options
Diffstat (limited to 'packages/core/src/tools/grep.ts')
| -rw-r--r-- | packages/core/src/tools/grep.ts | 266 |
1 files changed, 160 insertions, 106 deletions
diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index 027ab1b1..8e2b84f1 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -10,7 +10,13 @@ import path from 'path'; import { EOL } from 'os'; import { spawn } from 'child_process'; import { globStream } from 'glob'; -import { BaseTool, Icon, ToolResult } from './tools.js'; +import { + BaseDeclarativeTool, + BaseToolInvocation, + Icon, + ToolInvocation, + ToolResult, +} from './tools.js'; import { Type } from '@google/genai'; import { SchemaValidator } from '../utils/schemaValidator.js'; import { makeRelative, shortenPath } from '../utils/paths.js'; @@ -49,46 +55,17 @@ interface GrepMatch { line: string; } -// --- GrepLogic Class --- - -/** - * Implementation of the Grep tool logic (moved from CLI) - */ -export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { - static readonly Name = 'search_file_content'; // Keep static name - - constructor(private readonly config: Config) { - super( - GrepTool.Name, - '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.', - Icon.Regex, - { - properties: { - pattern: { - description: - "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').", - type: Type.STRING, - }, - path: { - description: - 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', - type: Type.STRING, - }, - 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).", - type: Type.STRING, - }, - }, - required: ['pattern'], - type: Type.OBJECT, - }, - ); +class GrepToolInvocation extends BaseToolInvocation< + GrepToolParams, + ToolResult +> { + constructor( + private readonly config: Config, + params: GrepToolParams, + ) { + super(params); } - // --- Validation Methods --- - /** * Checks if a path is within the root directory and resolves it. * @param relativePath Path relative to the root directory (or undefined for root). @@ -130,58 +107,11 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { return targetPath; } - /** - * Validates the parameters for the tool - * @param params Parameters to validate - * @returns An error message string if invalid, null otherwise - */ - validateToolParams(params: GrepToolParams): string | null { - const errors = SchemaValidator.validate(this.schema.parameters, params); - if (errors) { - return errors; - } - - try { - new RegExp(params.pattern); - } catch (error) { - return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`; - } - - // Only validate path if one is provided - if (params.path) { - try { - this.resolveAndValidatePath(params.path); - } catch (error) { - return getErrorMessage(error); - } - } - - return null; // Parameters are valid - } - - // --- Core Execution --- - - /** - * Executes the grep search with the given parameters - * @param params Parameters for the grep search - * @returns Result of the grep search - */ - async execute( - params: GrepToolParams, - signal: AbortSignal, - ): Promise<ToolResult> { - const validationError = this.validateToolParams(params); - if (validationError) { - return { - llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`, - returnDisplay: `Model provided invalid parameters. Error: ${validationError}`, - }; - } - + async execute(signal: AbortSignal): Promise<ToolResult> { try { const workspaceContext = this.config.getWorkspaceContext(); - const searchDirAbs = this.resolveAndValidatePath(params.path); - const searchDirDisplay = params.path || '.'; + const searchDirAbs = this.resolveAndValidatePath(this.params.path); + const searchDirDisplay = this.params.path || '.'; // Determine which directories to search let searchDirectories: readonly string[]; @@ -197,9 +127,9 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { let allMatches: GrepMatch[] = []; for (const searchDir of searchDirectories) { const matches = await this.performGrepSearch({ - pattern: params.pattern, + pattern: this.params.pattern, path: searchDir, - include: params.include, + include: this.params.include, signal, }); @@ -226,7 +156,7 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { } if (allMatches.length === 0) { - const noMatchMsg = `No matches found for pattern "${params.pattern}" ${searchLocationDescription}${params.include ? ` (filter: "${params.include}")` : ''}.`; + const noMatchMsg = `No matches found for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}.`; return { llmContent: noMatchMsg, returnDisplay: `No matches found` }; } @@ -247,7 +177,7 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { const matchCount = allMatches.length; const matchTerm = matchCount === 1 ? 'match' : 'matches'; - let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${params.pattern}" ${searchLocationDescription}${params.include ? ` (filter: "${params.include}")` : ''}: + let llmContent = `Found ${matchCount} ${matchTerm} for pattern "${this.params.pattern}" ${searchLocationDescription}${this.params.include ? ` (filter: "${this.params.include}")` : ''}: --- `; @@ -274,8 +204,6 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { } } - // --- Grep Implementation Logic --- - /** * Checks if a command is available in the system's PATH. * @param {string} command The command name (e.g., 'git', 'grep'). @@ -353,17 +281,20 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { * @param params Parameters for the grep operation * @returns A string describing the grep */ - getDescription(params: GrepToolParams): string { - let description = `'${params.pattern}'`; - if (params.include) { - description += ` in ${params.include}`; + getDescription(): string { + let description = `'${this.params.pattern}'`; + if (this.params.include) { + description += ` in ${this.params.include}`; } - if (params.path) { + if (this.params.path) { const resolvedPath = path.resolve( this.config.getTargetDir(), - params.path, + this.params.path, ); - if (resolvedPath === this.config.getTargetDir() || params.path === '.') { + if ( + resolvedPath === this.config.getTargetDir() || + this.params.path === '.' + ) { description += ` within ./`; } else { const relativePath = makeRelative( @@ -445,7 +376,9 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { return this.parseGrepOutput(output, absolutePath); } catch (gitError: unknown) { console.debug( - `GrepLogic: git grep failed: ${getErrorMessage(gitError)}. Falling back...`, + `GrepLogic: git grep failed: ${getErrorMessage( + gitError, + )}. Falling back...`, ); } } @@ -525,7 +458,9 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { return this.parseGrepOutput(output, absolutePath); } catch (grepError: unknown) { console.debug( - `GrepLogic: System grep failed: ${getErrorMessage(grepError)}. Falling back...`, + `GrepLogic: System grep failed: ${getErrorMessage( + grepError, + )}. Falling back...`, ); } } @@ -576,7 +511,9 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { // Ignore errors like permission denied or file gone during read if (!isNodeError(readError) || readError.code !== 'ENOENT') { console.debug( - `GrepLogic: Could not read/process ${fileAbsolutePath}: ${getErrorMessage(readError)}`, + `GrepLogic: Could not read/process ${fileAbsolutePath}: ${getErrorMessage( + readError, + )}`, ); } } @@ -585,9 +522,126 @@ export class GrepTool extends BaseTool<GrepToolParams, ToolResult> { return allMatches; } catch (error: unknown) { console.error( - `GrepLogic: Error in performGrepSearch (Strategy: ${strategyUsed}): ${getErrorMessage(error)}`, + `GrepLogic: Error in performGrepSearch (Strategy: ${strategyUsed}): ${getErrorMessage( + error, + )}`, ); throw error; // Re-throw } } } + +// --- GrepLogic Class --- + +/** + * Implementation of the Grep tool logic (moved from CLI) + */ +export class GrepTool extends BaseDeclarativeTool<GrepToolParams, ToolResult> { + static readonly Name = 'search_file_content'; // Keep static name + + constructor(private readonly config: Config) { + super( + GrepTool.Name, + '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.', + Icon.Regex, + { + properties: { + pattern: { + description: + "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').", + type: Type.STRING, + }, + path: { + description: + 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.', + type: Type.STRING, + }, + 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).", + type: Type.STRING, + }, + }, + required: ['pattern'], + type: Type.OBJECT, + }, + ); + } + + /** + * 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 + */ + validateToolParams(params: GrepToolParams): string | null { + const errors = SchemaValidator.validate(this.schema.parameters, params); + if (errors) { + return errors; + } + + try { + new RegExp(params.pattern); + } catch (error) { + return `Invalid regular expression pattern provided: ${params.pattern}. Error: ${getErrorMessage(error)}`; + } + + // Only validate path if one is provided + if (params.path) { + try { + this.resolveAndValidatePath(params.path); + } catch (error) { + return getErrorMessage(error); + } + } + + return null; // Parameters are valid + } + + protected createInvocation( + params: GrepToolParams, + ): ToolInvocation<GrepToolParams, ToolResult> { + return new GrepToolInvocation(this.config, params); + } +} |
