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

209 lines
5.4 KiB
TypeScript

// src/tools/search/glob.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_RESULTS = 1000;
class GlobTool extends DtpTool {
get name(): string {
return "GlobTool";
}
get slug(): string {
return "glob";
}
get metadata() {
return {
name: this.definition.function.name || "glob",
category: "search",
tags: ["search", "find", "file", "pattern", "glob"],
modes: [
ChatSessionMode.Plan,
ChatSessionMode.Build,
ChatSessionMode.Test,
ChatSessionMode.Ship,
ChatSessionMode.Develop,
],
};
}
public definition: ToolDefinition = {
type: "function",
function: {
name: "glob",
description:
"Find files by name using pattern matching. Supports glob patterns like **/*.ts, *.js, src/**/*.tsx. Returns a list of matching file paths.",
parameters: {
type: "object",
properties: {
pattern: {
type: "string",
description:
"Glob pattern to match files (e.g., '**/*.ts', 'src/**/*.tsx', '*.json').",
},
root: {
type: "string",
description:
"Root directory to search from. Defaults to current working directory.",
},
},
required: ["pattern"],
},
},
};
public async execute(
_context: ToolContext,
args: ToolArguments,
): Promise<string> {
const pattern = args.pattern as string | undefined;
const root = (args.root as string | undefined) || process.cwd();
if (!pattern || pattern.trim().length === 0) {
return this.error("MISSING_PARAMETER", "pattern is required.", {
parameter: "pattern",
recoveryHint:
"Provide a glob pattern like '**/*.ts' or 'src/**/*.tsx'.",
});
}
try {
const matches = await this.globMatch(pattern, root);
const limited = matches.slice(0, MAX_RESULTS);
if (matches.length > MAX_RESULTS) {
const output = `Found ${matches.length} files matching "${pattern}" (showing first ${MAX_RESULTS}):\n\n${limited.join("\n")}\n\n... and ${matches.length - MAX_RESULTS} more files`;
const byteCount = Buffer.byteLength(output, "utf-8");
HostMonitorService.toolCall(byteCount);
return this.success(
{
files: limited,
total: matches.length,
truncated: true,
},
output,
);
}
const output = `Found ${matches.length} file(s) matching "${pattern}":\n\n${matches.join("\n")}`;
const byteCount = Buffer.byteLength(output, "utf-8");
HostMonitorService.toolCall(byteCount);
return this.success(
{
files: matches,
total: matches.length,
truncated: false,
},
output,
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return this.error(
"OPERATION_FAILED",
`Failed to search: ${errorMessage}`,
);
}
}
private async globMatch(pattern: string, root: string): Promise<string[]> {
const results: string[] = [];
const normalizedRoot = path.resolve(root);
const parts = pattern.split("/");
let isRecursive = false;
let searchPattern = pattern;
if (parts[0] === "**") {
isRecursive = true;
searchPattern = parts.slice(1).join("/");
}
const regexPattern = this.globToRegex(searchPattern);
await this.recurseDir(
normalizedRoot,
regexPattern,
isRecursive,
results,
0,
20,
);
return results.sort();
}
private globToRegex(pattern: string): RegExp {
let regexStr = pattern
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*/g, ".*")
.replace(/\?/g, ".");
regexStr = "^" + regexStr + "$";
return new RegExp(regexStr);
}
private async recurseDir(
dir: string,
pattern: RegExp,
recursive: boolean,
results: string[],
depth: number,
maxDepth: number,
): Promise<void> {
if (depth > maxDepth) return;
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relativePath = path.relative(process.cwd(), fullPath);
if (entry.isFile()) {
if (pattern.test(entry.name)) {
results.push(relativePath);
}
} else if (entry.isDirectory()) {
if (
entry.name !== "node_modules" &&
entry.name !== ".git" &&
entry.name !== "dist" &&
entry.name !== "build"
) {
if (recursive) {
await this.recurseDir(
fullPath,
pattern,
recursive,
results,
depth + 1,
maxDepth,
);
} else if (pattern.test(entry.name)) {
results.push(relativePath + "/");
}
}
}
}
}
}
export default new GlobTool();