diff --git a/openclaw/CHANGELOG.md b/openclaw/CHANGELOG.md index d034305f2..bc9742379 100644 --- a/openclaw/CHANGELOG.md +++ b/openclaw/CHANGELOG.md @@ -9,6 +9,8 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th - **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API - **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all` - **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status` (renamed from `stats`), `openclaw mem0 config show`, `openclaw mem0 config set` +- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides +- **`event list` / `event status` CLI commands**: Monitor background processing events - **`fs-safe.ts` module**: Isolated filesystem wrappers (sync read/write/exists/mkdir/unlink) in a separate entry point — keeps file I/O out of the main bundle - **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands - **`cli/config-file.ts`**: Persistent plugin auth storage in `~/.openclaw/openclaw.json` @@ -16,7 +18,7 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th - **Test suite**: 329 tests across 10 test files covering tools, CLI, config, dream gate, providers, and skill-loader ### Changed -- **Modular architecture**: Extracted tools into `tools/` directory (7 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines +- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines - **Code splitting**: tsup builds with `splitting: true` and two entry points (`index.ts`, `fs-safe.ts`), separating filesystem I/O from the main bundle - **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`) matching the plugin manifest - **WRITE_TOOLS updated**: Dream gate tracks `memory_delete` and `memory_add` instead of `memory_forget` and `memory_store` @@ -25,11 +27,15 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th - **Auto-capture fire-and-forget**: `provider.add()` now runs in the background via `.then()/.catch()` — the `agent_end` hook returns immediately, zero event loop blocking - **Auto-capture minimum content gate**: Skips extraction when total user content is <50 chars after filtering — trivial conversations ("ok", "thanks") no longer trigger LLM calls - **CLI search**: Lowered threshold to 0.3 so explicit searches are more permissive than auto-recall +- **Init defaults**: Choice defaults to `1` (email login) on Enter, User ID defaults to OS username — no more empty values +- **Init no longer stores `baseUrl`**: Uses `https://api.mem0.ai` directly instead of persisting it to config +- **Help output reordered**: `openclaw mem0 help` now shows high-value commands first (search, add) instead of alphabetical ### Removed - `memory_store` tool — replaced by `memory_add` - `memory_forget` tool — replaced by `memory_delete` - `memory_delete_all` tool — merged into `memory_delete` +- `memory_history` tool and `history` CLI command — deprecated - **`custom_instructions` / `custom_categories` in `buildAddOptions`**: No longer injected into every auto-capture API call. Config fields (`customInstructions`, `customCategories`) remain as user-configurable options. ## [1.0.3] - 2026-04-03 diff --git a/openclaw/cli/commands.ts b/openclaw/cli/commands.ts index eaa39451f..2fbffde34 100644 --- a/openclaw/cli/commands.ts +++ b/openclaw/cli/commands.ts @@ -10,7 +10,7 @@ * - 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) - * - history : View edit history of a memory + * - import : Import memories from a JSON file * * Management: * - init : Authenticate with Mem0 Platform (email or API key) @@ -18,6 +18,8 @@ * - 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.) @@ -35,12 +37,12 @@ import type { 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, - getBaseUrl, OPENCLAW_CONFIG_FILE, } from "./config-file.ts"; @@ -49,13 +51,14 @@ import { // ============================================================================ /** Prompt user for input on stderr (keeps stdout clean for piping). */ -function promptInput(question: string): Promise { +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); }); } @@ -196,14 +199,13 @@ async function verifyEmailCode( */ function saveLoginConfig( apiKey: string, - baseUrl: string, userIdFlag?: string, userEmail?: string, ): void { const existingAuth = readPluginAuth(); const userId = resolveUserId(userIdFlag, existingAuth.userId); - writePluginAuth({ apiKey, baseUrl, userId, mode: "platform", ...(userEmail && { userEmail }) }); + writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) }); console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`); console.log(` Mode: platform`); @@ -245,7 +247,8 @@ export function registerCliCommands( ({ program }) => { const mem0 = program .command("mem0") - .description("Mem0 memory plugin commands"); + .description("Mem0 memory plugin commands") + .configureHelp({ sortSubcommands: false, subcommandTerm: (cmd) => cmd.name() }); // Telemetry: fire event for each CLI subcommand if (captureCliEvent) { @@ -278,7 +281,7 @@ export function registerCliCommands( userId?: string; }) => { try { - const baseUrl = getBaseUrl(); + const baseUrl = "https://api.mem0.ai"; const existingAuth = readPluginAuth(); const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode); @@ -290,7 +293,7 @@ export function registerCliCommands( } const check = await validateApiKey(baseUrl, opts.apiKey); - saveLoginConfig(opts.apiKey, baseUrl, opts.userId, check.userEmail); + saveLoginConfig(opts.apiKey, opts.userId, check.userEmail); if (hasExistingConfig) { console.log( " Existing configuration detected — updated API key (other settings preserved).", @@ -322,7 +325,7 @@ export function registerCliCommands( const apiKey = await verifyEmailCode(baseUrl, email, opts.code); if (!apiKey) return; - saveLoginConfig(apiKey, baseUrl, opts.userId, email); + saveLoginConfig(apiKey, opts.userId, email); if (hasExistingConfig) { console.log( " Existing configuration detected — updated API key (other settings preserved).", @@ -390,7 +393,7 @@ export function registerCliCommands( // Validate existing key before asking if (existingAuth.apiKey) { const check = await validateApiKey( - existingAuth.baseUrl || baseUrl, + baseUrl, existingAuth.apiKey, ); if (check.ok) { @@ -429,7 +432,7 @@ export function registerCliCommands( console.log(" 2. Enter API key manually"); console.log(" 3. Open-source mode (self-hosted)\n"); - const choice = await promptInput(" Choice (1/2/3): "); + const choice = (await promptInput(" Choice: ", "1")) || "1"; if (choice === "1") { // --- Email interactive flow --- @@ -461,13 +464,13 @@ export function registerCliCommands( if (!userIdValue) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( - ` User ID (${defaultUid}): `, + ` User ID: `, defaultUid, ); - userIdValue = uidInput || undefined; + userIdValue = uidInput || defaultUid; } console.log(""); - saveLoginConfig(apiKey, baseUrl, userIdValue, email); + saveLoginConfig(apiKey, userIdValue, email); console.log(" Authenticated!"); console.log( " Restart the gateway: openclaw gateway restart\n", @@ -485,14 +488,14 @@ export function registerCliCommands( if (!userIdValue2) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( - ` User ID (${defaultUid}): `, + ` User ID: `, defaultUid, ); - userIdValue2 = uidInput || undefined; + userIdValue2 = uidInput || defaultUid; } console.log(""); const check = await validateApiKey(baseUrl, key); - saveLoginConfig(key, baseUrl, userIdValue2, check.userEmail); + saveLoginConfig(key, userIdValue2, check.userEmail); if (check.ok) { console.log( @@ -557,9 +560,9 @@ export function registerCliCommands( if (!userIdValue3) { const defaultUid = resolveUserId(undefined, existingAuth.userId); const uidInput = await promptInput( - ` User ID (${defaultUid}): `, + ` User ID: `, defaultUid, ); - userIdValue3 = uidInput || undefined; + userIdValue3 = uidInput || defaultUid; } console.log(""); @@ -901,27 +904,6 @@ export function registerCliCommands( }, ); - // ==================================================================== - // history (matches: mem0 history ) - // ==================================================================== - - mem0 - .command("history") - .description("View edit history of a memory") - .argument("", "Memory ID to view history for") - .action(async (memoryId: string) => { - try { - const entries = await provider.history(memoryId); - if (!entries.length) { - console.log("No history found for this memory."); - return; - } - console.log(JSON.stringify(entries, null, 2)); - } catch (err) { - console.error(`History failed: ${String(err)}`); - } - }); - // ==================================================================== // status (matches: mem0 status) // ==================================================================== @@ -1016,6 +998,7 @@ export function registerCliCommands( orgId: auth.orgId ?? cfg.orgId, projectId: auth.projectId ?? cfg.projectId, mode: auth.mode ?? cfg.mode, + userEmail: auth.userEmail, enableGraph: cfg.enableGraph, autoRecall: cfg.autoRecall, autoCapture: cfg.autoCapture, @@ -1048,7 +1031,7 @@ export function registerCliCommands( // Display order matching Python CLI: platform first, then defaults const entries: Array<[string, string, string]> = [ ["platform.api_key", "apiKey", ""], - ["platform.base_url", "baseUrl", ""], + ["platform.email", "userEmail", ""], ["defaults.user_id", "userId", ""], ["defaults.org_id", "orgId", ""], ["defaults.project_id", "projectId", ""], @@ -1145,6 +1128,160 @@ export function registerCliCommands( ); }); + // ==================================================================== + // 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 { + 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 { + 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) // ==================================================================== @@ -1156,20 +1293,21 @@ export function registerCliCommands( .action((opts: { json?: boolean }) => { const commands = { memory: { - add: "Add a memory from text, messages, or stdin", 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", - history: "View edit history of a memory", + import: "Import memories from a JSON file", }, management: { init: "Interactive setup wizard for mem0 CLI", status: "Check connectivity and authentication", - help: "Show help. Use --json for machine-readable output (for LLM agents)", 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)", }, }; diff --git a/openclaw/config.ts b/openclaw/config.ts index 60329157c..2b16ba2ac 100644 --- a/openclaw/config.ts +++ b/openclaw/config.ts @@ -151,6 +151,7 @@ const ALLOWED_KEYS = [ "apiKey", "baseUrl", "userId", + "userEmail", "orgId", "projectId", "autoCapture", diff --git a/openclaw/fs-safe.ts b/openclaw/fs-safe.ts index c12482d7a..a5f85cc06 100644 --- a/openclaw/fs-safe.ts +++ b/openclaw/fs-safe.ts @@ -34,3 +34,10 @@ export function mkdirp(dirPath: string, mode?: number): void { export function unlink(filePath: string): void { fs.unlinkSync(filePath); } + +export function bootstrapTelemetryFlag(): void { + const val = process.env.MEM0_TELEMETRY; + if (val !== undefined) { + (globalThis as any).__mem0_telemetry_override = val; + } +} diff --git a/openclaw/index.ts b/openclaw/index.ts index df06665fb..ef6668ca9 100644 --- a/openclaw/index.ts +++ b/openclaw/index.ts @@ -5,8 +5,8 @@ * and the open-source self-hosted SDK. Uses the official `mem0ai` package. * * Features: - * - 7 core tools: memory_search, memory_add, memory_get, memory_list, - * memory_update, memory_delete, memory_history + * - 6 core tools: memory_search, memory_add, memory_get, memory_list, + * memory_update, memory_delete * - Short-term (session-scoped) and long-term (user-scoped) memory * - Auto-recall: injects relevant memories (both scopes) before each agent turn * - Auto-capture: stores key facts scoped to the current session after each agent turn @@ -56,6 +56,9 @@ import { readPluginAuth } from "./cli/config-file.ts"; import { registerAllTools } from "./tools/index.ts"; import type { ToolDeps } from "./tools/index.ts"; import { captureEvent } from "./telemetry.ts"; +import { bootstrapTelemetryFlag } from "./fs-safe.ts"; + +bootstrapTelemetryFlag(); // ============================================================================ // Re-exports (for tests and external consumers) diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index 11e934f23..ce71b1786 100644 --- a/openclaw/openclaw.plugin.json +++ b/openclaw/openclaw.plugin.json @@ -7,7 +7,7 @@ "contracts": { "tools": [ "memory_search", "memory_add", "memory_get", "memory_list", - "memory_update", "memory_delete", "memory_history" + "memory_update", "memory_delete" ] }, "providerAuthEnvVars": { @@ -26,12 +26,6 @@ "placeholder": "m0-...", "help": "Platform mode only. Use a SecretRef ({\"source\":\"env\",\"provider\":\"default\",\"id\":\"MEM0_API_KEY\"}) or ${MEM0_API_KEY} instead of storing the key directly." }, - "baseUrl": { - "label": "API Base URL", - "placeholder": "https://api.mem0.ai", - "advanced": true, - "help": "Override the Mem0 API base URL. Only needed for custom deployments." - }, "userId": { "label": "Default User ID", "placeholder": "default", @@ -109,10 +103,10 @@ "apiKey": { "type": "string" }, - "baseUrl": { + "userId": { "type": "string" }, - "userId": { + "userEmail": { "type": "string" }, "orgId": { diff --git a/openclaw/telemetry.ts b/openclaw/telemetry.ts index 21094891e..85440a831 100644 --- a/openclaw/telemetry.ts +++ b/openclaw/telemetry.ts @@ -22,9 +22,21 @@ const FLUSH_THRESHOLD = 10; let eventQueue: Record[] = []; let flushTimer: ReturnType | undefined; +let _telemetryEnabled: boolean | undefined; function isTelemetryEnabled(): boolean { - const val = (process.env.MEM0_TELEMETRY ?? "true").toLowerCase(); - return val !== "false" && val !== "0" && val !== "no"; + if (_telemetryEnabled !== undefined) return _telemetryEnabled; + try { + const val = (globalThis as any).__mem0_telemetry_override; + if (val !== undefined) { + const s = String(val).toLowerCase(); + _telemetryEnabled = s !== "false" && s !== "0" && s !== "no"; + } else { + _telemetryEnabled = true; + } + } catch { + _telemetryEnabled = true; + } + return _telemetryEnabled; } /** diff --git a/openclaw/tools/index.ts b/openclaw/tools/index.ts index db4130667..d82e8db42 100644 --- a/openclaw/tools/index.ts +++ b/openclaw/tools/index.ts @@ -7,7 +7,6 @@ import { createMemoryGetTool } from "./memory-get.ts"; import { createMemoryListTool } from "./memory-list.ts"; import { createMemoryUpdateTool } from "./memory-update.ts"; import { createMemoryDeleteTool } from "./memory-delete.ts"; -import { createMemoryHistoryTool } from "./memory-history.ts"; export interface ToolDeps { api: OpenClawPluginApi; @@ -32,5 +31,4 @@ export function registerAllTools(deps: ToolDeps): void { api.registerTool(createMemoryListTool(deps)); api.registerTool(createMemoryUpdateTool(deps)); api.registerTool(createMemoryDeleteTool(deps)); - api.registerTool(createMemoryHistoryTool(deps)); } diff --git a/openclaw/tools/memory-history.ts b/openclaw/tools/memory-history.ts deleted file mode 100644 index ae8c797b9..000000000 --- a/openclaw/tools/memory-history.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Type } from "@sinclair/typebox"; -import type { ToolDeps } from "./index.ts"; - -export function createMemoryHistoryTool(deps: ToolDeps) { - const { provider } = deps; - - return { - name: "memory_history", - label: "Memory History", - description: "View the edit history of a specific memory.", - parameters: Type.Object({ - memoryId: Type.String({ description: "The memory ID to view history for" }), - }), - - async execute(_toolCallId: string, params: Record) { - const { memoryId } = params as { memoryId: string }; - const start = Date.now(); - try { - const history = await provider.history(memoryId); - deps.captureToolEvent("memory_history", { success: true, latency_ms: Date.now() - start, result_count: history?.length ?? 0 }); - if (!history || history.length === 0) { - return { content: [{ type: "text", text: `No history found for memory ${memoryId}.` }], details: { count: 0 } }; - } - const text = history.map((h, i) => - `${i + 1}. [${h.event}] ${h.created_at}\n Old: ${h.old_memory || "(none)"}\n New: ${h.new_memory || "(none)"}` - ).join("\n\n"); - return { - content: [{ type: "text", text: `History for memory ${memoryId} (${history.length} entries):\n\n${text}` }], - details: { count: history.length, history }, - }; - } catch (err) { - deps.captureToolEvent("memory_history", { success: false, latency_ms: Date.now() - start, error: String(err) }); - return { content: [{ type: "text", text: `Memory history failed: ${String(err)}` }], details: { error: String(err) } }; - } - }, - }; -}