From f5dc825d47b0920915ac19bd9f8a515f0c41a08b Mon Sep 17 00:00:00 2001
From: Kartik All
+
+| Flag | Description |
+| ---- | ----------- |
+| `--oss-llm --oss-* flags
", "Verification code (use with --email)")
.option("--api-key ", "Direct API key entry")
.option("--user-id ", "Set user ID for memory namespace")
+ .option("--mode ", "platform or open-source (skips menu)")
+ .option("--oss-llm ", "LLM: openai, ollama, anthropic")
+ .option("--oss-llm-key ", "API key for LLM provider")
+ .option("--oss-llm-model ", "Override default LLM model")
+ .option("--oss-llm-url ", "Base URL (ollama only)")
+ .option("--oss-embedder ", "Embedder: openai, ollama, huggingface")
+ .option("--oss-embedder-key ", "API key for embedder")
+ .option("--oss-embedder-model ", "Override default embedder model")
+ .option("--oss-embedder-url ", "Base URL (ollama only)")
+ .option("--oss-vector ", "Vector store: qdrant, pgvector")
+ .option("--oss-vector-url ", "Qdrant server URL (default: http://localhost:6333)")
+ .option("--oss-vector-host ", "PGVector host")
+ .option("--oss-vector-port ", "PGVector port")
+ .option("--oss-vector-user ", "PGVector user")
+ .option("--oss-vector-password ", "PGVector password")
+ .option("--oss-vector-dbname ", "PGVector database name")
+ .option("--oss-vector-dims ", "Override embedding dimensions")
+ .option("--json", "Machine-readable JSON output")
.action(
async (opts: {
email?: string;
code?: string;
apiKey?: string;
userId?: string;
+ mode?: string;
+ ossLlm?: string;
+ ossLlmKey?: string;
+ ossLlmModel?: string;
+ ossLlmUrl?: string;
+ ossEmbedder?: string;
+ ossEmbedderKey?: string;
+ ossEmbedderModel?: string;
+ ossEmbedderUrl?: string;
+ ossVector?: string;
+ ossVectorUrl?: string;
+ ossVectorHost?: string;
+ ossVectorPort?: string;
+ ossVectorUser?: string;
+ ossVectorPassword?: string;
+ ossVectorDbname?: string;
+ ossVectorDims?: string;
+ json?: boolean;
}) => {
try {
const baseUrl = "https://api.mem0.ai";
const existingAuth = readPluginAuth();
const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode);
+ // -- Non-interactive OSS via --mode open-source + flags --
+ if (opts.mode === "open-source") {
+ const validation = validateOssFlags(opts);
+ if (validation.error) {
+ if (jsonErr(opts, validation.error)) return;
+ console.error(validation.error);
+ return;
+ }
+
+ const llmId = opts.ossLlm || "openai";
+ const embId = opts.ossEmbedder || "openai";
+ const vecId = opts.ossVector || "qdrant";
+
+ const embKey = opts.ossEmbedderKey || (embId === llmId ? opts.ossLlmKey : undefined);
+ const embUrl = opts.ossEmbedderUrl || (embId === llmId ? opts.ossLlmUrl : undefined);
+
+ const llmCfg = buildOssLlmConfig(llmId, {
+ apiKey: opts.ossLlmKey, model: opts.ossLlmModel, url: opts.ossLlmUrl,
+ });
+ const embCfg = buildOssEmbedderConfig(embId, {
+ apiKey: embKey, model: opts.ossEmbedderModel, url: embUrl,
+ });
+ const dims = opts.ossVectorDims ? parseInt(opts.ossVectorDims, 10) : embCfg.dims;
+ const vecCfg = buildOssVectorConfig(vecId, {
+ url: opts.ossVectorUrl, host: opts.ossVectorHost,
+ port: opts.ossVectorPort, user: opts.ossVectorUser,
+ password: opts.ossVectorPassword, dbname: opts.ossVectorDbname,
+ dims,
+ });
+
+ // Connectivity checks — Ollama, Qdrant, PGVector
+ const ollamaUrls = new Set();
+ if (llmId === "ollama") ollamaUrls.add((llmCfg.config.url as string) || "http://localhost:11434");
+ if (embId === "ollama") ollamaUrls.add((embCfg.config.url as string) || "http://localhost:11434");
+ for (const oUrl of ollamaUrls) {
+ const check = await checkOllamaConnectivity(oUrl);
+ if (!check.ok) {
+ const msg = `Ollama not reachable at ${oUrl}. Install: https://ollama.com/download`;
+ if (jsonErr(opts, msg)) return;
+ console.error(`\n ${msg}\n`);
+ return;
+ }
+ }
+
+ if (vecId === "qdrant") {
+ const qdrantUrl = (vecCfg.config.url as string) || "http://localhost:6333";
+ const check = await checkQdrantConnectivity(qdrantUrl);
+ if (!check.ok) {
+ const msg = `Qdrant not reachable at ${qdrantUrl}. Start with: docker run -d -p 6333:6333 qdrant/qdrant`;
+ if (jsonErr(opts, msg)) return;
+ console.error(`\n ${msg}\n`);
+ return;
+ }
+ } else if (vecId === "pgvector") {
+ const pgHost = (vecCfg.config.host as string) || "localhost";
+ const pgPort = (vecCfg.config.port as number) || 5432;
+ const check = await checkPgConnectivity(pgHost, pgPort);
+ if (!check.ok) {
+ const msg = `PostgreSQL not reachable at ${pgHost}:${pgPort}. Start with: docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg17`;
+ if (jsonErr(opts, msg)) return;
+ console.error(`\n ${msg}\n`);
+ return;
+ }
+ }
+
+ writePluginConfigField(["oss", "llm"], llmCfg);
+ writePluginConfigField(["oss", "embedder"], { provider: embCfg.provider, config: embCfg.config });
+ writePluginConfigField(["oss", "vectorStore"], vecCfg);
+ saveOssConfig(opts.userId, !!opts.json);
+
+ const vecDisplay = vecId === "qdrant" ? vecCfg.config.url : vecCfg.config.host;
+ const summary = {
+ ok: true as const,
+ mode: "open-source",
+ config: {
+ llm: { provider: llmCfg.provider, model: llmCfg.config.model },
+ embedder: { provider: embCfg.provider, model: embCfg.config.model },
+ vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }) },
+ },
+ userId: resolveUserId(opts.userId, existingAuth.userId),
+ message: "Open-source mode configured. Restart the gateway: openclaw gateway restart",
+ };
+ if (jsonOut(opts, summary)) return;
+
+ console.log("\n Open-source mode configured!\n");
+ console.log(` LLM: ${llmCfg.provider} (${llmCfg.config.model})`);
+ console.log(` Embedder: ${embCfg.provider} (${embCfg.config.model})`);
+ console.log(` Vector: ${vecCfg.provider} (${vecDisplay})`);
+ console.log(` User ID: ${resolveUserId(opts.userId, existingAuth.userId)}`);
+ console.log("\n Restart the gateway: openclaw gateway restart\n");
+ return;
+ }
+
+ // -- Non-interactive --mode platform routing -----------------
+ if (opts.mode === "platform") {
+ if (!opts.apiKey && !opts.email) {
+ const msg = "--api-key or --email required for platform mode";
+ if (jsonErr(opts, msg)) return;
+ console.error(msg);
+ return;
+ }
+ // fall through to existing --api-key / --email handlers below
+ }
+
// -- API key flow ------------------------------------------------
if (opts.apiKey) {
if (opts.email) {
- console.error("Cannot use both --api-key and --email.");
+ const msg = "Cannot use both --api-key and --email.";
+ if (jsonErr(opts, msg)) return;
+ console.error(msg);
return;
}
const check = await validateApiKey(baseUrl, opts.apiKey);
- saveLoginConfig(opts.apiKey, opts.userId, check.userEmail);
+ saveLoginConfig(opts.apiKey, opts.userId, check.userEmail, !!opts.json);
+
+ let message: string;
+ if (check.ok) {
+ message = "API key validated. Connected to Mem0 Platform.";
+ } else if (check.status) {
+ message = `API key saved but validation returned HTTP ${check.status}. Check that the key is correct.`;
+ } else {
+ message = `API key saved but could not reach ${baseUrl}: ${check.error}. Check your network connection.`;
+ }
+
+ const summary = {
+ ok: check.ok,
+ mode: "platform" as const,
+ userId: resolveUserId(opts.userId, existingAuth.userId),
+ validated: check.ok,
+ ...(check.status && !check.ok && { httpStatus: check.status }),
+ message,
+ };
+ if (jsonOut(opts, summary)) return;
+
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.`,
- );
+ console.log(` ${message}`);
} else {
- console.warn(
- ` API key saved but could not reach ${baseUrl}: ${check.error}. Check your network connection.`,
- );
+ console.warn(` ${message}`);
}
console.log(
" Restart the gateway: openclaw gateway restart\n",
@@ -323,9 +629,21 @@ export function registerCliCommands(
if (opts.email && opts.code) {
const email = opts.email.trim().toLowerCase();
const apiKey = await verifyEmailCode(baseUrl, email, opts.code);
- if (!apiKey) return;
+ if (!apiKey) {
+ if (jsonErr(opts, "Email verification failed — no API key returned.")) return;
+ return;
+ }
+
+ saveLoginConfig(apiKey, opts.userId, email, !!opts.json);
+ const summary = {
+ ok: true as const,
+ mode: "platform" as const,
+ userId: resolveUserId(opts.userId, existingAuth.userId),
+ email,
+ message: "Authenticated. Restart the gateway: openclaw gateway restart",
+ };
+ if (jsonOut(opts, summary)) return;
- saveLoginConfig(apiKey, opts.userId, email);
if (hasExistingConfig) {
console.log(
" Existing configuration detected — updated API key (other settings preserved).",
@@ -343,9 +661,13 @@ export function registerCliCommands(
const email = opts.email.trim().toLowerCase();
const sent = await sendVerificationCode(baseUrl, email);
if (sent) {
+ const nextCmd = `openclaw mem0 init --email ${email} --code `;
+ if (jsonOut(opts, { ok: true, email, codeSent: true, nextCommand: nextCmd })) return;
console.log(
- `Verification code sent! Run:\n openclaw mem0 init --email ${email} --code `,
+ `Verification code sent! Run:\n ${nextCmd}`,
);
+ } else {
+ if (jsonErr(opts, `Failed to send verification code to ${email}.`)) return;
}
return;
}
@@ -353,21 +675,15 @@ export function registerCliCommands(
// -- 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 ",
- );
+ console.log(" Platform:");
+ 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(" Open Source:");
+ console.log(" openclaw mem0 init --mode open-source --oss-llm ollama --oss-embedder ollama --oss-vector qdrant");
+ console.log(" openclaw mem0 init --mode open-source --oss-llm openai --oss-llm-key ");
+ console.log(" Add --json for machine-readable output.");
return;
}
@@ -423,14 +739,19 @@ export function registerCliCommands(
}
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");
+ console.log(" How would you like to use Mem0?");
+ console.log(" 1. Platform (recommended) — hosted memory, managed by Mem0");
+ console.log(" 2. Open Source — self-hosted, choose your own providers\n");
- const choice = (await promptInput(" Choice (1/2/3): ")) || "1";
+ const modeChoice = (await promptInput(" Choice (1/2): ")) || "1";
- if (choice === "1") {
+ if (modeChoice === "1") {
+ console.log("\n How would you like to authenticate?");
+ console.log(" 1. Login with email (recommended)");
+ console.log(" 2. Enter API key manually\n");
+ const authChoice = (await promptInput(" Choice (1/2): ")) || "1";
+
+ if (authChoice === "1") {
// --- Email interactive flow ---
const email = (
await promptInput(" Email: ")
@@ -471,7 +792,7 @@ export function registerCliCommands(
console.log(
" Restart the gateway: openclaw gateway restart\n",
);
- } else if (choice === "2") {
+ } else if (authChoice === "2") {
// --- API key interactive flow ---
const key = await promptInput(" API Key: ");
if (!key) {
@@ -509,68 +830,13 @@ export function registerCliCommands(
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",
- );
+ console.log("Invalid choice. Run `openclaw mem0 init` again.");
}
-
- // 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 if (modeChoice === "2") {
+ await runOssWizardInteractive(opts, existingAuth);
} else {
- console.log(
- "Invalid choice. Run `openclaw mem0 init` again.",
- );
+ console.log("Invalid choice. Run `openclaw mem0 init` again.");
}
} catch (err) {
console.error(`Init failed: ${String(err)}`);
@@ -594,6 +860,7 @@ export function registerCliCommands(
)
.option("--agent-id ", "Search agent's memory namespace")
.option("--user-id ", "Override user ID")
+ .option("--json", "Output as JSON")
.action(
async (
query: string,
@@ -602,6 +869,7 @@ export function registerCliCommands(
scope: string;
agentId?: string;
userId?: string;
+ json?: boolean;
},
) => {
try {
@@ -675,6 +943,7 @@ export function registerCliCommands(
}
if (!allResults.length) {
+ if (jsonOut(opts, { ok: true, results: [] })) return;
console.log("No memories found.");
return;
}
@@ -687,8 +956,10 @@ export function registerCliCommands(
categories: r.categories,
created_at: r.created_at,
}));
+ if (jsonOut(opts, { ok: true, results: output })) return;
console.log(JSON.stringify(output, null, 2));
} catch (err) {
+ if (jsonErr(opts, `Search failed: ${String(err)}`)) return;
console.error(`Search failed: ${String(err)}`);
}
},
@@ -704,10 +975,11 @@ export function registerCliCommands(
.argument("", "Text to store as a memory")
.option("--user-id ", "Override user ID")
.option("--agent-id ", "Store in agent's memory namespace")
+ .option("--json", "Output as JSON")
.action(
async (
text: string,
- opts: { userId?: string; agentId?: string },
+ opts: { userId?: string; agentId?: string; json?: boolean },
) => {
try {
const uid = opts.userId
@@ -720,6 +992,7 @@ export function registerCliCommands(
{ user_id: uid, source: "OPENCLAW" },
);
const count = result.results?.length ?? 0;
+ if (jsonOut(opts, { ok: true, memories: (result.results || []).map((r: any) => ({ id: r.id, memory: r.memory, event: r.event })) })) return;
if (count > 0) {
console.log(`Added ${count} memory(s):`);
for (const r of result.results) {
@@ -731,6 +1004,7 @@ export function registerCliCommands(
);
}
} catch (err) {
+ if (jsonErr(opts, `Add failed: ${String(err)}`)) return;
console.error(`Add failed: ${String(err)}`);
}
},
@@ -744,9 +1018,11 @@ export function registerCliCommands(
.command("get")
.description("Get a specific memory by ID")
.argument("", "Memory ID to retrieve")
- .action(async (memoryId: string) => {
+ .option("--json", "Output as JSON")
+ .action(async (memoryId: string, opts: { json?: boolean } = {}) => {
try {
const memory = await provider.get(memoryId);
+ if (jsonOut(opts, { ok: true, memory: { 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 } })) return;
console.log(
JSON.stringify(
{
@@ -763,6 +1039,7 @@ export function registerCliCommands(
),
);
} catch (err) {
+ if (jsonErr(opts, `Get failed: ${String(err)}`)) return;
console.error(`Get failed: ${String(err)}`);
}
});
@@ -777,11 +1054,13 @@ export function registerCliCommands(
.option("--user-id ", "Override user ID")
.option("--agent-id ", "List agent's memories")
.option("--top-k ", "Max results", "50")
+ .option("--json", "Output as JSON")
.action(
async (opts: {
userId?: string;
agentId?: string;
topK: string;
+ json?: boolean;
}) => {
try {
const uid = opts.userId
@@ -797,6 +1076,7 @@ export function registerCliCommands(
});
if (!Array.isArray(memories) || memories.length === 0) {
+ if (jsonOut(opts, { ok: true, memories: [], count: 0 })) return;
console.log("No memories found.");
return;
}
@@ -808,9 +1088,11 @@ export function registerCliCommands(
created_at: m.created_at,
updated_at: m.updated_at,
}));
+ if (jsonOut(opts, { ok: true, memories: output, count: memories.length })) return;
console.log(JSON.stringify(output, null, 2));
console.log(`\nTotal: ${memories.length} memories`);
} catch (err) {
+ if (jsonErr(opts, `List failed: ${String(err)}`)) return;
console.error(`List failed: ${String(err)}`);
}
},
@@ -825,11 +1107,14 @@ export function registerCliCommands(
.description("Update a memory's text")
.argument("", "Memory ID to update")
.argument("", "New text for the memory")
- .action(async (memoryId: string, text: string) => {
+ .option("--json", "Output as JSON")
+ .action(async (memoryId: string, text: string, opts: { json?: boolean } = {}) => {
try {
await provider.update(memoryId, text);
+ if (jsonOut(opts, { ok: true, memory: { id: memoryId, memory: text } })) return;
console.log(`Memory ${memoryId} updated.`);
} catch (err) {
+ if (jsonErr(opts, `Update failed: ${String(err)}`)) return;
console.error(`Update failed: ${String(err)}`);
}
});
@@ -846,6 +1131,7 @@ export function registerCliCommands(
.option("--user-id ", "Override user ID (with --all)")
.option("--agent-id ", "Delete from agent's namespace")
.option("--confirm", "Skip confirmation for bulk delete")
+ .option("--json", "Output as JSON")
.action(
async (
memoryId: string | undefined,
@@ -854,6 +1140,7 @@ export function registerCliCommands(
userId?: string;
agentId?: string;
confirm?: boolean;
+ json?: boolean;
},
) => {
try {
@@ -880,11 +1167,13 @@ export function registerCliCommands(
}
await provider.deleteAll(uid);
+ if (jsonOut(opts, { ok: true, deleted: true, id: "all", userId: uid })) return;
console.log(`All memories deleted for user "${uid}".`);
return;
}
if (!memoryId) {
+ if (jsonErr(opts, "Provide a memory_id or use --all to delete all memories.")) return;
console.error(
"Provide a memory_id or use --all to delete all memories.",
);
@@ -892,8 +1181,10 @@ export function registerCliCommands(
}
await provider.delete(memoryId);
+ if (jsonOut(opts, { ok: true, deleted: true, id: memoryId })) return;
console.log(`Memory ${memoryId} deleted.`);
} catch (err) {
+ if (jsonErr(opts, `Delete failed: ${String(err)}`)) return;
console.error(`Delete failed: ${String(err)}`);
}
},
@@ -906,15 +1197,24 @@ export function registerCliCommands(
mem0
.command("status")
.description("Check API connectivity and current config")
- .action(async () => {
+ .option("--json", "Output as JSON")
+ .action(async (opts: { json?: boolean } = {}) => {
try {
const auth = readPluginAuth();
+ const result = await backend.status();
+ if (jsonOut(opts, {
+ ok: true,
+ mode: cfg.mode,
+ connected: result.connected,
+ userId: cfg.userId,
+ ...(result.url && { url: result.url }),
+ ...(result.error && { error: String(result.error) }),
+ })) return;
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 {
@@ -927,6 +1227,7 @@ export function registerCliCommands(
console.log(`Error: ${String(result.error)}`);
}
} catch (err) {
+ if (jsonErr(opts, `Status check failed: ${String(err)}`)) return;
console.error(`Status check failed: ${String(err)}`);
}
});
@@ -1011,19 +1312,14 @@ export function registerCliCommands(
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);
+ const redacted = redactSecrets({ v: value }, new Set(["v"]));
+ return redacted.v as string;
}
return String(value);
}
@@ -1031,7 +1327,41 @@ export function registerCliCommands(
configCmd
.command("show")
.description("Show current configuration")
- .action(() => {
+ .option("--json", "Output as JSON")
+ .action((opts: { json?: boolean } = {}) => {
+ if (opts.json) {
+ const showEntries: Array<[string, string]> = [
+ ["mode", "mode"],
+ ["user_id", "userId"],
+ ["auto_recall", "autoRecall"],
+ ["auto_capture", "autoCapture"],
+ ["top_k", "topK"],
+ ];
+ if (cfg.mode === "platform") {
+ showEntries.push(["api_key", "apiKey"], ["email", "userEmail"]);
+ } else {
+ showEntries.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"],
+ );
+ }
+ const configObj: Record = {};
+ for (const [displayKey, field] of showEntries) {
+ const val = getConfigValue(field);
+ configObj[displayKey] = SECRET_KEYS.has(field) && typeof val === "string"
+ ? (redactSecrets({ v: val }, new Set(["v"])).v)
+ : val;
+ }
+ jsonOut(opts, { ok: true, config: configObj });
+ return;
+ }
// Display order: general first, then mode-specific
const entries: Array<[string, string]> = [
["mode", "mode"],
@@ -1102,15 +1432,18 @@ export function registerCliCommands(
.command("get")
.description("Get a config value")
.argument("", "Config key (e.g. user_id, api_key, llm_model)")
- .action((key: string) => {
+ .option("--json", "Output as JSON")
+ .action((key: string, opts: { json?: boolean } = {}) => {
const field = resolveConfigKey(key);
if (!field) {
+ if (jsonErr(opts, `Unknown config key: ${key}`)) return;
console.error(
`Unknown config key: ${key}`,
);
return;
}
const value = getConfigValue(field);
+ if (jsonOut(opts, { ok: true, key, value })) return;
console.log(displayValue(field, value));
});
@@ -1119,9 +1452,11 @@ export function registerCliCommands(
.description("Set a config value")
.argument("", "Config key (e.g. user_id, api_key, llm_model)")
.argument("", "New value")
- .action((key: string, rawValue: string) => {
+ .option("--json", "Output as JSON")
+ .action((key: string, rawValue: string, opts: { json?: boolean } = {}) => {
const field = resolveConfigKey(key);
if (!field) {
+ if (jsonErr(opts, `Unknown config key: ${key}`)) return;
console.error(
`Unknown config key: ${key}`,
);
@@ -1138,6 +1473,7 @@ export function registerCliCommands(
} else if (INTEGER_KEYS.has(field)) {
const parsed = parseInt(rawValue, 10);
if (isNaN(parsed)) {
+ if (jsonErr(opts, `Invalid integer value: ${rawValue}`)) return;
console.error(`Invalid integer value: ${rawValue}`);
return;
}
@@ -1150,6 +1486,7 @@ export function registerCliCommands(
} else {
writePluginAuth({ [field]: value } as PluginAuthConfig);
}
+ if (jsonOut(opts, { ok: true, key, value })) return;
console.log(
`${key} = ${displayValue(field, value)}`,
);
@@ -1165,10 +1502,11 @@ export function registerCliCommands(
.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")
+ .option("--json", "Output as JSON")
.action(
async (
file: string,
- opts: { userId?: string; agentId?: string },
+ opts: { userId?: string; agentId?: string; json?: boolean },
) => {
try {
let data: unknown;
@@ -1202,11 +1540,13 @@ export function registerCliCommands(
}
}
+ if (jsonOut(opts, { ok: true, imported: added, failed, total: items.length })) return;
console.log(`Imported ${added} memories.`);
if (failed) {
console.error(`${failed} memories failed to import.`);
}
} catch (err) {
+ if (jsonErr(opts, `Import failed: ${String(err)}`)) return;
console.error(`Import failed: ${String(err)}`);
}
},
@@ -1223,7 +1563,8 @@ export function registerCliCommands(
eventCmd
.command("list")
.description("List recent background events")
- .action(async () => {
+ .option("--json", "Output as JSON")
+ .action(async (opts: { json?: boolean } = {}) => {
try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
@@ -1234,6 +1575,7 @@ export function registerCliCommands(
console.log("No events found.");
return;
}
+ if (jsonOut(opts, { ok: true, events: results })) return;
// Table header
const header = [
@@ -1263,6 +1605,7 @@ export function registerCliCommands(
}
console.log(`\n${results.length} event${results.length !== 1 ? "s" : ""}`);
} catch (err) {
+ if (jsonErr(opts, `Failed to list events: ${String(err)}`)) return;
console.error(`Failed to list events: ${String(err)}`);
}
});
@@ -1271,13 +1614,15 @@ export function registerCliCommands(
.command("status")
.description("Get status of a specific background event")
.argument("", "Event ID to check")
- .action(async (eventId: string) => {
+ .option("--json", "Output as JSON")
+ .action(async (eventId: string, opts: { json?: boolean } = {}) => {
try {
if (!backend || cfg.mode === "open-source") {
console.log("Event tracking is only available in platform mode.");
return;
}
const ev = await backend.getEvent(eventId);
+ if (jsonOut(opts, { ok: true, event: ev })) return;
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
@@ -1313,6 +1658,7 @@ export function registerCliCommands(
}
}
} catch (err) {
+ if (jsonErr(opts, `Failed to get event: ${String(err)}`)) return;
console.error(`Failed to get event: ${String(err)}`);
}
});
@@ -1347,7 +1693,28 @@ export function registerCliCommands(
};
if (opts.json) {
- console.log(JSON.stringify({ commands }, null, 2));
+ const detailed = {
+ commands: {
+ memory: {
+ search: { description: "Query your memory store", flags: { "--top-k ": "Max results", "--scope ": "Memory scope", "--user-id ": "Override user ID", "--agent-id ": "Agent namespace", "--json": "JSON output" } },
+ add: { description: "Add a memory from text", flags: { "--user-id ": "Override user ID", "--agent-id ": "Agent namespace", "--json": "JSON output" } },
+ get: { description: "Get a specific memory by ID", flags: { "--json": "JSON output" } },
+ list: { description: "List memories", flags: { "--user-id ": "Override user ID", "--top-k ": "Max results", "--json": "JSON output" } },
+ update: { description: "Update a memory's text", flags: { "--json": "JSON output" } },
+ delete: { description: "Delete a memory or all memories", flags: { "--all": "Delete all", "--confirm": "Skip confirmation", "--json": "JSON output" } },
+ import: { description: "Import memories from JSON file", flags: { "--user-id ": "Override user ID", "--json": "JSON output" } },
+ },
+ management: {
+ init: { description: "Set up Mem0", flags: { "--mode ": "platform or open-source", "--api-key ": "API key", "--email ": "Email login", "--oss-llm ": "LLM provider", "--oss-embedder
": "Embedder", "--oss-vector
": "Vector store", "--json": "JSON output" } },
+ status: { description: "Check connectivity", flags: { "--json": "JSON output" } },
+ config: { description: "Manage configuration (show, get, set)", flags: { "--json": "JSON output" } },
+ event: { description: "Manage background events (list, status)", flags: { "--json": "JSON output" } },
+ dream: { description: "Run memory consolidation", flags: { "--dry-run": "Show inventory only", "--json": "JSON output" } },
+ help: { description: "Show help", flags: { "--json": "JSON output" } },
+ },
+ },
+ };
+ process.stdout.write(JSON.stringify(detailed, null, 2) + "\n");
return;
}
@@ -1379,7 +1746,8 @@ export function registerCliCommands(
"--dry-run",
"Show memory inventory without running consolidation",
)
- .action(async (opts: { dryRun?: boolean }) => {
+ .option("--json", "Output as JSON")
+ .action(async (opts: { dryRun?: boolean; json?: boolean }) => {
try {
const uid = cfg.userId;
const memories = await provider.getAll({
@@ -1389,6 +1757,7 @@ export function registerCliCommands(
const count = Array.isArray(memories) ? memories.length : 0;
if (count === 0) {
+ if (jsonOut(opts, { ok: true, count: 0, message: "No memories to consolidate." })) return;
console.log("No memories to consolidate.");
return;
}
@@ -1401,6 +1770,17 @@ export function registerCliCommands(
"uncategorized";
catCounts.set(cat, (catCounts.get(cat) ?? 0) + 1);
}
+
+ if (opts.dryRun && opts.json) {
+ jsonOut(opts, { ok: true, count, categories: Object.fromEntries(catCounts) });
+ return;
+ }
+
+ if (opts.json && !opts.dryRun) {
+ jsonOut(opts, { ok: true, count, message: `${count} memories available for consolidation` });
+ return;
+ }
+
process.stderr.write(`\nMemory inventory for "${uid}":\n`);
for (const [cat, num] of [...catCounts.entries()].sort(
(a, b) => b[1] - a[1],
@@ -1451,6 +1831,7 @@ export function registerCliCommands(
`Dream prompt written to stdout (${fullPrompt.length} chars). Paste it into an OpenClaw session to run consolidation.\n`,
);
} catch (err) {
+ if (jsonErr(opts, `Dream failed: ${String(err)}`)) return;
console.error(`Dream failed: ${String(err)}`);
}
});
diff --git a/openclaw/cli/json-helpers.ts b/openclaw/cli/json-helpers.ts
new file mode 100644
index 000000000..8637702ac
--- /dev/null
+++ b/openclaw/cli/json-helpers.ts
@@ -0,0 +1,40 @@
+/**
+ * JSON output helpers for agent-friendly CLI commands.
+ */
+
+function writeStdout(data: Record): void {
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
+}
+
+export function jsonOut(
+ opts: { json?: boolean },
+ data: Record,
+): boolean {
+ if (!opts.json) return false;
+ writeStdout(data);
+ return true;
+}
+
+export function jsonErr(
+ opts: { json?: boolean },
+ error: string,
+): boolean {
+ if (!opts.json) return false;
+ writeStdout({ ok: false, error });
+ return true;
+}
+
+export function redactSecrets(
+ obj: Record,
+ secretKeys: Set,
+): Record {
+ const result = { ...obj };
+ for (const key of secretKeys) {
+ const val = result[key];
+ if (typeof val !== "string") continue;
+ result[key] = val.length <= 8
+ ? val.slice(0, 2) + "***"
+ : val.slice(0, 4) + "..." + val.slice(-4);
+ }
+ return result;
+}
diff --git a/openclaw/cli/oss-wizard.ts b/openclaw/cli/oss-wizard.ts
new file mode 100644
index 000000000..f8d693fab
--- /dev/null
+++ b/openclaw/cli/oss-wizard.ts
@@ -0,0 +1,227 @@
+/**
+ * OSS provider wizard — provider definitions, config builders, and validation.
+ *
+ * Used by the init command for both interactive wizard and non-interactive
+ * --oss-* flag paths.
+ */
+
+import { join } from "node:path";
+import { homedir } from "node:os";
+
+// ============================================================================
+// Provider definitions
+// ============================================================================
+
+export interface ProviderDef {
+ id: string;
+ label: string;
+ needsApiKey: boolean;
+ needsUrl: boolean;
+ envVar?: string;
+ defaultModel: string;
+ defaultUrl?: string;
+}
+
+export const LLM_PROVIDERS: ProviderDef[] = [
+ { id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "gpt-5-mini" },
+ { id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "llama3.1:8b", defaultUrl: "http://localhost:11434" },
+ { id: "anthropic", label: "Anthropic (requires API key)", needsApiKey: true, needsUrl: false, envVar: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-4-5-20250514" },
+];
+
+export interface EmbedderDef extends ProviderDef {
+ defaultDims: number;
+}
+
+export const EMBEDDER_PROVIDERS: EmbedderDef[] = [
+ { id: "openai", label: "OpenAI (requires API key)", needsApiKey: true, needsUrl: false, envVar: "OPENAI_API_KEY", defaultModel: "text-embedding-3-small", defaultDims: 1536 },
+ { id: "ollama", label: "Ollama (local, no API key)", needsApiKey: false, needsUrl: true, defaultModel: "nomic-embed-text", defaultUrl: "http://localhost:11434", defaultDims: 768 },
+];
+
+export interface VectorDef {
+ id: string;
+ label: string;
+ needsConnection: boolean;
+ defaultUrl?: string;
+ defaultPort?: number;
+ setupHint?: string;
+}
+
+export const VECTOR_PROVIDERS: VectorDef[] = [
+ { id: "qdrant", label: "Qdrant (requires server — Docker or cloud)", needsConnection: true, defaultUrl: "http://localhost:6333", defaultPort: 6333, setupHint: "docker run -d -p 6333:6333 qdrant/qdrant" },
+ { id: "pgvector", label: "PGVector (requires PostgreSQL + pgvector extension)", needsConnection: true, defaultPort: 5432, setupHint: "docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres pgvector/pgvector:pg17" },
+];
+
+export const KNOWN_EMBEDDER_DIMS: Record = {
+ "text-embedding-3-small": 1536,
+ "text-embedding-3-large": 3072,
+ "text-embedding-ada-002": 1536,
+ "nomic-embed-text": 768,
+};
+
+// ============================================================================
+// Config builders
+// ============================================================================
+
+export interface LlmConfigInput {
+ apiKey?: string;
+ model?: string;
+ url?: string;
+}
+
+export function buildOssLlmConfig(
+ providerId: string,
+ input: LlmConfigInput,
+): { provider: string; config: Record } {
+ const def = LLM_PROVIDERS.find((p) => p.id === providerId);
+ if (!def) throw new Error(`Unknown LLM provider: ${providerId}`);
+
+ const config: Record = {
+ model: input.model || def.defaultModel,
+ };
+ if (input.apiKey) config.apiKey = input.apiKey;
+ if (providerId === "ollama") {
+ config.url = input.url || def.defaultUrl;
+ }
+ return { provider: providerId, config };
+}
+
+export interface EmbedderConfigInput {
+ apiKey?: string;
+ model?: string;
+ url?: string;
+}
+
+export function buildOssEmbedderConfig(
+ providerId: string,
+ input: EmbedderConfigInput,
+): { provider: string; config: Record; dims: number | undefined } {
+ const def = EMBEDDER_PROVIDERS.find((p) => p.id === providerId);
+ if (!def) throw new Error(`Unknown embedder provider: ${providerId}`);
+
+ const model = input.model || def.defaultModel;
+ const config: Record = { model };
+ if (input.apiKey) config.apiKey = input.apiKey;
+ if (providerId === "ollama") {
+ config.url = input.url || def.defaultUrl;
+ }
+
+ const dims = KNOWN_EMBEDDER_DIMS[model] ?? undefined;
+ return { provider: providerId, config, dims };
+}
+
+export interface VectorConfigInput {
+ url?: string;
+ host?: string;
+ port?: string;
+ user?: string;
+ password?: string;
+ dbname?: string;
+ apiKey?: string;
+ dims?: number;
+}
+
+export function buildOssVectorConfig(
+ providerId: string,
+ input: VectorConfigInput,
+): { provider: string; config: Record } {
+ const config: Record = {};
+
+ if (providerId === "qdrant") {
+ config.url = input.url || "http://localhost:6333";
+ config.onDisk = true;
+ if (input.apiKey) config.apiKey = input.apiKey;
+ } else if (providerId === "pgvector") {
+ config.host = input.host || "localhost";
+ config.port = parseInt(input.port || "5432", 10);
+ if (input.user) config.user = input.user;
+ if (input.password) config.password = input.password;
+ config.dbname = input.dbname || "postgres";
+ }
+
+ if (input.dims) config.dimension = input.dims;
+ return { provider: providerId, config };
+}
+
+export async function checkOllamaConnectivity(url: string): Promise<{ ok: boolean; error?: string }> {
+ try {
+ const resp = await fetch(`${url.replace(/\/+$/, "")}/api/tags`, { signal: AbortSignal.timeout(3000) });
+ if (resp.ok) return { ok: true };
+ return { ok: false, error: `Ollama returned HTTP ${resp.status}` };
+ } catch {
+ return { ok: false, error: `Cannot reach Ollama at ${url}. Install: https://ollama.com/download` };
+ }
+}
+
+export async function checkPgConnectivity(host: string, port: number): Promise<{ ok: boolean; error?: string }> {
+ return new Promise((resolve) => {
+ import("node:net").then(({ createConnection }) => {
+ const sock = createConnection({ host, port, timeout: 3000 });
+ sock.once("connect", () => { sock.destroy(); resolve({ ok: true }); });
+ sock.once("timeout", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}` }); });
+ sock.once("error", () => { sock.destroy(); resolve({ ok: false, error: `PostgreSQL not reachable at ${host}:${port}. Ensure PostgreSQL with pgvector extension is running.` }); });
+ });
+ });
+}
+
+export async function checkQdrantConnectivity(url: string): Promise<{ ok: boolean; error?: string }> {
+ try {
+ const resp = await fetch(`${url.replace(/\/+$/, "")}/healthz`, { signal: AbortSignal.timeout(3000) });
+ if (resp.ok) return { ok: true };
+ return { ok: false, error: `Qdrant returned HTTP ${resp.status}` };
+ } catch (err) {
+ return { ok: false, error: `Cannot reach Qdrant at ${url}. Start it with: docker run -d -p 6333:6333 qdrant/qdrant` };
+ }
+}
+
+// ============================================================================
+// Non-interactive flag validation
+// ============================================================================
+
+export interface OssFlags {
+ ossLlm?: string;
+ ossLlmKey?: string;
+ ossLlmModel?: string;
+ ossLlmUrl?: string;
+ ossEmbedder?: string;
+ ossEmbedderKey?: string;
+ ossEmbedderModel?: string;
+ ossEmbedderUrl?: string;
+ ossVector?: string;
+ ossVectorUrl?: string;
+ ossVectorHost?: string;
+ ossVectorPort?: string;
+ ossVectorUser?: string;
+ ossVectorPassword?: string;
+ ossVectorDbname?: string;
+ ossVectorDims?: string;
+}
+
+export function validateOssFlags(
+ flags: OssFlags,
+): { error?: string } {
+ const llmId = flags.ossLlm || "openai";
+ const llmDef = LLM_PROVIDERS.find((p) => p.id === llmId);
+ if (!llmDef) return { error: `Unknown LLM provider: ${llmId}. Valid: ${LLM_PROVIDERS.map((p) => p.id).join(", ")}` };
+
+ if (llmDef.needsApiKey && !flags.ossLlmKey) {
+ return { error: `--oss-llm-key required when --oss-llm is ${llmId}` };
+ }
+
+ const embId = flags.ossEmbedder || "openai";
+ const embDef = EMBEDDER_PROVIDERS.find((p) => p.id === embId);
+ if (!embDef) return { error: `Unknown embedder provider: ${embId}. Valid: ${EMBEDDER_PROVIDERS.map((p) => p.id).join(", ")}` };
+
+ if (embDef.needsApiKey && !flags.ossEmbedderKey && !flags.ossLlmKey) {
+ return { error: `--oss-embedder-key required when --oss-embedder is ${embId}` };
+ }
+
+ const vecId = flags.ossVector || "qdrant";
+ const vecDef = VECTOR_PROVIDERS.find((p) => p.id === vecId);
+ if (!vecDef) return { error: `Unknown vector store provider: ${vecId}. Valid: ${VECTOR_PROVIDERS.map((p) => p.id).join(", ")}` };
+
+ if (vecId === "pgvector" && !flags.ossVectorUser) {
+ return { error: "--oss-vector-user required when --oss-vector is pgvector" };
+ }
+
+ return {};
+}
diff --git a/openclaw/config.ts b/openclaw/config.ts
index f413dade5..e16ef3c7a 100644
--- a/openclaw/config.ts
+++ b/openclaw/config.ts
@@ -231,8 +231,8 @@ export const mem0ConfigSchema = {
return "default";
}
})(),
- autoCapture: cfg.autoCapture !== false,
- autoRecall: cfg.autoRecall !== false,
+ autoCapture: cfg.autoCapture === true,
+ autoRecall: cfg.autoRecall === true,
// v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either)
customInstructions:
typeof cfg.customInstructions === "string"
diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json
index 09a2d1105..f8c709000 100644
--- a/openclaw/openclaw.plugin.json
+++ b/openclaw/openclaw.plugin.json
@@ -1,15 +1,14 @@
{
"id": "openclaw-mem0",
"name": "Memory (Mem0)",
- "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source. PLATFORM MODE: Sends conversation data to mem0.ai cloud (requires MEM0_API_KEY). OPEN-SOURCE MODE: Stores vectors locally (~/.mem0/history.db) but uses external APIs for embeddings/LLM (default: OpenAI, requires OPENAI_API_KEY). Auto-recall injects memories before agent turns; auto-capture extracts facts after turns. Both configurable via autoRecall/autoCapture settings. Config stored in ~/.openclaw/openclaw.json.",
- "version": "1.0.7",
+ "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source. PLATFORM MODE: Sends conversation data to mem0.ai cloud (requires MEM0_API_KEY). OPEN-SOURCE MODE: Stores vectors locally (~/.mem0/history.db) but uses external APIs for embeddings/LLM (default: OpenAI, requires OPENAI_API_KEY). Auto-recall injects relevant memories into agent context before each turn; auto-capture extracts durable facts after turns. Both are opt-in via autoRecall/autoCapture config settings (default: false). The plugin injects a memory triage protocol into system context when skills mode is enabled. Config stored in ~/.openclaw/openclaw.json.",
+ "version": "1.0.8",
"kind": "memory",
"skills": ["skills"],
"commandAliases": [
{
"name": "mem0",
- "cliCommand": "mem0",
- "description": "Mem0 memory plugin commands"
+ "cliCommand": "mem0"
}
],
"contracts": {
@@ -20,7 +19,7 @@
},
"providerAuthEnvVars": {
"mem0": ["MEM0_API_KEY"],
- "openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AZURE_OPENAI_API_KEY", "COHERE_API_KEY"]
+ "openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
},
"providerAuthChoices": [
{
@@ -35,6 +34,32 @@
"cliFlag": "--mem0-api-key",
"cliOption": "--mem0-api-key ",
"cliDescription": "Mem0 platform API key"
+ },
+ {
+ "provider": "openclaw-mem0-oss",
+ "method": "config",
+ "choiceId": "oss-openai",
+ "choiceLabel": "Open Source with OpenAI",
+ "choiceHint": "Self-hosted mode using OpenAI for LLM and embeddings",
+ "groupId": "oss",
+ "groupLabel": "Open Source (self-hosted)",
+ "optionKey": "oss.llm.config.apiKey",
+ "cliFlag": "--oss-llm-key",
+ "cliOption": "--oss-llm-key ",
+ "cliDescription": "OpenAI API key for OSS LLM"
+ },
+ {
+ "provider": "openclaw-mem0-oss",
+ "method": "config",
+ "choiceId": "oss-ollama",
+ "choiceLabel": "Open Source with Ollama (local)",
+ "choiceHint": "Fully local mode, no API keys needed",
+ "groupId": "oss",
+ "groupLabel": "Open Source (self-hosted)",
+ "optionKey": "oss.llm.config.ollama_base_url",
+ "cliFlag": "--oss-llm-url",
+ "cliOption": "--oss-llm-url ",
+ "cliDescription": "Ollama base URL for local LLM"
}
],
"uiHints": {
@@ -86,11 +111,29 @@
"placeholder": "5",
"help": "Maximum number of memories to retrieve"
},
+ "userEmail": {
+ "label": "User Email",
+ "sensitive": true,
+ "advanced": true,
+ "help": "Email address associated with the Mem0 account. Set automatically during platform login."
+ },
"oss": {
"label": "Open-Source Configuration",
"advanced": true,
"help": "Optional. Configure custom embedder, vector store, LLM, or history DB for open-source mode. For API keys in sub-provider configs, use SecretRef objects or ${VAR} syntax instead of plaintext values."
},
+ "oss.llm.config.apiKey": {
+ "label": "OSS LLM API Key",
+ "sensitive": true,
+ "advanced": true,
+ "help": "API key for open-source LLM provider. Use SecretRef or ${VAR} syntax."
+ },
+ "oss.embedder.config.apiKey": {
+ "label": "OSS Embedder API Key",
+ "sensitive": true,
+ "advanced": true,
+ "help": "API key for open-source embedder provider. Use SecretRef or ${VAR} syntax."
+ },
"skills": {
"label": "Agentic Memory Skills",
"advanced": true,
@@ -114,6 +157,10 @@
"userId": {
"type": "string"
},
+ "baseUrl": {
+ "type": "string",
+ "description": "API base URL override (default: https://api.mem0.ai)"
+ },
"userEmail": {
"type": "string"
},
@@ -238,5 +285,23 @@
}
},
"required": []
+ },
+ "setup": {
+ "providers": [
+ {
+ "id": "mem0",
+ "authMethods": ["api-key"],
+ "envVars": ["MEM0_API_KEY"],
+ "description": "Platform mode: hosted memory at mem0.ai"
+ },
+ {
+ "id": "openclaw-mem0-oss",
+ "authMethods": ["api-key", "config"],
+ "envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
+ "description": "Open-source mode: self-hosted with chosen LLM/embedder providers. No env vars needed when using Ollama (local)."
+ }
+ ],
+ "requiresRuntime": false,
+ "postInstallHint": "Run 'openclaw mem0 init' to configure mode and credentials"
}
}
\ No newline at end of file
diff --git a/openclaw/package.json b/openclaw/package.json
index a7aa6a325..8754633b4 100644
--- a/openclaw/package.json
+++ b/openclaw/package.json
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
- "version": "1.0.7",
+ "version": "1.0.8",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
diff --git a/openclaw/providers.ts b/openclaw/providers.ts
index 6acf6cfcd..a29795cde 100644
--- a/openclaw/providers.ts
+++ b/openclaw/providers.ts
@@ -214,6 +214,7 @@ class PlatformProvider implements Mem0Provider {
// ============================================================================
class OSSProvider implements Mem0Provider {
+ private static _warnPatched = false;
private memory: any; // Memory from mem0ai/oss
private initPromise: Promise | null = null;
@@ -241,7 +242,7 @@ class OSSProvider implements Mem0Provider {
provider: "openai",
config: { model: "text-embedding-3-small" },
};
- const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
+ const defaultLlm = { provider: "openai", config: { model: "gpt-5-mini" } };
const stripEmpty = (obj: Record) => {
const out = { ...obj };
@@ -325,13 +326,35 @@ class OSSProvider implements Mem0Provider {
VectorCls.prototype.__patched = true;
}
+ // Proactively detect broken better-sqlite3 native binding (e.g. Node
+ // version mismatch) and skip history to avoid noisy constructor failures.
+ let sqliteOk = true;
+ if (!this.ossConfig?.disableHistory) {
+ try {
+ // @ts-ignore — better-sqlite3 is a transitive dep; no types in this package
+ const bs3Mod = await import("better-sqlite3");
+ const BS3 = bs3Mod.default ?? bs3Mod;
+ const testDb = new (BS3 as any)(":memory:");
+ (testDb as any).close();
+ } catch {
+ sqliteOk = false;
+ }
+ }
+
+ if (!OSSProvider._warnPatched) {
+ const origWarn = console.warn;
+ console.warn = (...args: unknown[]) => {
+ if (typeof args[0] === "string" && args[0].includes("checkCompatibility")) return;
+ origWarn.apply(console, args);
+ };
+ OSSProvider._warnPatched = true;
+ }
+
let mem: any;
try {
- mem = new Memory(this._buildConfig());
+ mem = new Memory(this._buildConfig(!sqliteOk));
} catch (err) {
- // If constructor fails (e.g. native SQLite binding under jiti/Docker),
- // retry with a FRESH config that has history disabled.
- if (!this.ossConfig?.disableHistory) {
+ if (!this.ossConfig?.disableHistory && sqliteOk) {
console.warn(
"[mem0] Memory initialization failed, retrying with history disabled:",
err instanceof Error ? err.message : err,
diff --git a/openclaw/skill-loader.ts b/openclaw/skill-loader.ts
index b1224b629..616194266 100644
--- a/openclaw/skill-loader.ts
+++ b/openclaw/skill-loader.ts
@@ -192,21 +192,16 @@ function renderCategoriesBlock(
}
function renderTriageKnobs(config: SkillsConfig): string {
- const triage = config.triage;
- if (!triage) return "";
-
const lines: string[] = [];
- if (triage.importanceThreshold !== undefined) {
+ if (config.triage?.importanceThreshold !== undefined) {
lines.push(
- `- Only store facts with importance >= ${triage.importanceThreshold}`,
+ `- Only store facts with importance >= ${config.triage.importanceThreshold}`,
);
}
const patterns = resolveCredentialPatterns(config);
- if (config.triage?.credentialPatterns) {
- lines.push(`- Credential patterns to scan: ${patterns.join(", ")}`);
- }
+ lines.push(`- Credential patterns to scan: ${patterns.map((p) => `\`${p}\``).join(", ")}`);
if (lines.length === 0) return "";
return "\n## Active Configuration Overrides\n\n" + lines.join("\n");
@@ -265,8 +260,8 @@ export function loadSkill(
parts.push(renderCategoriesBlock(mergedCats));
}
- // Inject triage knobs (maxFactsPerTurn, importanceThreshold, credentialPatterns)
- if (skillName === "memory-triage") {
+ // Inject triage knobs (importanceThreshold, credentialPatterns)
+ if (skillName === "memory-triage" || skillName === "memory-dream") {
const knobs = renderTriageKnobs(config);
if (knobs) parts.push(knobs);
}
diff --git a/openclaw/skills/memory-dream/SKILL.md b/openclaw/skills/memory-dream/SKILL.md
index 9224eafea..20411c161 100644
--- a/openclaw/skills/memory-dream/SKILL.md
+++ b/openclaw/skills/memory-dream/SKILL.md
@@ -46,7 +46,7 @@ Execute the actions identified in Phase 2. Work in this priority order:
### 3a. Delete dangerous and expired entries
Delete immediately using `memory_delete`:
-- Credentials, API keys, tokens, passwords, secrets (patterns: sk-, m0-, ghp_, AKIA, Bearer, password=, token=, secret=)
+- Credentials, API keys, tokens, passwords, secrets (matching known credential prefixes and auth patterns injected by the plugin at runtime)
- Pure timestamps with no context
- Raw tool output stored as memory
- Heartbeat or cron execution records
diff --git a/openclaw/skills/memory-triage/SKILL.md b/openclaw/skills/memory-triage/SKILL.md
index d78acffb1..dd2dcb8ca 100644
--- a/openclaw/skills/memory-triage/SKILL.md
+++ b/openclaw/skills/memory-triage/SKILL.md
@@ -37,9 +37,9 @@ Every candidate fact must pass ALL four gates:
- Fail: vague impressions, questions, small talk, acknowledgments, generic assistant responses ("Sure, I can help") → SKIP
**Gate 4 — SAFE**: Does this contain ANY credential, secret, or token?
- - Scan for: `sk-`, `m0-`, `ghp_`, `AKIA`, `ak_`, `Bearer `, bot tokens (digits:alphanumeric), webhook URLs with tokens, pairing codes, long alphanumeric strings in config/env context, `password=`, `token=`, `secret=`, `.env` values
+ - Scan for known credential prefixes, auth tokens, webhook URLs with tokens, pairing codes, long alphanumeric strings in config/env context, and key-value assignment patterns. The plugin injects the full pattern list at runtime.
- ANY match → NEVER STORE the value. Instead, store that the credential was configured:
- - WRONG: "User's API key is sk-abc123..."
+ - WRONG: "User's API key is [redacted]"
- RIGHT: "API key was configured for the service (as of 2026-03-30)"
- When in doubt → SKIP. No exceptions.
@@ -218,7 +218,7 @@ When a recalled memory needs updating (fact changed, status changed, new detail
## What NEVER to Store
-- **Credentials and secrets** — even embedded in config blocks, setup logs, or tool output. Includes sk-, m0-, ak_, ghp_, bot tokens, bearer tokens, webhook URLs with tokens, pairing codes, long alphanumeric strings in config/env contexts. Record that the credential was configured, never the value itself.
+- **Credentials and secrets** — even embedded in config blocks, setup logs, or tool output. Includes any known credential prefixes, auth tokens, bearer tokens, webhook URLs with tokens, pairing codes, and long alphanumeric strings in config/env contexts. Record that the credential was configured, never the value itself.
- **Raw tool output** — bash results, file contents, API responses, logs, diffs, test output. Extract only the durable OUTCOME or ROOT CAUSE.
- **One-time commands** — "stop the script", "continue where you left off", "run this"
- **Acknowledgments and emotional reactions** — "ok", "sure", "sounds good", "sir", "got it", "thanks", "you're right"
@@ -280,7 +280,7 @@ Agent: [updates the sheet successfully]
### Example 7: Credential — store the fact, not the value
```
-User: "Use this API key for the new service: sk-proj-abc123def456"
+User: "Use this API key for the new service: [credential value]"
Agent: [configures the service]
→ memory_add(facts: ["API key was configured for the new service (as of 2026-03-30)"], category: "configuration")
```
diff --git a/openclaw/sqlite-resilience.test.ts b/openclaw/sqlite-resilience.test.ts
index 484f75390..80fb47872 100644
--- a/openclaw/sqlite-resilience.test.ts
+++ b/openclaw/sqlite-resilience.test.ts
@@ -119,7 +119,12 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
expect(capturedConfig!.disableHistory).toBe(true);
});
- it("does not set disableHistory when not configured", async () => {
+ it("does not set disableHistory when not configured and sqlite works", async () => {
+ // Mock better-sqlite3 so the proactive probe succeeds
+ vi.doMock("better-sqlite3", () => {
+ return { default: class { close() {} } };
+ });
+
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
@@ -248,6 +253,12 @@ describe("OSSProvider — graceful SQLite fallback", () => {
});
it("retries with disableHistory: true when initial construction fails", async () => {
+ // Mock better-sqlite3 so the proactive probe succeeds — tests the
+ // catch-retry fallback path for other constructor errors.
+ vi.doMock("better-sqlite3", () => {
+ return { default: class { close() {} } };
+ });
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -274,6 +285,29 @@ describe("OSSProvider — graceful SQLite fallback", () => {
warnSpy.mockRestore();
});
+ it("proactively disables history when better-sqlite3 binary is broken", async () => {
+ // Do NOT mock better-sqlite3 — let probe detect the real version mismatch
+ // (or force it to fail if native binary happens to work on this Node).
+ vi.doMock("better-sqlite3", () => {
+ return { default: class { constructor() { throw new Error("NODE_MODULE_VERSION mismatch"); } } };
+ });
+
+ const { createProvider } = await import("./index.ts");
+ const cfg = mem0ConfigSchema.parse({
+ mode: "open-source",
+ oss: {},
+ });
+ const api = { resolvePath: (p: string) => p } as any;
+ const provider = createProvider(cfg, api);
+
+ const results = await provider.search("test", { user_id: "u1" });
+ expect(results).toBeDefined();
+
+ // Only ONE constructor call — probe detected broken sqlite, skipped retry
+ expect(capturedConfigs).toHaveLength(1);
+ expect(capturedConfigs[0].disableHistory).toBe(true);
+ });
+
it("does not retry when disableHistory is already true", async () => {
// Force the constructor to always throw, regardless of disableHistory
forceConstructorError = "vector store connection refused";
diff --git a/openclaw/tests/cli-commands.test.ts b/openclaw/tests/cli-commands.test.ts
index 04d832b55..9dec74467 100644
--- a/openclaw/tests/cli-commands.test.ts
+++ b/openclaw/tests/cli-commands.test.ts
@@ -32,6 +32,7 @@ vi.mock("../skill-loader.ts", () => ({
loadDreamPrompt: vi.fn().mockReturnValue("dream prompt"),
}));
+
// ---------------------------------------------------------------------------
// Imports (after mocks)
// ---------------------------------------------------------------------------
@@ -251,6 +252,7 @@ describe("registerCliCommands", () => {
warn: ReturnType;
};
let stderrSpy: ReturnType;
+ let stdoutSpy: ReturnType;
beforeEach(() => {
vi.resetAllMocks();
@@ -267,6 +269,7 @@ describe("registerCliCommands", () => {
warn: vi.spyOn(console, "warn").mockImplementation(() => {}),
};
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
+ stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
});
afterEach(() => {
@@ -274,6 +277,7 @@ describe("registerCliCommands", () => {
consoleSpy.error.mockRestore();
consoleSpy.warn.mockRestore();
stderrSpy.mockRestore();
+ stdoutSpy.mockRestore();
vi.restoreAllMocks();
});
@@ -527,6 +531,158 @@ describe("registerCliCommands", () => {
vi.unstubAllGlobals();
});
+
+ it("outputs JSON for --api-key flow when --json is set", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({}),
+ }));
+
+ await initCmd._action!({ apiKey: "m0-key", json: true });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return typeof p.ok === "boolean";
+ } catch { return false; }
+ });
+ expect(jsonCall).toBeDefined();
+ const parsed = JSON.parse(jsonCall![0] as string);
+ expect(parsed.ok).toBe(true);
+ expect(parsed.mode).toBe("platform");
+ expect(parsed.validated).toBe(true);
+
+ vi.unstubAllGlobals();
+ });
+
+ it("outputs JSON for --api-key flow with failed validation when --json is set", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
+ ok: false,
+ status: 401,
+ json: vi.fn().mockResolvedValue({}),
+ }));
+
+ await initCmd._action!({ apiKey: "bad-key", json: true });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return typeof p.ok === "boolean";
+ } catch { return false; }
+ });
+ expect(jsonCall).toBeDefined();
+ const parsed = JSON.parse(jsonCall![0] as string);
+ expect(parsed.ok).toBe(false);
+ expect(parsed.mode).toBe("platform");
+ expect(parsed.validated).toBe(false);
+ expect(parsed.httpStatus).toBe(401);
+
+ vi.unstubAllGlobals();
+ });
+
+ it("outputs JSON for --api-key + --email conflict when --json is set", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ await initCmd._action!({ apiKey: "key", email: "a@b.com", json: true });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return p.ok === false;
+ } catch { return false; }
+ });
+ expect(jsonCall).toBeDefined();
+ const parsed = JSON.parse(jsonCall![0] as string);
+ expect(parsed.error).toContain("Cannot use both");
+ expect(writePluginAuth).not.toHaveBeenCalled();
+ });
+
+ it("outputs JSON for email send-code flow when --json is set", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({}),
+ }));
+
+ await initCmd._action!({ email: "user@example.com", json: true });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return p.codeSent === true;
+ } catch { return false; }
+ });
+ expect(jsonCall).toBeDefined();
+ const parsed = JSON.parse(jsonCall![0] as string);
+ expect(parsed.ok).toBe(true);
+ expect(parsed.email).toBe("user@example.com");
+ expect(parsed.nextCommand).toContain("--code");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("outputs JSON for email verify flow when --json is set", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue({ api_key: "m0-verified" }),
+ }));
+
+ await initCmd._action!({ email: "u@b.com", code: "123456", json: true });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return p.ok === true && p.mode === "platform";
+ } catch { return false; }
+ });
+ expect(jsonCall).toBeDefined();
+ const parsed = JSON.parse(jsonCall![0] as string);
+ expect(parsed.email).toBe("u@b.com");
+ expect(parsed.message).toContain("Authenticated");
+
+ vi.unstubAllGlobals();
+ });
+
+ it("clears stale apiKey when switching to OSS mode", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
+
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ (readPluginAuth as ReturnType).mockReturnValue({
+ apiKey: "m0-old-platform-key",
+ mode: "platform",
+ userId: "testuser",
+ });
+
+ await initCmd._action!({
+ mode: "open-source",
+ ossLlm: "ollama",
+ ossEmbedder: "ollama",
+ ossVector: "qdrant",
+ });
+
+ expect(writePluginAuth).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKey: "",
+ mode: "open-source",
+ }),
+ );
+
+ vi.unstubAllGlobals();
+ });
});
// ========================================================================
@@ -1456,4 +1612,144 @@ describe("registerCliCommands", () => {
);
});
});
+
+ // ========================================================================
+ // Restructured init menu flags
+ // ========================================================================
+
+ describe("init — restructured menu", () => {
+ it("registers --mode flag", () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+ const modeOpt = initCmd._options.find((o) => o.flags.includes("--mode"));
+ expect(modeOpt).toBeDefined();
+ });
+
+ it("registers --oss-llm flag", () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+ const opt = initCmd._options.find((o) => o.flags.includes("--oss-llm "));
+ expect(opt).toBeDefined();
+ });
+
+ it("registers --json flag on init", () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+ const opt = initCmd._options.find((o) => o.flags.includes("--json"));
+ expect(opt).toBeDefined();
+ });
+ });
+
+ // ========================================================================
+ // --json flag registration on all commands
+ // ========================================================================
+
+ describe("--json flag registration", () => {
+ for (const name of ["search", "add", "get", "list", "update", "delete", "status", "import", "dream"]) {
+ it(`registers --json on ${name}`, () => {
+ const { mem0 } = setup();
+ const cmd = findCommand(mem0, name)!;
+ const opt = cmd._options.find((o) => o.flags.includes("--json"));
+ expect(opt).toBeDefined();
+ });
+ }
+
+ it("registers --json on config show", () => {
+ const { mem0 } = setup();
+ const configCmd = findCommand(mem0, "config")!;
+ const showCmd = findCommand(configCmd, "show")!;
+ expect(showCmd).toBeDefined();
+ const opt = showCmd._options.find((o) => o.flags.includes("--json"));
+ expect(opt).toBeDefined();
+ });
+ });
+
+ // ========================================================================
+ // Non-interactive OSS init
+ // ========================================================================
+
+ describe("init --mode open-source (non-interactive)", () => {
+ it("writes LLM, embedder, and vector config for ollama + qdrant", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
+
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ await initCmd._action!({
+ mode: "open-source",
+ ossLlm: "ollama",
+ ossEmbedder: "ollama",
+ ossVector: "qdrant",
+ userId: "test-user",
+ });
+
+ expect(writePluginConfigField).toHaveBeenCalledWith(
+ ["oss", "llm"],
+ expect.objectContaining({ provider: "ollama" }),
+ );
+ expect(writePluginConfigField).toHaveBeenCalledWith(
+ ["oss", "embedder"],
+ expect.objectContaining({ provider: "ollama" }),
+ );
+ expect(writePluginConfigField).toHaveBeenCalledWith(
+ ["oss", "vectorStore"],
+ expect.objectContaining({ provider: "qdrant" }),
+ );
+ expect(writePluginAuth).toHaveBeenCalledWith(
+ expect.objectContaining({ mode: "open-source", userId: "test-user" }),
+ );
+
+ vi.unstubAllGlobals();
+ });
+
+ it("outputs JSON when --json is passed", async () => {
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }));
+
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ await initCmd._action!({
+ mode: "open-source",
+ ossLlm: "ollama",
+ ossEmbedder: "ollama",
+ ossVector: "qdrant",
+ json: true,
+ });
+
+ const jsonCall = stdoutSpy.mock.calls.find((c) => {
+ try {
+ const p = JSON.parse(c[0] as string);
+ return p.ok === true;
+ } catch {
+ return false;
+ }
+ });
+ expect(jsonCall).toBeDefined();
+ if (jsonCall) {
+ const parsed = JSON.parse(jsonCall[0] as string);
+ expect(parsed.mode).toBe("open-source");
+ expect(parsed.config.llm.provider).toBe("ollama");
+ }
+
+ vi.unstubAllGlobals();
+ });
+
+ it("errors when openai LLM has no key", async () => {
+ const { mem0 } = setup();
+ const initCmd = findCommand(mem0, "init")!;
+
+ // Ensure env var is not set so validation fails
+ const savedEnv = process.env.OPENAI_API_KEY;
+ delete process.env.OPENAI_API_KEY;
+
+ await initCmd._action!({ mode: "open-source", ossLlm: "openai" });
+
+ expect(consoleSpy.error).toHaveBeenCalledWith(
+ expect.stringContaining("--oss-llm-key"),
+ );
+
+ // Restore env var
+ if (savedEnv !== undefined) process.env.OPENAI_API_KEY = savedEnv;
+ });
+ });
});
diff --git a/openclaw/tests/config.test.ts b/openclaw/tests/config.test.ts
index 4b630f704..712133be6 100644
--- a/openclaw/tests/config.test.ts
+++ b/openclaw/tests/config.test.ts
@@ -44,14 +44,14 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
expect(cfg.userId.length).toBeGreaterThan(0);
});
- it("autoCapture defaults to true", () => {
+ it("autoCapture defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
- expect(cfg.autoCapture).toBe(true);
+ expect(cfg.autoCapture).toBe(false);
});
- it("autoRecall defaults to true", () => {
+ it("autoRecall defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
- expect(cfg.autoRecall).toBe(true);
+ expect(cfg.autoRecall).toBe(false);
});
it("topK defaults to 5", () => {
diff --git a/openclaw/tests/json-helpers.test.ts b/openclaw/tests/json-helpers.test.ts
new file mode 100644
index 000000000..1a8a26c6b
--- /dev/null
+++ b/openclaw/tests/json-helpers.test.ts
@@ -0,0 +1,55 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { jsonOut, jsonErr, redactSecrets } from "../cli/json-helpers.ts";
+
+describe("jsonOut", () => {
+ let writeSpy: ReturnType;
+ beforeEach(() => { writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); });
+ afterEach(() => { writeSpy.mockRestore(); });
+
+ it("returns false and prints nothing when json is falsy", () => {
+ expect(jsonOut({}, { ok: true })).toBe(false);
+ expect(writeSpy).not.toHaveBeenCalled();
+ });
+
+ it("returns true and prints JSON to stdout when json is true", () => {
+ expect(jsonOut({ json: true }, { ok: true, count: 3 })).toBe(true);
+ expect(writeSpy).toHaveBeenCalledOnce();
+ const parsed = JSON.parse(writeSpy.mock.calls[0][0] as string);
+ expect(parsed).toEqual({ ok: true, count: 3 });
+ });
+});
+
+describe("jsonErr", () => {
+ let writeSpy: ReturnType;
+ beforeEach(() => { writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true); });
+ afterEach(() => { writeSpy.mockRestore(); });
+
+ it("returns false when json is falsy", () => {
+ expect(jsonErr({}, "bad")).toBe(false);
+ });
+
+ it("returns true and prints error JSON to stdout", () => {
+ expect(jsonErr({ json: true }, "Something broke")).toBe(true);
+ const parsed = JSON.parse(writeSpy.mock.calls[0][0] as string);
+ expect(parsed).toEqual({ ok: false, error: "Something broke" });
+ });
+});
+
+describe("redactSecrets", () => {
+ it("redacts string values for known secret keys", () => {
+ const input = { apiKey: "m0-abcdefghijklmnop", name: "test" };
+ const result = redactSecrets(input, new Set(["apiKey"]));
+ expect(result.apiKey).toBe("m0-a...mnop");
+ expect(result.name).toBe("test");
+ });
+
+ it("handles short keys", () => {
+ const result = redactSecrets({ apiKey: "ab" }, new Set(["apiKey"]));
+ expect(result.apiKey).toBe("ab***");
+ });
+
+ it("skips non-string values", () => {
+ const result = redactSecrets({ count: 5 }, new Set(["count"]));
+ expect(result.count).toBe(5);
+ });
+});
diff --git a/openclaw/tests/oss-wizard.test.ts b/openclaw/tests/oss-wizard.test.ts
new file mode 100644
index 000000000..185f9e197
--- /dev/null
+++ b/openclaw/tests/oss-wizard.test.ts
@@ -0,0 +1,196 @@
+import { describe, it, expect } from "vitest";
+import {
+ LLM_PROVIDERS,
+ EMBEDDER_PROVIDERS,
+ VECTOR_PROVIDERS,
+ KNOWN_EMBEDDER_DIMS,
+ buildOssLlmConfig,
+ buildOssEmbedderConfig,
+ buildOssVectorConfig,
+ validateOssFlags,
+ checkQdrantConnectivity,
+ checkOllamaConnectivity,
+ checkPgConnectivity,
+} from "../cli/oss-wizard.ts";
+
+describe("LLM_PROVIDERS", () => {
+ it("has 3 providers", () => {
+ expect(LLM_PROVIDERS).toHaveLength(3);
+ expect(LLM_PROVIDERS.map((p) => p.id)).toEqual(["openai", "ollama", "anthropic"]);
+ });
+
+ it("openai requires API key", () => {
+ const openai = LLM_PROVIDERS.find((p) => p.id === "openai")!;
+ expect(openai.needsApiKey).toBe(true);
+ expect(openai.defaultModel).toBe("gpt-5-mini");
+ });
+
+ it("ollama needs no API key but needs URL", () => {
+ const ollama = LLM_PROVIDERS.find((p) => p.id === "ollama")!;
+ expect(ollama.needsApiKey).toBe(false);
+ expect(ollama.needsUrl).toBe(true);
+ expect(ollama.defaultUrl).toBe("http://localhost:11434");
+ });
+});
+
+describe("EMBEDDER_PROVIDERS", () => {
+ it("has 2 providers", () => {
+ expect(EMBEDDER_PROVIDERS).toHaveLength(2);
+ });
+});
+
+describe("KNOWN_EMBEDDER_DIMS", () => {
+ it("maps default models to dims", () => {
+ expect(KNOWN_EMBEDDER_DIMS["text-embedding-3-small"]).toBe(1536);
+ expect(KNOWN_EMBEDDER_DIMS["nomic-embed-text"]).toBe(768);
+ });
+});
+
+describe("buildOssLlmConfig", () => {
+ it("builds openai config with API key", () => {
+ const result = buildOssLlmConfig("openai", { apiKey: "sk-test" });
+ expect(result).toEqual({
+ provider: "openai",
+ config: { model: "gpt-5-mini", apiKey: "sk-test" },
+ });
+ });
+
+ it("builds ollama config with custom URL and model", () => {
+ const result = buildOssLlmConfig("ollama", { url: "http://myhost:11434", model: "mistral" });
+ expect(result).toEqual({
+ provider: "ollama",
+ config: { model: "mistral", url: "http://myhost:11434" },
+ });
+ });
+
+ it("builds ollama config with default URL", () => {
+ const result = buildOssLlmConfig("ollama", {});
+ expect(result.config.url).toBe("http://localhost:11434");
+ });
+
+ it("ignores url for non-ollama providers", () => {
+ const result = buildOssLlmConfig("anthropic", { apiKey: "sk-ant", url: "http://ignored" });
+ expect(result.config).not.toHaveProperty("url");
+ expect(result.config).toHaveProperty("apiKey", "sk-ant");
+ });
+});
+
+describe("buildOssEmbedderConfig", () => {
+ it("builds openai embedder", () => {
+ const result = buildOssEmbedderConfig("openai", { apiKey: "sk-test" });
+ expect(result.config.model).toBe("text-embedding-3-small");
+ expect(result.dims).toBe(1536);
+ });
+
+ it("builds ollama embedder with url field", () => {
+ const result = buildOssEmbedderConfig("ollama", { url: "http://myhost:11434" });
+ expect(result.config.url).toBe("http://myhost:11434");
+ expect(result.config).not.toHaveProperty("ollama_base_url");
+ expect(result.config.model).toBe("nomic-embed-text");
+ expect(result.dims).toBe(768);
+ });
+
+ it("returns unknown dims for custom model", () => {
+ const result = buildOssEmbedderConfig("ollama", { model: "custom-embed" });
+ expect(result.dims).toBeUndefined();
+ });
+});
+
+describe("VECTOR_PROVIDERS", () => {
+ it("has 2 providers", () => {
+ expect(VECTOR_PROVIDERS).toHaveLength(2);
+ expect(VECTOR_PROVIDERS.map((p) => p.id)).toEqual(["qdrant", "pgvector"]);
+ });
+
+ it("qdrant requires server connection", () => {
+ const qdrant = VECTOR_PROVIDERS.find((p) => p.id === "qdrant")!;
+ expect(qdrant.needsConnection).toBe(true);
+ expect(qdrant.defaultUrl).toBe("http://localhost:6333");
+ expect(qdrant.setupHint).toContain("docker");
+ });
+
+ it("pgvector requires connection and has setup hint", () => {
+ const pg = VECTOR_PROVIDERS.find((p) => p.id === "pgvector")!;
+ expect(pg.needsConnection).toBe(true);
+ expect(pg.defaultPort).toBe(5432);
+ expect(pg.setupHint).toContain("pgvector");
+ });
+});
+
+describe("buildOssVectorConfig", () => {
+ it("builds qdrant with default url and dims", () => {
+ const result = buildOssVectorConfig("qdrant", { dims: 1536 });
+ expect(result.config.url).toBe("http://localhost:6333");
+ expect(result.config.onDisk).toBe(true);
+ expect(result.config.dimension).toBe(1536);
+ });
+
+ it("builds qdrant with custom url", () => {
+ const result = buildOssVectorConfig("qdrant", { url: "http://qdrant.local:6333", dims: 768 });
+ expect(result.config.url).toBe("http://qdrant.local:6333");
+ expect(result.config.onDisk).toBe(true);
+ expect(result.config.dimension).toBe(768);
+ });
+
+ it("builds qdrant with api key for cloud", () => {
+ const result = buildOssVectorConfig("qdrant", { url: "https://cloud.qdrant.io", apiKey: "qd-key", dims: 1536 });
+ expect(result.config.apiKey).toBe("qd-key");
+ expect(result.config.url).toBe("https://cloud.qdrant.io");
+ });
+
+ it("builds pgvector with connection details", () => {
+ const result = buildOssVectorConfig("pgvector", {
+ host: "db.local", port: "5432", user: "me", password: "pw", dbname: "mydb", dims: 512,
+ });
+ expect(result.config.host).toBe("db.local");
+ expect(result.config.dimension).toBe(512);
+ });
+});
+
+describe("checkQdrantConnectivity", () => {
+ it("returns error for unreachable host", async () => {
+ const result = await checkQdrantConnectivity("http://localhost:19999");
+ expect(result.ok).toBe(false);
+ expect(result.error).toContain("Cannot reach Qdrant");
+ });
+});
+
+describe("checkOllamaConnectivity", () => {
+ it("returns error for unreachable host", async () => {
+ const result = await checkOllamaConnectivity("http://localhost:19998");
+ expect(result.ok).toBe(false);
+ expect(result.error).toContain("Cannot reach Ollama");
+ });
+});
+
+describe("checkPgConnectivity", () => {
+ it("returns error for unreachable host", async () => {
+ const result = await checkPgConnectivity("localhost", 19997);
+ expect(result.ok).toBe(false);
+ expect(result.error).toContain("PostgreSQL not reachable");
+ });
+});
+
+describe("validateOssFlags", () => {
+ it("returns error when openai LLM has no key", () => {
+ const result = validateOssFlags({ ossLlm: "openai" });
+ expect(result.error).toContain("--oss-llm-key");
+ });
+
+ it("passes for ollama with no key", () => {
+ const result = validateOssFlags({ ossLlm: "ollama", ossEmbedder: "ollama", ossVector: "qdrant" });
+ expect(result.error).toBeUndefined();
+ });
+
+ it("returns error for unknown provider", () => {
+ const result = validateOssFlags({ ossLlm: "bogus" });
+ expect(result.error).toContain("Unknown LLM provider");
+ });
+
+ it("returns error when pgvector missing user", () => {
+ const result = validateOssFlags({
+ ossLlm: "ollama", ossEmbedder: "ollama", ossVector: "pgvector",
+ });
+ expect(result.error).toContain("--oss-vector-user");
+ });
+});
diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts
index 1c6f843ca..d28aa94b5 100644
--- a/openclaw/tests/tools.test.ts
+++ b/openclaw/tests/tools.test.ts
@@ -263,10 +263,11 @@ describe("memory_search execute", () => {
scope: "session",
});
- // Should call buildSearchOptions with session ID
+ // Should call buildSearchOptions with session ID as 4th arg (sessionKey)
expect(ctx.buildSearchOptions).toHaveBeenCalledWith(
"testuser",
undefined,
+ undefined,
"session-abc",
);
expect(result.details.count).toBe(1);
@@ -446,8 +447,8 @@ describe("memory_add execute", () => {
await tool.execute("call-7", { text: "new fact" });
- // Search should be called for dedup before add
- expect(searchMock).toHaveBeenCalledOnce();
+ // Mem0 backend handles dedup internally — no separate search call
+ expect(searchMock).not.toHaveBeenCalled();
expect(addMock).toHaveBeenCalledOnce();
});
});
diff --git a/openclaw/tools/memory-add.ts b/openclaw/tools/memory-add.ts
index c021b30ec..59d5e1709 100644
--- a/openclaw/tools/memory-add.ts
+++ b/openclaw/tools/memory-add.ts
@@ -82,9 +82,6 @@ export function createMemoryAddTool(deps: ToolDeps) {
}
const combinedText = allFacts.join("\n");
- const dedupOpts = buildSearchOptions(uid, 3);
- dedupOpts.threshold = 0.85;
- await provider.search(combinedText.slice(0, 200), dedupOpts);
const result = await provider.add([{ role: "user", content: combinedText }], buildAddOptions(uid, runId, currentSessionId));
const added = result.results?.filter((r) => r.event === "ADD") ?? [];
diff --git a/openclaw/tools/memory-search.ts b/openclaw/tools/memory-search.ts
index 5c8f7f46a..1eddb88e7 100644
--- a/openclaw/tools/memory-search.ts
+++ b/openclaw/tools/memory-search.ts
@@ -47,7 +47,7 @@ export function createMemorySearchTool(deps: ToolDeps) {
if (scope === "session") {
if (currentSessionId) {
- results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, currentSessionId)));
+ results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, undefined, currentSessionId)));
}
} else if (scope === "long-term") {
results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
@@ -55,7 +55,7 @@ export function createMemorySearchTool(deps: ToolDeps) {
const longTerm = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
let session: MemoryItem[] = [];
if (currentSessionId) {
- session = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, currentSessionId)));
+ session = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, undefined, currentSessionId)));
}
const seen = new Set(longTerm.map((r) => r.id));
results = [...longTerm, ...session.filter((r) => !seen.has(r.id))];