// src/plan/list.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import fs from "node:fs/promises"; import path from "node:path"; import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai"; import { GadgetTool } from "../tool.ts"; import { toolError } from "../system/common.ts"; import { getGadgetDir, resolvePlanPath } from "./common.ts"; export class PlanListTool extends GadgetTool { get name(): string { return "plan_list"; } get category(): string { return "plan"; } public definition: IToolDefinition = { type: "function", function: { name: this.name, description: "List contents of Gadget's .gadget directory. Operates exclusively in the .gadget directory for exploring stored plans, todos, and other knowledge. Does not affect project files.", parameters: { type: "object", properties: { path: { type: "string", description: "Subdirectory path to list within .gadget, relative to the .gadget directory.", }, }, required: [], }, }, }; public async execute(args: IToolArguments, logger: IAiLogger): Promise { const gadgetDir = getGadgetDir(this.toolbox); if (!gadgetDir) { return toolError({ code: "OPERATION_NOT_ALLOWED", message: "No active project workspace is configured for plan file tools.", recoveryHint: "Run this tool during an active Agent work order in Plan mode.", }); } let targetPath: string; let displayPath: string; if (args.path !== undefined && typeof args.path === "string") { const resolved = resolvePlanPath(this.toolbox, args.path); if (typeof resolved === "string") return resolved; targetPath = resolved.absolutePath; displayPath = resolved.displayPath; } else { targetPath = path.resolve(gadgetDir); displayPath = "."; } try { let stat; try { stat = await fs.stat(targetPath); } catch { return "Plan storage is empty. The .gadget directory has not been created yet. Use plan_file_write to store your first file."; } if (!stat.isDirectory()) { return toolError({ code: "INVALID_PARAMETER", message: `"${displayPath}" is not a directory.`, parameter: "path", recoveryHint: "Provide a directory path to list.", }); } const entries = await fs.readdir(targetPath, { withFileTypes: true }); if (entries.length === 0) { return `The .gadget directory "${displayPath}" is empty.`; } const lines: string[] = [ `Contents of ".gadget${displayPath === "." ? "" : `/${displayPath}`}" (${entries.length} entries):`, "", ]; for (const entry of entries) { const typeIndicator = entry.isDirectory() ? "d" : entry.isSymbolicLink() ? "l" : "-"; let size = "-"; let modified = "-"; try { const entryStat = await fs.stat(path.join(targetPath, entry.name)); if (entry.isFile()) size = entryStat.size.toString(); modified = entryStat.mtime.toISOString().split("T")[0]; } catch { // stat unavailable — skip size and date } lines.push(`${typeIndicator} ${size.padStart(10)} ${modified} ${entry.name}`); } return lines.join("\n"); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error("failed to list .gadget directory", { error: errorMessage }); return toolError({ code: "OPERATION_FAILED", message: `Failed to list directory: ${errorMessage}`, }); } } }