mirror of
https://github.com/supermemoryai/claude-supermemory.git
synced 2026-09-14 14:58:03 +08:00
44610b0e1a
Plugin is now 24 files of committed plain source. All API interaction via fetch or the hosted MCP at mcp.supermemory.ai (through a stdio proxy that shares the hooks' credentials.json — one browser login covers both). - hooks/: session-start (profile + welcome-back + auth bootstrap + statusline symlink), recall-directive (search-liberal + ◆ attribution + container tag), recall-approve (MCP tool-name matching, read-only allowlist), capture (transcript delta + entityContext) - hooks/mcp-proxy.js: stdio ⇄ streamable-HTTP bridge with session tracking - statusline.js: self-contained renderer; state in fixed ~/.supermemory-claude/statusline; settings point at a hook-maintained symlink - agents/context-gatherer.md: deep multi-container recall subagent - deleted: esbuild bundles + build step, supermemory SDK, legacy container-tag resolvers (Cursor/Codex/OpenCode), bash-teaching skills, custom integrity headers, statusline installer - requires mcp.supermemory.ai API-key auth (supermemoryai/supermemory#1537) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
179 lines
7.4 KiB
TypeScript
179 lines
7.4 KiB
TypeScript
import { readdirSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const ROOT = import.meta.dir;
|
|
const PLUGIN = join(ROOT, "plugin");
|
|
|
|
function parseFrontmatter(text: string) {
|
|
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
const fm: Record<string, string> = {};
|
|
if (m) {
|
|
for (const line of m[1].split("\n")) {
|
|
const kv = line.match(/^(\S+?):\s*(.*)$/);
|
|
if (kv) fm[kv[1]] = kv[2];
|
|
}
|
|
}
|
|
return fm;
|
|
}
|
|
|
|
function listFiles(dir: string, prefix = ""): { path: string; size: number }[] {
|
|
const out: { path: string; size: number }[] = [];
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
if (entry.isDirectory()) out.push(...listFiles(join(dir, entry.name), rel));
|
|
else out.push({ path: rel, size: statSync(join(dir, entry.name)).size });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
async function inspect() {
|
|
const manifest = await Bun.file(join(PLUGIN, ".claude-plugin/plugin.json")).json();
|
|
const hooksJson = await Bun.file(join(PLUGIN, "hooks/hooks.json")).json();
|
|
const mcpJson = await Bun.file(join(PLUGIN, ".mcp.json")).json();
|
|
|
|
const hooks = Object.entries(hooksJson.hooks).flatMap(([event, groups]: [string, any]) =>
|
|
groups.flatMap((g: any) =>
|
|
g.hooks.map((h: any) => {
|
|
const script = h.command.match(/hooks\/[\w-]+\.js/)?.[0] ?? "";
|
|
return {
|
|
event,
|
|
matcher: g.matcher ?? "*",
|
|
script,
|
|
timeout: h.timeout,
|
|
exists: script ? statSyncSafe(join(PLUGIN, script)) !== null : false,
|
|
};
|
|
}),
|
|
),
|
|
);
|
|
|
|
const commands = await Promise.all(
|
|
readdirSync(join(PLUGIN, "commands")).map(async (f) => {
|
|
const content = await Bun.file(join(PLUGIN, "commands", f)).text();
|
|
const fm = parseFrontmatter(content);
|
|
return { name: f.replace(".md", ""), description: fm.description ?? "", content };
|
|
}),
|
|
);
|
|
|
|
const agents = await Promise.all(
|
|
readdirSync(join(PLUGIN, "agents")).map(async (f) => {
|
|
const content = await Bun.file(join(PLUGIN, "agents", f)).text();
|
|
const fm = parseFrontmatter(content);
|
|
return { name: f.replace(".md", ""), description: fm.description ?? "", content };
|
|
}),
|
|
);
|
|
|
|
const directiveSrc = await Bun.file(join(PLUGIN, "hooks/recall-directive.js")).text();
|
|
const directive = directiveSrc.match(/return `([\s\S]*?)`;/)?.[1] ?? "";
|
|
const approveSrc = await Bun.file(join(PLUGIN, "hooks/recall-approve.js")).text();
|
|
const readOnlyTools = [...approveSrc.matchAll(/^\s*'([\w-]+)',$/gm)].map((m) => m[1]);
|
|
|
|
let git = { branch: "unknown", commit: "unknown" };
|
|
try {
|
|
git = {
|
|
branch: (await Bun.$`git branch --show-current`.cwd(ROOT).quiet().text()).trim(),
|
|
commit: (await Bun.$`git log -1 --format=%h %s`.cwd(ROOT).quiet().text()).trim(),
|
|
};
|
|
} catch {}
|
|
|
|
return {
|
|
manifest,
|
|
git,
|
|
mcp: mcpJson,
|
|
hooks,
|
|
commands,
|
|
agents,
|
|
directive,
|
|
readOnlyTools,
|
|
files: listFiles(PLUGIN),
|
|
};
|
|
}
|
|
|
|
function statSyncSafe(p: string) {
|
|
try {
|
|
return statSync(p);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const html = `<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Supermemory Plugin Inspector</title>
|
|
<style>
|
|
:root { color-scheme: dark; }
|
|
body { font: 14px/1.5 ui-monospace, monospace; background: #0d0d0f; color: #e4e4e7; max-width: 1080px; margin: 2rem auto; padding: 0 1rem 4rem; }
|
|
h1 { font-size: 18px; } h1 small { color: #71717a; font-weight: normal; }
|
|
h2 { font-size: 14px; color: #a1a1aa; border-bottom: 1px solid #27272a; padding-bottom: 4px; margin-top: 2.5rem; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
td, th { text-align: left; padding: 4px 12px 4px 0; vertical-align: top; border-bottom: 1px solid #1c1c1f; }
|
|
th { color: #71717a; font-weight: normal; }
|
|
.ok { color: #4ade80; } .bad { color: #f87171; }
|
|
.dim { color: #71717a; }
|
|
.badge { background: #1c1c1f; border: 1px solid #27272a; border-radius: 4px; padding: 1px 8px; margin-right: 6px; }
|
|
pre { background: #131316; border: 1px solid #27272a; border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; font-size: 12.5px; }
|
|
details { margin: 6px 0; }
|
|
summary { cursor: pointer; color: #93c5fd; }
|
|
summary .dim { margin-left: 8px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>supermemory plugin <small id="meta"></small></h1>
|
|
<div id="badges"></div>
|
|
<h2>hooks</h2><table id="hooks"></table>
|
|
<h2>mcp server + auto-approved (read-only) tools</h2><div id="mcp"></div>
|
|
<h2>recall directive — injected every prompt</h2><pre id="directive"></pre>
|
|
<h2>agents</h2><div id="agents"></div>
|
|
<h2>commands</h2><div id="commands"></div>
|
|
<h2>plugin files (all committed source, no build)</h2><table id="files"></table>
|
|
<script>
|
|
const esc = s => String(s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c]));
|
|
const kb = n => n < 1024 ? n + ' b' : (n / 1024).toFixed(1) + ' kb';
|
|
const HOOK_NOTES = {
|
|
SessionStart: 'profile fetch → context + "N memories loaded" + welcome-back; auth bootstrap; statusline symlink upkeep',
|
|
UserPromptSubmit: 'injects recall directive with active container tag (local, no network)',
|
|
PreToolUse: 'auto-approves read-only supermemory MCP tools + "recalling: <query>" message',
|
|
Stop: 'captures transcript delta with entityContext; writes statusline state',
|
|
};
|
|
fetch('/api/inspect').then(r => r.json()).then(d => {
|
|
document.getElementById('meta').textContent = 'v' + d.manifest.version + ' \\u00b7 ' + d.git.branch + ' \\u00b7 ' + d.git.commit;
|
|
const total = d.files.reduce((a, f) => a + f.size, 0);
|
|
document.getElementById('badges').innerHTML =
|
|
'<span class="badge">' + d.files.length + ' files</span>' +
|
|
'<span class="badge">' + kb(total) + ' total</span>' +
|
|
'<span class="ok">zero dependencies · zero build steps</span>';
|
|
document.getElementById('hooks').innerHTML =
|
|
'<tr><th>event</th><th>matcher</th><th>script</th><th>timeout</th><th>role</th></tr>' +
|
|
d.hooks.map(h => '<tr><td>' + esc(h.event) + '</td><td class="dim">' + esc(h.matcher) + '</td><td class="' +
|
|
(h.exists ? 'ok' : 'bad') + '">' + esc(h.script) + '</td><td class="dim">' + h.timeout + 's</td><td class="dim">' +
|
|
esc(HOOK_NOTES[h.event] || '') + '</td></tr>').join('');
|
|
const server = Object.entries(d.mcp)[0];
|
|
document.getElementById('mcp').innerHTML =
|
|
'<p class="dim">server "' + esc(server[0]) + '": ' + esc(server[1].command + ' ' + server[1].args.join(' ')) +
|
|
' (proxy \\u2192 mcp.supermemory.ai, authed via credentials.json)</p>' +
|
|
d.readOnlyTools.map(t => '<span class="badge ok">' + esc(t) + '</span>').join('');
|
|
document.getElementById('directive').textContent = d.directive;
|
|
const fileSection = items => items.map(i =>
|
|
'<details><summary>' + esc(i.name) + '<span class="dim">' + esc(i.description) + '</span></summary><pre>' + esc(i.content) + '</pre></details>').join('');
|
|
document.getElementById('agents').innerHTML = fileSection(d.agents);
|
|
document.getElementById('commands').innerHTML = fileSection(d.commands);
|
|
document.getElementById('files').innerHTML =
|
|
'<tr><th>file</th><th>size</th></tr>' +
|
|
d.files.sort((a, b) => a.path.localeCompare(b.path)).map(f =>
|
|
'<tr><td>' + esc(f.path) + '</td><td class="dim">' + kb(f.size) + '</td></tr>').join('');
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
|
|
const server = Bun.serve({
|
|
port: 4747,
|
|
routes: {
|
|
"/": () => new Response(html, { headers: { "Content-Type": "text/html" } }),
|
|
"/api/inspect": async () => Response.json(await inspect()),
|
|
},
|
|
});
|
|
|
|
console.log(`plugin inspector → ${server.url}`);
|