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>
46 lines
2.1 KiB
TypeScript
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) };
|
|
},
|
|
};
|