gadget/gadget-drone/src/tools/plan/read.ts
2026-05-11 07:08:18 -04:00

140 lines
4.9 KiB
TypeScript

// Copyright (C) 2026 Rob Colbert <rob.colbert@openplatform.us>
// Licensed under the Apache License, Version 2.0
import fs from "node:fs/promises";
import type { IAiLogger, IToolArguments, IToolDefinition } from "@gadget/ai";
import { DroneTool } from "../tool.ts";
import {
asPositiveInteger,
formatNumberedLines,
isBinaryBuffer,
toolError,
} from "../system/common.ts";
import { getGadgetDir, resolvePlanPath } from "./common.ts";
export class PlanFileReadTool extends DroneTool {
get name(): string {
return "plan_file_read";
}
get category(): string {
return "plan";
}
public definition: IToolDefinition = {
type: "function",
function: {
name: this.name,
description:
"Read a file from Gadget's .gadget directory with line numbers. This Plan-mode tool works in Gadget's own directory (.gadget) for storing and reading plans, todos, and other knowledge. It does not affect project files.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Path to the file to read, relative to the .gadget directory." },
startLine: { type: "number", description: "Starting line number (1-indexed). Defaults to 1." },
endLine: { type: "number", description: "Ending line number (inclusive). Defaults to end of file." },
},
required: ["path"],
},
},
};
public async execute(args: IToolArguments, logger: IAiLogger): Promise<string> {
const filePath = args.path;
if (typeof filePath !== "string" || filePath.trim().length === 0) {
return toolError({
code: "MISSING_PARAMETER",
message: "File path must not be empty.",
parameter: "path",
recoveryHint: "Provide a valid file path within the .gadget directory.",
});
}
const startLine = asPositiveInteger(args.startLine) ?? 1;
const endLine = args.endLine === undefined ? undefined : asPositiveInteger(args.endLine);
if (startLine < 1) {
return toolError({
code: "INVALID_PARAMETER",
message: "startLine must be >= 1.",
parameter: "startLine",
expected: "A positive integer >= 1",
});
}
if (endLine !== undefined && endLine < startLine) {
return toolError({
code: "INVALID_PARAMETER",
message: "endLine must be >= startLine.",
parameter: "endLine",
expected: "An integer >= startLine",
});
}
const resolved = resolvePlanPath(this.toolbox, filePath);
if (typeof resolved === "string") return resolved;
try {
const stat = await fs.stat(resolved.absolutePath);
if (!stat.isFile()) {
return toolError({
code: "INVALID_PARAMETER",
message: `"${resolved.displayPath}" is not a file.`,
parameter: "path",
recoveryHint: "Provide a path to a regular file, not a directory.",
});
}
const raw = await fs.readFile(resolved.absolutePath);
if (isBinaryBuffer(raw)) {
return [
`PATH: ${resolved.displayPath}`,
"TOTAL LINES: 0",
"LINES SHOWN: 0",
"FILE OPERATION: read",
"---",
`Binary file, cannot display: ${resolved.displayPath}`,
].join("\n");
}
const content = raw.toString("utf-8");
const lines = content.split("\n");
const startIdx = Math.max(0, startLine - 1);
const endIdx = endLine !== undefined ? Math.min(endLine, lines.length) : lines.length;
const selectedLines = lines.slice(startIdx, endIdx);
const numberedLines = formatNumberedLines(selectedLines, startIdx);
const totalLines = lines.length;
const rangeLabel = endLine !== undefined || startLine > 1
? `lines ${startIdx + 1}-${endIdx} of ${totalLines}`
: `${totalLines} lines`;
return [
`PATH: ${resolved.displayPath}`,
`TOTAL LINES: ${totalLines}`,
`LINES SHOWN: ${selectedLines.length}`,
"FILE OPERATION: read",
"---",
`File: ${resolved.displayPath} (${rangeLabel})`,
"",
numberedLines,
].join("\n");
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes("ENOENT")) {
const gadgetDir = getGadgetDir(this.toolbox);
return toolError({
code: "NOT_FOUND",
message: `File not found in .gadget: ${resolved.displayPath}${gadgetDir ? `. The .gadget directory exists but this file does not.` : ". Plan storage is empty. Create a file with plan_file_write first."}`,
parameter: "path",
recoveryHint: "Check the file path. Use plan_list to see available files.",
});
}
logger.error("failed to read plan file", { path: resolved.displayPath, error: errorMessage });
return toolError({
code: "OPERATION_FAILED",
message: `Failed to read file: ${errorMessage}`,
});
}
}
}