/** * CLI subcommand registration for the OpenClaw Mem0 plugin. * * Registers all `openclaw mem0 ` commands: * * Memory: * - add : Add a memory from text (--user-id, --agent-id) * - search : Search memories (--top-k, --scope, --agent-id, --user-id) * - get : Get a specific memory by ID * - list : List memories with optional filters (--user-id, --agent-id, --top-k) * - update : Update a memory's text * - delete : Delete a memory or all memories (--all, --confirm) * - import : Import memories from a JSON file * * Management: * - init : Authenticate with Mem0 Platform (email or API key) * - status : Check API connectivity and show current config * - config show : Display current plugin configuration * - config get : Get a single config value * - config set : Update a plugin config field * - event list : List recent background events * - event status: Get status of a specific event * - dream : Run memory consolidation * * Naming conventions match the Python CLI (`mem0 init`, `mem0 search`, etc.) */ import { createInterface } from "node:readline"; import { userInfo as osUserInfo } from "node:os"; 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 { readText } from "../fs-safe.ts"; import type { PluginAuthConfig } from "./config-file.ts"; import { readPluginAuth, writePluginAuth, writePluginConfigField, OPENCLAW_CONFIG_FILE, } from "./config-file.ts"; // ============================================================================ // Reusable helpers (DRY) // ============================================================================ /** Prompt user for input on stderr (keeps stdout clean for piping). */ function promptInput(question: string, prefill?: string): Promise { const rl = createInterface({ input: process.stdin, output: process.stderr }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); if (prefill) rl.write(prefill); }); } /** Get system username for userId fallback. */ function getSystemUsername(): string { try { return osUserInfo().username || "default"; } catch { return "default"; } } /** * Resolve userId silently (no interactive prompt). * Matches Python CLI: --user-id flag > existing config > system username > "default" * Uses os.userInfo().username which covers all platforms. */ function resolveUserId(flagValue?: string, existingValue?: string): string { if (flagValue) return flagValue; if (existingValue) return existingValue; return getSystemUsername(); } /** * POST JSON to a Mem0 API endpoint. Returns parsed body on success, null on failure. * Handles rate limiting, network errors, and HTTP errors with consistent messaging. */ async function apiPost( url: string, body: Record, errorPrefix: string, ): Promise | null> { let resp: Response; try { resp = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-Mem0-Source": "OPENCLAW", "X-Mem0-Client-Language": "node", }, body: JSON.stringify(body), }); } catch (err) { console.error(`Could not reach ${url}: ${String(err)}`); return null; } if (resp.status === 429) { console.error("Too many attempts. Try again in a few minutes."); return null; } if (!resp.ok) { let detail: string; try { const data = (await resp.json()) as Record; detail = String(data.error ?? resp.statusText); } catch { detail = resp.statusText; } console.error(`${errorPrefix}: ${detail}`); return null; } try { return (await resp.json()) as Record; } catch { return {}; } } /** Validate an API key by pinging the platform. Returns true if valid. */ async function validateApiKey( baseUrl: string, apiKey: string, ): Promise<{ ok: boolean; status?: number; error?: string; userEmail?: string }> { try { const resp = await fetch(`${baseUrl}/v1/ping/`, { headers: { Authorization: `Token ${apiKey}`, "X-Mem0-Source": "OPENCLAW", "X-Mem0-Client-Language": "node", }, }); if (!resp.ok) return { ok: false, status: resp.status }; try { const data = (await resp.json()) as Record; return { ok: true, userEmail: data.user_email as string | undefined }; } catch { return { ok: true }; } } catch (err) { return { ok: false, error: String(err) }; } } /** Send email verification code. Returns true on success. */ async function sendVerificationCode( baseUrl: string, email: string, ): Promise { const url = baseUrl.replace(/\/+$/, ""); const result = await apiPost( `${url}/api/v1/auth/email_code/`, { email }, "Failed to send code", ); return result !== null; } /** Verify email code and extract API key. Returns apiKey or null. */ async function verifyEmailCode( baseUrl: string, email: string, code: string, ): Promise { const url = baseUrl.replace(/\/+$/, ""); const result = await apiPost( `${url}/api/v1/auth/email_code/verify/`, { email, code: code.trim() }, "Verification failed", ); if (!result) return null; const apiKey = result.api_key as string | undefined; if (!apiKey) { console.error( "Auth succeeded but no API key was returned. Contact support.", ); return null; } return apiKey; } /** * Save login config and print summary. * Matches Python CLI: saves api_key, base_url, user_id only. */ function saveLoginConfig( apiKey: string, userIdFlag?: string, userEmail?: string, ): void { const existingAuth = readPluginAuth(); const userId = resolveUserId(userIdFlag, existingAuth.userId); writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) }); console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`); console.log(` Mode: platform`); console.log(` User ID: ${userId}`); } function saveOssConfig(userIdFlag?: string): void { const existingAuth = readPluginAuth(); const userId = resolveUserId(userIdFlag, existingAuth.userId); writePluginAuth({ userId, mode: "open-source" }); console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`); console.log(` Mode: open-source`); console.log(` User ID: ${userId}`); } // ============================================================================ // 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, captureCliEvent?: (command: string) => void, ): void { api.registerCli( ({ program }) => { const mem0 = program .command("mem0") .description("Mem0 memory plugin commands") .configureHelp({ sortSubcommands: false, subcommandTerm: (cmd) => cmd.name() }); // Telemetry: fire event for each CLI subcommand if (captureCliEvent) { mem0.hook("preAction", (_thisCmd, actionCmd) => { try { const name = actionCmd.name(); const parent = actionCmd.parent?.name(); const full = parent && parent !== "mem0" ? `${parent}.${name}` : name; captureCliEvent(full); } catch { /* silently swallow */ } }); } // ==================================================================== // init (matches: mem0 init) // ==================================================================== mem0 .command("init") .description("Set up Mem0 — authenticate and configure") .option("--email ", "Login via email verification code") .option("--code ", "Verification code (use with --email)") .option("--api-key ", "Direct API key entry") .option("--user-id ", "Set user ID for memory namespace") .action( async (opts: { email?: string; code?: string; apiKey?: string; userId?: string; }) => { try { const baseUrl = "https://api.mem0.ai"; const existingAuth = readPluginAuth(); const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode); // -- API key flow ------------------------------------------------ if (opts.apiKey) { if (opts.email) { console.error("Cannot use both --api-key and --email."); return; } const check = await validateApiKey(baseUrl, opts.apiKey); saveLoginConfig(opts.apiKey, opts.userId, check.userEmail); if (hasExistingConfig) { console.log( " Existing configuration detected — updated API key (other settings preserved).", ); } if (check.ok) { console.log( " API key validated. Connected to Mem0 Platform.", ); } else if (check.status) { console.warn( ` API key saved but validation returned HTTP ${check.status}. Check that the key is correct.`, ); } else { console.warn( ` API key saved but could not reach ${baseUrl}: ${check.error}. Check your network connection.`, ); } console.log( " Restart the gateway: openclaw gateway restart\n", ); return; } // -- Email + code (verify) — non-interactive ---------------------- if (opts.email && opts.code) { const email = opts.email.trim().toLowerCase(); const apiKey = await verifyEmailCode(baseUrl, email, opts.code); if (!apiKey) return; saveLoginConfig(apiKey, opts.userId, email); if (hasExistingConfig) { console.log( " Existing configuration detected — updated API key (other settings preserved).", ); } console.log(" Authenticated!"); console.log( " Restart the gateway: openclaw gateway restart\n", ); return; } // -- Email only (send code) --------------------------------------- if (opts.email) { const email = opts.email.trim().toLowerCase(); const sent = await sendVerificationCode(baseUrl, email); if (sent) { console.log( `Verification code sent! Run:\n openclaw mem0 init --email ${email} --code `, ); } return; } // -- No flags: interactive flow ----------------------------------- if (!process.stdin.isTTY) { console.log("Usage (non-interactive):"); console.log( " openclaw mem0 init --api-key ", ); console.log( " openclaw mem0 init --api-key --user-id ", ); console.log( " openclaw mem0 init --email ", ); console.log( " openclaw mem0 init --email --code ", ); console.log( " openclaw mem0 init --email --code --user-id ", ); return; } // Detect existing config and offer to reuse or reconfigure if (hasExistingConfig) { console.log("\n Existing Mem0 configuration found:\n"); if (existingAuth.apiKey) { const masked = existingAuth.apiKey.length > 8 ? existingAuth.apiKey.slice(0, 4) + "..." + existingAuth.apiKey.slice(-4) : existingAuth.apiKey.slice(0, 2) + "***"; console.log(` API Key: ${masked}`); } if (existingAuth.userId) console.log(` User ID: ${existingAuth.userId}`); if (existingAuth.mode) console.log(` Mode: ${existingAuth.mode}`); console.log(""); // Validate existing key before asking if (existingAuth.apiKey) { const check = await validateApiKey( baseUrl, existingAuth.apiKey, ); if (check.ok) { console.log( " Existing API key is valid and connected.\n", ); } else { console.log( " Existing API key could not be validated (may be expired or revoked).\n", ); } } const reuse = await promptInput( " Keep existing configuration? (y/n): ", ); if ( reuse === "" || reuse.toLowerCase() === "y" || reuse.toLowerCase() === "yes" ) { console.log( "\n Configuration preserved. No changes made.", ); console.log( " To update individual settings: openclaw mem0 config set \n", ); return; } console.log(""); } console.log("\n Mem0 Setup\n"); console.log(" How would you like to set up Mem0?"); console.log(" 1. Login with email (recommended)"); console.log(" 2. Enter API key manually"); console.log(" 3. Open-source mode (self-hosted)\n"); const choice = (await promptInput(" Choice (1/2/3): ")) || "1"; if (choice === "1") { // --- Email interactive flow --- const email = ( await promptInput(" Email: ") ).toLowerCase(); if (!email) { console.error("Email is required."); return; } const sent = await sendVerificationCode(baseUrl, email); if (!sent) return; console.log( " Verification code sent! Check your email.\n", ); const code = await promptInput(" Code: "); if (!code) { console.error("Code is required."); return; } const apiKey = await verifyEmailCode(baseUrl, email, code); if (!apiKey) return; // Prompt for userId if not passed via flag let userIdValue = opts.userId; if (!userIdValue) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( ` User ID: `, defaultUid, ); userIdValue = uidInput || defaultUid; } console.log(""); saveLoginConfig(apiKey, userIdValue, email); console.log(" Authenticated!"); console.log( " Restart the gateway: openclaw gateway restart\n", ); } else if (choice === "2") { // --- API key interactive flow --- const key = await promptInput(" API Key: "); if (!key) { console.error("API key is required."); return; } // Prompt for userId if not passed via flag let userIdValue2 = opts.userId; if (!userIdValue2) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( ` User ID: `, defaultUid, ); userIdValue2 = uidInput || defaultUid; } console.log(""); const check = await validateApiKey(baseUrl, key); saveLoginConfig(key, userIdValue2, check.userEmail); if (check.ok) { console.log( " API key validated. Connected to Mem0 Platform.", ); } else if (check.status) { console.warn( ` API key saved but validation returned HTTP ${check.status}.`, ); } else { console.warn( ` API key saved but could not reach ${baseUrl}: ${check.error}`, ); } console.log( " Restart the gateway: openclaw gateway restart\n", ); } else if (choice === "3") { // --- Open-source interactive flow --- console.log( "\n Open-source mode uses the Mem0 OSS SDK locally.", ); console.log( " By default it requires an OpenAI API key for embeddings and LLM.\n", ); console.log( " You need an OpenAI API key for embeddings and LLM.", ); console.log( " Get one from https://platform.openai.com/api-keys\n", ); const openaiKey = await promptInput( " OpenAI API Key (or press Enter to skip): ", ); if (openaiKey) { writePluginConfigField( ["oss", "embedder"], { provider: "openai", config: { apiKey: openaiKey } }, ); writePluginConfigField( ["oss", "llm"], { provider: "openai", config: { apiKey: openaiKey } }, ); console.log( "\n OpenAI API key saved to config.\n", ); } else { console.log( "\n Skipped. You can add it later via:", ); console.log( " openclaw mem0 config set embedder_key ", ); console.log( " Or set OPENAI_API_KEY in your environment.\n", ); } // Prompt for userId let userIdValue3 = opts.userId; if (!userIdValue3) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( ` User ID: `, defaultUid, ); userIdValue3 = uidInput || defaultUid; } console.log(""); saveOssConfig(userIdValue3); console.log(" Open-source mode configured!"); console.log( " Restart the gateway: openclaw gateway restart\n", ); } else { console.log( "Invalid choice. Run `openclaw mem0 init` again.", ); } } catch (err) { console.error(`Init failed: ${String(err)}`); } }, ); // ==================================================================== // search (matches: mem0 search --top-k --user-id --agent-id) // ==================================================================== mem0 .command("search") .description("Search memories") .argument("", "Search query") .option("--top-k ", "Max results", String(cfg.topK)) .option( "--scope ", 'Memory scope: "session", "long-term", or "all"', "all", ) .option("--agent-id ", "Search agent's memory namespace") .option("--user-id ", "Override user ID") .action( async ( query: string, opts: { topK: string; scope: string; agentId?: string; userId?: string; }, ) => { try { const limit = parseInt(opts.topK, 10); const scope = opts.scope as "session" | "long-term" | "all"; const currentSessionId = getCurrentSessionId(); const uid = opts.userId ? opts.userId : opts.agentId ? agentUserId(opts.agentId) : effectiveUserId(currentSessionId); // CLI search: no source filter so users find ALL memories const cliSearchOpts = ( userIdOverride?: string, lim?: number, runId?: string, ): SearchOptions => { const base = buildSearchOptions(userIdOverride, lim, runId); 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)}`); } }, ); // ==================================================================== // add (matches: mem0 add --user-id --agent-id) // ==================================================================== mem0 .command("add") .description("Add a memory from text") .argument("", "Text to store as a memory") .option("--user-id ", "Override user ID") .option("--agent-id ", "Store in agent's memory namespace") .action( async ( text: string, opts: { userId?: string; agentId?: string }, ) => { try { const uid = opts.userId ? opts.userId : opts.agentId ? agentUserId(opts.agentId) : effectiveUserId(getCurrentSessionId()); const result = await provider.add( [{ role: "user", content: text }], { user_id: uid, source: "OPENCLAW" }, ); const count = result.results?.length ?? 0; if (count > 0) { console.log(`Added ${count} memory(s):`); for (const r of result.results) { console.log(` ${r.id}: ${r.memory} [${r.event}]`); } } else { console.log( "No new memories extracted (text may already be stored or not contain durable facts).", ); } } catch (err) { console.error(`Add failed: ${String(err)}`); } }, ); // ==================================================================== // get (matches: mem0 get ) // ==================================================================== mem0 .command("get") .description("Get a specific memory by ID") .argument("", "Memory ID to retrieve") .action(async (memoryId: string) => { try { const memory = await provider.get(memoryId); console.log( JSON.stringify( { id: memory.id, memory: memory.memory, user_id: memory.user_id, categories: memory.categories, metadata: memory.metadata, created_at: memory.created_at, updated_at: memory.updated_at, }, null, 2, ), ); } catch (err) { console.error(`Get failed: ${String(err)}`); } }); // ==================================================================== // list (matches: mem0 list --user-id --agent-id --top-k) // ==================================================================== mem0 .command("list") .description("List memories with optional filters") .option("--user-id ", "Override user ID") .option("--agent-id ", "List agent's memories") .option("--top-k ", "Max results", "50") .action( async (opts: { userId?: string; agentId?: string; topK: string; }) => { try { const uid = opts.userId ? opts.userId : opts.agentId ? agentUserId(opts.agentId) : cfg.userId; const limit = parseInt(opts.topK, 10); const memories = await provider.getAll({ user_id: uid, page_size: limit, source: "OPENCLAW", }); if (!Array.isArray(memories) || memories.length === 0) { console.log("No memories found."); return; } const output = memories.map((m) => ({ id: m.id, memory: m.memory, categories: m.categories, created_at: m.created_at, updated_at: m.updated_at, })); console.log(JSON.stringify(output, null, 2)); console.log(`\nTotal: ${memories.length} memories`); } catch (err) { console.error(`List failed: ${String(err)}`); } }, ); // ==================================================================== // update (matches: mem0 update ) // ==================================================================== mem0 .command("update") .description("Update a memory's text") .argument("", "Memory ID to update") .argument("", "New text for the memory") .action(async (memoryId: string, text: string) => { try { await provider.update(memoryId, text); console.log(`Memory ${memoryId} updated.`); } catch (err) { console.error(`Update failed: ${String(err)}`); } }); // ==================================================================== // delete (matches: mem0 delete --all --user-id) // ==================================================================== mem0 .command("delete") .description("Delete a memory, or all memories for a user") .argument("[memory_id]", "Memory ID to delete") .option("--all", "Delete all memories for the user") .option("--user-id ", "Override user ID (with --all)") .option("--agent-id ", "Delete from agent's namespace") .option("--confirm", "Skip confirmation for bulk delete") .action( async ( memoryId: string | undefined, opts: { all?: boolean; userId?: string; agentId?: string; confirm?: boolean; }, ) => { try { if (opts.all) { const uid = opts.userId ? opts.userId : opts.agentId ? agentUserId(opts.agentId) : cfg.userId; if (!opts.confirm && process.stdin.isTTY) { const answer = await promptInput( ` Delete ALL memories for user "${uid}"? This cannot be undone. (yes/N): `, ); if (answer.toLowerCase() !== "yes") { console.log("Cancelled."); return; } } else if (!opts.confirm) { console.error( "Bulk delete requires --confirm flag in non-interactive mode.", ); return; } await provider.deleteAll(uid); console.log(`All memories deleted for user "${uid}".`); return; } if (!memoryId) { console.error( "Provide a memory_id or use --all to delete all memories.", ); return; } await provider.delete(memoryId); console.log(`Memory ${memoryId} deleted.`); } catch (err) { console.error(`Delete failed: ${String(err)}`); } }, ); // ==================================================================== // status (matches: mem0 status) // ==================================================================== mem0 .command("status") .description("Check API connectivity and current config") .action(async () => { try { const auth = readPluginAuth(); console.log(`Mode: ${cfg.mode}`); console.log(`User ID: ${cfg.userId}`); console.log(`Config: ${OPENCLAW_CONFIG_FILE}`); console.log(""); const result = await backend.status(); if (result.connected) { console.log("Connected to Mem0"); } else { console.log("Not connected to Mem0"); } 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)}`); } }); // ==================================================================== // config (matches: mem0 config show, mem0 config get, mem0 config set) // ==================================================================== const configCmd = mem0 .command("config") .description("Manage plugin configuration"); // All settable config keys: short alias → camelCase field in openclaw.json // Matches Python CLI key names (snake_case) with dot-notation support. const CONFIG_KEYS: Record = { // Short aliases (matches Python CLI) api_key: "apiKey", email: "userEmail", base_url: "baseUrl", user_id: "userId", auto_recall: "autoRecall", auto_capture: "autoCapture", top_k: "topK", mode: "mode", embedder_provider: "oss.embedder.provider", embedder_model: "oss.embedder.config.model", embedder_key: "oss.embedder.config.apiKey", llm_provider: "oss.llm.provider", llm_model: "oss.llm.config.model", llm_key: "oss.llm.config.apiKey", vector_provider: "oss.vectorStore.provider", vector_host: "oss.vectorStore.config.host", vector_port: "oss.vectorStore.config.port", collection_name: "oss.vectorStore.config.collectionName", vector_db_name: "oss.vectorStore.config.dbname", vector_db_user: "oss.vectorStore.config.user", vector_db_path: "oss.vectorStore.config.dbPath", history_db_path: "oss.historyDbPath", disable_history: "oss.disableHistory", }; // Keys that contain secrets — redact in show/get output const SECRET_KEYS = new Set(["apiKey", "oss.embedder.config.apiKey", "oss.llm.config.apiKey"]); // Boolean config fields — coerce "true"/"1"/"yes" on set const BOOLEAN_KEYS = new Set([ "autoRecall", "autoCapture", "oss.disableHistory", ]); // Integer config fields — coerce to number on set const INTEGER_KEYS = new Set(["topK", "oss.vectorStore.config.port"]); /** Resolve a user-facing key to the internal camelCase field name. */ function resolveConfigKey(key: string): string | null { return CONFIG_KEYS[key] ?? null; } /** Read a config value by internal field name. */ function getConfigValue(field: string): unknown { if (field.startsWith("oss.")) { const parts = field.split("."); let current: unknown = cfg.oss; for (let i = 1; i < parts.length && current != null; i++) { current = (current as Record)[parts[i]]; } return current; } const auth = readPluginAuth(); const values: Record = { apiKey: auth.apiKey ?? cfg.apiKey, baseUrl: auth.baseUrl ?? cfg.baseUrl ?? "https://api.mem0.ai", userId: auth.userId ?? cfg.userId, mode: auth.mode ?? cfg.mode, userEmail: auth.userEmail, autoRecall: cfg.autoRecall, autoCapture: cfg.autoCapture, topK: cfg.topK, }; return values[field]; } /** Redact a secret value for display: first 4 + ... + last 4 */ function redact(value: string): string { if (value.length <= 8) return value.slice(0, 2) + "***"; return value.slice(0, 4) + "..." + value.slice(-4); } /** Format a config value for display (redacts secrets). */ function displayValue(field: string, value: unknown): string { if (value === undefined || value === null || value === "") { return "(not set)"; } if (SECRET_KEYS.has(field) && typeof value === "string") { return redact(value); } return String(value); } configCmd .command("show") .description("Show current configuration") .action(() => { // Display order: general first, then mode-specific const entries: Array<[string, string]> = [ ["mode", "mode"], ["user_id", "userId"], ["auto_recall", "autoRecall"], ["auto_capture", "autoCapture"], ["top_k", "topK"], ]; if (cfg.mode === "platform") { entries.push( ["api_key", "apiKey"], ["email", "userEmail"], ); } else { entries.push( ["embedder_provider", "oss.embedder.provider"], ["embedder_model", "oss.embedder.config.model"], ["embedder_key", "oss.embedder.config.apiKey"], ["llm_provider", "oss.llm.provider"], ["llm_model", "oss.llm.config.model"], ["llm_key", "oss.llm.config.apiKey"], ["vector_provider", "oss.vectorStore.provider"], ["history_db_path", "oss.historyDbPath"], ["disable_history", "oss.disableHistory"], ); } // Calculate column widths const maxKeyLen = Math.max( ...entries.map(([k]) => k.length), 3, ); console.log(""); console.log( ` ${"Key".padEnd(maxKeyLen)} Value`, ); console.log( ` ${"─".repeat(maxKeyLen)} ${"─".repeat(30)}`, ); for (const [displayKey, field] of entries) { const value = getConfigValue(field); const display = displayValue(field, value); console.log( ` ${displayKey.padEnd(maxKeyLen)} ${display}`, ); } console.log(""); console.log(` Config file: ${OPENCLAW_CONFIG_FILE}`); console.log(""); console.log(" To change a setting:"); console.log(" openclaw mem0 config set "); console.log(""); console.log(" Examples:"); if (cfg.mode === "platform") { console.log(" openclaw mem0 config set mode open-source"); console.log(" openclaw mem0 config set auto_recall false"); } else { console.log(" openclaw mem0 config set vector_provider qdrant"); console.log(" openclaw mem0 config set llm_model gpt-4o"); console.log(" openclaw mem0 config set embedder_provider openai"); } console.log(""); }); configCmd .command("get") .description("Get a config value") .argument("", "Config key (e.g. user_id, api_key, llm_model)") .action((key: string) => { const field = resolveConfigKey(key); if (!field) { console.error( `Unknown config key: ${key}`, ); return; } const value = getConfigValue(field); console.log(displayValue(field, value)); }); configCmd .command("set") .description("Set a config value") .argument("", "Config key (e.g. user_id, api_key, llm_model)") .argument("", "New value") .action((key: string, rawValue: string) => { const field = resolveConfigKey(key); if (!field) { console.error( `Unknown config key: ${key}`, ); return; } // Type coercion (matches Python CLI behavior) let value: unknown = rawValue; if (BOOLEAN_KEYS.has(field)) { value = rawValue.toLowerCase() === "true" || rawValue === "1" || rawValue.toLowerCase() === "yes"; } else if (INTEGER_KEYS.has(field)) { const parsed = parseInt(rawValue, 10); if (isNaN(parsed)) { console.error(`Invalid integer value: ${rawValue}`); return; } value = parsed; } // Nested OSS fields use dot-path writer; flat fields use auth writer if (field.startsWith("oss.")) { writePluginConfigField(field.split("."), value); } else { writePluginAuth({ [field]: value } as PluginAuthConfig); } console.log( `${key} = ${displayValue(field, value)}`, ); }); // ==================================================================== // import (matches: mem0 import ) // ==================================================================== mem0 .command("import") .description("Import memories from a JSON file") .argument("", "Path to JSON file containing memories") .option("--user-id ", "Override user ID for all imported memories") .option("--agent-id ", "Override agent ID for all imported memories") .action( async ( file: string, opts: { userId?: string; agentId?: string }, ) => { try { let data: unknown; try { data = JSON.parse(readText(file)); } catch (err) { console.error(`Failed to read file: ${String(err)}`); return; } const items = Array.isArray(data) ? data : [data]; let added = 0; let failed = 0; for (const item of items) { const content = item?.memory ?? item?.text ?? item?.content ?? ""; if (!content) { failed++; continue; } try { await backend.add(content, undefined, { userId: opts.userId ?? item?.user_id ?? cfg.userId, agentId: opts.agentId ?? item?.agent_id, metadata: item?.metadata, }); added++; } catch { failed++; } } console.log(`Imported ${added} memories.`); if (failed) { console.error(`${failed} memories failed to import.`); } } catch (err) { console.error(`Import failed: ${String(err)}`); } }, ); // ==================================================================== // event (matches: mem0 event list, mem0 event status ) // ==================================================================== const eventCmd = mem0 .command("event") .description("Manage background processing events"); eventCmd .command("list") .description("List recent background events") .action(async () => { try { if (!backend || cfg.mode === "open-source") { console.log("Event tracking is only available in platform mode."); return; } const results = await backend.listEvents(); if (!results.length) { console.log("No events found."); return; } // Table header const header = [ "Event ID".padEnd(36), "Type".padEnd(14), "Status".padEnd(12), "Latency".padStart(10), "Created".padEnd(20), ].join(" "); console.log(header); console.log("-".repeat(header.length)); for (const ev of results) { const evId = String(ev.id ?? ""); const evType = String(ev.event_type ?? "—").padEnd(14); const status = String(ev.status ?? "—").padEnd(12); const latency = typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—"; const created = String(ev.created_at ?? "—") .slice(0, 19) .replace("T", " "); console.log( `${evId.padEnd(36)} ${evType} ${status} ${latency.padStart(10)} ${created}`, ); } console.log(`\n${results.length} event${results.length !== 1 ? "s" : ""}`); } catch (err) { console.error(`Failed to list events: ${String(err)}`); } }); eventCmd .command("status") .description("Get status of a specific background event") .argument("", "Event ID to check") .action(async (eventId: string) => { try { if (!backend || cfg.mode === "open-source") { console.log("Event tracking is only available in platform mode."); return; } const ev = await backend.getEvent(eventId); const status = String(ev.status ?? "—"); const evType = String(ev.event_type ?? "—"); const latency = typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—"; const created = String(ev.created_at ?? "—") .slice(0, 19) .replace("T", " "); const updated = String(ev.updated_at ?? "—") .slice(0, 19) .replace("T", " "); console.log(`Event ID: ${eventId}`); console.log(`Type: ${evType}`); console.log(`Status: ${status}`); console.log(`Latency: ${latency}`); console.log(`Created: ${created}`); console.log(`Updated: ${updated}`); const results = ev.results as Record[] | undefined; if (results && Array.isArray(results) && results.length) { console.log(`\nResults (${results.length}):`); for (const r of results) { const memId = String(r.id ?? "").slice(0, 8); const data = r.data as Record | undefined; const memory = data?.memory ?? ""; const evName = String(r.event ?? ""); const user = String(r.user_id ?? ""); let detail = `${evName} ${memory}`; if (user) detail += ` (user_id=${user})`; console.log(` · ${detail} (${memId})`); } } } catch (err) { console.error(`Failed to get event: ${String(err)}`); } }); // ==================================================================== // help (matches: mem0 help, mem0 help --json) // ==================================================================== mem0 .command("help") .description("Show help. Use --json for machine-readable output (for LLM agents)") .option("--json", "Output as JSON for agent/programmatic use") .action((opts: { json?: boolean }) => { const commands = { memory: { search: "Query your memory store — semantic, keyword, or hybrid retrieval", add: "Add a memory from text, messages, or stdin", get: "Get a specific memory by ID", list: "List memories with optional filters", update: "Update a memory's text or metadata", delete: "Delete a memory, all memories, or an entity", import: "Import memories from a JSON file", }, management: { init: "Interactive setup wizard for mem0 CLI", status: "Check connectivity and authentication", config: "Manage mem0 configuration (show, get, set)", event: "Manage background processing events (list, status)", dream: "Run memory consolidation (review, merge, prune)", help: "Show help. Use --json for machine-readable output (for LLM agents)", }, }; if (opts.json) { console.log(JSON.stringify({ commands }, null, 2)); return; } console.log(""); console.log(" openclaw mem0 "); console.log(""); console.log(" Memory:"); for (const [cmd, desc] of Object.entries(commands.memory)) { console.log(` ${cmd.padEnd(12)} ${desc}`); } console.log(""); console.log(" Management:"); for (const [cmd, desc] of Object.entries(commands.management)) { console.log(` ${cmd.padEnd(12)} ${desc}`); } console.log(""); }); // ==================================================================== // 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; } 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; } const dreamPrompt = loadDreamPrompt(cfg.skills ?? {}); if (!dreamPrompt) { process.stderr.write( "Dream skill file not found at skills/memory-dream/SKILL.md\n", ); return; } 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"); process.stdout.write(fullPrompt + "\n"); process.stderr.write( `Dream prompt written to stdout (${fullPrompt.length} chars). Paste it into an OpenClaw session to run consolidation.\n`, ); } catch (err) { console.error(`Dream failed: ${String(err)}`); } }); }, { descriptors: [ { name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true }, ], }, ); }