import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type MemoryClient from "mem0ai"; import type { Mem0Config, ScopeContext, Scope } from "./types.ts"; import { DEFAULT_CUSTOM_CATEGORIES } from "./types.ts"; import { resolveSearchFilters, resolveAddParams } from "./memory/scoping.ts"; import { formatMemoryList, formatMemoryCompact, groupByCategory } from "./memory/formatting.ts"; import { captureCommandEvent } from "./telemetry.ts"; const SEARCH_TOP_K = 10; export function registerCommands( pi: ExtensionAPI, mem0: MemoryClient, config: Mem0Config, getScopeCtx: () => ScopeContext, telemetryCtx?: { apiKey?: string }, ): void { const sendFeedback = (customType: string, content: string): void => { pi.sendMessage({ customType, content, display: true }); }; const pluralize = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`; const searchMemories = async (query: string, scope: Scope) => { const filters = resolveSearchFilters(scope, getScopeCtx()); const result = await mem0.search(query, { filters, threshold: config.searchThreshold, topK: SEARCH_TOP_K, rerank: true, }); return result.results ?? []; }; pi.registerCommand("mem0-remember", { description: "Store a memory verbatim (no inference)", handler: async (args, ctx) => { const text = args?.trim(); if (!text) { ctx.ui.notify("Usage: /mem0-remember ", "warning"); return; } const addParams = resolveAddParams(config.defaultScope, getScopeCtx()); const result = await mem0.add( [{ role: "user", content: text }], { ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES, infer: false }, ); captureCommandEvent("mem0-remember", {}, telemetryCtx); const storedItems = (Array.isArray(result) ? result : []) .map((m) => (m as { memory?: string }).memory) .filter((m): m is string => Boolean(m)); const items = storedItems.length > 0 ? storedItems : [text]; sendFeedback( "mem0-remember", [`**Stored to ${config.defaultScope} memory**`, ...items.map((m) => `- ${m}`)].join("\n"), ); }, }); pi.registerCommand("mem0-forget", { description: "Delete memories matching a natural language query", handler: async (args, ctx) => { const query = args?.trim(); if (!query) { ctx.ui.notify("Usage: /mem0-forget ", "warning"); return; } const memories = await searchMemories(query, config.defaultScope); if (memories.length === 0) { captureCommandEvent("mem0-forget", { result_count: 0 }, telemetryCtx); sendFeedback("mem0-forget", `**No matches for "${query}"** — nothing to forget.`); return; } const forgotten = (mem: Parameters[0]) => { captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx); sendFeedback( "mem0-forget", [`**Forgotten from ${config.defaultScope} memory**`, `- ${formatMemoryCompact(mem)}`].join("\n"), ); }; if (memories.length === 1) { const target = memories[0]; const confirmed = await ctx.ui.confirm("Delete this memory?", formatMemoryCompact(target)); if (!confirmed) { sendFeedback("mem0-forget", "**Cancelled** — no memories deleted."); return; } await mem0.delete(target.id); forgotten(target); return; } const labels = memories.map((m) => formatMemoryCompact(m)); const selected = await ctx.ui.select( `Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I delete?`, labels, ); if (!selected) { sendFeedback("mem0-forget", "**Cancelled** — no memories deleted."); return; } const idx = labels.indexOf(selected); if (idx < 0) return; const target = memories[idx]; await mem0.delete(target.id); forgotten(target); }, }); pi.registerCommand("mem0-search", { description: "Semantic search across memories", handler: async (args, ctx) => { const query = args?.trim(); if (!query) { ctx.ui.notify("Usage: /mem0-search ", "warning"); return; } const memories = await searchMemories(query, config.defaultScope); captureCommandEvent("mem0-search", { result_count: memories.length }, telemetryCtx); if (memories.length === 0) { sendFeedback("mem0-search", `**No matches for "${query}"** · ${config.defaultScope} scope`); return; } sendFeedback( "mem0-search", [ `**${pluralize(memories.length, "match", "matches")} for "${query}"** · ${config.defaultScope} scope`, "", formatMemoryList(memories), ].join("\n"), ); }, }); pi.registerCommand("mem0-tour", { description: "Browse all memories grouped by category", handler: async (args, ctx) => { const raw = args?.trim().toLowerCase(); const validScopes: Scope[] = ["project", "session", "global"]; if (raw && !validScopes.includes(raw as Scope)) { ctx.ui.notify(`Invalid scope "${raw}". Must be one of: ${validScopes.join(", ")}`, "warning"); return; } const scope: Scope = (raw as Scope) || config.defaultScope; const filters = resolveSearchFilters(scope, getScopeCtx()); const result = await mem0.getAll({ filters }); const memories = result.results ?? []; if (memories.length === 0) { captureCommandEvent("mem0-tour", { memory_count: 0, scope }, telemetryCtx); sendFeedback("mem0-tour", `**No memories in ${scope} scope yet** — store one with \`/mem0-remember\`.`); return; } const groups = groupByCategory(memories); const lines: string[] = [ `**Memory tour** · ${pluralize(memories.length, "memory", "memories")} · ${scope} scope`, "", ]; for (const [category, items] of groups) { lines.push(`### ${category} (${items.length})`); for (const m of items) { lines.push(`- ${formatMemoryCompact(m)}`); } lines.push(""); } captureCommandEvent("mem0-tour", { memory_count: memories.length, scope }, telemetryCtx); sendFeedback("mem0-tour", lines.join("\n")); }, }); pi.registerCommand("mem0-scope", { description: "Change default memory scope for this session (project, session, global)", handler: async (args, ctx) => { const scope = args?.trim().toLowerCase(); const valid: Scope[] = ["project", "session", "global"]; if (!scope) { sendFeedback( "mem0-scope", [ `**Current scope: ${config.defaultScope}**`, `New memories save to the **${config.defaultScope}** pool. Switch with \`/mem0-scope <${valid.join(" | ")}>\`.`, ].join("\n"), ); return; } if (!valid.includes(scope as Scope)) { ctx.ui.notify(`Invalid scope "${scope}". Must be one of: ${valid.join(", ")}`, "warning"); return; } config.defaultScope = scope as Scope; captureCommandEvent("mem0-scope", { scope }, telemetryCtx); sendFeedback( "mem0-scope", [ `**Scope changed to ${scope}**`, `New memories now save to the **${scope}** pool for this session.`, ].join("\n"), ); }, }); pi.registerCommand("mem0-status", { description: "Show connection health, identity, project, and memory count", handler: async (_args, _ctx) => { const scopeCtx = getScopeCtx(); const filters = resolveSearchFilters("project", scopeCtx); let count = 0; let connected = false; try { const result = await mem0.getAll({ filters }); count = result.count ?? (result.results ?? []).length; connected = true; } catch { connected = false; } const lines = [ "**Mem0 status**", "", `- Connection: ${connected ? "connected" : "disconnected"}`, `- User: ${scopeCtx.userId}`, `- Project: ${scopeCtx.appId}`, `- Session: ${scopeCtx.runId}`, `- Default scope: ${config.defaultScope}`, `- Search relevance threshold: ${config.searchThreshold}`, `- Project memories: ${count}`, `- Auto-capture: ${config.autoCapture ? "on" : "off"}`, ]; captureCommandEvent("mem0-status", { connected, memory_count: count }, telemetryCtx); sendFeedback("mem0-status", lines.join("\n")); }, }); }