gadget/packages/ai-toolbox/src/plan/list.ts
Rob Colbert c90e8ce0e8 plan tool updates
Plan tools extended to all modes; recent chat sessions added to
Authenticated Home view; drone locking and chat session startup factored
out of Project Manager into Chat Session view and made universal; update
for AGENTS.md.
2026-05-17 14:09:24 -04:00

116 lines
3.7 KiB
TypeScript

// src/plan/list.ts
// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// 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<string> {
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}`,
});
}
}
}