Files
locode/src/plugins/config.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

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;
}