Last active
June 24, 2026 06:40
-
-
Save audreyt/fd11a2733671d205a4d6b05df95ebc10 to your computer and use it in GitHub Desktop.
Mnemon extension for https://omp.sh — builds on Claude Code hook in https://github.com/mnemon-dev/mnemon
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ~/.omp/agent/extensions/mnemon.js | |
| import { existsSync } from "node:fs"; | |
| import { execFileSync, spawnSync } from "node:child_process"; | |
| import { homedir } from "node:os"; | |
| import { join } from "node:path"; | |
| const HOME = homedir(); | |
| const HOOK_DIR = join(HOME, ".claude", "hooks", "mnemon"); | |
| const PROMPT_DIR = join(process.env.MNEMON_DATA_DIR ?? join(HOME, ".mnemon"), "prompt"); | |
| const MAX_HOOK_OUTPUT_CHARS = 12_000; | |
| const MAX_ASSISTANT_TEXT_CHARS = 8_000; | |
| const MAX_RECALL_QUERY_CHARS = 500; | |
| const MNEMON_EXT_VERSION = "2026-06-22-squashed-ui"; | |
| const SILENT_PRIME_MAX_CHARS = 2_400; | |
| function mnemonSuppressCallRender() { | |
| return { render() { return []; } }; | |
| } | |
| function mnemonResultStatusLine(theme, { glyph, tool, preview, ok }) { | |
| const mark = ok | |
| ? (typeof theme?.fg === "function" ? theme.fg("success", "✔") : "✔") | |
| : (typeof theme?.fg === "function" ? theme.fg("error", "✘") : "✘"); | |
| const label = typeof theme?.fg === "function" ? theme.fg("accent", `${glyph} ${tool}`) : `${glyph} ${tool}`; | |
| const line = String(preview ?? "").split("\n")[0].trim() || (ok ? "Done" : "Failed"); | |
| return { | |
| render(width) { | |
| const limit = Math.max(10, width - (6 + tool.length + glyph.length)); | |
| const clipped = line.length > limit ? `${line.slice(0, limit)}...` : line; | |
| const body = clipped.startsWith("\"") && clipped.endsWith("\"") ? clipped : `"${clipped}"`; | |
| return [`${mark} ${label}: ${body}`]; | |
| }, | |
| }; | |
| } | |
| function normalizeHookOutput(result) { | |
| if (typeof result === "string") return result; | |
| if (Array.isArray(result?.content)) { | |
| return result.content | |
| .map((chunk) => (typeof chunk === "string" ? chunk : chunk?.text ?? "")) | |
| .join(""); | |
| } | |
| return result?.stdout ?? result?.output ?? result?.text ?? ""; | |
| } | |
| function runHookSync(name) { | |
| const script = join(HOOK_DIR, name); | |
| if (!existsSync(script)) return ""; | |
| const result = spawnSync("/bin/bash", [script], { | |
| encoding: "utf8", | |
| env: process.env, | |
| timeout: 5_000, | |
| maxBuffer: 256 * 1024, | |
| }); | |
| if (result.error || result.status !== 0) { | |
| return ""; | |
| } | |
| return String(result.stdout ?? "").slice(0, MAX_HOOK_OUTPUT_CHARS).trim(); | |
| } | |
| async function runHook(pi, name) { | |
| const script = join(HOOK_DIR, name); | |
| if (!existsSync(script)) return ""; | |
| if (typeof pi?.exec === "function") { | |
| try { | |
| const result = await pi.exec(`/bin/bash ${JSON.stringify(script)}`); | |
| return normalizeHookOutput(result).slice(0, MAX_HOOK_OUTPUT_CHARS).trim(); | |
| } catch { | |
| // Fall through to the local process runner. Hook failure should not block OMP. | |
| } | |
| } | |
| return runHookSync(name); | |
| } | |
| function silentPrimeOps() { | |
| return `Before answering: When the message may need past context, call the "mnemon_recall" tool. After substantive replies: if durable memory is warranted, call the "mnemon_remember" tool in a follow-up turn to store it. If nothing to store on the remember follow-up turn: emit a single zero-width space ("\u200b") as the entire response.`; | |
| } | |
| function recallBlockSilent(query) { | |
| const q = String(query ?? "").trim().slice(0, MAX_RECALL_QUERY_CHARS); | |
| if (!q) return ""; | |
| try { | |
| execFileSync("mnemon", ["status"], { stdio: "ignore", timeout: 2_000 }); | |
| } catch { | |
| return ""; | |
| } | |
| try { | |
| const raw = execFileSync("mnemon", ["recall", q, "--limit", "3"], { | |
| encoding: "utf8", | |
| stdio: ["ignore", "pipe", "ignore"], | |
| timeout: 8_000, | |
| maxBuffer: 128 * 1024, | |
| }); | |
| const parsed = JSON.parse(raw); | |
| const results = parsed.results ?? []; | |
| if (!results.length) return ""; | |
| const lines = results.slice(0, 3).map((r) => { | |
| const body = String(r.content ?? "").trim().replace(/\s+/g, " "); | |
| const clipped = body.length > 320 ? `${body.slice(0, 317)}…` : body; | |
| return `- (${r.category ?? "general"}, imp ${r.importance ?? "?"}) ${clipped}`; | |
| }); | |
| return lines.join("\n"); | |
| } catch { | |
| return ""; | |
| } | |
| } | |
| function collectText(value, out = []) { | |
| if (typeof value === "string") { | |
| out.push(value); | |
| return out; | |
| } | |
| if (!value || typeof value !== "object") return out; | |
| if (Array.isArray(value)) { | |
| for (const item of value) collectText(item, out); | |
| return out; | |
| } | |
| for (const [key, child] of Object.entries(value)) { | |
| if (key === "thinking" || key === "thoughtSignature") continue; | |
| collectText(child, out); | |
| } | |
| return out; | |
| } | |
| function latestUserText(ctx) { | |
| const branch = ctx?.sessionManager?.getBranch?.() ?? []; | |
| for (let i = branch.length - 1; i >= 0; i -= 1) { | |
| const entry = branch[i]; | |
| if (entry?.type === "message" && entry?.message?.role === "user") { | |
| const text = collectText(entry.message.content).join("\n").trim(); | |
| if (text) return text; | |
| } | |
| if (entry?.role === "user") { | |
| const text = collectText(entry.content ?? entry).join("\n").trim(); | |
| if (text) return text; | |
| } | |
| } | |
| return ""; | |
| } | |
| function latestAssistantText(ctx) { | |
| const branch = ctx?.sessionManager?.getBranch?.() ?? []; | |
| for (let i = branch.length - 1; i >= 0; i -= 1) { | |
| const entry = branch[i]; | |
| let raw = ""; | |
| try { | |
| raw = JSON.stringify(entry); | |
| } catch { | |
| raw = String(entry ?? ""); | |
| } | |
| if (!raw.includes("assistant")) continue; | |
| const text = collectText(entry).join("\n").trim(); | |
| if (text) return text.slice(-MAX_ASSISTANT_TEXT_CHARS); | |
| return raw.slice(-MAX_ASSISTANT_TEXT_CHARS); | |
| } | |
| return ""; | |
| } | |
| const REMEMBER_ALREADY_HANDLED_RE = | |
| /mnemon remember|mnemon_remember|Stored.*imp=|mnemon memory writer|Saved.*mnemon|Stored insight|Spawned agent.*[Mm]nemon/i; | |
| const NO_MEMORY_OUTPUT_RE = /^\[mnemon\]\s*no memory needed\.?$/i; | |
| const SILENT_ASSISTANT_OUTPUT_RE = | |
| /^(?:\[mnemon\]\s*no memory needed\.?|Saved the .* to mnemon\s*\([^)]+\)\.?\s*)$/i; | |
| function isSilentRememberReply(text) { | |
| const t = String(text ?? "").replace(/\u200b/g, "").trim(); | |
| return !t || NO_MEMORY_OUTPUT_RE.test(t) || SILENT_ASSISTANT_OUTPUT_RE.test(t); | |
| } | |
| function rememberFollowupContent() { | |
| return `<system-directive attribution="system" display="false"> | |
| Evaluate the exchange that just ended (user message + your prior reply). Guide: ~/.mnemon/prompt/guide.md decision tree. | |
| If durable memory is warranted: call the "mnemon_remember" tool with the memory text, category, and importance. Review the candidates returned, and call "mnemon_link" if a causal or semantic relationship exists. Do NOT spawn a subagent or call "task". | |
| If nothing durable: emit a single zero-width space ("\u200b") as the entire response (no status line, no "Saved...", no other characters/rationales). | |
| FORBIDDEN in visible output: thinking blocks, step-by-step rationale. | |
| </system-directive>`; | |
| } | |
| function injectionMessage(parts) { | |
| const content = parts.filter(Boolean).join("\n\n"); | |
| if (!content) return undefined; | |
| return { | |
| customType: "mnemon", | |
| content: `<system-directive attribution="system" display="false">\n${content}\n</system-directive>`, | |
| display: false, | |
| attribution: "system", | |
| }; | |
| } | |
| export default function mnemonExtension(pi) { | |
| let inputSerial = 0; | |
| let remindedSerial = -1; | |
| let primeInjected = false; | |
| let suppressFollowupThinking = false; | |
| if (typeof pi.registerAssistantThinkingRenderer === "function") { | |
| pi.registerAssistantThinkingRenderer(() => { | |
| if (!suppressFollowupThinking) return undefined; | |
| return null; | |
| }); | |
| } | |
| if (typeof pi.registerMessageRenderer === "function") { | |
| pi.registerMessageRenderer("mnemon-followup", () => null); | |
| pi.registerMessageRenderer("mnemon", () => null); | |
| } | |
| const { z } = pi.zod; | |
| if (typeof pi.registerTool === "function") { | |
| pi.registerTool({ | |
| name: "mnemon_remember", | |
| label: "Mnemon Remember", | |
| renderShell: "self", | |
| description: "Write a durable memory to the persistent store.", | |
| parameters: z.object({ | |
| memory: z.string().describe("Verbatim text of the memory to store"), | |
| category: z.enum(["preference", "decision", "insight", "fact", "context"]).describe("Memory category"), | |
| importance: z.number().min(1).max(5).describe("Significance rating 1-5"), | |
| entities: z.string().optional().describe("Comma-separated associated entities"), | |
| }), | |
| renderCall() { | |
| return mnemonSuppressCallRender(); | |
| }, | |
| renderResult(result, options, theme, args) { | |
| const ok = result?.details?.success !== false && !result?.isError; | |
| const preview = ok | |
| ? String(result?.details?.summary ?? args?.memory ?? "Stored successfully") | |
| : String(result?.details?.summary ?? args?.memory ?? "Failed"); | |
| return mnemonResultStatusLine(theme, { glyph: "💾", tool: "mnemon_remember", preview, ok }); | |
| }, | |
| async execute(toolCallId, params, signal, onUpdate, ctx) { | |
| const memoryText = params?.memory ?? ""; | |
| if (!memoryText) { | |
| return { | |
| content: [{ type: "text", text: "No memory provided" }], | |
| details: { success: false, summary: "No memory provided" }, | |
| }; | |
| } | |
| const args = ["remember", memoryText]; | |
| if (params.category) { | |
| args.push("--cat", params.category); | |
| } | |
| if (params.importance) { | |
| args.push("--imp", String(params.importance)); | |
| } | |
| if (params.entities) { | |
| args.push("--entities", params.entities); | |
| } | |
| args.push("--source", "agent"); | |
| try { | |
| let output = ""; | |
| if (typeof pi?.exec === "function") { | |
| const result = await pi.exec("mnemon", args, { signal }); | |
| output = typeof result === "string" ? result : (result?.stdout ?? result?.output ?? result?.text ?? ""); | |
| } else { | |
| const result = spawnSync("mnemon", args, { | |
| encoding: "utf8", | |
| env: process.env, | |
| timeout: 5_000, | |
| }); | |
| if (result.error || result.status !== 0) { | |
| const errMsg = result.stderr?.trim() || result.error?.message || "Execution failed"; | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: `Error: ${errMsg}` }, | |
| }; | |
| } | |
| output = String(result.stdout ?? "").trim(); | |
| } | |
| let summary = params.memory; | |
| try { | |
| const parsed = JSON.parse(output); | |
| if (parsed.content) { | |
| summary = parsed.content; | |
| } | |
| } catch { | |
| // fallback to parameter | |
| } | |
| return { | |
| content: [{ type: "text", text: output }], | |
| details: { success: true, summary }, | |
| }; | |
| } catch (err) { | |
| const errMsg = err.stderr || err.message || String(err); | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: errMsg }, | |
| }; | |
| } | |
| } | |
| }); | |
| pi.registerTool({ | |
| name: "mnemon_link", | |
| label: "Mnemon Link", | |
| renderShell: "self", | |
| description: "Create an association link between two memories.", | |
| parameters: z.object({ | |
| id1: z.string().describe("First memory ID"), | |
| id2: z.string().describe("Second memory ID"), | |
| type: z.enum(["causal", "semantic", "temporal", "entity"]).describe("Association type"), | |
| weight: z.number().min(0).max(1).describe("Association weight (0-1)"), | |
| }), | |
| renderCall() { | |
| return mnemonSuppressCallRender(); | |
| }, | |
| renderResult(result, options, theme, args) { | |
| const ok = result?.details?.success !== false && !result?.isError; | |
| const fallback = `${args?.id1 ?? "?"} <-> ${args?.id2 ?? "?"} (${args?.type ?? "?"})`; | |
| const preview = String(result?.details?.summary ?? fallback); | |
| return mnemonResultStatusLine(theme, { glyph: "🔗", tool: "mnemon_link", preview, ok }); | |
| }, | |
| async execute(toolCallId, params, signal, onUpdate, ctx) { | |
| const { id1, id2, type, weight } = params; | |
| const args = ["link", id1, id2, "--type", type, "--weight", String(weight)]; | |
| try { | |
| let output = ""; | |
| if (typeof pi?.exec === "function") { | |
| const result = await pi.exec("mnemon", args, { signal }); | |
| output = typeof result === "string" ? result : (result?.stdout ?? result?.output ?? result?.text ?? ""); | |
| } else { | |
| const result = spawnSync("mnemon", args, { | |
| encoding: "utf8", | |
| env: process.env, | |
| timeout: 5_000, | |
| }); | |
| if (result.error || result.status !== 0) { | |
| const errMsg = result.stderr?.trim() || result.error?.message || "Execution failed"; | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: `Error: ${errMsg}` }, | |
| }; | |
| } | |
| output = String(result.stdout ?? "").trim(); | |
| } | |
| let summary = `${id1.slice(0, 8)} <-> ${id2.slice(0, 8)} (${type})`; | |
| try { | |
| const parsed = JSON.parse(output); | |
| if (parsed.source_id && parsed.target_id) { | |
| summary = `${parsed.source_id.slice(0, 8)} <-> ${parsed.target_id.slice(0, 8)} (${parsed.edge_type ?? type})`; | |
| } | |
| } catch { | |
| // fallback | |
| } | |
| return { | |
| content: [{ type: "text", text: output }], | |
| details: { success: true, summary }, | |
| }; | |
| } catch (err) { | |
| const errMsg = err.stderr || err.message || String(err); | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: errMsg }, | |
| }; | |
| } | |
| } | |
| }); | |
| pi.registerTool({ | |
| name: "mnemon_recall", | |
| label: "Mnemon Recall", | |
| renderShell: "self", | |
| description: "Retrieve relevant memories from the persistent store by semantic query.", | |
| parameters: z.object({ | |
| query: z.string().describe("Natural-language query to match memories against"), | |
| limit: z.number().min(1).max(50).optional().describe("Maximum memories to return (default 10)"), | |
| }), | |
| renderCall() { | |
| return mnemonSuppressCallRender(); | |
| }, | |
| renderResult(result, options, theme, args) { | |
| const ok = result?.details?.success !== false && !result?.isError; | |
| const preview = ok | |
| ? String(result?.details?.summary ?? args?.query ?? "Recalled") | |
| : String(result?.details?.summary ?? args?.query ?? "Failed"); | |
| return mnemonResultStatusLine(theme, { glyph: "🔎", tool: "mnemon_recall", preview, ok }); | |
| }, | |
| async execute(toolCallId, params, signal, onUpdate, ctx) { | |
| const query = String(params?.query ?? "").trim(); | |
| if (!query) { | |
| return { | |
| content: [{ type: "text", text: "No query provided" }], | |
| details: { success: false, summary: "No query provided" }, | |
| }; | |
| } | |
| const args = ["recall", query, "--limit", String(params.limit ?? 10)]; | |
| try { | |
| let output = ""; | |
| if (typeof pi?.exec === "function") { | |
| const result = await pi.exec("mnemon", args, { signal }); | |
| output = typeof result === "string" ? result : (result?.stdout ?? result?.output ?? result?.text ?? ""); | |
| } else { | |
| const result = spawnSync("mnemon", args, { | |
| encoding: "utf8", | |
| env: process.env, | |
| timeout: 8_000, | |
| }); | |
| if (result.error || result.status !== 0) { | |
| const errMsg = result.stderr?.trim() || result.error?.message || "Execution failed"; | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: `Error: ${errMsg}` }, | |
| }; | |
| } | |
| output = String(result.stdout ?? "").trim(); | |
| } | |
| let summary = "Recalled"; | |
| let count = 0; | |
| try { | |
| const parsed = JSON.parse(output); | |
| count = Array.isArray(parsed?.results) ? parsed.results.length : 0; | |
| summary = count ? `${count} memor${count === 1 ? "y" : "ies"} recalled` : "No memories found"; | |
| } catch { | |
| summary = output ? "Recalled" : "No memories found"; | |
| } | |
| return { | |
| content: [{ type: "text", text: output }], | |
| details: { success: true, summary, count }, | |
| }; | |
| } catch (err) { | |
| const errMsg = err.stderr || err.message || String(err); | |
| return { | |
| content: [{ type: "text", text: `Error: ${errMsg}` }], | |
| details: { success: false, summary: errMsg }, | |
| }; | |
| } | |
| } | |
| }); | |
| } | |
| pi.on("session_start", async () => { | |
| primeInjected = false; | |
| await runHook(pi, "prime.sh"); | |
| }); | |
| pi.on("message_end", async (event) => { | |
| if (!suppressFollowupThinking) return; | |
| const msg = event?.message; | |
| if (msg?.role !== "assistant") return; | |
| const text = collectText(msg.content).join("\n").trim(); | |
| if (!isSilentRememberReply(text)) { | |
| suppressFollowupThinking = false; | |
| return; | |
| } | |
| suppressFollowupThinking = false; | |
| if (msg) { | |
| if ("display" in msg) msg.display = false; | |
| msg.content = typeof msg.content === "string" ? "\u200b" : [{ type: "text", text: "\u200b" }]; | |
| } | |
| return { message: msg }; | |
| }); | |
| pi.on("input", async () => { | |
| inputSerial += 1; | |
| }); | |
| pi.on("before_agent_start", async (_event, ctx) => { | |
| const parts = []; | |
| if (!primeInjected) { | |
| parts.push(silentPrimeOps()); | |
| primeInjected = true; | |
| } | |
| const userText = latestUserText(ctx); | |
| const recalled = recallBlockSilent(userText); | |
| if (recalled) parts.push(recalled); | |
| const message = injectionMessage(parts); | |
| return message ? { message } : undefined; | |
| }); | |
| pi.on("agent_end", async (_event, ctx) => { | |
| if (suppressFollowupThinking) { | |
| return; | |
| } | |
| const serial = inputSerial; | |
| if (remindedSerial === serial) return; | |
| remindedSerial = serial; | |
| const assistant = latestAssistantText(ctx); | |
| if (REMEMBER_ALREADY_HANDLED_RE.test(assistant)) return; | |
| const text = rememberFollowupContent(); | |
| if (typeof pi.sendMessage === "function") { | |
| try { | |
| suppressFollowupThinking = true; | |
| await pi.sendMessage( | |
| { | |
| customType: "mnemon-followup", | |
| content: text, | |
| display: false, | |
| attribution: "system", | |
| }, | |
| { deliverAs: "followUp", triggerTurn: true, hideThinking: true }, | |
| ); | |
| } catch { | |
| suppressFollowupThinking = false; | |
| } | |
| } | |
| }); | |
| pi.on("session_before_compact", async () => { | |
| return { | |
| customInstructions: | |
| "Preserve only critical continuity via mnemon remember when justified; do not store the full transcript.", | |
| }; | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment