// 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 { 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 { 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 { 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();