/** * CLI subcommand registration for the OpenClaw Mem0 plugin. * * Registers all `openclaw mem0 ` commands: * - login : Authenticate with Mem0 Platform (email or API key) * - search : Search memories * - stats : Show memory statistics * - status : Check API connectivity * - dream : Run memory consolidation */ import { createInterface } from "node:readline"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { Backend } from "../backend/base.ts"; import type { Mem0Config, Mem0Provider, MemoryItem, SearchOptions, } from "../types.ts"; import { loadDreamPrompt } from "../skill-loader.ts"; import { readMem0Config, getBaseUrl, setPlatformAuth, writeMem0Config, } from "./config-file.ts"; // ============================================================================ // Login config helpers // ============================================================================ function prompt(question: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stderr }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); }); } // ============================================================================ // Main registration function // ============================================================================ export function registerCliCommands( api: OpenClawPluginApi, backend: Backend, provider: Mem0Provider, cfg: Mem0Config, effectiveUserId: (sessionKey?: string) => string, agentUserId: (id: string) => string, buildSearchOptions: ( userIdOverride?: string, limit?: number, runId?: string, sessionKey?: string, ) => SearchOptions, getCurrentSessionId: () => string | undefined, ): void { api.registerCli( ({ program }) => { const mem0 = program .command("mem0") .description("Mem0 memory plugin commands"); // ==================================================================== // login // ==================================================================== mem0 .command("login") .description("Authenticate with Mem0 Platform") .option("--email ", "Login via email verification code") .option("--code ", "Verification code (use with --email)") .option("--api-key ", "Direct API key entry") .action( async (opts: { email?: string; code?: string; apiKey?: string }) => { try { const config = readMem0Config(); const baseUrl = getBaseUrl(config); // -- API key flow ------------------------------------------------ if (opts.apiKey) { if (opts.email) { console.error("Cannot use both --api-key and --email."); return; } setPlatformAuth(config, opts.apiKey, baseUrl); writeMem0Config(config); // Validate with ping try { const resp = await fetch(`${baseUrl}/v1/ping/`, { headers: { Authorization: `Token ${opts.apiKey}` }, }); if (resp.ok) { console.log( "API key saved and validated. Connected to Mem0 Platform.", ); } else { console.warn( `API key saved but validation returned HTTP ${resp.status}. ` + "Check that the key is correct.", ); } } catch (err) { console.warn( `API key saved but could not reach ${baseUrl}: ${String(err)}. ` + "Check your network connection.", ); } return; } // -- Email + code (verify) flow ---------------------------------- if (opts.email && opts.code) { const email = opts.email.trim().toLowerCase(); const url = baseUrl.replace(/\/+$/, ""); let resp: Response; try { resp = await fetch(`${url}/api/v1/auth/email_code/verify/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, code: opts.code.trim() }), }); } catch (err) { console.error(`Could not reach ${url}: ${String(err)}`); return; } if (resp.status === 429) { console.error( "Too many attempts. Try again in a few minutes.", ); return; } if (!resp.ok) { let detail: string; try { const body = (await resp.json()) as Record; detail = String(body.error ?? resp.statusText); } catch { detail = resp.statusText; } console.error(`Verification failed: ${detail}`); return; } const body = (await resp.json()) as Record; const apiKey = body.api_key as string | undefined; if (!apiKey) { console.error( "Auth succeeded but no API key was returned. Contact support.", ); return; } setPlatformAuth(config, apiKey, baseUrl); writeMem0Config(config); console.log( "Authenticated! Configuration saved to ~/.mem0/config.json", ); return; } // -- Email only (send code) flow --------------------------------- if (opts.email) { const email = opts.email.trim().toLowerCase(); const url = baseUrl.replace(/\/+$/, ""); let resp: Response; try { resp = await fetch(`${url}/api/v1/auth/email_code/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); } catch (err) { console.error(`Could not reach ${url}: ${String(err)}`); return; } if (resp.status === 429) { console.error( "Too many attempts. Try again in a few minutes.", ); return; } if (!resp.ok) { let detail: string; try { const body = (await resp.json()) as Record; detail = String(body.error ?? resp.statusText); } catch { detail = resp.statusText; } console.error(`Failed to send code: ${detail}`); return; } console.log( `Verification code sent! Run:\n openclaw mem0 login --email ${email} --code `, ); return; } // -- No flags: interactive login flow ------------------------------ if (!process.stdin.isTTY) { console.log("Usage (non-interactive):"); console.log( " openclaw mem0 login --api-key Save API key directly", ); console.log( " openclaw mem0 login --email Send verification code", ); console.log( " openclaw mem0 login --email --code Verify & authenticate", ); return; } console.log("\n Mem0 Login\n"); console.log(" How would you like to authenticate?"); console.log(" 1. Login with email (recommended)"); console.log(" 2. Enter API key manually\n"); const choice = await prompt(" Choice (1/2): "); if (choice === "1") { const email = (await prompt(" Email: ")).toLowerCase(); if (!email) { console.error("Email is required."); return; } const url = baseUrl.replace(/\/+$/, ""); let sendResp: Response; try { sendResp = await fetch(`${url}/api/v1/auth/email_code/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); } catch (err) { console.error(`Could not reach ${url}: ${String(err)}`); return; } if (sendResp.status === 429) { console.error("Too many attempts. Try again later."); return; } if (!sendResp.ok) { let detail: string; try { const b = (await sendResp.json()) as Record< string, unknown >; detail = String(b.error ?? sendResp.statusText); } catch { detail = sendResp.statusText; } console.error(`Failed to send code: ${detail}`); return; } console.log(" Verification code sent! Check your email.\n"); const code = await prompt(" Code: "); if (!code) { console.error("Code is required."); return; } let verifyResp: Response; try { verifyResp = await fetch( `${url}/api/v1/auth/email_code/verify/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, code }), }, ); } catch (err) { console.error(`Could not reach ${url}: ${String(err)}`); return; } if (verifyResp.status === 429) { console.error("Too many attempts. Try again later."); return; } if (!verifyResp.ok) { let detail: string; try { const b = (await verifyResp.json()) as Record< string, unknown >; detail = String(b.error ?? verifyResp.statusText); } catch { detail = verifyResp.statusText; } console.error(`Verification failed: ${detail}`); return; } const verifyBody = (await verifyResp.json()) as Record< string, unknown >; const verifiedKey = verifyBody.api_key as string | undefined; if (!verifiedKey) { console.error( "Auth succeeded but no API key returned. Contact support.", ); return; } setPlatformAuth(config, verifiedKey, baseUrl); writeMem0Config(config); console.log( "\n Authenticated! Configuration saved to ~/.mem0/config.json", ); console.log( " Restart the gateway: openclaw gateway restart\n", ); } else if (choice === "2") { const key = await prompt(" API Key: "); if (!key) { console.error("API key is required."); return; } setPlatformAuth(config, key, baseUrl); writeMem0Config(config); try { const resp = await fetch(`${baseUrl}/v1/ping/`, { headers: { Authorization: `Token ${key}` }, }); if (resp.ok) { console.log( "\n API key saved and validated. Connected to Mem0 Platform.", ); } else { console.warn( `\n API key saved but validation returned HTTP ${resp.status}.`, ); } } catch (err) { console.warn( `\n API key saved but could not reach ${baseUrl}: ${String(err)}`, ); } console.log( " Restart the gateway: openclaw gateway restart\n", ); } else { console.log("Invalid choice. Run `openclaw mem0 login` again."); } } catch (err) { console.error(`Login failed: ${String(err)}`); } }, ); // ==================================================================== // search // ==================================================================== mem0 .command("search") .description("Search memories in Mem0") .argument("", "Search query") .option("--limit ", "Max results", String(cfg.topK)) .option( "--scope ", 'Memory scope: "session", "long-term", or "all"', "all", ) .option( "--agent ", "Search a specific agent's memory namespace", ) .action( async ( query: string, opts: { limit: string; scope: string; agent?: string }, ) => { try { const limit = parseInt(opts.limit, 10); const scope = opts.scope as "session" | "long-term" | "all"; const currentSessionId = getCurrentSessionId(); const uid = opts.agent ? agentUserId(opts.agent) : effectiveUserId(currentSessionId); // CLI search: build options WITHOUT source filter so users can // find ALL their memories, not just plugin-tagged ones. const cliSearchOpts = ( userIdOverride?: string, lim?: number, runId?: string, ): SearchOptions => { const base = buildSearchOptions(userIdOverride, lim, runId); delete (base as any).source; // Use a lower threshold for explicit CLI searches base.threshold = 0.3; return base; }; let allResults: MemoryItem[] = []; if (scope === "session" || scope === "all") { if (currentSessionId) { const sessionResults = await provider.search( query, cliSearchOpts(uid, limit, currentSessionId), ); if (sessionResults?.length) { allResults.push( ...sessionResults.map((r) => ({ ...r, _scope: "session" as const, })), ); } } else if (scope === "session") { console.log( "No active session ID available for session-scoped search.", ); return; } } if (scope === "long-term" || scope === "all") { const longTermResults = await provider.search( query, cliSearchOpts(uid, limit), ); if (longTermResults?.length) { allResults.push( ...longTermResults.map((r) => ({ ...r, _scope: "long-term" as const, })), ); } } // Deduplicate by ID when searching "all" if (scope === "all") { const seen = new Set(); allResults = allResults.filter((r) => { if (seen.has(r.id)) return false; seen.add(r.id); return true; }); } if (!allResults.length) { console.log("No memories found."); return; } const output = allResults.map((r) => ({ id: r.id, memory: r.memory, score: r.score, scope: (r as any)._scope, categories: r.categories, created_at: r.created_at, })); console.log(JSON.stringify(output, null, 2)); } catch (err) { console.error(`Search failed: ${String(err)}`); } }, ); // ==================================================================== // stats // ==================================================================== mem0 .command("stats") .description("Show memory statistics from Mem0") .option("--agent ", "Show stats for a specific agent") .action(async (opts: { agent?: string }) => { try { const uid = opts.agent ? agentUserId(opts.agent) : cfg.userId; const memories = await provider.getAll({ user_id: uid, source: "OPENCLAW", }); console.log(`Mode: ${cfg.mode}`); console.log( `User: ${uid}${opts.agent ? ` (agent: ${opts.agent})` : ""}`, ); console.log( `Total memories: ${Array.isArray(memories) ? memories.length : "unknown"}`, ); console.log(`Graph enabled: ${cfg.enableGraph}`); console.log( `Auto-recall: ${cfg.autoRecall}, Auto-capture: ${cfg.autoCapture}`, ); } catch (err) { console.error(`Stats failed: ${String(err)}`); } }); // ==================================================================== // status // ==================================================================== mem0 .command("status") .description("Check Mem0 API connectivity") .action(async () => { try { const result = await backend.status(); if (result.connected) { console.log("Connected to Mem0"); } else { console.log("Not connected to Mem0"); } if (result.mode) { console.log(`Mode: ${String(result.mode)}`); } if (result.url) { console.log(`URL: ${String(result.url)}`); } if (result.error) { console.log(`Error: ${String(result.error)}`); } } catch (err) { console.error(`Status check failed: ${String(err)}`); } }); // ==================================================================== // dream // ==================================================================== mem0 .command("dream") .description( "Run memory consolidation (review, merge, prune stored memories)", ) .option( "--dry-run", "Show memory inventory without running consolidation", ) .action(async (opts: { dryRun?: boolean }) => { try { const uid = cfg.userId; const memories = await provider.getAll({ user_id: uid, source: "OPENCLAW", }); const count = Array.isArray(memories) ? memories.length : 0; if (count === 0) { console.log("No memories to consolidate."); return; } // Show current state summary on stderr (keeps stdout clean for piping) const catCounts = new Map(); for (const mem of memories) { const cat = (mem.metadata as any)?.category ?? mem.categories?.[0] ?? "uncategorized"; catCounts.set(cat, (catCounts.get(cat) ?? 0) + 1); } process.stderr.write(`\nMemory inventory for "${uid}":\n`); for (const [cat, num] of [...catCounts.entries()].sort( (a, b) => b[1] - a[1], )) { process.stderr.write(` ${cat}: ${num}\n`); } process.stderr.write(` TOTAL: ${count}\n\n`); if (opts.dryRun) { process.stderr.write("Dry run — no changes made.\n"); return; } // Load dream prompt and format it with the full memory inventory const dreamPrompt = loadDreamPrompt(cfg.skills ?? {}); if (!dreamPrompt) { process.stderr.write( "Dream skill file not found at skills/memory-dream/SKILL.md\n", ); return; } // Build the full dream context: protocol + memory dump const memoryDump = (memories as MemoryItem[]) .map((m, i) => { const cat = (m.metadata as any)?.category ?? m.categories?.[0] ?? "uncategorized"; const imp = (m.metadata as any)?.importance ?? "?"; const created = m.created_at ?? "unknown"; return `${i + 1}. [${m.id}] (${cat}, importance: ${imp}, created: ${created}) ${m.memory}`; }) .join("\n"); const fullPrompt = [ "", dreamPrompt, "", "", ``, memoryDump, "", "", "Begin consolidation. Review all memories above and execute merge, delete, and rewrite operations using the available tools.", ].join("\n"); // Only the prompt goes to stdout — safe to pipe directly process.stdout.write(fullPrompt + "\n"); process.stderr.write( `Dream prompt written to stdout (${fullPrompt.length} chars). Pipe with: openclaw mem0 dream | openclaw run --stdin\n`, ); } catch (err) { console.error(`Dream failed: ${String(err)}`); } }); }, { commands: ["mem0"], descriptors: [ { name: "mem0", description: "Mem0 memory plugin commands" }, ], }, ); }