138 lines
4.5 KiB
TypeScript
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}`,
|
|
});
|
|
}
|
|
}
|
|
}
|