Files
locode/src/tools/readFile.ts
T
kimandClaude Sonnet 5 2a35f40c73 Add plugin/hook/skill support, images, @mentions, dashboard, and stability fixes
Builds out locode's Claude Code plugin parity: MCP servers, slash commands,
sub-agents, hooks (12 lifecycle events), and skills, all loadable from a
local path or git URL. Also adds image support (read_file, /import), @-mention
file autocomplete, a /dashboard stats view, /export with an editable filename
prompt, an expanded git tool (reset/stash/merge/rebase/delete_branch), and a
redesigned status bar.

On top of that, a review pass found and fixed 10 correctness/stability bugs:
argument-injection in git reset/merge/rebase (a ref like "--hard" was parsed
as a flag), tool-call image results interleaving with native tool_call_id
messages and breaking OpenAI-compatible message ordering, backgrounded bash
jobs still being silently killed by their original timeout with the kill
masked as a clean exit, a SubagentStart hook's block being ignored, the
sub-agent timeout clock starting before the hook it should exclude, a
template-expansion bug that could re-substitute $1..$9 placeholders,
unbounded background-job output buffers, a missing directory-target fallback
in /export, and image size caps checked after reading the whole file instead
of before.

Session saves and exports now go through a shared atomic
write-then-rename helper so a crash can't leave a truncated file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 18:17:45 +09:00

46 lines
2.1 KiB
TypeScript

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<z.infer<typeof schema>> = {
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) };
},
};