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.
116 lines
3.7 KiB
TypeScript
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}`,
|
|
});
|
|
}
|
|
}
|
|
}
|