gadget/gadget-drone/src/tools/system/read.ts
Rob Colbert a4cff5be69 agent toolbox refactor and updates
- reorganized tools into better named directories
- added the file & shell tools
2026-05-10 07:35:09 -04:00

138 lines
4.5 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,
resolveProjectPath,
toolError,
} from "./common.ts";
export class FileReadTool extends DroneTool {
get name(): string {
return "file_read";
}
get category(): string {
return "system";
}
public definition: IToolDefinition = {
type: "function",
function: {
name: this.name,
description: "Read a project file with line numbers. Supports startLine and endLine ranges. Binary files cannot be displayed.",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Path to the file to read, relative to the project root." },
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 project 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 = resolveProjectPath(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")) {
return toolError({
code: "NOT_FOUND",
message: `File not found: ${resolved.displayPath}`,
parameter: "path",
recoveryHint: "Check the file path and ensure the file exists.",
});
}
logger.error("failed to read file", { path: resolved.displayPath, error: errorMessage });
return toolError({
code: "OPERATION_FAILED",
message: `Failed to read file: ${errorMessage}`,
});
}
}
}