// Copyright (C) 2026 Rob Colbert // 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 { 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}`, }); } } }