From b13748c446954267a6a53633ddc3733d8621eed9 Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 6 Apr 2026 16:54:27 +0530 Subject: [PATCH] feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704) --- openclaw/CHANGELOG.md | 8 +- openclaw/README.md | 104 ++++---- openclaw/cli/commands.ts | 331 ++++++++++++++++++++------ openclaw/config.ts | 1 + openclaw/fs-safe.ts | 7 + openclaw/index.ts | 33 ++- openclaw/openclaw-plugin-sdk.d.ts | 33 ++- openclaw/openclaw.plugin.json | 12 +- openclaw/providers.ts | 80 +++++-- openclaw/skills/memory-dream/SKILL.md | 3 - openclaw/telemetry.ts | 16 +- openclaw/test-shims/plugin-entry.ts | 7 + openclaw/test-shims/plugin-sdk.ts | 19 ++ openclaw/tests/cli-commands.test.ts | 249 +++++++++++++++---- openclaw/tests/fs-safe.test.ts | 37 +++ openclaw/tests/telemetry.test.ts | 65 +++++ openclaw/tests/tools.test.ts | 124 +--------- openclaw/tools/index.ts | 8 +- openclaw/tools/memory-event-list.ts | 60 +++++ openclaw/tools/memory-event-status.ts | 63 +++++ openclaw/tools/memory-history.ts | 37 --- openclaw/tsconfig.json | 2 +- openclaw/tsup.config.ts | 2 +- openclaw/vitest.config.ts | 18 ++ 24 files changed, 924 insertions(+), 395 deletions(-) create mode 100644 openclaw/test-shims/plugin-entry.ts create mode 100644 openclaw/test-shims/plugin-sdk.ts create mode 100644 openclaw/tests/fs-safe.test.ts create mode 100644 openclaw/tests/telemetry.test.ts create mode 100644 openclaw/tools/memory-event-list.ts create mode 100644 openclaw/tools/memory-event-status.ts delete mode 100644 openclaw/tools/memory-history.ts create mode 100644 openclaw/vitest.config.ts 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/README.md b/openclaw/README.md index a815a624f..b2826d372 100644 --- a/openclaw/README.md +++ b/openclaw/README.md @@ -12,7 +12,7 @@ openclaw plugins install @mem0/openclaw-mem0 ### Platform (Mem0 Cloud) -Get an API key from [app.mem0.ai](https://app.mem0.ai): +Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys): ```bash openclaw mem0 init --api-key --user-id @@ -32,7 +32,9 @@ Or configure manually in `openclaw.json`: ### Open-Source (Self-hosted) -No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. +No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Vectors are stored locally in SQLite at `~/.mem0/vector_store.db` — no external database required. + +Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction. ```json5 "openclaw-mem0": { @@ -53,7 +55,7 @@ Customize the embedder, vector store, or LLM via the `oss` block: "oss": { "embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } }, "vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } }, - "llm": { "provider": "openai", "config": { "model": "gpt-4o" } } + "llm": { "provider": "openai", "config": { "model": "gpt-5.4" } } } } ``` @@ -74,79 +76,57 @@ Both run silently. No prompting, no manual calls required. ### Memory Scopes -| Scope | Description | -|-------|-------------| -| **Session (short-term)** | Memories scoped to the current conversation via `run_id`. Automatically recalled alongside long-term memories. | -| **User (long-term)** | Persistent memories that span all sessions. Stored via `memory_add` with `longTerm: true` (the default). | - -During auto-recall, both scopes are searched and presented separately — long-term first, then session — so the agent has full context. +- **Session (short-term)** — Scoped to the current conversation via `run_id`. Recalled alongside long-term memories. +- **User (long-term)** — Persistent across all sessions. Default for `memory_add`. ### Multi-Agent Isolation -In multi-agent setups, each agent gets its own memory namespace automatically. Session keys matching `agent::` route memories to `userId:agent:`. Single-agent deployments are unaffected. - -All memory tools accept an optional `agentId` parameter for cross-agent queries: - -``` -memory_search({ query: "user's tech stack", agentId: "researcher" }) -``` +Each agent gets its own memory namespace automatically via session key routing (`agent::` maps to `userId:agent:`). Single-agent setups are unaffected. ## Agent Tools -Seven tools are available to the agent during conversations: +Eight tools are registered for agent use: | Tool | Description | -|------|-------------| -| **`memory_search`** | Search memories by natural language query. Supports `scope` (`session`, `long-term`, `all`) and `agentId` filtering. | -| **`memory_add`** | Save a fact to memory. Supports `category`, `importance`, `longTerm`, and `agentId`. | -| **`memory_get`** | Retrieve a specific memory by ID. | -| **`memory_list`** | List stored memories with optional `userId`, `agentId`, and `limit` filters. | -| **`memory_update`** | Update an existing memory's text in place. Preserves edit history. | -| **`memory_delete`** | Delete by ID, search query, or bulk (`all: true`). Requires `confirm: true` for bulk. | -| **`memory_history`** | View the edit history of a specific memory. | +| ---- | ----------- | +| `memory_search` | Search by natural language query. Supports `scope` (`session`, `long-term`, `all`), `categories`, `filters`, and `agentId`. | +| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `longTerm`, `metadata`. | +| `memory_get` | Retrieve a single memory by ID. | +| `memory_list` | List all memories. Filter by `userId`, `agentId`, `scope`. | +| `memory_update` | Update a memory's text in place. Preserves history. | +| `memory_delete` | Delete by `memoryId`, `query` (search-and-delete), or `all: true` (requires `confirm: true`). | +| `memory_event_list` | List recent background processing events. Platform mode only. | +| `memory_event_status` | Get status of a specific event by ID. Platform mode only. | ## CLI -All commands follow the pattern `openclaw mem0 `. - -### Memory Operations +All commands: `openclaw mem0 `. ```bash -# Add a memory +# Memory operations openclaw mem0 add "User prefers TypeScript over JavaScript" - -# Search memories openclaw mem0 search "what languages does the user know" openclaw mem0 search "preferences" --scope long-term -openclaw mem0 search "context" --scope session - -# Get, list, update, delete openclaw mem0 get openclaw mem0 list --user-id alice --top-k 20 openclaw mem0 update "Updated preference text" openclaw mem0 delete openclaw mem0 delete --all --user-id alice --confirm +openclaw mem0 import memories.json -# View edit history -openclaw mem0 history -``` - -### Management - -```bash -# Authenticate and configure +# Management openclaw mem0 init openclaw mem0 init --api-key --user-id alice - -# Check connectivity openclaw mem0 status - -# Manage configuration openclaw mem0 config show openclaw mem0 config get api_key openclaw mem0 config set user_id alice -# Memory consolidation (review, merge, prune) +# Events (platform only) +openclaw mem0 event list +openclaw mem0 event status + +# Memory consolidation openclaw mem0 dream openclaw mem0 dream --dry-run ``` @@ -156,9 +136,9 @@ openclaw mem0 dream --dry-run ### General | Key | Type | Default | Description | -|-----|------|---------|-------------| +| --- | ---- | ------- | ----------- | | `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode | -| `userId` | `string` | `"default"` | Unique identifier for the user. You define this — it's not found in any dashboard. All memories are scoped to this value. | +| `userId` | `string` | OS username | User identifier. All memories scoped to this value. | | `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn | | `autoCapture` | `boolean` | `true` | Extract and store facts after each turn | | `topK` | `number` | `5` | Max memories returned per recall | @@ -167,32 +147,30 @@ openclaw mem0 dream --dry-run ### Platform Mode | Key | Type | Default | Description | -|-----|------|---------|-------------| +| --- | ---- | ------- | ----------- | | `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) | | `orgId` | `string` | — | Organization ID | | `projectId` | `string` | — | Project ID | -| `enableGraph` | `boolean` | `false` | Enable entity graph for relationship tracking | -| `customInstructions` | `string` | *(built-in)* | Custom extraction rules for what to store and how to format | -| `customCategories` | `object` | *(12 defaults)* | Category name to description map for memory tagging | +| `enableGraph` | `boolean` | `false` | Entity graph for relationship tracking | +| `customInstructions` | `string` | *(built-in)* | Custom extraction rules | +| `customCategories` | `object` | *(12 defaults)* | Category name to description map | ### Open-Source Mode -All fields below are optional. Defaults use OpenAI embeddings, in-memory vector store, and OpenAI LLM. +All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM. | Key | Type | Default | Description | -|-----|------|---------|-------------| -| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing | +| --- | ---- | ------- | ----------- | +| `customPrompt` | `string` | *(built-in)* | Extraction prompt | | `oss.embedder.provider` | `string` | `"openai"` | Embedding provider | | `oss.embedder.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) | -| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider | -| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`) | +| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider (see list above) | +| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`, `dbPath`) | | `oss.llm.provider` | `string` | `"openai"` | LLM provider | | `oss.llm.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) | -| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history | -| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization | - -Supported providers: `openai`, `anthropic`, `ollama`, `lmstudio`, `qdrant`, `chroma`, and more. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for the full list. +| `oss.historyDbPath` | `string` | — | SQLite path for edit history | +| `oss.disableHistory` | `boolean` | `false` | Skip history DB | ## License -Apache 2.0 +[Apache 2.0](LICENSE) diff --git a/openclaw/cli/commands.ts b/openclaw/cli/commands.ts index eaa39451f..015279e3a 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( @@ -545,7 +548,7 @@ export function registerCliCommands( "\n Skipped. You can add it later via:", ); console.log( - " openclaw mem0 config set oss.embedder.config.apiKey ", + " openclaw mem0 config set embedder_key ", ); console.log( " Or set OPENAI_API_KEY in your environment.\n", @@ -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) // ==================================================================== @@ -967,6 +949,7 @@ export function registerCliCommands( const CONFIG_KEYS: Record = { // Short aliases (matches Python CLI) api_key: "apiKey", + email: "userEmail", base_url: "baseUrl", user_id: "userId", org_id: "orgId", @@ -976,30 +959,34 @@ export function registerCliCommands( auto_capture: "autoCapture", top_k: "topK", mode: "mode", - // Dot notation (matches Python CLI: platform.api_key, defaults.user_id) - "platform.api_key": "apiKey", - "platform.base_url": "baseUrl", - "defaults.user_id": "userId", - "defaults.org_id": "orgId", - "defaults.project_id": "projectId", - "defaults.enable_graph": "enableGraph", - "defaults.auto_recall": "autoRecall", - "defaults.auto_capture": "autoCapture", - "defaults.top_k": "topK", + embedder_provider: "oss.embedder.provider", + embedder_model: "oss.embedder.config.model", + embedder_key: "oss.embedder.config.apiKey", + llm_provider: "oss.llm.provider", + llm_model: "oss.llm.config.model", + llm_key: "oss.llm.config.apiKey", + vector_provider: "oss.vectorStore.provider", + vector_host: "oss.vectorStore.config.host", + vector_port: "oss.vectorStore.config.port", + collection_name: "oss.vectorStore.config.collectionName", + vector_db_path: "oss.vectorStore.config.dbPath", + history_db_path: "oss.historyDbPath", + disable_history: "oss.disableHistory", }; // Keys that contain secrets — redact in show/get output - const SECRET_KEYS = new Set(["apiKey"]); + const SECRET_KEYS = new Set(["apiKey", "oss.embedder.config.apiKey", "oss.llm.config.apiKey"]); // Boolean config fields — coerce "true"/"1"/"yes" on set const BOOLEAN_KEYS = new Set([ "enableGraph", "autoRecall", "autoCapture", + "oss.disableHistory", ]); // Integer config fields — coerce to number on set - const INTEGER_KEYS = new Set(["topK"]); + const INTEGER_KEYS = new Set(["topK", "oss.vectorStore.config.port"]); /** Resolve a user-facing key to the internal camelCase field name. */ function resolveConfigKey(key: string): string | null { @@ -1008,6 +995,15 @@ export function registerCliCommands( /** Read a config value by internal field name. */ function getConfigValue(field: string): unknown { + if (field.startsWith("oss.")) { + const parts = field.split("."); + let current: unknown = cfg.oss; + for (let i = 1; i < parts.length && current != null; i++) { + current = (current as Record)[parts[i]]; + } + return current; + } + const auth = readPluginAuth(); const values: Record = { apiKey: auth.apiKey ?? cfg.apiKey, @@ -1016,6 +1012,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, @@ -1045,20 +1042,37 @@ export function registerCliCommands( .command("show") .description("Show current configuration") .action(() => { - // Display order matching Python CLI: platform first, then defaults - const entries: Array<[string, string, string]> = [ - ["platform.api_key", "apiKey", ""], - ["platform.base_url", "baseUrl", ""], - ["defaults.user_id", "userId", ""], - ["defaults.org_id", "orgId", ""], - ["defaults.project_id", "projectId", ""], - ["defaults.enable_graph", "enableGraph", ""], - ["defaults.auto_recall", "autoRecall", ""], - ["defaults.auto_capture", "autoCapture", ""], - ["defaults.top_k", "topK", ""], - ["mode", "mode", ""], + // Display order: general first, then mode-specific + const entries: Array<[string, string]> = [ + ["mode", "mode"], + ["user_id", "userId"], + ["auto_recall", "autoRecall"], + ["auto_capture", "autoCapture"], + ["top_k", "topK"], ]; + if (cfg.mode === "platform") { + entries.push( + ["api_key", "apiKey"], + ["email", "userEmail"], + ["org_id", "orgId"], + ["project_id", "projectId"], + ["enable_graph", "enableGraph"], + ); + } else { + entries.push( + ["embedder_provider", "oss.embedder.provider"], + ["embedder_model", "oss.embedder.config.model"], + ["embedder_key", "oss.embedder.config.apiKey"], + ["llm_provider", "oss.llm.provider"], + ["llm_model", "oss.llm.config.model"], + ["llm_key", "oss.llm.config.apiKey"], + ["vector_provider", "oss.vectorStore.provider"], + ["history_db_path", "oss.historyDbPath"], + ["disable_history", "oss.disableHistory"], + ); + } + // Calculate column widths const maxKeyLen = Math.max( ...entries.map(([k]) => k.length), @@ -1086,17 +1100,21 @@ export function registerCliCommands( console.log(" openclaw mem0 config set "); console.log(""); console.log(" Examples:"); - console.log(" openclaw mem0 config set mode open-source"); - console.log(" openclaw mem0 config set mode platform"); - console.log(" openclaw mem0 config set auto_recall false"); - console.log(" openclaw mem0 config set top_k 10"); + if (cfg.mode === "platform") { + console.log(" openclaw mem0 config set mode open-source"); + console.log(" openclaw mem0 config set auto_recall false"); + } else { + console.log(" openclaw mem0 config set vector_provider qdrant"); + console.log(" openclaw mem0 config set llm_model gpt-4o"); + console.log(" openclaw mem0 config set embedder_provider openai"); + } console.log(""); }); configCmd .command("get") .description("Get a config value") - .argument("", "Config key (e.g. user_id, platform.api_key)") + .argument("", "Config key (e.g. user_id, api_key, llm_model)") .action((key: string) => { const field = resolveConfigKey(key); if (!field) { @@ -1112,7 +1130,7 @@ export function registerCliCommands( configCmd .command("set") .description("Set a config value") - .argument("", "Config key (e.g. user_id, platform.api_key)") + .argument("", "Config key (e.g. user_id, api_key, llm_model)") .argument("", "New value") .action((key: string, rawValue: string) => { const field = resolveConfigKey(key); @@ -1139,12 +1157,171 @@ export function registerCliCommands( value = parsed; } - writePluginAuth({ [field]: value } as PluginAuthConfig); + // Nested OSS fields use dot-path writer; flat fields use auth writer + if (field.startsWith("oss.")) { + writePluginConfigField(field.split("."), value); + } else { + writePluginAuth({ [field]: value } as PluginAuthConfig); + } console.log( `${key} = ${displayValue(field, value)}`, ); }); + // ==================================================================== + // import (matches: mem0 import ) + // ==================================================================== + + mem0 + .command("import") + .description("Import memories from a JSON file") + .argument("", "Path to JSON file containing memories") + .option("--user-id ", "Override user ID for all imported memories") + .option("--agent-id ", "Override agent ID for all imported memories") + .action( + async ( + file: string, + opts: { userId?: string; agentId?: string }, + ) => { + try { + let data: unknown; + try { + data = JSON.parse(readText(file)); + } catch (err) { + console.error(`Failed to read file: ${String(err)}`); + return; + } + + const items = Array.isArray(data) ? data : [data]; + let added = 0; + let failed = 0; + + for (const item of items) { + const content = + item?.memory ?? item?.text ?? item?.content ?? ""; + if (!content) { + failed++; + continue; + } + try { + await backend.add(content, undefined, { + userId: opts.userId ?? item?.user_id ?? cfg.userId, + agentId: opts.agentId ?? item?.agent_id, + metadata: item?.metadata, + }); + added++; + } catch { + failed++; + } + } + + console.log(`Imported ${added} memories.`); + if (failed) { + console.error(`${failed} memories failed to import.`); + } + } catch (err) { + console.error(`Import failed: ${String(err)}`); + } + }, + ); + + // ==================================================================== + // event (matches: mem0 event list, mem0 event status ) + // ==================================================================== + + const eventCmd = mem0 + .command("event") + .description("Manage background processing events"); + + eventCmd + .command("list") + .description("List recent background events") + .action(async () => { + try { + 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 +1333,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)", }, }; @@ -1283,9 +1461,8 @@ export function registerCliCommands( }); }, { - commands: ["mem0"], descriptors: [ - { name: "mem0", description: "Mem0 memory plugin commands" }, + { name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true }, ], }, ); 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..32e3648cb 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 @@ -16,6 +16,7 @@ * - Dual mode: platform or open-source (self-hosted) */ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { @@ -56,6 +57,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) @@ -87,12 +91,10 @@ export { createProvider } from "./providers.ts"; // Plugin Definition // ============================================================================ -const memoryPlugin = { +const memoryPlugin = definePluginEntry({ id: "openclaw-mem0", name: "Memory (Mem0)", description: "Mem0 memory backend — Mem0 platform or self-hosted open-source", - kind: "memory" as const, - configSchema: mem0ConfigSchema, register(api: OpenClawPluginApi) { // Read auth from openclaw.json plugin config (picks up post-startup login). @@ -231,6 +233,7 @@ const memoryPlugin = { api, provider, cfg, + backend, resolveUserId: _resolveUserId, effectiveUserId: _effectiveUserId, agentUserId: _agentUserId, @@ -298,7 +301,7 @@ const memoryPlugin = { }, }); }, -}; +}); // ============================================================================ // Lifecycle Hook Registration @@ -579,11 +582,11 @@ function registerHooks( // Track last seen session ID to detect actual new sessions (not every turn) let lastRecallSessionId: string | undefined; - // Auto-recall: inject relevant memories before agent starts + // Auto-recall: inject relevant memories before prompt is built if (cfg.autoRecall) { const RECALL_TIMEOUT_MS = 8_000; - api.on("before_agent_start", async (event: any, ctx: any) => { + api.on("before_prompt_build", async (event: any, ctx: any) => { if (!event.prompt || event.prompt.length < 5) return; // Skip non-interactive triggers (cron, heartbeat, automation) @@ -772,6 +775,20 @@ function registerHooks( // Update shared state for tools (best-effort — tools don't have ctx) if (sessionId) session.setCurrentSessionId(sessionId); + const MEMORY_MUTATE_TOOLS = new Set(["memory_add", "memory_update", "memory_delete"]); + const agentUsedMemoryTool = event.messages.some((msg: any) => { + if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false; + return msg.content.some( + (block: any) => block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name), + ); + }); + if (agentUsedMemoryTool) { + api.logger.info( + "openclaw-mem0: skipping auto-capture — agent already used memory tools this turn", + ); + return; + } + // --- Build capture payload synchronously (cheap), then fire-and-forget --- // Patterns indicating an assistant message contains a summary of diff --git a/openclaw/openclaw-plugin-sdk.d.ts b/openclaw/openclaw-plugin-sdk.d.ts index c1719fac8..95d923fb3 100644 --- a/openclaw/openclaw-plugin-sdk.d.ts +++ b/openclaw/openclaw-plugin-sdk.d.ts @@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" { }; resolvePath(p: string): string; registerTool( - definition: Record, - metadata?: Record, + definition: { + name: string; + description: string; + parameters: unknown; + execute: ( + toolCallId: string, + params: Record, + ) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>; + [key: string]: unknown; + }, + metadata?: { optional?: boolean; [key: string]: unknown }, ): void; on(event: string, handler: (event: any, ctx: any) => any): void; registerCli( handler: (context: { program: any }) => void, options?: Record, ): void; + registerCommand?(definition: Record): void; registerService(service: { id: string; - start: () => void; + start: (...args: any[]) => void; stop: () => void; }): void; [key: string]: unknown; } } + +declare module "openclaw/plugin-sdk/plugin-entry" { + import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; + + export interface PluginEntry { + id: string; + name: string; + description?: string; + register(api: OpenClawPluginApi): void; + } + + export function definePluginEntry(entry: T): T; +} + +declare module "openclaw/plugin-sdk/core" { + export * from "openclaw/plugin-sdk"; +} diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index 11e934f23..70f56d6f1 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", "memory_event_list", "memory_event_status" ] }, "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/providers.ts b/openclaw/providers.ts index ace456270..22a56226c 100644 --- a/openclaw/providers.ts +++ b/openclaw/providers.ts @@ -81,6 +81,7 @@ class PlatformProvider implements Mem0Provider { constructor( private readonly apiKey: string, + private readonly baseUrl?: string, private readonly orgId?: string, private readonly projectId?: string, ) {} @@ -97,11 +98,17 @@ class PlatformProvider implements Mem0Provider { private async _init(): Promise { const { default: MemoryClient } = await import("mem0ai"); - const opts: { apiKey: string; org_id?: string; project_id?: string } = { + const opts: { + apiKey: string; + host?: string; + organizationId?: string; + projectId?: string; + } = { apiKey: this.apiKey, }; - if (this.orgId) opts.org_id = this.orgId; - if (this.projectId) opts.project_id = this.projectId; + if (this.baseUrl) opts.host = this.baseUrl; + if (this.orgId) opts.organizationId = this.orgId; + if (this.projectId) opts.projectId = this.projectId; this.client = new MemoryClient(opts); } @@ -133,20 +140,11 @@ class PlatformProvider implements Mem0Provider { async search(query: string, options: SearchOptions): Promise { await this.ensureClient(); - // Base filters: always scope by user_id, optionally by run_id - const baseFilters: Record = { user_id: options.user_id }; - if (options.run_id) baseFilters.run_id = options.run_id; - - // Merge agent-provided filters (created_at ranges, metadata, etc.) - // with base filters. Agent filters extend, never override user scoping. - const mergedFilters = options.filters - ? { AND: [baseFilters, options.filters] } - : baseFilters; - const opts: Record = { api_version: "v2", - filters: mergedFilters, + user_id: options.user_id, }; + if (options.run_id) opts.run_id = options.run_id; if (options.top_k != null) opts.top_k = options.top_k; if (options.threshold != null) opts.threshold = options.threshold; if (options.keyword_search != null) @@ -155,6 +153,14 @@ class PlatformProvider implements Mem0Provider { if (options.filter_memories != null) opts.filter_memories = options.filter_memories; if (options.categories != null) opts.categories = options.categories; + const baseFilters: Record = { user_id: options.user_id }; + if (options.run_id) baseFilters.run_id = options.run_id; + + if (options.filters) { + opts.filters = { AND: [baseFilters, options.filters] }; + } else { + opts.filters = baseFilters; + } const results = await this.client.search(query, opts); return normalizeSearchResults(results); @@ -168,10 +174,16 @@ class PlatformProvider implements Mem0Provider { async getAll(options: ListOptions): Promise { await this.ensureClient(); - const opts: Record = { user_id: options.user_id }; - if (options.run_id) opts.run_id = options.run_id; + const opts: Record = { + api_version: "v2", + user_id: options.user_id, + filters: { user_id: options.user_id }, + }; + if (options.run_id) { + opts.run_id = options.run_id; + (opts.filters as Record).run_id = options.run_id; + } if (options.page_size != null) opts.page_size = options.page_size; - if (options.source) opts.source = options.source; const results = await this.client.getAll(opts); if (Array.isArray(results)) return results.map(normalizeMemoryItem); @@ -242,10 +254,38 @@ class OSSProvider implements Mem0Provider { const config: Record = { version: "v1.1" }; - if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder; + const defaultEmbedder = { provider: "openai", config: { model: "text-embedding-3-small" } }; + const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } }; + + // Helper: strip empty-string values so they don't clobber defaults + const stripEmpty = (obj: Record) => { + const out = { ...obj }; + for (const k of Object.keys(out)) { if (out[k] === "") delete out[k]; } + return out; + }; + + if (this.ossConfig?.embedder) { + const ec = stripEmpty(this.ossConfig.embedder.config ?? {}); + config.embedder = { + provider: this.ossConfig.embedder.provider || defaultEmbedder.provider, + config: { ...defaultEmbedder.config, ...ec }, + }; + } else { + config.embedder = defaultEmbedder; + } + + if (this.ossConfig?.llm) { + const lc = stripEmpty(this.ossConfig.llm.config ?? {}); + config.llm = { + provider: this.ossConfig.llm.provider || defaultLlm.provider, + config: { ...defaultLlm.config, ...lc }, + }; + } else { + config.llm = defaultLlm; + } + if (this.ossConfig?.vectorStore) config.vectorStore = this.ossConfig.vectorStore; - if (this.ossConfig?.llm) config.llm = this.ossConfig.llm; if (this.ossConfig?.historyDbPath) { const dbPath = this.resolvePath @@ -419,7 +459,7 @@ export function createProvider( ); } - return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId); + return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId); } // ============================================================================ diff --git a/openclaw/skills/memory-dream/SKILL.md b/openclaw/skills/memory-dream/SKILL.md index 4d6a54eff..651bdcd83 100644 --- a/openclaw/skills/memory-dream/SKILL.md +++ b/openclaw/skills/memory-dream/SKILL.md @@ -34,9 +34,6 @@ Identify which memories need action. Use the tools to investigate. **Search for recent additions:** Call `memory_search` with a `created_at` filter to find memories added since the last consolidation. These are the most likely to need merging or cleanup. -**Check edit history on suspicious entries:** -If a memory looks like it was updated multiple times or has contradictory content, call `memory_history` on it to understand how it evolved. - **Classify each target into one of these actions:** - DELETE: contains credentials, expired by TTL, pure noise, raw tool output, standalone timestamps - MERGE: two or more memories express the same fact in different words, or a series tracks incremental changes to the same entity 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/test-shims/plugin-entry.ts b/openclaw/test-shims/plugin-entry.ts new file mode 100644 index 000000000..c7c2a8938 --- /dev/null +++ b/openclaw/test-shims/plugin-entry.ts @@ -0,0 +1,7 @@ +/** + * Test shim for openclaw/plugin-sdk/plugin-entry. + * At runtime this is resolved from the OpenClaw gateway. + */ +export function definePluginEntry(entry: T): T { + return entry; +} diff --git a/openclaw/test-shims/plugin-sdk.ts b/openclaw/test-shims/plugin-sdk.ts new file mode 100644 index 000000000..5ef59417c --- /dev/null +++ b/openclaw/test-shims/plugin-sdk.ts @@ -0,0 +1,19 @@ +/** + * Test shim for openclaw/plugin-sdk. + * At runtime this is resolved from the OpenClaw gateway. + */ +export interface OpenClawPluginApi { + pluginConfig: Record; + logger: { + info(msg: string): void; + warn(msg: string): void; + error(msg: string): void; + debug(msg: string): void; + }; + resolvePath(p: string): string; + registerTool(definition: Record, metadata?: Record): void; + on(event: string, handler: (event: any, ctx: any) => any): void; + registerCli(handler: (context: { program: any }) => void, options?: Record): void; + registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void; + [key: string]: unknown; +} diff --git a/openclaw/tests/cli-commands.test.ts b/openclaw/tests/cli-commands.test.ts index b8dab9eb4..9233f54d2 100644 --- a/openclaw/tests/cli-commands.test.ts +++ b/openclaw/tests/cli-commands.test.ts @@ -58,6 +58,8 @@ interface MockCommand { _args: Array<{ name: string; desc: string }>; command(name: string): MockCommand; description(desc: string): MockCommand; + configureHelp(opts: any): MockCommand; + hook(event: string, fn: (...args: any[]) => any): MockCommand; option(flags: string, desc: string, defaultVal?: string): MockCommand; argument(name: string, desc: string): MockCommand; action(fn: (...args: any[]) => any): MockCommand; @@ -80,6 +82,12 @@ function createMockCommand(name: string): MockCommand { cmd._description = desc; return cmd; }, + configureHelp(_opts: any) { + return cmd; + }, + hook(_event: string, _fn: (...args: any[]) => any) { + return cmd; + }, option(flags: string, desc: string, defaultVal?: string) { cmd._options.push({ flags, desc, defaultVal }); return cmd; @@ -145,6 +153,18 @@ function createMockBackend() { connected: true, url: "https://api.mem0.ai", }), + add: vi.fn().mockResolvedValue({ id: "new-1" }), + listEvents: vi.fn().mockResolvedValue([ + { id: "evt-1111-2222-3333-4444", event_type: "ADD", status: "SUCCEEDED", latency: 1500, created_at: "2026-04-01T12:00:00Z" }, + ]), + getEvent: vi.fn().mockResolvedValue({ + id: "evt-1111-2222-3333-4444", + event_type: "ADD", + status: "SUCCEEDED", + latency: 1500, + created_at: "2026-04-01T12:00:00Z", + updated_at: "2026-04-01T12:00:01Z", + }), }; } @@ -284,7 +304,6 @@ describe("registerCliCommands", () => { expect(names).toContain("list"); expect(names).toContain("update"); expect(names).toContain("delete"); - expect(names).toContain("history"); expect(names).toContain("status"); expect(names).toContain("config"); expect(names).toContain("dream"); @@ -321,7 +340,6 @@ describe("registerCliCommands", () => { expect(writePluginAuth).toHaveBeenCalledWith( expect.objectContaining({ apiKey: "m0-my-key-1234", - baseUrl: "https://api.mem0.ai", mode: "platform", }), ); @@ -857,48 +875,6 @@ describe("registerCliCommands", () => { }); }); - // ======================================================================== - // history subcommand - // ======================================================================== - - describe("history subcommand", () => { - it("calls provider.history and prints entries as JSON", async () => { - const { mem0, provider } = setup(); - const historyCmd = findCommand(mem0, "history")!; - - await historyCmd._action!("m1"); - - expect(provider.history).toHaveBeenCalledWith("m1"); - expect(consoleSpy.log).toHaveBeenCalledWith( - expect.stringContaining("UPDATE"), - ); - }); - - it("prints message when no history found", async () => { - const { mem0, provider } = setup(); - provider.history.mockResolvedValueOnce([]); - const historyCmd = findCommand(mem0, "history")!; - - await historyCmd._action!("m1"); - - expect(consoleSpy.log).toHaveBeenCalledWith( - "No history found for this memory.", - ); - }); - - it("handles history errors gracefully", async () => { - const { mem0, provider } = setup(); - provider.history.mockRejectedValueOnce(new Error("history boom")); - const historyCmd = findCommand(mem0, "history")!; - - await historyCmd._action!("m1"); - - expect(consoleSpy.error).toHaveBeenCalledWith( - expect.stringContaining("History failed"), - ); - }); - }); - // ======================================================================== // status subcommand // ======================================================================== @@ -964,9 +940,9 @@ describe("registerCliCommands", () => { const allOutput = consoleSpy.log.mock.calls.map((c) => c[0]).join("\n"); expect(allOutput).toContain("Key"); expect(allOutput).toContain("Value"); - expect(allOutput).toContain("platform.api_key"); - expect(allOutput).toContain("platform.base_url"); - expect(allOutput).toContain("defaults.user_id"); + expect(allOutput).toContain("api_key"); + expect(allOutput).toContain("email"); + expect(allOutput).toContain("user_id"); expect(allOutput).toContain("Config file:"); }); }); @@ -1026,14 +1002,14 @@ describe("registerCliCommands", () => { expect(logged).not.toContain("supersecret"); }); - it("supports dot-notation keys like platform.api_key", () => { + it("supports short alias keys like email", () => { const { mem0 } = setup(); const configCmd = findCommand(mem0, "config")!; const getCmd = findCommand(configCmd, "get")!; - getCmd._action!("platform.base_url"); + getCmd._action!("email"); - expect(consoleSpy.log).toHaveBeenCalledWith("https://api.mem0.ai"); + expect(consoleSpy.log).toHaveBeenCalledWith("(not set)"); }); }); @@ -1143,12 +1119,12 @@ describe("registerCliCommands", () => { expect(writePluginAuth).not.toHaveBeenCalled(); }); - it("supports dot-notation keys for set", () => { + it("supports short alias keys for set", () => { const { mem0 } = setup(); const configCmd = findCommand(mem0, "config")!; const setCmd = findCommand(configCmd, "set")!; - setCmd._action!("defaults.user_id", "bob"); + setCmd._action!("user_id", "bob"); expect(writePluginAuth).toHaveBeenCalledWith( expect.objectContaining({ userId: "bob" }), @@ -1268,4 +1244,173 @@ describe("registerCliCommands", () => { ); }); }); + + // ======================================================================== + // import subcommand + // ======================================================================== + + describe("import subcommand", () => { + it("imports memories from a JSON array file", async () => { + const { mem0, backend } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockReturnValueOnce( + JSON.stringify([ + { memory: "fact one" }, + { memory: "fact two" }, + ]), + ); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("memories.json", {}); + + expect(backend.add).toHaveBeenCalledTimes(2); + expect(consoleSpy.log).toHaveBeenCalledWith("Imported 2 memories."); + }); + + it("imports a single JSON object", async () => { + const { mem0, backend } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockReturnValueOnce( + JSON.stringify({ memory: "single fact" }), + ); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("single.json", {}); + + expect(backend.add).toHaveBeenCalledTimes(1); + expect(consoleSpy.log).toHaveBeenCalledWith("Imported 1 memories."); + }); + + it("skips items with no extractable content", async () => { + const { mem0, backend } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockReturnValueOnce( + JSON.stringify([{ memory: "valid" }, { nofield: true }]), + ); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("mixed.json", {}); + + expect(backend.add).toHaveBeenCalledTimes(1); + expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import."); + }); + + it("uses --user-id and --agent-id overrides", async () => { + const { mem0, backend } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockReturnValueOnce( + JSON.stringify([{ memory: "fact" }]), + ); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("f.json", { userId: "override-user", agentId: "agent-1" }); + + expect(backend.add).toHaveBeenCalledWith("fact", undefined, expect.objectContaining({ + userId: "override-user", + agentId: "agent-1", + })); + }); + + it("handles file read errors gracefully", async () => { + const { mem0 } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockImplementationOnce(() => { + throw new Error("ENOENT"); + }); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("missing.json", {}); + + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to read file"), + ); + }); + + it("handles backend.add failures gracefully", async () => { + const { mem0, backend } = setup(); + const { readText } = await import("../fs-safe.ts"); + (readText as ReturnType).mockReturnValueOnce( + JSON.stringify([{ memory: "will fail" }]), + ); + backend.add.mockRejectedValueOnce(new Error("API error")); + const importCmd = findCommand(mem0, "import")!; + + await importCmd._action!("fail.json", {}); + + expect(consoleSpy.log).toHaveBeenCalledWith("Imported 0 memories."); + expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import."); + }); + }); + + // ======================================================================== + // event subcommand + // ======================================================================== + + describe("event subcommand", () => { + it("lists events in table format", async () => { + const { mem0, backend } = setup(); + const eventCmd = findCommand(mem0, "event")!; + const listCmd = findCommand(eventCmd, "list")!; + + await listCmd._action!(); + + expect(backend.listEvents).toHaveBeenCalled(); + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringContaining("evt-1111-2222-3333-4444"), + ); + expect(consoleSpy.log).toHaveBeenCalledWith( + expect.stringContaining("1 event"), + ); + }); + + it("prints message when no events found", async () => { + const { mem0, backend } = setup(); + backend.listEvents.mockResolvedValueOnce([]); + const eventCmd = findCommand(mem0, "event")!; + const listCmd = findCommand(eventCmd, "list")!; + + await listCmd._action!(); + + expect(consoleSpy.log).toHaveBeenCalledWith("No events found."); + }); + + it("handles event list errors gracefully", async () => { + const { mem0, backend } = setup(); + backend.listEvents.mockRejectedValueOnce(new Error("event boom")); + const eventCmd = findCommand(mem0, "event")!; + const listCmd = findCommand(eventCmd, "list")!; + + await listCmd._action!(); + + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to list events"), + ); + }); + + it("shows event status details", async () => { + const { mem0, backend } = setup(); + const eventCmd = findCommand(mem0, "event")!; + const statusCmd = findCommand(eventCmd, "status")!; + + await statusCmd._action!("evt-1111-2222-3333-4444"); + + expect(backend.getEvent).toHaveBeenCalledWith("evt-1111-2222-3333-4444"); + expect(consoleSpy.log).toHaveBeenCalledWith("Event ID: evt-1111-2222-3333-4444"); + expect(consoleSpy.log).toHaveBeenCalledWith("Type: ADD"); + expect(consoleSpy.log).toHaveBeenCalledWith("Status: SUCCEEDED"); + }); + + it("handles event status errors gracefully", async () => { + const { mem0, backend } = setup(); + backend.getEvent.mockRejectedValueOnce(new Error("not found")); + const eventCmd = findCommand(mem0, "event")!; + const statusCmd = findCommand(eventCmd, "status")!; + + await statusCmd._action!("bad-id"); + + expect(consoleSpy.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to get event"), + ); + }); + }); }); diff --git a/openclaw/tests/fs-safe.test.ts b/openclaw/tests/fs-safe.test.ts new file mode 100644 index 000000000..29b6eddfe --- /dev/null +++ b/openclaw/tests/fs-safe.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { bootstrapTelemetryFlag } from "../fs-safe.ts"; + +describe("bootstrapTelemetryFlag", () => { + const originalEnv = process.env.MEM0_TELEMETRY; + + beforeEach(() => { + delete (globalThis as any).__mem0_telemetry_override; + delete process.env.MEM0_TELEMETRY; + }); + + afterEach(() => { + delete (globalThis as any).__mem0_telemetry_override; + if (originalEnv !== undefined) { + process.env.MEM0_TELEMETRY = originalEnv; + } else { + delete process.env.MEM0_TELEMETRY; + } + }); + + it("sets globalThis override when MEM0_TELEMETRY is set", () => { + process.env.MEM0_TELEMETRY = "false"; + bootstrapTelemetryFlag(); + expect((globalThis as any).__mem0_telemetry_override).toBe("false"); + }); + + it("does not set globalThis override when MEM0_TELEMETRY is unset", () => { + bootstrapTelemetryFlag(); + expect((globalThis as any).__mem0_telemetry_override).toBeUndefined(); + }); + + it("passes through truthy values", () => { + process.env.MEM0_TELEMETRY = "true"; + bootstrapTelemetryFlag(); + expect((globalThis as any).__mem0_telemetry_override).toBe("true"); + }); +}); diff --git a/openclaw/tests/telemetry.test.ts b/openclaw/tests/telemetry.test.ts new file mode 100644 index 000000000..23ddd51ef --- /dev/null +++ b/openclaw/tests/telemetry.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Mock config-file before importing telemetry +vi.mock("../cli/config-file.ts", () => ({ + readPluginAuth: vi.fn().mockReturnValue({}), +})); + +import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts"; +import { readPluginAuth } from "../cli/config-file.ts"; + +describe("telemetry", () => { + let fetchSpy: ReturnType; + + beforeEach(() => { + // Reset telemetry enabled state + (globalThis as any).__mem0_telemetry_override = undefined; + fetchSpy = vi.fn().mockResolvedValue({ ok: true }); + vi.stubGlobal("fetch", fetchSpy); + }); + + afterEach(() => { + vi.restoreAllMocks(); + delete (globalThis as any).__mem0_telemetry_override; + }); + + it("exports PLUGIN_VERSION", () => { + expect(PLUGIN_VERSION).toBe("1.0.4"); + }); + + it("captureEvent does not throw", () => { + expect(() => captureEvent("test_event")).not.toThrow(); + }); + + it("captureEvent accepts properties and context", () => { + expect(() => + captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }), + ).not.toThrow(); + }); + + it("captureEvent is silent when telemetry disabled", () => { + (globalThis as any).__mem0_telemetry_override = "false"; + // Force re-evaluation by resetting cached value + // Since _telemetryEnabled is module-level, we test indirectly + expect(() => captureEvent("test_event")).not.toThrow(); + }); + + it("uses userEmail as distinct ID when available", () => { + (readPluginAuth as ReturnType).mockReturnValueOnce({ + userEmail: "test@example.com", + }); + expect(() => captureEvent("test_event")).not.toThrow(); + }); + + it("falls back to anonymous-openclaw when no apiKey", () => { + (readPluginAuth as ReturnType).mockReturnValueOnce({}); + expect(() => captureEvent("test_event", {}, {})).not.toThrow(); + }); + + it("handles readPluginAuth errors gracefully", () => { + (readPluginAuth as ReturnType).mockImplementationOnce(() => { + throw new Error("config read failed"); + }); + expect(() => captureEvent("test_event")).not.toThrow(); + }); +}); diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts index 2285e6832..9515dbf50 100644 --- a/openclaw/tests/tools.test.ts +++ b/openclaw/tests/tools.test.ts @@ -15,7 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts"; import { createMemoryDeleteTool } from "../tools/memory-delete.ts"; import { createMemoryListTool } from "../tools/memory-list.ts"; import { createMemoryUpdateTool } from "../tools/memory-update.ts"; -import { createMemoryHistoryTool } from "../tools/memory-history.ts"; +import { createMemoryEventListTool } from "../tools/memory-event-list.ts"; +import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts"; // --------------------------------------------------------------------------- // Mock helper @@ -80,10 +81,10 @@ function createMockToolDeps(overrides = {}): ToolDeps { // --------------------------------------------------------------------------- describe("registerAllTools", () => { - it("calls api.registerTool exactly 7 times", () => { + it("calls api.registerTool exactly 8 times", () => { const ctx = createMockToolDeps(); registerAllTools(ctx); - expect(ctx.api.registerTool).toHaveBeenCalledTimes(7); + expect(ctx.api.registerTool).toHaveBeenCalledTimes(8); }); it("registers tools with the correct names", () => { @@ -103,7 +104,8 @@ describe("registerAllTools", () => { "memory_list", "memory_update", "memory_delete", - "memory_history", + "memory_event_list", + "memory_event_status", ]); }); @@ -129,6 +131,8 @@ describe("tool factory shape", () => { { fn: createMemoryGetTool, expectedName: "memory_get" }, { fn: createMemoryDeleteTool, expectedName: "memory_delete" }, { fn: createMemoryListTool, expectedName: "memory_list" }, + { fn: createMemoryEventListTool, expectedName: "memory_event_list" }, + { fn: createMemoryEventStatusTool, expectedName: "memory_event_status" }, ]; for (const { fn, expectedName } of factories) { @@ -883,115 +887,3 @@ describe("memory_update execute", () => { }); }); -// --------------------------------------------------------------------------- -// memory_history execute -// --------------------------------------------------------------------------- - -describe("memory_history execute", () => { - it("returns formatted history", async () => { - const historyEntries = [ - { - event: "ADD", - created_at: "2026-01-01T00:00:00Z", - old_memory: null, - new_memory: "Initial memory text", - }, - { - event: "UPDATE", - created_at: "2026-01-02T00:00:00Z", - old_memory: "Initial memory text", - new_memory: "Updated memory text", - }, - ]; - const ctx = createMockToolDeps({ - provider: { - search: vi.fn(), - add: vi.fn(), - getAll: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - get: vi.fn(), - history: vi.fn().mockResolvedValue(historyEntries), - }, - }); - const tool = createMemoryHistoryTool(ctx); - - const result = await tool.execute("call-1", { memoryId: "mem-abc" }); - - expect(ctx.provider!.history).toHaveBeenCalledWith("mem-abc"); - expect(result.content[0].text).toContain("History for memory mem-abc"); - expect(result.content[0].text).toContain("2 entries"); - expect(result.content[0].text).toContain("[ADD]"); - expect(result.content[0].text).toContain("[UPDATE]"); - expect(result.content[0].text).toContain("Initial memory text"); - expect(result.content[0].text).toContain("Updated memory text"); - expect(result.content[0].text).toContain("Old: (none)"); - expect(result.details.count).toBe(2); - expect(result.details.history).toEqual(historyEntries); - }); - - it("handles empty history", async () => { - const ctx = createMockToolDeps({ - provider: { - search: vi.fn(), - add: vi.fn(), - getAll: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - get: vi.fn(), - history: vi.fn().mockResolvedValue([]), - }, - }); - const tool = createMemoryHistoryTool(ctx); - - const result = await tool.execute("call-2", { memoryId: "mem-empty" }); - - expect(result.content[0].text).toBe( - "No history found for memory mem-empty.", - ); - expect(result.details.count).toBe(0); - }); - - it("handles null history", async () => { - const ctx = createMockToolDeps({ - provider: { - search: vi.fn(), - add: vi.fn(), - getAll: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - get: vi.fn(), - history: vi.fn().mockResolvedValue(null), - }, - }); - const tool = createMemoryHistoryTool(ctx); - - const result = await tool.execute("call-3", { memoryId: "mem-null" }); - - expect(result.content[0].text).toBe( - "No history found for memory mem-null.", - ); - expect(result.details.count).toBe(0); - }); - - it("handles errors gracefully", async () => { - const ctx = createMockToolDeps({ - provider: { - search: vi.fn(), - add: vi.fn(), - getAll: vi.fn(), - update: vi.fn(), - delete: vi.fn(), - get: vi.fn(), - history: vi.fn().mockRejectedValue(new Error("history unavailable")), - }, - }); - const tool = createMemoryHistoryTool(ctx); - - const result = await tool.execute("call-4", { memoryId: "mem-err" }); - - expect(result.content[0].text).toContain("Memory history failed"); - expect(result.content[0].text).toContain("history unavailable"); - expect(result.details.error).toContain("history unavailable"); - }); -}); diff --git a/openclaw/tools/index.ts b/openclaw/tools/index.ts index db4130667..14219d4de 100644 --- a/openclaw/tools/index.ts +++ b/openclaw/tools/index.ts @@ -1,5 +1,6 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts"; +import type { Backend } from "../backend/base.ts"; import { createMemorySearchTool } from "./memory-search.ts"; import { createMemoryAddTool } from "./memory-add.ts"; @@ -7,12 +8,14 @@ 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"; +import { createMemoryEventListTool } from "./memory-event-list.ts"; +import { createMemoryEventStatusTool } from "./memory-event-status.ts"; export interface ToolDeps { api: OpenClawPluginApi; provider: Mem0Provider; cfg: Mem0Config; + backend?: Backend; resolveUserId: (opts: { agentId?: string; userId?: string }) => string; effectiveUserId: (sessionKey?: string) => string; agentUserId: (id: string) => string; @@ -32,5 +35,6 @@ export function registerAllTools(deps: ToolDeps): void { api.registerTool(createMemoryListTool(deps)); api.registerTool(createMemoryUpdateTool(deps)); api.registerTool(createMemoryDeleteTool(deps)); - api.registerTool(createMemoryHistoryTool(deps)); + api.registerTool(createMemoryEventListTool(deps)); + api.registerTool(createMemoryEventStatusTool(deps)); } diff --git a/openclaw/tools/memory-event-list.ts b/openclaw/tools/memory-event-list.ts new file mode 100644 index 000000000..37f89b79e --- /dev/null +++ b/openclaw/tools/memory-event-list.ts @@ -0,0 +1,60 @@ +import { Type } from "@sinclair/typebox"; +import type { ToolDeps } from "./index.ts"; + +export function createMemoryEventListTool(deps: ToolDeps) { + return { + name: "memory_event_list", + label: "Memory Event List", + description: + "List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.", + parameters: Type.Object({}), + + async execute(_toolCallId: string, _params: Record) { + const start = Date.now(); + try { + if (!deps.backend) { + deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" }); + return { + content: [{ type: "text", text: "Event tracking is only available in platform mode." }], + details: { error: "not_platform" }, + }; + } + + const results = await deps.backend.listEvents(); + if (!results.length) { + deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 }); + return { + content: [{ type: "text", text: "No events found." }], + details: { count: 0 }, + }; + } + + const rows = results.map((ev) => { + const evId = String(ev.id ?? ""); + const evType = String(ev.event_type ?? "—"); + const status = String(ev.status ?? "—"); + const latency = + typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—"; + const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " "); + return { id: evId, type: evType, status, latency, created }; + }); + + const text = rows + .map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`) + .join("\n"); + + deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length }); + return { + content: [{ type: "text", text: `${results.length} event(s):\n${text}` }], + details: { count: results.length, events: rows }, + }; + } catch (err) { + deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) }); + return { + content: [{ type: "text", text: `Failed to list events: ${String(err)}` }], + details: { error: String(err) }, + }; + } + }, + }; +} diff --git a/openclaw/tools/memory-event-status.ts b/openclaw/tools/memory-event-status.ts new file mode 100644 index 000000000..b11c5bca0 --- /dev/null +++ b/openclaw/tools/memory-event-status.ts @@ -0,0 +1,63 @@ +import { Type } from "@sinclair/typebox"; +import type { ToolDeps } from "./index.ts"; + +export function createMemoryEventStatusTool(deps: ToolDeps) { + return { + name: "memory_event_status", + label: "Memory Event Status", + description: + "Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.", + parameters: Type.Object({ + event_id: Type.String({ description: "The event ID to check" }), + }), + + async execute(_toolCallId: string, params: Record) { + const { event_id: eventId } = params as { event_id: string }; + const start = Date.now(); + try { + if (!deps.backend) { + deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" }); + return { + content: [{ type: "text", text: "Event tracking is only available in platform mode." }], + details: { error: "not_platform" }, + }; + } + + const ev = await deps.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", " "); + + let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`; + + const results = ev.results as Record[] | undefined; + if (results && Array.isArray(results) && results.length) { + const resultLines = results.map((r) => { + const memId = String(r.id ?? "").slice(0, 8); + const data = r.data as Record | undefined; + const memory = data?.memory ?? ""; + const evName = String(r.event ?? ""); + return `- [${evName}] ${memory} (${memId})`; + }); + text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`; + } + + deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start }); + return { + content: [{ type: "text", text }], + details: { event: ev }, + }; + } catch (err) { + deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) }); + return { + content: [{ type: "text", text: `Failed to get event: ${String(err)}` }], + details: { error: String(err) }, + }; + } + }, + }; +} 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) } }; - } - }, - }; -} diff --git a/openclaw/tsconfig.json b/openclaw/tsconfig.json index 1e360fd7a..2292f3295 100644 --- a/openclaw/tsconfig.json +++ b/openclaw/tsconfig.json @@ -19,6 +19,6 @@ "allowImportingTsExtensions": true, "noEmit": true }, - "include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"], + "include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"], "exclude": ["node_modules", "dist", "**/*.test.ts"] } diff --git a/openclaw/tsup.config.ts b/openclaw/tsup.config.ts index f47f6d802..397df11fe 100644 --- a/openclaw/tsup.config.ts +++ b/openclaw/tsup.config.ts @@ -7,5 +7,5 @@ export default defineConfig({ dts: true, sourcemap: true, clean: true, - external: [/^node:/, "fs", "os", "path", "url", "readline", "module"], + external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"], }); diff --git a/openclaw/vitest.config.ts b/openclaw/vitest.config.ts new file mode 100644 index 000000000..94b1bab79 --- /dev/null +++ b/openclaw/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + alias: { + // OpenClaw SDK modules are resolved from the gateway at runtime. + // During unit tests we provide lightweight stubs. + "openclaw/plugin-sdk/plugin-entry": new URL( + "./test-shims/plugin-entry.ts", + import.meta.url, + ).pathname, + "openclaw/plugin-sdk": new URL( + "./test-shims/plugin-sdk.ts", + import.meta.url, + ).pathname, + }, + }, +});