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:
@@ -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" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user