import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { truncate } from "../utils/truncate.js"; import { imageMimeType, MAX_IMAGE_BYTES } from "../utils/image.js"; import type { ToolDef } from "./types.js"; const schema = z.object({ path: z.string().describe("File path to read, relative to the working directory or absolute."), offset: z.number().int().min(1).optional().describe("1-indexed line number to start reading from (text files only)."), limit: z.number().int().min(1).max(2000).optional().describe("Maximum number of lines to read (text files only)."), }); export const readFileTool: ToolDef> = { name: "read_file", description: "Read a file from the local filesystem. Text files return content with 1-indexed line numbers, optionally a specific " + "line range. Image files (png, jpg, jpeg, gif, webp, bmp) are returned as image content for the model to see directly " + "— this requires a vision-capable model/backend; others may error on the request.", schema, mutating: false, handler: async ({ path: filePath, offset, limit }, ctx) => { const resolved = path.resolve(ctx.cwd, filePath); const mimeType = imageMimeType(resolved); if (mimeType) { const stats = await fsStat(resolved); if (stats.size > MAX_IMAGE_BYTES) { throw new Error( `${filePath} is ${(stats.size / 1_048_576).toFixed(1)}MB, over the ${MAX_IMAGE_BYTES / 1_048_576}MB limit for image reads.`, ); } const buffer = await fsReadFile(resolved); return { path: resolved, image: true, mimeType, bytes: buffer.byteLength, base64: buffer.toString("base64") }; } const content = await fsReadFile(resolved, "utf-8"); const lines = content.split("\n"); const start = offset ? offset - 1 : 0; const end = limit ? start + limit : lines.length; const slice = lines.slice(start, end); const numbered = slice.map((line, i) => `${start + i + 1}\t${line}`).join("\n"); return { path: resolved, totalLines: lines.length, content: truncate(numbered) }; }, };