summaryrefslogtreecommitdiff
path: root/packages/core/src/tools/grep.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/tools/grep.ts')
-rw-r--r--packages/core/src/tools/grep.ts266
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);
+ }
+}