209 lines
5.4 KiB
TypeScript
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();
|