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>
49 lines
1.6 KiB
TypeScript
49 lines
1.6 KiB
TypeScript
import envPaths from "env-paths";
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
|
|
export interface InstalledPluginRecord {
|
|
name: string;
|
|
/** Absolute local path — either a local directory the user pointed at directly, or a git clone
|
|
* under pluginsDir(). */
|
|
path: string;
|
|
/** What the user originally passed to `locode plugin add` (path or git URL) — kept for display. */
|
|
source: string;
|
|
}
|
|
|
|
const paths = envPaths("locode", { suffix: "" });
|
|
const registryFile = path.join(paths.config, "plugins.json");
|
|
|
|
export function pluginsDir(): string {
|
|
return path.join(paths.data, "plugins");
|
|
}
|
|
|
|
export function loadInstalledPlugins(): InstalledPluginRecord[] {
|
|
if (!existsSync(registryFile)) return [];
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(registryFile, "utf-8")) as { plugins: InstalledPluginRecord[] };
|
|
return parsed.plugins ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function saveInstalledPlugins(records: InstalledPluginRecord[]): void {
|
|
mkdirSync(path.dirname(registryFile), { recursive: true });
|
|
writeFileSync(registryFile, JSON.stringify({ plugins: records }, null, 2));
|
|
}
|
|
|
|
export function addInstalledPlugin(record: InstalledPluginRecord): void {
|
|
const records = loadInstalledPlugins().filter((r) => r.name !== record.name);
|
|
records.push(record);
|
|
saveInstalledPlugins(records);
|
|
}
|
|
|
|
export function removeInstalledPlugin(name: string): boolean {
|
|
const records = loadInstalledPlugins();
|
|
const next = records.filter((r) => r.name !== name);
|
|
if (next.length === records.length) return false;
|
|
saveInstalledPlugins(next);
|
|
return true;
|
|
}
|