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>
This commit is contained in:
kim
2026-07-07 18:17:45 +09:00
co-authored by Claude Sonnet 5
parent 265f8930a3
commit 2a35f40c73
64 changed files with 3840 additions and 338 deletions
+49
View File
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import { loadPlugin } from "./loader.js";
describe("loadPlugin", () => {
let tempDir: string;
beforeEach(() => {
tempDir = path.join(os.tmpdir(), `locode-plugin-${Math.random().toString(36).slice(2)}`);
mkdirSync(tempDir, { recursive: true });
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
it("loads a skill with references/*.md files", () => {
mkdirSync(path.join(tempDir, ".claude-plugin"), { recursive: true });
mkdirSync(path.join(tempDir, "skills", "dataviz", "references"), { recursive: true });
writeFileSync(
path.join(tempDir, ".claude-plugin", "plugin.json"),
JSON.stringify({ name: "test-plugin" }),
);
writeFileSync(path.join(tempDir, "skills", "dataviz", "SKILL.md"), "---\ndescription: dataviz skill\n---\nmain skill");
writeFileSync(path.join(tempDir, "skills", "dataviz", "references", "colors.md"), "use blue");
const plugin = loadPlugin(tempDir);
expect(plugin.skills).toHaveLength(1);
expect(plugin.skills[0]).toMatchObject({ name: "dataviz", content: "main skill" });
expect(plugin.skills[0]?.references).toEqual([{ name: "colors", content: "use blue" }]);
});
it("loads commands and agents", () => {
mkdirSync(path.join(tempDir, ".claude-plugin"), { recursive: true });
writeFileSync(path.join(tempDir, ".claude-plugin", "plugin.json"), JSON.stringify({ name: "test-plugin" }));
mkdirSync(path.join(tempDir, "commands"), { recursive: true });
writeFileSync(path.join(tempDir, "commands", "hello.md"), "---\ndescription: Say hello\n---\nHello $ARGUMENTS");
mkdirSync(path.join(tempDir, "agents"), { recursive: true });
writeFileSync(path.join(tempDir, "agents", "review.md"), "---\ndescription: Code reviewer\n---\nYou review code.");
const plugin = loadPlugin(tempDir);
expect(plugin.commands).toHaveLength(1);
expect(plugin.commands[0]).toMatchObject({ name: "hello", description: "Say hello" });
expect(plugin.agents).toHaveLength(1);
expect(plugin.agents[0]).toMatchObject({ name: "review", description: "Code reviewer" });
});
});