// src/tools/search/list.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_ENTRIES = 1000; class ListTool extends DtpTool { get name(): string { return "ListTool"; } get slug(): string { return "list"; } get metadata() { return { name: this.definition.function.name || "list", category: "search", tags: ["search", "find", "directory", "ls", "list"], modes: [ ChatSessionMode.Plan, ChatSessionMode.Build, ChatSessionMode.Test, ChatSessionMode.Ship, ChatSessionMode.Develop, ], }; } public definition: ToolDefinition = { type: "function", function: { name: "list", description: "List directory contents with optional filtering. Can show file types, sizes, and modification dates. Supports filtering by glob pattern.", parameters: { type: "object", properties: { path: { type: "string", description: "Directory path to list. Defaults to current working directory.", }, pattern: { type: "string", description: "Optional glob pattern to filter results (e.g., '*.ts', 'src/**/*').", }, recursive: { type: "boolean", description: "List subdirectories recursively (default: false).", }, showHidden: { type: "boolean", description: "Show hidden files (files starting with dot) (default: false).", }, maxDepth: { type: "number", description: "Maximum directory depth for recursive listing (default: 3).", }, }, required: [], }, }, }; public async execute( _context: ToolContext, args: ToolArguments, ): Promise { const targetPath = (args.path as string | undefined) || process.cwd(); const patternStr = args.pattern as string | undefined; let pattern: RegExp | undefined; if (patternStr) { try { const globPattern = patternStr .replace(/[.+^${}()|[\]\\]/g, "\\$&") .replace(/\*/g, ".*") .replace(/\?/g, "."); pattern = new RegExp("^" + globPattern + "$"); } catch { // Invalid regex, ignore } } const recursive = (args.recursive as boolean) || false; const showHidden = (args.showHidden as boolean) || false; const maxDepth = (args.maxDepth as number) || 3; let resolvedPath: string; try { resolvedPath = path.resolve(targetPath); } catch { return this.error("INVALID_PARAMETER", "Invalid path.", { parameter: "path", }); } try { const stat = await fs.stat(resolvedPath); if (!stat.isDirectory()) { return this.error( "INVALID_PARAMETER", `"${targetPath}" is not a directory.`, { parameter: "path", recoveryHint: "Provide a directory path to list.", }, ); } const entries = await this.listDirectory( resolvedPath, pattern || undefined, recursive, showHidden, 0, maxDepth, ); const limited = entries.slice(0, MAX_ENTRIES); let output = ""; if (entries.length === 0) { output = `No entries found in "${targetPath}"`; } else { const truncated = entries.length > MAX_ENTRIES; output = `Contents of "${targetPath}" (${ truncated ? `showing first ${MAX_ENTRIES} of ` : "" }${entries.length} entries):\n\n`; for (const entry of limited) { const typeIndicator = entry.isDirectory ? "d" : entry.isSymlink ? "l" : "-"; const size = entry.isDirectory ? "-" : entry.size.toString(); const modified = entry.modified ? new Date(entry.modified).toISOString().split("T")[0] : "-"; output += `${typeIndicator} ${size.padStart(10)} ${modified} ${entry.name}\n`; } if (truncated) { output += `\n... and ${entries.length - MAX_ENTRIES} more entries`; } } const byteCount = Buffer.byteLength(output, "utf-8"); HostMonitorService.toolCall(byteCount); return this.success( { entries: limited, total: entries.length, truncated: entries.length > MAX_ENTRIES, }, output, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return this.error( "OPERATION_FAILED", `Failed to list directory: ${errorMessage}`, ); } } private async listDirectory( dir: string, pattern: RegExp | undefined, recursive: boolean, showHidden: boolean, depth: number, maxDepth: number, ): Promise< Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null; }> > { const results: Array<{ name: string; isDirectory: boolean; isSymlink: boolean; size: number; modified: Date | null; }> = []; let entries; try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return results; } for (const entry of entries) { if (!showHidden && entry.name.startsWith(".")) { continue; } if (entry.name === "node_modules" || entry.name === ".git") { continue; } const fullPath = path.join(dir, entry.name); let relativePath = fullPath; try { relativePath = path.relative(process.cwd(), fullPath); } catch { // ignore } if (pattern && !pattern.test(relativePath) && !pattern.test(entry.name)) { continue; } let stat; try { stat = await fs.stat(fullPath); } catch { continue; } results.push({ name: relativePath, isDirectory: entry.isDirectory(), isSymlink: entry.isSymbolicLink(), size: stat.size, modified: stat.mtime, }); if (recursive && entry.isDirectory() && depth < maxDepth) { const subResults = await this.listDirectory( fullPath, pattern, recursive, showHidden, depth + 1, maxDepth, ); results.push(...subResults); } } return results; } } export default new ListTool();