import { readFile, stat } from "node:fs/promises";
import { PilotDeckToolRuntimeError } from "../../protocol/errors.js";
export type ReadFileRangeResult = {
content: string;
fullContent: string;
lineCount: number;
totalLines: number;
totalBytes: number;
readBytes: number;
mtimeMs: number;
startLine: number;
endLine: number;
truncated: boolean;
};
const UTF8_BOM = "\uFEFF";
export async function readFileInRange(
filePath: string,
startLine: number,
limit?: number,
): Promise<ReadFileRangeResult> {
const fileStat = await stat(filePath).catch((error: unknown) => {
if (isNodeError(error) && error.code === "ENOENT") {
throw new PilotDeckToolRuntimeError("file_not_found", `File ${filePath} does not exist.`);
}
throw error;
});
if (!fileStat.isFile()) {
throw new PilotDeckToolRuntimeError("file_conflict", `${filePath} is not a regular file.`);
}
const buffer = await readFile(filePath);
if (buffer.includes(0)) {
throw new PilotDeckToolRuntimeError("invalid_tool_input", `${filePath} appears to be a binary file.`);
}
const text = stripBom(buffer.toString("utf8"));
const lines = text.split(/\r?\n/);
const normalizedStart = Math.max(1, startLine);
const startIndex = normalizedStart - 1;
const normalizedLimit = limit === undefined ? undefined : Math.max(0, limit);
const selected = normalizedLimit === undefined
? lines.slice(startIndex)
: lines.slice(startIndex, startIndex + normalizedLimit);
const content = selected.join("\n");
const actualStart = selected.length > 0 ? normalizedStart : Math.min(normalizedStart, lines.length + 1);
const actualEnd = selected.length > 0 ? actualStart + selected.length - 1 : actualStart - 1;
return {
content,
fullContent: text,
lineCount: selected.length,
totalLines: lines.length,
totalBytes: buffer.byteLength,
readBytes: Buffer.byteLength(content, "utf8"),
mtimeMs: Math.floor(fileStat.mtimeMs),
startLine: actualStart,
endLine: actualEnd,
truncated: startIndex > 0 || (normalizedLimit !== undefined && startIndex + normalizedLimit < lines.length),
};
}
function stripBom(value: string): string {
return value.startsWith(UTF8_BOM) ? value.slice(1) : value;
}
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return error instanceof Error && "code" in error;
}