summaryrefslogtreecommitdiff
path: root/packages/cli/src/tools/grep.tool.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/tools/grep.tool.ts')
-rw-r--r--packages/cli/src/tools/grep.tool.ts233
1 files changed, 160 insertions, 73 deletions
diff --git a/packages/cli/src/tools/grep.tool.ts b/packages/cli/src/tools/grep.tool.ts
index ed75890b..72e28d01 100644
--- a/packages/cli/src/tools/grep.tool.ts
+++ b/packages/cli/src/tools/grep.tool.ts
@@ -42,8 +42,7 @@ interface GrepMatch {
/**
* Result from the GrepTool
*/
-export interface GrepToolResult extends ToolResult {
-}
+export interface GrepToolResult extends ToolResult {}
// --- GrepTool Class ---
@@ -65,21 +64,24 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
{
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: 'string'
+ description:
+ "The regular expression (regex) pattern to search for within file contents (e.g., 'function\\s+myFunction', 'import\\s+\\{.*\\}\\s+from\\s+.*').",
+ type: 'string',
},
path: {
- description: 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
- type: 'string'
+ description:
+ 'Optional: The absolute path to the directory to search within. If omitted, searches the current working directory.',
+ 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: 'string'
- }
+ 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: 'string',
+ },
},
required: ['pattern'],
- type: 'object'
- }
+ type: 'object',
+ },
);
// Ensure rootDirectory is absolute and normalized
this.rootDirectory = path.resolve(rootDirectory);
@@ -97,8 +99,13 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
const targetPath = path.resolve(this.rootDirectory, relativePath || '.');
// Security Check: Ensure the resolved path is still within the root directory.
- if (!targetPath.startsWith(this.rootDirectory) && targetPath !== this.rootDirectory) {
- throw new Error(`Path validation failed: Attempted path "${relativePath || '.'}" resolves outside the allowed root directory "${this.rootDirectory}".`);
+ if (
+ !targetPath.startsWith(this.rootDirectory) &&
+ targetPath !== this.rootDirectory
+ ) {
+ throw new Error(
+ `Path validation failed: Attempted path "${relativePath || '.'}" resolves outside the allowed root directory "${this.rootDirectory}".`,
+ );
}
// Check existence and type after resolving
@@ -111,7 +118,9 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
if (err.code === 'ENOENT') {
throw new Error(`Path does not exist: ${targetPath}`);
}
- throw new Error(`Failed to access path stats for ${targetPath}: ${err.message}`);
+ throw new Error(
+ `Failed to access path stats for ${targetPath}: ${err.message}`,
+ );
}
return targetPath;
@@ -123,8 +132,14 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
* @returns An error message string if invalid, null otherwise
*/
invalidParams(params: GrepToolParams): string | null {
- if (this.schema.parameters && !SchemaValidator.validate(this.schema.parameters as Record<string, unknown>, params)) {
- return "Parameters failed schema validation.";
+ if (
+ this.schema.parameters &&
+ !SchemaValidator.validate(
+ this.schema.parameters as Record<string, unknown>,
+ params,
+ )
+ ) {
+ return 'Parameters failed schema validation.';
}
try {
@@ -142,7 +157,6 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
return null; // Parameters are valid
}
-
// --- Core Execution ---
/**
@@ -156,7 +170,7 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
console.error(`GrepTool Parameter Validation Failed: ${validationError}`);
return {
llmContent: `Error: Invalid parameters provided. Reason: ${validationError}`,
- returnDisplay: `**Error:** Failed to execute tool.`
+ returnDisplay: `**Error:** Failed to execute tool.`,
};
}
@@ -177,40 +191,49 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
return { llmContent: noMatchMsg, returnDisplay: noMatchUser };
}
- const matchesByFile = matches.reduce((acc, match) => {
- const relativeFilePath = path.relative(searchDirAbs, path.resolve(searchDirAbs, match.filePath)) || path.basename(match.filePath);
- if (!acc[relativeFilePath]) {
- acc[relativeFilePath] = [];
- }
- acc[relativeFilePath].push(match);
- acc[relativeFilePath].sort((a, b) => a.lineNumber - b.lineNumber);
- return acc;
- }, {} as Record<string, GrepMatch[]>);
+ const matchesByFile = matches.reduce(
+ (acc, match) => {
+ const relativeFilePath =
+ path.relative(
+ searchDirAbs,
+ path.resolve(searchDirAbs, match.filePath),
+ ) || path.basename(match.filePath);
+ if (!acc[relativeFilePath]) {
+ acc[relativeFilePath] = [];
+ }
+ acc[relativeFilePath].push(match);
+ acc[relativeFilePath].sort((a, b) => a.lineNumber - b.lineNumber);
+ return acc;
+ },
+ {} as Record<string, GrepMatch[]>,
+ );
let llmContent = `Found ${matches.length} match(es) for pattern "${params.pattern}" in path "${searchDirDisplay}"${params.include ? ` (filter: "${params.include}")` : ''}:\n---\n`;
for (const filePath in matchesByFile) {
llmContent += `File: ${filePath}\n`;
- matchesByFile[filePath].forEach(match => {
+ matchesByFile[filePath].forEach((match) => {
const trimmedLine = match.line.trim();
llmContent += `L${match.lineNumber}: ${trimmedLine}\n`;
});
llmContent += '---\n';
}
- return { llmContent: llmContent.trim(), returnDisplay: `Found ${matches.length} matche(s)` };
-
+ return {
+ llmContent: llmContent.trim(),
+ returnDisplay: `Found ${matches.length} matche(s)`,
+ };
} catch (error) {
console.error(`Error during GrepTool execution: ${error}`);
- const errorMessage = error instanceof Error ? error.message : String(error);
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
return {
llmContent: `Error during grep search operation: ${errorMessage}`,
- returnDisplay: errorMessage
+ returnDisplay: errorMessage,
};
}
}
-
// --- Inlined Grep Logic and Helpers ---
/**
@@ -221,9 +244,13 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
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];
+ const checkArgs =
+ process.platform === 'win32' ? [command] : ['-v', command];
try {
- const child = spawn(checkCommand, checkArgs, { stdio: 'ignore', shell: process.platform === 'win32' });
+ const child = spawn(checkCommand, checkArgs, {
+ stdio: 'ignore',
+ shell: process.platform === 'win32',
+ });
child.on('close', (code) => resolve(code === 0));
child.on('error', () => resolve(false));
} catch (e) {
@@ -252,7 +279,9 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
return false;
} catch (err: any) {
if (err.code !== 'ENOENT') {
- console.error(`Error checking for .git in ${currentPath}: ${err.message}`);
+ console.error(
+ `Error checking for .git in ${currentPath}: ${err.message}`,
+ );
return false;
}
}
@@ -263,19 +292,21 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
currentPath = path.dirname(currentPath);
}
} catch (err: any) {
- console.error(`Error traversing directory structure upwards from ${dirPath}: ${err instanceof Error ? err.message : String(err)}`);
+ console.error(
+ `Error traversing directory structure upwards from ${dirPath}: ${err instanceof Error ? err.message : String(err)}`,
+ );
}
return 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.
- */
+ * 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;
@@ -302,7 +333,10 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
// Extract parts based on the found colon indices
const filePathRaw = line.substring(0, firstColonIndex);
- const lineNumberStr = line.substring(firstColonIndex + 1, secondColonIndex);
+ const lineNumberStr = line.substring(
+ firstColonIndex + 1,
+ secondColonIndex,
+ );
// The rest of the line, starting after the second colon, is the content.
const lineContent = line.substring(secondColonIndex + 1);
@@ -327,10 +361,10 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
}
/**
- * Gets a description of the grep operation
- * @param params Parameters for the grep operation
- * @returns A string describing the grep
- */
+ * Gets a description of the grep operation
+ * @param params Parameters for the grep operation
+ * @returns A string describing the grep
+ */
getDescription(params: GrepToolParams): string {
let description = `'${params.pattern}'`;
@@ -363,37 +397,59 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
try {
// --- Strategy 1: git grep ---
const isGit = await this.isGitRepository(absolutePath);
- const gitAvailable = isGit && await this.isCommandAvailable('git');
+ const gitAvailable = isGit && (await this.isCommandAvailable('git'));
if (gitAvailable) {
strategyUsed = 'git grep';
- const gitArgs = ['grep', '--untracked', '-n', '-E', '--ignore-case', pattern];
+ 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 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.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('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 is not an error
- else reject(new Error(`git grep exited with code ${code}: ${stderrData}`));
+ else if (code === 1)
+ resolve(''); // No matches is not an error
+ else
+ reject(
+ new Error(`git grep exited with code ${code}: ${stderrData}`),
+ );
});
});
return this.parseGrepOutput(output, absolutePath);
} catch (gitError: any) {
- console.error(`GrepTool: git grep strategy failed: ${gitError.message}. Falling back...`);
+ console.error(
+ `GrepTool: git grep strategy failed: ${gitError.message}. Falling back...`,
+ );
}
}
@@ -403,7 +459,7 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
strategyUsed = 'system grep';
const grepArgs = ['-r', '-n', '-H', '-E'];
const commonExcludes = ['.git', 'node_modules', 'bower_components'];
- commonExcludes.forEach(dir => grepArgs.push(`--exclude-dir=${dir}`));
+ commonExcludes.forEach((dir) => grepArgs.push(`--exclude-dir=${dir}`));
if (include) {
grepArgs.push(`--include=${include}`);
}
@@ -412,41 +468,67 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
try {
const output = await new Promise<string>((resolve, reject) => {
- const child = spawn('grep', grepArgs, { cwd: absolutePath, windowsHide: true });
+ const child = spawn('grep', grepArgs, {
+ cwd: absolutePath,
+ windowsHide: true,
+ });
const stdoutChunks: Buffer[] = [];
const stderrChunks: Buffer[] = [];
- child.stdout.on('data', (chunk) => { stdoutChunks.push(chunk); });
+ child.stdout.on('data', (chunk) => {
+ stdoutChunks.push(chunk);
+ });
child.stderr.on('data', (chunk) => {
const stderrStr = chunk.toString();
- if (!stderrStr.includes('Permission denied') && !/grep:.*: Is a directory/i.test(stderrStr)) {
+ if (
+ !stderrStr.includes('Permission denied') &&
+ !/grep:.*: Is a directory/i.test(stderrStr)
+ ) {
stderrChunks.push(chunk);
}
});
- child.on('error', (err) => reject(new Error(`Failed to start system grep: ${err.message}`)));
+ child.on('error', (err) =>
+ reject(new Error(`Failed to start system grep: ${err.message}`)),
+ );
child.on('close', (code) => {
const stdoutData = Buffer.concat(stdoutChunks).toString('utf8');
- const stderrData = Buffer.concat(stderrChunks).toString('utf8').trim();
+ const stderrData = Buffer.concat(stderrChunks)
+ .toString('utf8')
+ .trim();
if (code === 0) resolve(stdoutData);
- else if (code === 1) resolve(''); // No matches
+ else if (code === 1)
+ resolve(''); // No matches
else {
- if (stderrData) reject(new Error(`System grep exited with code ${code}: ${stderrData}`));
+ if (stderrData)
+ reject(
+ new Error(
+ `System grep exited with code ${code}: ${stderrData}`,
+ ),
+ );
else resolve('');
}
});
});
return this.parseGrepOutput(output, absolutePath);
} catch (grepError: any) {
- console.error(`GrepTool: System grep strategy failed: ${grepError.message}. Falling back...`);
+ console.error(
+ `GrepTool: System grep strategy failed: ${grepError.message}. Falling back...`,
+ );
}
}
// --- Strategy 3: Pure JavaScript Fallback ---
strategyUsed = 'javascript fallback';
const globPattern = include ? include : '**/*';
- const ignorePatterns = ['.git', 'node_modules', 'bower_components', '.svn', '.hg'];
+ const ignorePatterns = [
+ '.git',
+ 'node_modules',
+ 'bower_components',
+ '.svn',
+ '.hg',
+ ];
const filesStream = fastGlob.stream(globPattern, {
cwd: absolutePath,
@@ -469,7 +551,9 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
lines.forEach((line, index) => {
if (regex.test(line)) {
allMatches.push({
- filePath: path.relative(absolutePath, fileAbsolutePath) || path.basename(fileAbsolutePath),
+ filePath:
+ path.relative(absolutePath, fileAbsolutePath) ||
+ path.basename(fileAbsolutePath),
lineNumber: index + 1,
line: line,
});
@@ -477,16 +561,19 @@ export class GrepTool extends BaseTool<GrepToolParams, GrepToolResult> {
});
} catch (readError: any) {
if (readError.code !== 'ENOENT') {
- console.error(`GrepTool: Could not read or process file ${fileAbsolutePath}: ${readError.message}`);
+ console.error(
+ `GrepTool: Could not read or process file ${fileAbsolutePath}: ${readError.message}`,
+ );
}
}
}
return allMatches;
-
} catch (error: any) {
- console.error(`GrepTool: Error during performGrepSearch (Strategy: ${strategyUsed}): ${error.message}`);
+ console.error(
+ `GrepTool: Error during performGrepSearch (Strategy: ${strategyUsed}): ${error.message}`,
+ );
throw error; // Re-throw to be caught by the execute method's handler
}
}
-} \ No newline at end of file
+}