gadget/docs/archive/tools/search/grep.ts

294 lines
7.7 KiB
TypeScript

// src/tools/search/grep.ts
// Copyright (C) 2025 DTP Technologies, LLC
// All Rights Reserved
import fs from "node:fs/promises";
import path from "node:path";
import type { ToolDefinition } from "../../lib/ai-client.js";
import {
DtpTool,
type ToolArguments,
type ToolContext,
} from "../../lib/tool.js";
import { ChatSessionMode } from "../../models/chat-session.js";
import HostMonitorService from "../../services/host-monitor.js";
const MAX_MATCHES = 500;
const MAX_FILE_SIZE = 1024 * 1024; // 1MB
class GrepTool extends DtpTool {
get name(): string {
return "GrepTool";
}
get slug(): string {
return "grep";
}
get metadata() {
return {
name: this.definition.function.name || "grep",
category: "search",
tags: ["search", "find", "regex", "content", "grep"],
modes: [
ChatSessionMode.Plan,
ChatSessionMode.Build,
ChatSessionMode.Test,
ChatSessionMode.Ship,
ChatSessionMode.Develop,
],
};
}
public definition: ToolDefinition = {
type: "function",
function: {
name: "grep",
description:
"Search file contents using regular expressions. Returns matching lines with file paths and line numbers. Supports case-insensitive matching and context lines.",
parameters: {
type: "object",
properties: {
pattern: {
type: "string",
description:
"Regular expression pattern to search for. Use standard JavaScript regex syntax.",
},
path: {
type: "string",
description:
"File or directory path to search in. Can be a specific file or directory.",
},
caseInsensitive: {
type: "boolean",
description: "Case insensitive search (default: false).",
},
contextBefore: {
type: "number",
description:
"Number of lines to show before each match (default: 0).",
},
contextAfter: {
type: "number",
description:
"Number of lines to show after each match (default: 0).",
},
},
required: ["pattern", "path"],
},
},
};
public async execute(
_context: ToolContext,
args: ToolArguments,
): Promise<string> {
const pattern = args.pattern as string | undefined;
const searchPath = args.path as string | undefined;
const caseInsensitive = (args.caseInsensitive as boolean) || false;
const contextBefore = (args.contextBefore as number) || 0;
const contextAfter = (args.contextAfter as number) || 0;
if (!pattern || pattern.trim().length === 0) {
return this.error("MISSING_PARAMETER", "pattern is required.", {
parameter: "pattern",
recoveryHint: "Provide a regex pattern to search for.",
});
}
if (!searchPath || searchPath.trim().length === 0) {
return this.error("MISSING_PARAMETER", "path is required.", {
parameter: "path",
recoveryHint: "Provide a file or directory path to search in.",
});
}
let regex: RegExp;
try {
regex = new RegExp(pattern, caseInsensitive ? "i" : "");
} catch (error) {
return this.error(
"INVALID_PARAMETER",
`Invalid regex pattern: ${error instanceof Error ? error.message : String(error)}`,
{ parameter: "pattern" },
);
}
try {
const stat = await fs.stat(searchPath);
const matches: Array<{
file: string;
line: number;
content: string;
}> = [];
if (stat.isFile()) {
const fileMatches = await this.searchFile(
searchPath,
regex,
contextBefore,
contextAfter,
);
matches.push(...fileMatches);
} else if (stat.isDirectory()) {
await this.searchDirectory(
searchPath,
regex,
matches,
contextBefore,
contextAfter,
);
}
const limited = matches.slice(0, MAX_MATCHES);
let output = "";
if (matches.length === 0) {
output = `No matches found for "${pattern}" in ${searchPath}`;
} else {
const truncated = matches.length > MAX_MATCHES;
output = `Found ${matches.length} match(es) for "${pattern}" in ${searchPath}${
truncated ? ` (showing first ${MAX_MATCHES})` : ""
}:\n\n`;
for (const match of limited) {
output += `${match.file}:${match.line}: ${match.content}\n`;
}
if (truncated) {
output += `\n... and ${matches.length - MAX_MATCHES} more matches`;
}
}
const byteCount = Buffer.byteLength(output, "utf-8");
HostMonitorService.toolCall(byteCount);
return this.success(
{
matches: limited,
total: matches.length,
truncated: matches.length > MAX_MATCHES,
},
output,
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return this.error(
"OPERATION_FAILED",
`Failed to search: ${errorMessage}`,
);
}
}
private async searchFile(
filePath: string,
regex: RegExp,
contextBefore: number,
contextAfter: number,
): Promise<Array<{ file: string; line: number; content: string }>> {
const matches: Array<{ file: string; line: number; content: string }> = [];
let stat;
try {
stat = await fs.stat(filePath);
} catch {
return matches;
}
if (stat.size > MAX_FILE_SIZE) {
return matches;
}
let content: string;
try {
content = await fs.readFile(filePath, "utf-8");
} catch {
return matches;
}
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line !== undefined && regex.test(line)) {
const startLine = Math.max(0, i - contextBefore);
const endLine = Math.min(lines.length - 1, i + contextAfter);
for (let j = startLine; j <= endLine; j++) {
const contextLine = lines[j];
if (contextLine !== undefined) {
matches.push({
file: filePath,
line: j + 1,
content: contextLine,
});
}
}
regex.lastIndex = 0;
}
}
return matches;
}
private async searchDirectory(
dir: string,
regex: RegExp,
matches: Array<{ file: string; line: number; content: string }>,
contextBefore: number,
contextAfter: number,
): Promise<void> {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (
entry.name !== "node_modules" &&
entry.name !== ".git" &&
entry.name !== "dist" &&
entry.name !== "build"
) {
await this.searchDirectory(
fullPath,
regex,
matches,
contextBefore,
contextAfter,
);
}
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (
ext === ".ts" ||
ext === ".tsx" ||
ext === ".js" ||
ext === ".jsx" ||
ext === ".json" ||
ext === ".md" ||
ext === ".txt"
) {
const fileMatches = await this.searchFile(
fullPath,
regex,
contextBefore,
contextAfter,
);
matches.push(...fileMatches);
}
}
if (matches.length >= MAX_MATCHES * 2) {
return;
}
}
}
}
export default new GrepTool();