From 4e611e8dbabe218854893bc80eba4e177fd6a59f Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 20 Apr 2026 20:09:45 +0530 Subject: [PATCH] docs: update memory tool list, CLI usage, and config file reading logic (#4861) Co-authored-by: Livia Ellen --- docs/changelog/openclaw.mdx | 13 + docs/integrations/openclaw.mdx | 257 ++++++++++-- mem0-ts/src/oss/src/prompts/index.ts | 146 ++++++- mem0-ts/src/oss/tests/extract-json.test.ts | 81 ++++ openclaw/.gitignore | 2 + openclaw/README.md | 118 +++++- openclaw/backend/base.ts | 3 - openclaw/backend/platform.ts | 3 - openclaw/cli/config-file.ts | 147 ++++++- openclaw/config.ts | 9 +- openclaw/filtering.ts | 77 +++- openclaw/index.ts | 36 +- openclaw/openclaw-plugin-sdk.d.ts | 26 ++ openclaw/openclaw.plugin.json | 16 +- openclaw/package.json | 7 +- openclaw/pnpm-lock.yaml | 456 +++++++++++++++------ openclaw/providers.ts | 116 +++--- openclaw/public-artifacts.ts | 159 +++++++ openclaw/recall.ts | 9 +- openclaw/sqlite-resilience.test.ts | 11 +- openclaw/telemetry.ts | 7 +- openclaw/tests/config-file.test.ts | 8 +- openclaw/tests/config.test.ts | 12 +- openclaw/tests/filtering.test.ts | 96 +++++ openclaw/tests/providers.test.ts | 13 +- openclaw/tests/telemetry.test.ts | 2 +- openclaw/tests/tools.test.ts | 6 +- openclaw/tools/index.ts | 17 +- openclaw/tools/memory-add.ts | 27 +- openclaw/tools/memory-search.ts | 4 +- openclaw/types.ts | 12 +- 31 files changed, 1531 insertions(+), 365 deletions(-) create mode 100644 openclaw/public-artifacts.ts create mode 100644 openclaw/tests/filtering.test.ts diff --git a/docs/changelog/openclaw.mdx b/docs/changelog/openclaw.mdx index f56dffb15..9de66efa7 100644 --- a/docs/changelog/openclaw.mdx +++ b/docs/changelog/openclaw.mdx @@ -4,6 +4,19 @@ description: "Release notes for the OpenClaw plugin and agent harness." mode: "wide" --- + + +**New Features:** +- **Chat-Based Setup:** Added chat-based Platform setup flow — users can now configure the plugin conversationally instead of editing config files manually +- **Installation Docs Rewrite:** Rewrote README and integration docs with chat-first setup, numbered manual steps. + +**Improvements:** +- **SDK Upgrade:** Bumped `mem0ai` dependency to 3.0.1 for V3 API compatibility +- **Config Cleanup:** Dropped deprecated `orgId`, `projectId`, `enableGraph` config options; updated CLI prompts ([#4734](https://github.com/mem0ai/mem0/pull/4734), [#4764](https://github.com/mem0ai/mem0/pull/4764)) +- **Noise Filtering:** Expanded noise patterns in memory add tool; handle leading text in JSON extraction + + + **Bug Fixes:** diff --git a/docs/integrations/openclaw.mdx b/docs/integrations/openclaw.mdx index 1028ce86b..a48088a56 100644 --- a/docs/integrations/openclaw.mdx +++ b/docs/integrations/openclaw.mdx @@ -14,16 +14,37 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents The plugin provides: 1. **Auto-Recall** — Before the agent responds, memories matching the current message are injected into context 2. **Auto-Capture** — After the agent responds, the exchange is sent to Mem0 which decides what's worth keeping -3. **Agent Tools** — Five tools for explicit memory operations during conversations +3. **Agent Tools** — Eight tools for explicit memory operations during conversations Both auto-recall and auto-capture run silently with no manual configuration required. -## Installation +## Requirements + +Check your OpenClaw version: ```bash -openclaw plugins install @mem0/openclaw-mem0 +openclaw --version +# OpenClaw 2026.4.15 (041266a) ``` +| OpenClaw Version | Plugin Support | +|------------------|----------------| +| `>= 2026.4.15` | Fully supported | + +## Installation + +The fastest way is to install directly from your OpenClaw chat, no CLI or config editing needed. + +**Copy and paste this into your OpenClaw chat**; Telegram, WhatsApp, default chat, or any channel where your agent lives: + +``` +Setup Mem0 from mem0.ai/claw-setup +``` + +OpenClaw installs the plugin, prompts you for your email, and connects your Mem0 account with OTP verification. See [Chat Setup](#option-1-chat-setup-recommended) below for the full walkthrough. + +If you prefer the OpenClaw CLI, or are setting up self-hosted / open-source mode, see [Manual Config](#option-2-manual-config) and [Open-Source Mode](#open-source-mode-self-hosted) below. + ## Setup and Configuration ### Understanding `userId` @@ -36,37 +57,113 @@ Pick any stable, unique identifier for the user. Common choices: - A UUID (e.g. `"550e8400-e29b-41d4-a716-446655440000"`) - A simple username (e.g. `"alice"`) -All memories are scoped to this `userId` — different values create separate memory namespaces. If you don't set it, it defaults to `"default"`, which means all users share the same memory space. +All memories are scoped to this `userId` — different values create separate memory namespaces. If you don't set it, it defaults to your OS username. In a multi-user application, set `userId` dynamically per user (e.g. from your auth system) rather than hardcoding a single value. ### Platform Mode (Mem0 Cloud) -Get your API key from app.mem0.ai. +There are two ways to set up `@mem0/openclaw-mem0` on the Mem0 platform: -Add to your `openclaw.json`: +- **Chat setup (recommended)** — run the setup inside any OpenClaw chat. No config editing, no API key handling. +- **Manual config** — edit `openclaw.json` directly. -```json5 -// plugins.entries -"openclaw-mem0": { - "enabled": true, - "config": { - "apiKey": "${MEM0_API_KEY}", - "userId": "alice" // any unique identifier you choose for this user - } -} -``` +#### Option 1: Chat Setup (Recommended) + +You no longer need manual config editing to get started. Everything happens inside the OpenClaw chat itself. + + + + Open any OpenClaw channel — Telegram, WhatsApp, your default chat, wherever your agent lives. Paste and send this command: + + ``` + Setup Mem0 from mem0.ai/claw-setup + ``` + + OpenClaw responds with a Mem0 setup card and immediately asks: + + > "What's your email address? I'll send you a verification code to connect your Mem0 account." + + + + Type your email address and send it. Mem0 sends back: + + > "Check your email for a 6-digit code and paste it here." + + + + Copy the 6-digit code from your email inbox and paste it into the chat. + + You'll see the confirmation: + + > "Connected to Mem0." + + + +That's it. No API key, no config file editing, no environment variables. The plugin is now active and auto-capture and auto-recall are running on every turn. + +The chat flow uses the same underlying config as manual setup — it writes `apiKey` and `userId` into `openclaw.json` for you. You can still open the file to inspect or override values afterward. + +#### Option 2: Manual Config + + + + ```bash + openclaw plugins install @mem0/openclaw-mem0 + ``` + + + + Get your API key from app.mem0.ai. + + + + Add the full config to your `openclaw.json`: + + ```json5 + { + "plugins": { + "slots": { + "memory": "openclaw-mem0" + }, + "entries": { + "openclaw-mem0": { + "enabled": true, + "config": { + "apiKey": "${MEM0_API_KEY}", + "userId": "alice" // any unique identifier you choose for this user + } + } + } + } + } + ``` + + + + +OpenClaw treats memory plugins as an exclusive slot. Installing the plugin alone does **not** activate it — you must also set `plugins.slots.memory` as shown above. + ### Open-Source Mode (Self-hosted) No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM. ```json5 -"openclaw-mem0": { - "enabled": true, - "config": { - "mode": "open-source", - "userId": "alice" // any unique identifier you choose for this user +{ + "plugins": { + "slots": { + "memory": "openclaw-mem0" + }, + "entries": { + "openclaw-mem0": { + "enabled": true, + "config": { + "mode": "open-source", + "userId": "alice" // any unique identifier you choose for this user + } + } + } } } ``` @@ -74,13 +171,25 @@ No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM. Sensible defaults work out of the box. To customize the embedder, vector store, or LLM: ```json5 -"config": { - "mode": "open-source", - "userId": "your-user-id", - "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" } } +{ + "plugins": { + "slots": { + "memory": "openclaw-mem0" + }, + "entries": { + "openclaw-mem0": { + "enabled": true, + "config": { + "mode": "open-source", + "userId": "your-user-id", + "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" } } + } + } + } + } } } ``` @@ -93,23 +202,26 @@ Memories are organized into two scopes: - **Session (short-term)** — Auto-capture stores memories scoped to the current session via Mem0's `run_id` / `runId` parameter. These are contextual to the ongoing conversation. -- **User (long-term)** — The agent can explicitly store long-term memories using the `memory_store` tool (with `longTerm: true`, the default). These persist across all sessions for the user. +- **User (long-term)** — The agent can explicitly store long-term memories using the `memory_add` tool (with `longTerm: true`, the default). These persist across all sessions for the user. During **auto-recall**, the plugin searches both scopes and presents them separately — long-term memories first, then session memories — so the agent has full context. ## Agent Tools -The agent gets five tools it can call during conversations: +The agent gets eight tools it can call during conversations: | Tool | Description | |------|-------------| -| `memory_search` | Search memories by natural language | -| `memory_list` | List all stored memories for a user | -| `memory_store` | Explicitly save a fact | -| `memory_get` | Retrieve a memory by ID | -| `memory_forget` | Delete by ID or by query | +| `memory_search` | Search memories by natural language query. Supports `scope`, `categories`, `filters`. | +| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `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`. | +| `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). | -The `memory_search` and `memory_list` tools accept a `scope` parameter (`"session"`, `"long-term"`, or `"all"`) to control which memories are queried. The `memory_store` tool accepts a `longTerm` boolean (default: `true`) to choose where to store. +The `memory_search` and `memory_list` tools accept a `scope` parameter (`"session"`, `"long-term"`, or `"all"`) to control which memories are queried. ## CLI Commands @@ -123,8 +235,9 @@ openclaw mem0 search "what languages does the user know" --scope long-term # Search only session/short-term memories openclaw mem0 search "what languages does the user know" --scope session -# View stats -openclaw mem0 stats +# List all memories +openclaw mem0 list +openclaw mem0 list --user-id alice --top-k 20 ``` ## Configuration Options @@ -134,7 +247,7 @@ openclaw mem0 stats | Key | Type | Default | Description | |-----|------|---------|-------------| | `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use | -| `userId` | `string` | `"default"` | Scope memories per user | +| `userId` | `string` | OS username | Scope memories per user | | `autoRecall` | `boolean` | `true` | Inject memories before each turn | | `autoCapture` | `boolean` | `true` | Store facts after each turn | | `topK` | `number` | `5` | Max memories per recall | @@ -145,8 +258,6 @@ openclaw mem0 stats | Key | Type | Default | Description | |-----|------|---------|-------------| | `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) | -| `orgId` | `string` | — | Organization ID | -| `projectId` | `string` | — | Project ID | | `customInstructions` | `string` | *(built-in)* | Extraction rules — what to store, how to format | | `customCategories` | `object` | *(12 defaults)* | Category name → description map for tagging | @@ -162,15 +273,77 @@ openclaw mem0 stats | `oss.llm.provider` | `string` | `"openai"` | LLM provider (`"openai"`, `"anthropic"`, `"ollama"`, etc.) | | `oss.llm.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL`, `temperature` | | `oss.historyDbPath` | `string` | — | SQLite path for memory edit history | +| `oss.disableHistory` | `boolean` | `false` | Disable memory edit history tracking | Everything inside `oss` is optional — defaults use OpenAI embeddings (`text-embedding-3-small`), in-memory vector store, and OpenAI LLM. +## Plugin Management + +### Updating the Plugin + +```bash +openclaw plugins update @mem0/openclaw-mem0 +``` + +Use the npm package name (`@mem0/openclaw-mem0`) for plugin management commands, not the plugin ID (`openclaw-mem0`). + +### Checking Plugin Status + +```bash +openclaw plugins list +openclaw plugins inspect openclaw-mem0 +``` + +## Troubleshooting + +### "plugins.allow excludes mem0" Error + +If you see an error like: + +``` +[openclaw] Failed to start CLI: Error: The `openclaw mem0` command is unavailable +because `plugins.allow` excludes "mem0". Add "mem0" to `plugins.allow` if you want +that bundled plugin CLI surface. +``` + +Add `mem0` to your `plugins.allow` list in `openclaw.json`: + +```json5 +{ + "plugins": { + "allow": ["mem0"], + "slots": { + "memory": "openclaw-mem0" + } + } +} +``` + +### Plugin Not Activating + +If the plugin installs but doesn't work: + +1. Verify `plugins.slots.memory` is set to `"openclaw-mem0"` (not the npm package name) +2. Check `openclaw plugins list --enabled` to confirm the plugin is loaded +3. Run `openclaw mem0 status` to verify configuration + +### Plugin Update Not Working + +If `openclaw plugins update` fails: + +1. Use the full npm package name: `openclaw plugins update @mem0/openclaw-mem0` +2. If that fails, uninstall and reinstall: + ```bash + openclaw plugins uninstall openclaw-mem0 + openclaw plugins install @mem0/openclaw-mem0 + ``` + ## Key Features 1. **Zero Configuration** — Auto-recall and auto-capture work out of the box with no prompting required 2. **Dual Memory Scopes** — Session-scoped short-term and user-scoped long-term memories 3. **Flexible Backend** — Use Mem0 Cloud for managed service or self-host with open-source mode -4. **Rich Tool Suite** — Five agent tools for explicit memory operations when needed +4. **Rich Tool Suite** — Eight agent tools for explicit memory operations when needed ## Conclusion diff --git a/mem0-ts/src/oss/src/prompts/index.ts b/mem0-ts/src/oss/src/prompts/index.ts index 98d3ab45d..700289d4f 100644 --- a/mem0-ts/src/oss/src/prompts/index.ts +++ b/mem0-ts/src/oss/src/prompts/index.ts @@ -883,38 +883,160 @@ export function removeCodeBlocks(text: string): string { /** * Extracts a JSON object from text that may be wrapped in explanation text. * - * Some LLMs (especially local models like Ollama/LM Studio) return JSON - * wrapped in conversational text without code fences, e.g.: + * Some LLMs (especially local models like Ollama/LM Studio, or OpenRouter) + * return JSON wrapped in conversational text without code fences, e.g.: * * "Here are the facts I extracted:\n{\"facts\": [\"fact1\"]}\nI hope this helps!" * - * This function first tries `removeCodeBlocks` for code-fence-wrapped JSON, - * then falls back to locating the first `{` and last `}` to extract the - * outermost JSON object. + * This function: + * 1. Strips known noise tokens from OpenRouter and other providers + * 2. Removes code fences and blocks + * 3. Tries to find a valid JSON object by testing each `{` as a starting point + * 4. Falls back to first/last brace matching if validation isn't possible * * @param text - The raw LLM response text * @returns The extracted JSON string, or the original text if no JSON object * boundaries are found */ export function extractJson(text: string): string { - // Step 1: Strip code fences if present - const cleaned = removeCodeBlocks(text); + // Step 1: Strip known noise tokens from OpenRouter/local models + let cleaned = text + .replace(/<\|end_of_text\|>/g, "") + .replace(/<\|eot_id\|>/g, "") + .replace(/<\|im_end\|>/g, "") + .replace(/<\|im_start\|>/g, "") + .replace(/<\|endoftext\|>/g, ""); + + // Step 2: Strip code fences and blocks + cleaned = removeCodeBlocks(cleaned); const trimmed = cleaned.trim(); - // Step 2: Try to locate a JSON object by first `{` and last `}` boundaries + if (!trimmed) return ""; + + // Step 3: Try to find valid JSON object by testing each `{` as potential start + // This handles cases like "Here's the {formatted} output: {...actual json...}" + const braceIndices: number[] = []; + for (let i = 0; i < trimmed.length; i++) { + if (trimmed[i] === "{") braceIndices.push(i); + } + + for (const start of braceIndices) { + // Find the matching closing brace by tracking depth + let depth = 0; + let inString = false; + let escapeNext = false; + + for (let i = start; i < trimmed.length; i++) { + const char = trimmed[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === "\\") { + escapeNext = true; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + continue; + } + + if (inString) continue; + + if (char === "{") depth++; + else if (char === "}") { + depth--; + if (depth === 0) { + const candidate = trimmed.substring(start, i + 1); + try { + JSON.parse(candidate); + return candidate; // Valid JSON found + } catch { + // Not valid JSON, try next starting brace + break; + } + } + } + } + } + + // Step 4: Fallback - try first/last brace (original behavior for edge cases) + // Only use this if it produces valid JSON const firstBrace = trimmed.indexOf("{"); const lastBrace = trimmed.lastIndexOf("}"); if (firstBrace !== -1 && lastBrace > firstBrace) { - return trimmed.substring(firstBrace, lastBrace + 1); + const candidate = trimmed.substring(firstBrace, lastBrace + 1); + try { + JSON.parse(candidate); + return candidate; + } catch { + // Not valid JSON, continue to array extraction + } } - // Step 3: Try to locate a JSON array by first `[` and last `]` boundaries + // Step 5: Try to locate a JSON array by testing each `[` as potential start + const bracketIndices: number[] = []; + for (let i = 0; i < trimmed.length; i++) { + if (trimmed[i] === "[") bracketIndices.push(i); + } + + for (const start of bracketIndices) { + let depth = 0; + let inString = false; + let escapeNext = false; + + for (let i = start; i < trimmed.length; i++) { + const char = trimmed[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === "\\") { + escapeNext = true; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + continue; + } + + if (inString) continue; + + if (char === "[") depth++; + else if (char === "]") { + depth--; + if (depth === 0) { + const candidate = trimmed.substring(start, i + 1); + try { + JSON.parse(candidate); + return candidate; + } catch { + break; + } + } + } + } + } + + // Fallback for arrays - validate before returning const firstBracket = trimmed.indexOf("["); const lastBracket = trimmed.lastIndexOf("]"); if (firstBracket !== -1 && lastBracket > firstBracket) { - return trimmed.substring(firstBracket, lastBracket + 1); + const candidate = trimmed.substring(firstBracket, lastBracket + 1); + try { + JSON.parse(candidate); + return candidate; + } catch { + // Not valid JSON + } } - // No JSON boundaries found — return as-is and let the caller handle the error + // No valid JSON found — return as-is and let the caller handle the error return trimmed; } diff --git a/mem0-ts/src/oss/tests/extract-json.test.ts b/mem0-ts/src/oss/tests/extract-json.test.ts index c1132c429..bf5335a02 100644 --- a/mem0-ts/src/oss/tests/extract-json.test.ts +++ b/mem0-ts/src/oss/tests/extract-json.test.ts @@ -148,4 +148,85 @@ That's all I found.`; const result = extractJson(input); expect(JSON.parse(result)).toEqual({ facts: ["test"] }); }); + + it("strips <|end_of_text|> tokens from OpenRouter responses", () => { + const input = '{"facts": ["test"]}<|end_of_text|>'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["test"] }); + }); + + it("strips <|eot_id|> tokens from OpenRouter responses", () => { + const input = '{"facts": ["hello"]}<|eot_id|>'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["hello"] }); + }); + + it("strips <|im_end|> tokens from ChatML responses", () => { + const input = '{"memory": [{"text": "test"}]}<|im_end|>'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ memory: [{ text: "test" }] }); + }); + + it("strips multiple noise tokens", () => { + const input = + '<|im_start|>assistant\n{"facts": ["data"]}<|im_end|><|end_of_text|>'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["data"] }); + }); + + // Issue #4737: Leading text with braces that aren't JSON + it("handles leading text containing braces before actual JSON", () => { + const input = 'Here\'s the {formatted} output: {"facts": ["real data"]}'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["real data"] }); + }); + + it("handles multiple fake braces in leading text", () => { + const input = + "I'll format this {nicely} with {proper} structure:\n" + + '{"memory": [{"id": "1", "text": "actual memory"}]}'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ + memory: [{ id: "1", text: "actual memory" }], + }); + }); + + it("handles incomplete JSON-like structures in leading text", () => { + const input = + "Based on {user preferences} I found:\n" + + '{"facts": ["User likes TypeScript"]}'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["User likes TypeScript"] }); + }); + + it("validates JSON and skips malformed candidates", () => { + const input = 'The result is {broken and {"facts": ["valid"]} is here'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ facts: ["valid"] }); + }); + + it("handles deeply nested valid JSON after invalid starts", () => { + const input = + "Here {is some {context}} for you:\n" + + '{"memory": [{"nested": {"deep": "value"}}]}'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ + memory: [{ nested: { deep: "value" } }], + }); + }); + + it("handles JSON with escaped quotes correctly", () => { + const input = + 'Output: {"facts": ["User said \\"hello\\"", "Has a \\"test\\" project"]}'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual({ + facts: ['User said "hello"', 'Has a "test" project'], + }); + }); + + it("handles arrays with leading brace-like text", () => { + const input = 'Here\'s {some context}. The array is: ["fact1", "fact2"]'; + const result = extractJson(input); + expect(JSON.parse(result)).toEqual(["fact1", "fact2"]); + }); }); diff --git a/openclaw/.gitignore b/openclaw/.gitignore index 4913e17ad..e1932d0b9 100644 --- a/openclaw/.gitignore +++ b/openclaw/.gitignore @@ -1,3 +1,5 @@ node_modules/ package-lock.json *.db +.claude/ +.claude-*/ \ No newline at end of file diff --git a/openclaw/README.md b/openclaw/README.md index 43f3f76a6..d554459c0 100644 --- a/openclaw/README.md +++ b/openclaw/README.md @@ -4,31 +4,98 @@ Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, po Your agent forgets everything between sessions. This plugin fixes that — it watches conversations, extracts what matters, and brings it back when relevant. Automatically. -## Quick Start +## Requirements + +Check your OpenClaw version: ```bash -openclaw plugins install @mem0/openclaw-mem0 +openclaw --version +# OpenClaw 2026.4.15 (041266a) ``` +| OpenClaw Version | Plugin Support | +|------------------|----------------| +| `>= 2026.4.15` | Fully supported | + +## Quick Start + +The fastest way is to install directly from your OpenClaw chat — no CLI or config editing needed. + +Copy and paste this into your OpenClaw chat (Telegram, WhatsApp, default chat, or any channel where your agent lives): + +``` +Setup Mem0 from mem0.ai/claw-setup +``` + +OpenClaw installs the plugin, prompts you for your email, and connects your Mem0 account with OTP verification. See [Chat Setup](#chat-setup-recommended) below for the full walkthrough. + +If you prefer the OpenClaw CLI, or are setting up self-hosted / open-source mode, see [Manual Config](#manual-config) and [Open-Source (Self-hosted)](#open-source-self-hosted) below. + ### Platform (Mem0 Cloud) -Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys): +There are two ways to set up `@mem0/openclaw-mem0` on the Mem0 platform: -```bash -openclaw mem0 init --api-key --user-id -``` +- **Chat setup (recommended)** — run the setup inside any OpenClaw chat. No config editing, no API key handling. +- **Manual config** — edit `openclaw.json` directly. -Or configure manually in `openclaw.json`: +#### Chat Setup (Recommended) -```json5 -"openclaw-mem0": { - "enabled": true, - "config": { - "apiKey": "${MEM0_API_KEY}", - "userId": "alice" - } -} -``` +You no longer need manual config editing to get started. Everything happens inside the OpenClaw chat itself. + +1. **Send the setup command to your OpenClaw agent.** Open any OpenClaw channel and paste: + + ``` + Setup Mem0 from mem0.ai/claw-setup + ``` + + OpenClaw responds with a Mem0 setup card and asks: *"What's your email address? I'll send you a verification code to connect your Mem0 account."* + +2. **Enter your email.** Type your email address and send it. Mem0 replies: *"Check your email for a 6-digit code and paste it here."* + +3. **Paste the OTP.** Copy the 6-digit code from your email inbox and paste it into the chat. You'll see: *"Connected to Mem0."* + +That's it. No API key, no config file editing, no environment variables. The plugin is now active and auto-capture and auto-recall are running on every turn. + +> The chat flow uses the same underlying config as manual setup — it writes `apiKey` and `userId` into `openclaw.json` for you. You can still open the file to inspect or override values afterward. + +#### Manual Config + +1. **Install the plugin via the OpenClaw CLI:** + + ```bash + openclaw plugins install @mem0/openclaw-mem0 + ``` + +2. **Get your API key** from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys). + +3. **Select the plugin as your memory backend in `openclaw.json`.** Either initialize via the CLI: + + ```bash + openclaw mem0 init --api-key --user-id + ``` + + Or add the full config to your `openclaw.json`: + + ```json5 + { + "plugins": { + "slots": { + "memory": "openclaw-mem0" + }, + "entries": { + "openclaw-mem0": { + "enabled": true, + "config": { + "apiKey": "${MEM0_API_KEY}", + "userId": "alice" + } + } + } + } + } + ``` + +> **Note:** OpenClaw memory plugins load through an exclusive slot, so install alone does not activate the plugin. You must set `plugins.slots.memory` as shown above. ### Open-Source (Self-hosted) @@ -37,11 +104,20 @@ No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Ve Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction. ```json5 -"openclaw-mem0": { - "enabled": true, - "config": { - "mode": "open-source", - "userId": "alice" +{ + "plugins": { + "slots": { + "memory": "openclaw-mem0" + }, + "entries": { + "openclaw-mem0": { + "enabled": true, + "config": { + "mode": "open-source", + "userId": "alice" + } + } + } } } ``` diff --git a/openclaw/backend/base.ts b/openclaw/backend/base.ts index 6a52a3922..c46dc6d9f 100644 --- a/openclaw/backend/base.ts +++ b/openclaw/backend/base.ts @@ -10,9 +10,7 @@ export interface AddOptions { appId?: string; runId?: string; metadata?: Record; - immutable?: boolean; infer?: boolean; - expires?: string; categories?: string[]; } @@ -24,7 +22,6 @@ export interface SearchOptions { topK?: number; threshold?: number; rerank?: boolean; - keyword?: boolean; filters?: Record; fields?: string[]; } diff --git a/openclaw/backend/platform.ts b/openclaw/backend/platform.ts index 481de5873..06332453b 100644 --- a/openclaw/backend/platform.ts +++ b/openclaw/backend/platform.ts @@ -107,9 +107,7 @@ export class PlatformBackend implements Backend { if (opts.appId) payload.app_id = opts.appId; if (opts.runId) payload.run_id = opts.runId; if (opts.metadata) payload.metadata = opts.metadata; - if (opts.immutable) payload.immutable = true; if (opts.infer === false) payload.infer = false; - if (opts.expires) payload.expiration_date = opts.expires; if (opts.categories) payload.categories = opts.categories; return (await this._request("POST", "/v1/memories/", { @@ -168,7 +166,6 @@ export class PlatformBackend implements Backend { }); if (apiFilters) payload.filters = apiFilters; if (opts.rerank) payload.rerank = true; - if (opts.keyword) payload.keyword_search = true; if (opts.fields) payload.fields = opts.fields; const result = (await this._request("POST", "/v2/memories/search/", { diff --git a/openclaw/cli/config-file.ts b/openclaw/cli/config-file.ts index 8eacce714..3f9aa2ffe 100644 --- a/openclaw/cli/config-file.ts +++ b/openclaw/cli/config-file.ts @@ -19,6 +19,7 @@ export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json"); export const DEFAULT_BASE_URL = "https://api.mem0.ai"; const PLUGIN_ID = "openclaw-mem0"; +const NPM_PACKAGE = "@mem0/openclaw-mem0"; // ============================================================================ // Types @@ -41,16 +42,38 @@ export interface PluginAuthConfig { // OpenClaw config read/write // ============================================================================ -/** Read the full ~/.openclaw/openclaw.json */ +/** + * Read the full ~/.openclaw/openclaw.json. + * + * Returns {} only when the file doesn't exist (first-time setup). + * Throws on parse errors to prevent writes from destroying existing config. + */ function readFullConfig(): Record { - if (exists(OPENCLAW_CONFIG_FILE)) { - try { - return JSON.parse(readText(OPENCLAW_CONFIG_FILE)); - } catch { - /* ignore parse errors */ - } + if (!exists(OPENCLAW_CONFIG_FILE)) { + return {}; + } + + const text = readText(OPENCLAW_CONFIG_FILE); + + // Handle empty or whitespace-only files as first-time setup + if (!text.trim()) { + return {}; + } + + try { + const parsed = JSON.parse(text); + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Config is not a JSON object"); + } + return parsed; + } catch (err) { + // Fail closed: throw so writes don't proceed with empty config + const msg = err instanceof Error ? err.message : String(err); + throw new Error( + `[openclaw-mem0] Failed to parse ${OPENCLAW_CONFIG_FILE}: ${msg}\n` + + `Fix the JSON syntax error manually before running config commands.`, + ); } - return {}; } /** Write the full ~/.openclaw/openclaw.json (preserves all non-plugin config) */ @@ -87,15 +110,7 @@ export function readPluginAuth(): PluginAuthConfig { export function writePluginAuth(auth: PluginAuthConfig): void { const full = readFullConfig() as any; - // Ensure nested structure exists - if (!full.plugins) full.plugins = {}; - if (!full.plugins.entries) full.plugins.entries = {}; - if (!full.plugins.entries[PLUGIN_ID]) { - full.plugins.entries[PLUGIN_ID] = { enabled: true, config: {} }; - } - if (!full.plugins.entries[PLUGIN_ID].config) { - full.plugins.entries[PLUGIN_ID].config = {}; - } + ensurePluginStructure(full); const cfg = full.plugins.entries[PLUGIN_ID].config; @@ -107,12 +122,81 @@ export function writePluginAuth(auth: PluginAuthConfig): void { writeFullConfig(full); } -export function writePluginConfigField( - path: string[], - value: unknown, -): void { - const full = readFullConfig() as any; +/** + * Ensure the plugin has a valid install record and is in plugins.allow. + * + * OpenClaw's `plugins update` command requires a `plugins.installs.` + * record with `source: "npm"` and `spec` to know how to update. Without + * this, `openclaw plugins update` prints "No install record" and skips. + * + * Similarly, if `plugins.allow` exists as an array, the plugin ID must + * be in it or OpenClaw treats the plugin as untrusted. + * + * This is safe to call multiple times — it only writes missing fields. + */ +export function ensureInstallRecord(): void { + try { + const full = readFullConfig() as any; + const entry = full?.plugins?.entries?.[PLUGIN_ID]; + const record = full?.plugins?.installs?.[PLUGIN_ID]; + const allow = full?.plugins?.allow; + if ( + entry?.enabled === true && + record?.source && + record?.spec && + Array.isArray(allow) && + allow.includes(PLUGIN_ID) + ) { + return; + } + + ensurePluginStructure(full); + + let changed = false; + + // Ensure install record exists for `openclaw plugins update` support + if (!full.plugins.installs) full.plugins.installs = {}; + if (!full.plugins.installs[PLUGIN_ID]) { + full.plugins.installs[PLUGIN_ID] = { + source: "npm", + spec: `${NPM_PACKAGE}@latest`, + resolvedName: NPM_PACKAGE, + installedAt: new Date().toISOString(), + }; + changed = true; + } else { + const record = full.plugins.installs[PLUGIN_ID]; + if (!record.source) { + record.source = "npm"; + changed = true; + } + if (!record.spec) { + record.spec = `${NPM_PACKAGE}@latest`; + changed = true; + } + if (!record.resolvedName) { + record.resolvedName = NPM_PACKAGE; + changed = true; + } + } + + if (!Array.isArray(full.plugins.allow)) { + full.plugins.allow = [PLUGIN_ID]; + changed = true; + } else if (!full.plugins.allow.includes(PLUGIN_ID)) { + full.plugins.allow.push(PLUGIN_ID); + changed = true; + } + + if (changed) writeFullConfig(full); + } catch { + // Best-effort — don't break plugin loading if config is unreadable + } +} + +/** Ensure the nested plugin entry structure exists in the config object. */ +function ensurePluginStructure(full: any): void { if (!full.plugins) full.plugins = {}; if (!full.plugins.entries) full.plugins.entries = {}; if (!full.plugins.entries[PLUGIN_ID]) { @@ -121,6 +205,15 @@ export function writePluginConfigField( if (!full.plugins.entries[PLUGIN_ID].config) { full.plugins.entries[PLUGIN_ID].config = {}; } +} + +export function writePluginConfigField( + path: string[], + value: unknown, +): void { + const full = readFullConfig() as any; + + ensurePluginStructure(full); let target = full.plugins.entries[PLUGIN_ID].config; for (let i = 0; i < path.length - 1; i++) { @@ -139,3 +232,13 @@ export function getBaseUrl(): string { const auth = readPluginAuth(); return auth.baseUrl || DEFAULT_BASE_URL; } + +/** Remove anonymousTelemetryId from config (after PostHog aliasing) */ +export function clearAnonymousTelemetryId(): void { + const full = readFullConfig() as any; + const cfg = full?.plugins?.entries?.[PLUGIN_ID]?.config; + if (cfg && "anonymousTelemetryId" in cfg) { + delete cfg.anonymousTelemetryId; + writeFullConfig(full); + } +} diff --git a/openclaw/config.ts b/openclaw/config.ts index 32d6a8964..f413dade5 100644 --- a/openclaw/config.ts +++ b/openclaw/config.ts @@ -233,20 +233,19 @@ export const mem0ConfigSchema = { })(), autoCapture: cfg.autoCapture !== false, autoRecall: cfg.autoRecall !== false, + // v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either) customInstructions: typeof cfg.customInstructions === "string" ? cfg.customInstructions - : DEFAULT_CUSTOM_INSTRUCTIONS, + : typeof cfg.customPrompt === "string" + ? cfg.customPrompt + : DEFAULT_CUSTOM_INSTRUCTIONS, customCategories: cfg.customCategories && typeof cfg.customCategories === "object" && !Array.isArray(cfg.customCategories) ? (cfg.customCategories as Record) : DEFAULT_CUSTOM_CATEGORIES, - customPrompt: - typeof cfg.customPrompt === "string" - ? cfg.customPrompt - : DEFAULT_CUSTOM_INSTRUCTIONS, searchThreshold: typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5, topK: typeof cfg.topK === "number" ? cfg.topK : 5, diff --git a/openclaw/filtering.ts b/openclaw/filtering.ts index 6137777ab..6c51a2798 100644 --- a/openclaw/filtering.ts +++ b/openclaw/filtering.ts @@ -14,9 +14,41 @@ const NOISE_MESSAGE_PATTERNS: RegExp[] = [ /^(HEARTBEAT_OK|NO_REPLY)$/i, /^Current time:.*\d{4}/, /^Pre-compaction memory flush/i, - /^(ok|yes|no|sir|sure|thanks|done|good|nice|cool|got it|it's on|continue)$/i, + /^(ok|yes|no|sir|sure|thanks|done|good|nice|cool|got it|it's on|continue|alright|okay|yep|nope|uh-huh|mm-hmm|hmm)$/i, /^System: \[.*\] (Slack message edited|Gateway restart|Exec (failed|completed))/, /^System: \[.*\] ⚠️ Post-Compaction Audit:/, + // JSON-only messages (tool results, metadata) + /^[\s]*\{[\s\S]*\}[\s]*$/, + /^[\s]*\[[\s\S]*\][\s]*$/, + // Empty or whitespace-only after trimming + /^[\s\n\r]*$/, + // Technical noise patterns + /^(Error|Warning|Info|Debug):/i, + /^(Loading|Loaded|Fetching|Fetched|Processing|Processed)\b/i, + /^\[[\d:T\-\.Z]+\]/, // Timestamps like [2024-01-01T12:00:00.000Z] + /^(SUCCESS|FAILURE|PENDING|COMPLETED|FAILED)$/i, + // Tool/function call noise + /^(Calling|Called|Invoking|Invoked|Executing|Executed)\s+(function|tool|method)/i, + /^Tool (call|result|output):/i, + // Single emoji or very short messages + /^[\p{Emoji}\s]{1,5}$/u, +]; + +/** Patterns for session-specific technical content that should not be stored as memories. */ +const SESSION_SPECIFIC_PATTERNS: RegExp[] = [ + // Tool availability discussions + /tools?\s+(are|is)\s+(not\s+)?(exposed|available|accessible)/i, + /plugin\s+(does not|doesn't)\s+expose/i, + /I\s+(do not|don't)\s+(currently\s+)?see\s+.*tools?\s+exposed/i, + /memory_(search|get|add|update|delete|list)\s+(tool|is|are)/i, + // Session-specific capability statements + /in\s+this\s+session/i, + /my\s+(live\s+)?callable\s+tool\s+registry/i, + /tools?\s+I\s+(have|currently have)\s+access\s+to/i, + // Plugin/capability status statements + /openclaw-mem0\s+plugin/i, + /memory\s+wiki.*capability/i, + /workspace\s+memory\s+files/i, ]; /** Content fragments that should be stripped from otherwise-valid messages. */ @@ -50,6 +82,31 @@ const NOISE_CONTENT_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = /Replied message \(untrusted, for context\):\s*```json[\s\S]*?```/g, replacement: "", }, + // Strip embedded JSON blocks that might contain metadata + { + pattern: /```json\s*\{[\s\S]*?\}\s*```/g, + replacement: "", + }, + // Strip code blocks that are just tool outputs + { + pattern: /```(?:text|output|result|log)\s*[\s\S]*?```/gi, + replacement: "", + }, + // Strip inline tool call IDs + { + pattern: /\[tool_call_id:[^\]]+\]/g, + replacement: "", + }, + // Strip memory IDs from responses + { + pattern: /\(id:\s*[a-f0-9-]+\)/gi, + replacement: "", + }, + // Strip session/run IDs + { + pattern: /(?:session|run|agent)[_-]?(?:id|key)?:\s*[a-zA-Z0-9_:-]+/gi, + replacement: "", + }, ]; const MAX_MESSAGE_LENGTH = 2000; @@ -81,6 +138,20 @@ export function isNoiseMessage(content: string): boolean { return NOISE_MESSAGE_PATTERNS.some((p) => p.test(trimmed)); } +/** + * Check whether a message contains session-specific technical content + * that should not be stored (tool availability discussions, plugin + * capability statements, etc.). These are facts about the current session + * that have no value in future sessions. + */ +export function isSessionSpecificContent(content: string): boolean { + const trimmed = content.trim(); + if (!trimmed) return false; + // Check if multiple session-specific patterns match (more confident filtering) + const matches = SESSION_SPECIFIC_PATTERNS.filter((p) => p.test(trimmed)); + return matches.length >= 2; +} + /** * Check whether an assistant message is a generic acknowledgment with no * extractable facts (e.g. "I see you've shared an update. How can I help?"). @@ -120,7 +191,7 @@ function truncateMessage(content: string): string { /** * Full pre-extraction pipeline: drop noise messages, strip noise fragments, - * and truncate remaining messages to a reasonable length. + * filter session-specific content, and truncate remaining messages. */ export function filterMessagesForExtraction( messages: Array<{ role: string; content: string }>, @@ -131,6 +202,8 @@ export function filterMessagesForExtraction( // Drop generic assistant acknowledgments that contain no facts if (msg.role === "assistant" && isGenericAssistantMessage(msg.content)) continue; + // Drop session-specific technical content (tool availability, plugin capabilities) + if (isSessionSpecificContent(msg.content)) continue; const cleaned = stripNoiseFromContent(msg.content); if (!cleaned) continue; filtered.push({ role: msg.role, content: truncateMessage(cleaned) }); diff --git a/openclaw/index.ts b/openclaw/index.ts index 0ec92f30b..1dd99021b 100644 --- a/openclaw/index.ts +++ b/openclaw/index.ts @@ -28,6 +28,7 @@ import type { import { createProvider, providerToBackend } from "./providers.ts"; import { mem0ConfigSchema } from "./config.ts"; import type { FileConfig } from "./config.ts"; +import { createPublicArtifactsProvider } from "./public-artifacts.ts"; import { filterMessagesForExtraction } from "./filtering.ts"; import { effectiveUserId, @@ -53,13 +54,14 @@ import { import { PlatformBackend } from "./backend/platform.ts"; import type { Backend } from "./backend/base.ts"; import { registerCliCommands } from "./cli/commands.ts"; -import { readPluginAuth } from "./cli/config-file.ts"; +import { readPluginAuth, ensureInstallRecord } 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(); +ensureInstallRecord(); // ============================================================================ // Re-exports (for tests and external consumers) @@ -76,6 +78,7 @@ export { export { isNoiseMessage, isGenericAssistantMessage, + isSessionSpecificContent, stripNoiseFromContent, filterMessagesForExtraction, } from "./filtering.ts"; @@ -191,24 +194,43 @@ const memoryPlugin = definePluginEntry({ `openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`, ); + // ======================================================================== + // Public Artifacts (for memory-wiki bridge mode) + // ======================================================================== + if (typeof api.registerMemoryCapability === "function") { + api.registerMemoryCapability({ + publicArtifacts: createPublicArtifactsProvider({ + provider, + cfg, + get stateDir() { + return pluginStateDir; + }, + effectiveUserId: _effectiveUserId, + }), + }); + api.logger.debug("openclaw-mem0: publicArtifacts capability registered"); + } + // Helper: build add options function buildAddOptions( userIdOverride?: string, runId?: string, sessionKey?: string, ): AddOptions { + // v3.0.0: removed output_format, customPrompt renamed to customInstructions const opts: AddOptions = { user_id: userIdOverride || _effectiveUserId(sessionKey), source: "OPENCLAW", }; if (runId) opts.run_id = runId; - if (cfg.mode === "platform") { - opts.output_format = "v1.1"; - } + // Pass customInstructions and customCategories to control what Mem0 extracts + if (cfg.customInstructions) opts.custom_instructions = cfg.customInstructions; + if (cfg.customCategories) opts.custom_categories = cfg.customCategories; return opts; } // Helper: build search options (skills config overrides legacy defaults) + // v3.0.0: removed keyword_search, reranking, filter_memories, limit function buildSearchOptions( userIdOverride?: string, limit?: number, @@ -219,13 +241,9 @@ const memoryPlugin = definePluginEntry({ const opts: SearchOptions = { user_id: userIdOverride || _effectiveUserId(sessionKey), top_k: limit ?? cfg.topK, - limit: limit ?? cfg.topK, threshold: recallCfg?.threshold ?? cfg.searchThreshold, - keyword_search: recallCfg?.keywordSearch !== false, - reranking: recallCfg?.rerank !== false, source: "OPENCLAW", }; - if (recallCfg?.filterMemories) opts.filter_memories = true; if (runId) opts.run_id = runId; return opts; } @@ -950,7 +968,7 @@ function registerHooks( content: `Current date: ${timestamp}. The user is identified as "${cfg.userId}". Extract durable facts from this conversation. Include this date when storing time-sensitive information.`, }); - const addOpts = buildAddOptions(undefined, sessionId, sessionId); + const addOpts = buildAddOptions(undefined, undefined, sessionId); const captureStart = Date.now(); provider .add(formattedMessages, addOpts) diff --git a/openclaw/openclaw-plugin-sdk.d.ts b/openclaw/openclaw-plugin-sdk.d.ts index 95d923fb3..7484e4813 100644 --- a/openclaw/openclaw-plugin-sdk.d.ts +++ b/openclaw/openclaw-plugin-sdk.d.ts @@ -1,4 +1,29 @@ declare module "openclaw/plugin-sdk" { + export interface MemoryArtifact { + id: string; + type: "memory" | "dream" | "digest" | "entity"; + title: string; + content: string; + metadata?: Record; + createdAt?: string; + updatedAt?: string; + } + + export interface PublicArtifactsProvider { + listArtifacts(options?: { + userId?: string; + types?: string[]; + limit?: number; + }): Promise; + } + + export interface MemoryCapabilityConfig { + promptBuilder?: (ctx: any) => Promise; + flushPlanResolver?: (ctx: any) => Promise; + runtime?: Record; + publicArtifacts?: PublicArtifactsProvider; + } + export interface OpenClawPluginApi { pluginConfig: Record; logger: { @@ -32,6 +57,7 @@ declare module "openclaw/plugin-sdk" { start: (...args: any[]) => void; stop: () => void; }): void; + registerMemoryCapability?(config: MemoryCapabilityConfig): void; [key: string]: unknown; } } diff --git a/openclaw/openclaw.plugin.json b/openclaw/openclaw.plugin.json index 5bafe6d46..09a2d1105 100644 --- a/openclaw/openclaw.plugin.json +++ b/openclaw/openclaw.plugin.json @@ -1,10 +1,17 @@ { "id": "openclaw-mem0", "name": "Memory (Mem0)", - "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source. Injects recalled memories into agent context (auto-recall) and extracts facts after each turn (auto-capture). Both configurable via autoRecall/autoCapture settings.", - "version": "1.0.5", + "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source. PLATFORM MODE: Sends conversation data to mem0.ai cloud (requires MEM0_API_KEY). OPEN-SOURCE MODE: Stores vectors locally (~/.mem0/history.db) but uses external APIs for embeddings/LLM (default: OpenAI, requires OPENAI_API_KEY). Auto-recall injects memories before agent turns; auto-capture extracts facts after turns. Both configurable via autoRecall/autoCapture settings. Config stored in ~/.openclaw/openclaw.json.", + "version": "1.0.7", "kind": "memory", "skills": ["skills"], + "commandAliases": [ + { + "name": "mem0", + "cliCommand": "mem0", + "description": "Mem0 memory plugin commands" + } + ], "contracts": { "tools": [ "memory_search", "memory_add", "memory_get", "memory_list", @@ -13,7 +20,7 @@ }, "providerAuthEnvVars": { "mem0": ["MEM0_API_KEY"], - "openclaw-mem0-oss": ["OPENAI_API_KEY"] + "openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AZURE_OPENAI_API_KEY", "COHERE_API_KEY"] }, "providerAuthChoices": [ { @@ -176,6 +183,9 @@ }, "historyDbPath": { "type": "string" + }, + "disableHistory": { + "type": "boolean" } } }, diff --git a/openclaw/package.json b/openclaw/package.json index 47e5a0edd..a7aa6a325 100644 --- a/openclaw/package.json +++ b/openclaw/package.json @@ -1,6 +1,6 @@ { "name": "@mem0/openclaw-mem0", - "version": "1.0.6", + "version": "1.0.7", "type": "module", "description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source", "license": "Apache-2.0", @@ -35,7 +35,7 @@ }, "dependencies": { "@sinclair/typebox": "0.34.47", - "mem0ai": "2.4.5" + "mem0ai": "3.0.1" }, "openclaw": { "extensions": [ @@ -48,6 +48,9 @@ "build": { "openclawVersion": "2026.4.1", "pluginSdkVersion": "2026.4.1" + }, + "install": { + "npmSpec": "@mem0/openclaw-mem0" } }, "devDependencies": { diff --git a/openclaw/pnpm-lock.yaml b/openclaw/pnpm-lock.yaml index 8c5f7ac89..bdd3abd38 100644 --- a/openclaw/pnpm-lock.yaml +++ b/openclaw/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 0.34.47 version: 0.34.47 mem0ai: - specifier: 2.4.5 - version: 2.4.5(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0) + specifier: 3.0.1 + version: 3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0) devDependencies: '@types/node': specifier: ^22.15.0 @@ -340,6 +340,9 @@ packages: '@mistralai/mistralai@1.15.1': resolution: {integrity: sha512-fb995eiz3r0KsBGtRjFV+/iLbX+UpfalxpF+YitT3R6ukrPD4PN+FGwwmYcRFhNAzVzDUtTVxQYnjQWEnwV5nw==} + '@mongodb-js/saslprep@1.4.8': + resolution: {integrity: sha512-kpjr2jy2w71w0oqAMI8oibBmiF9lXxWkEQs5gMkW4hVE48bsqINGLxnCSYW62ck/NHXJQpQEfA9WlJ1sY0eqBg==} + '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -394,34 +397,41 @@ packages: resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==} engines: {node: '>=18.0.0', pnpm: '>=8'} - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + '@redis/bloom@5.12.1': + resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + '@redis/client@5.12.1': + resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + '@redis/json@5.12.1': + resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + '@redis/search@5.12.1': + resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + '@redis/time-series@5.12.1': + resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==} + engines: {node: '>= 18.19.0'} peerDependencies: - '@redis/client': ^1.0.0 + '@redis/client': ^5.12.1 '@rolldown/binding-android-arm64@1.0.0-rc.9': resolution: {integrity: sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==} @@ -724,6 +734,12 @@ packages: '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@13.0.0': + resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -784,6 +800,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + afinn-165-financialmarketnews@3.0.0: + resolution: {integrity: sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw==} + + afinn-165@2.0.2: + resolution: {integrity: sha512-mJ/RLUfpXfQA6bzugv+bBsc/QYkVrKaLYeS8fWBpKbTCsonv4iuV9ET0fgReEunm9vKLkaNgnekuSNlTC3WQ1Q==} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -815,6 +837,10 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + apparatus@0.0.10: + resolution: {integrity: sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg==} + engines: {node: '>=0.2.6'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -857,19 +883,16 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + bson@7.2.0: + resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} + engines: {node: '>=20.19.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-writer@2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -936,6 +959,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + compromise@14.15.0: + resolution: {integrity: sha512-YEMv5JGWyqRJw5hdZqDVQF3MMlHA6TRiXreR8IYffk6xB7GA5p/8DeDzvg0Jy2tHNGpD+qJGl0+oJwA+5R/sVA==} + engines: {node: '>=12.0.0'} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1008,6 +1035,10 @@ packages: digest-fetch@1.3.0: resolution: {integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1018,6 +1049,10 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + efrt@2.7.0: + resolution: {integrity: sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ==} + engines: {node: '>=12.0.0'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1154,10 +1189,6 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -1189,6 +1220,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + grad-school@0.0.5: + resolution: {integrity: sha512-rXunEHF9M9EkMydTBux7+IryYXEZinRk6g8OBOGDBzo/qWJjhTxy86i5q7lQYpCLHN8Sqv1XX3OIOc7ka2gtvQ==} + engines: {node: '>=8.0.0'} + groq-sdk@0.3.0: resolution: {integrity: sha512-Cdgjh4YoSBE2X4S9sxPGXaAy1dlN4bRtAaDZ3cnq+XsxhhN9WSBeHF64l7LWwuD5ntmw7YC5Vf4Ff1oHCg1LOg==} @@ -1324,6 +1359,10 @@ packages: jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + kareem@3.2.0: + resolution: {integrity: sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==} + engines: {node: '>=18.0.0'} + langsmith@0.3.87: resolution: {integrity: sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==} peerDependencies: @@ -1466,8 +1505,8 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - mem0ai@2.4.5: - resolution: {integrity: sha512-0XMRe5/KKZXkSJDb2YDqgWdIfJpL2cn8Lelx9pvDPJOvBfXx8uHjtSc80jyGyb9C/1wW2uPcaE+WS+Rs/2phCg==} + mem0ai@3.0.1: + resolution: {integrity: sha512-6phM544/3NRcCg7n5DBNRc9uUEMqGTe7sksULM2KM/FTihH27yTl5nPms8cnNpSKM5/ZKJ9jnkpzwGlvYYbtBQ==} engines: {node: '>=18'} peerDependencies: '@anthropic-ai/sdk': ^0.40.1 @@ -1483,12 +1522,20 @@ packages: '@types/pg': 8.11.0 better-sqlite3: ^12.6.2 cloudflare: ^4.2.0 + compromise: ^14.0.0 groq-sdk: 0.3.0 - neo4j-driver: ^5.28.1 + natural: ^8.0.1 ollama: ^0.5.14 pg: 8.11.3 redis: ^4.6.13 + memjs@1.3.2: + resolution: {integrity: sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg==} + engines: {node: '>=0.10.0'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -1522,6 +1569,49 @@ packages: mlly@1.8.1: resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + mongodb-connection-string-url@7.0.1: + resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} + engines: {node: '>=20.19.0'} + + mongodb@7.1.1: + resolution: {integrity: sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.806.0 + '@mongodb-js/zstd': ^7.0.0 + gcp-metadata: ^7.0.1 + kerberos: ^7.0.0 + mongodb-client-encryption: '>=7.0.0 <7.1.0' + snappy: ^7.3.2 + socks: ^2.8.6 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + + mongoose@9.4.1: + resolution: {integrity: sha512-4rFBWa+/wdBQSfvnOPJBpiSG6UCEbhSQh865dEdaH9Y8WfHBUC+I2XT28dp0IBIGrEwmh+gzrgZgea5PbmrHWA==} + engines: {node: '>=20.19.0'} + + mpath@0.9.0: + resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} + engines: {node: '>=4.0.0'} + + mquery@6.0.0: + resolution: {integrity: sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==} + engines: {node: '>=20.19.0'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1540,14 +1630,9 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - neo4j-driver-bolt-connection@5.28.3: - resolution: {integrity: sha512-wqHBYcU0FVRDmdsoZ+Fk0S/InYmu9/4BT6fPYh45Jimg/J7vQBUcdkiHGU7nop7HRb1ZgJmL305mJb6g5Bv35Q==} - - neo4j-driver-core@5.28.3: - resolution: {integrity: sha512-Jk+hAmjFmO5YzVH/U7FyKXigot9zmIfLz6SZQy0xfr4zfTE/S8fOYFOGqKQTHBE86HHOWH2RbTslbxIb+XtU2g==} - - neo4j-driver@5.28.3: - resolution: {integrity: sha512-k7c0wEh3HoONv1v5AyLp9/BDAbYHJhz2TZvzWstSEU3g3suQcXmKEaYBfrK2UMzxcy3bCT0DrnfRbzsOW5G/Ag==} + natural@8.1.1: + resolution: {integrity: sha512-Ucb+lsUcGxUqu3rn8cwHjT6gJQosO63nIX/aBQXB3+IDkNbFV7PuviysO+Rzz3aKn7PZhPj3bNF4PS9gDVjYCQ==} + engines: {node: '>=0.4.10'} node-abi@3.88.0: resolution: {integrity: sha512-At6b4UqIEVudaqPsXjmUO1r/N5BUr4yhDGs5PkBE8/oG5+TfLPhFechiskFsnT6Ql0VfUXbalUUCbfXxtj7K+w==} @@ -1622,9 +1707,6 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - packet-reader@1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -1666,9 +1748,9 @@ packages: resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} engines: {node: '>=10'} - pg@8.11.3: - resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} - engines: {node: '>= 8.0.0'} + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} peerDependencies: pg-native: '>=3.0.1' peerDependenciesMeta: @@ -1773,6 +1855,10 @@ packages: pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -1788,8 +1874,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} + redis@5.12.1: + resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==} + engines: {node: '>= 18.19.0'} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} @@ -1817,12 +1904,13 @@ packages: resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} engines: {node: '>=18'} - rxjs@7.8.2: - resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1836,6 +1924,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + sift@17.1.3: + resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1864,6 +1955,9 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -1878,6 +1972,10 @@ packages: std-env@4.0.0: resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + stopwords-iso@1.1.0: + resolution: {integrity: sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg==} + engines: {node: '>=0.10.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1906,10 +2004,17 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + suffix-thumb@5.0.2: + resolution: {integrity: sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + sylvester@0.0.21: + resolution: {integrity: sha512-yUT0ukFkFEt4nb+NY+n2ag51aS/u9UHXoZw+A4jgD77/jzZsBoSDHuqysrVCBC4CYR4TYvUJq54ONpXgDBH8tA==} + engines: {node: '>=0.2.6'} + tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} @@ -1949,6 +2054,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1989,6 +2098,9 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} @@ -2006,6 +2118,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -2103,9 +2219,17 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -2119,6 +2243,10 @@ packages: engines: {node: '>=8'} hasBin: true + wordnet-db@3.1.14: + resolution: {integrity: sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag==} + engines: {node: '>=0.6.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2150,9 +2278,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -2487,6 +2612,10 @@ snapshots: - bufferutil - utf-8-validate + '@mongodb-js/saslprep@1.4.8': + dependencies: + sparse-bitfield: 3.0.3 + '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.9.0 @@ -2533,31 +2662,25 @@ snapshots: '@qdrant/openapi-typescript-fetch@1.2.6': {} - '@redis/bloom@1.2.0(@redis/client@1.6.1)': + '@redis/bloom@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/client@1.6.1': + '@redis/client@5.12.1': dependencies: cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - '@redis/graph@1.1.1(@redis/client@1.6.1)': + '@redis/json@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/json@1.0.7(@redis/client@1.6.1)': + '@redis/search@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 - '@redis/search@1.2.0(@redis/client@1.6.1)': + '@redis/time-series@5.12.1(@redis/client@5.12.1)': dependencies: - '@redis/client': 1.6.1 - - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 + '@redis/client': 5.12.1 '@rolldown/binding-android-arm64@1.0.0-rc.9': optional: true @@ -2785,6 +2908,12 @@ snapshots: '@types/uuid@10.0.0': {} + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@13.0.0': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/ws@8.18.1': dependencies: '@types/node': 22.19.15 @@ -2864,6 +2993,10 @@ snapshots: acorn@8.16.0: {} + afinn-165-financialmarketnews@3.0.0: {} + + afinn-165@2.0.2: {} + agent-base@7.1.4: {} agentkeepalive@4.6.0: @@ -2884,6 +3017,10 @@ snapshots: any-promise@1.3.0: {} + apparatus@0.0.10: + dependencies: + sylvester: 0.0.21 + assertion-error@2.0.1: {} ast-v8-to-istanbul@1.0.0: @@ -2933,20 +3070,15 @@ snapshots: dependencies: fill-range: 7.1.1 + bson@7.2.0: {} + buffer-equal-constant-time@1.0.1: {} - buffer-writer@2.0.0: {} - buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -3008,6 +3140,12 @@ snapshots: commander@4.1.1: {} + compromise@14.15.0: + dependencies: + efrt: 2.7.0 + grad-school: 0.0.5 + suffix-thumb: 5.0.2 + confbox@0.1.8: {} consola@3.4.2: {} @@ -3060,6 +3198,8 @@ snapshots: base-64: 0.1.0 md5: 2.3.0 + dotenv@17.4.2: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3072,6 +3212,8 @@ snapshots: dependencies: safe-buffer: 5.2.1 + efrt@2.7.0: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3223,8 +3365,6 @@ snapshots: transitivePeerDependencies: - supports-color - generic-pool@3.9.0: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3271,6 +3411,8 @@ snapshots: graceful-fs@4.2.11: {} + grad-school@0.0.5: {} + groq-sdk@0.3.0: dependencies: '@types/node': 18.19.130 @@ -3437,6 +3579,8 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.2.1 + kareem@3.2.0: {} + langsmith@0.3.87(openai@4.104.0(ws@8.19.0)(zod@3.25.76)): dependencies: '@types/uuid': 10.0.0 @@ -3543,7 +3687,7 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 - mem0ai@2.4.5(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(groq-sdk@0.3.0)(neo4j-driver@5.28.3)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.19.0): + mem0ai@3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0): dependencies: '@anthropic-ai/sdk': 0.40.1 '@azure/identity': 4.13.0 @@ -3559,12 +3703,13 @@ snapshots: axios: 1.13.6 better-sqlite3: 12.8.0 cloudflare: 4.5.0 + compromise: 14.15.0 groq-sdk: 0.3.0 - neo4j-driver: 5.28.3 + natural: 8.1.1 ollama: 0.5.18 openai: 4.104.0(ws@8.19.0)(zod@3.25.76) - pg: 8.11.3 - redis: 4.7.1 + pg: 8.20.0 + redis: 5.12.1 uuid: 9.0.1 zod: 3.25.76 transitivePeerDependencies: @@ -3572,6 +3717,10 @@ snapshots: - encoding - ws + memjs@1.3.2: {} + + memory-pager@1.5.0: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -3602,6 +3751,38 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + mongodb-connection-string-url@7.0.1: + dependencies: + '@types/whatwg-url': 13.0.0 + whatwg-url: 14.2.0 + + mongodb@7.1.1: + dependencies: + '@mongodb-js/saslprep': 1.4.8 + bson: 7.2.0 + mongodb-connection-string-url: 7.0.1 + + mongoose@9.4.1: + dependencies: + kareem: 3.2.0 + mongodb: 7.1.1 + mpath: 0.9.0 + mquery: 6.0.0 + ms: 2.1.3 + sift: 17.1.3 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - snappy + - socks + + mpath@0.9.0: {} + + mquery@6.0.0: {} + ms@2.1.3: {} mustache@4.2.0: {} @@ -3616,19 +3797,33 @@ snapshots: napi-build-utils@2.0.0: {} - neo4j-driver-bolt-connection@5.28.3: + natural@8.1.1: dependencies: - buffer: 6.0.3 - neo4j-driver-core: 5.28.3 - string_decoder: 1.3.0 - - neo4j-driver-core@5.28.3: {} - - neo4j-driver@5.28.3: - dependencies: - neo4j-driver-bolt-connection: 5.28.3 - neo4j-driver-core: 5.28.3 - rxjs: 7.8.2 + afinn-165: 2.0.2 + afinn-165-financialmarketnews: 3.0.0 + apparatus: 0.0.10 + dotenv: 17.4.2 + memjs: 1.3.2 + mongoose: 9.4.1 + pg: 8.20.0 + redis: 5.12.1 + safe-stable-stringify: 2.5.0 + stopwords-iso: 1.1.0 + sylvester: 0.0.21 + underscore: 1.13.8 + uuid: 13.0.0 + wordnet-db: 3.1.14 + transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' + - '@node-rs/xxhash' + - '@opentelemetry/api' + - gcp-metadata + - kerberos + - mongodb-client-encryption + - pg-native + - snappy + - socks node-abi@3.88.0: dependencies: @@ -3700,8 +3895,6 @@ snapshots: package-json-from-dist@1.0.1: {} - packet-reader@1.0.0: {} - path-key@3.1.1: {} path-scurry@1.11.1: @@ -3720,9 +3913,9 @@ snapshots: pg-numeric@1.0.2: {} - pg-pool@3.13.0(pg@8.11.3): + pg-pool@3.13.0(pg@8.20.0): dependencies: - pg: 8.11.3 + pg: 8.20.0 pg-protocol@1.13.0: {} @@ -3744,12 +3937,10 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.4 - pg@8.11.3: + pg@8.20.0: dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 pg-connection-string: 2.12.0 - pg-pool: 3.13.0(pg@8.11.3) + pg-pool: 3.13.0(pg@8.20.0) pg-protocol: 1.13.0 pg-types: 2.2.0 pgpass: 1.0.5 @@ -3851,6 +4042,8 @@ snapshots: end-of-stream: 1.4.5 once: 1.4.0 + punycode@2.3.1: {} + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -3868,14 +4061,16 @@ snapshots: readdirp@4.1.2: {} - redis@4.7.1: + redis@5.12.1: dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) + '@redis/bloom': 5.12.1(@redis/client@5.12.1) + '@redis/client': 5.12.1 + '@redis/json': 5.12.1(@redis/client@5.12.1) + '@redis/search': 5.12.1(@redis/client@5.12.1) + '@redis/time-series': 5.12.1(@redis/client@5.12.1) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' resolve-from@5.0.0: {} @@ -3939,12 +4134,10 @@ snapshots: run-applescript@7.1.0: {} - rxjs@7.8.2: - dependencies: - tslib: 2.8.1 - safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + semver@7.7.4: {} shebang-command@2.0.0: @@ -3953,6 +4146,8 @@ snapshots: shebang-regex@3.0.0: {} + sift@17.1.3: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -3973,6 +4168,10 @@ snapshots: source-map@0.7.6: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + split2@4.2.0: {} stack-utils@2.0.6: @@ -3983,6 +4182,8 @@ snapshots: std-env@4.0.0: {} + stopwords-iso@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4019,10 +4220,14 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 + suffix-thumb@5.0.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + sylvester@0.0.21: {} + tar-fs@2.1.4: dependencies: chownr: 1.1.4 @@ -4065,6 +4270,10 @@ snapshots: tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -4107,6 +4316,8 @@ snapshots: ufo@1.6.3: {} + underscore@1.13.8: {} + undici-types@5.26.5: {} undici-types@6.21.0: {} @@ -4119,6 +4330,8 @@ snapshots: uuid@10.0.0: {} + uuid@13.0.0: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -4169,8 +4382,15 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + whatwg-fetch@3.6.20: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -4185,6 +4405,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wordnet-db@3.1.14: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -4207,8 +4429,6 @@ snapshots: xtend@4.0.2: {} - yallist@4.0.0: {} - zod-to-json-schema@3.25.1(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/openclaw/providers.ts b/openclaw/providers.ts index 1048586a5..6acf6cfcd 100644 --- a/openclaw/providers.ts +++ b/openclaw/providers.ts @@ -111,21 +111,19 @@ class PlatformProvider implements Mem0Provider { options: AddOptions, ): Promise { await this.ensureClient(); - const opts: Record = { user_id: options.user_id }; - if (options.run_id) opts.run_id = options.run_id; + // v3.0.0: SDK uses camelCase (userId, runId, etc.) - it converts to snake_case internally + const opts: Record = { userId: options.user_id }; + if (options.run_id) opts.runId = options.run_id; if (options.custom_instructions) - opts.custom_instructions = options.custom_instructions; + opts.customInstructions = options.custom_instructions; if (options.custom_categories) - opts.custom_categories = options.custom_categories; - if (options.output_format) opts.output_format = options.output_format; + opts.customCategories = options.custom_categories; if (options.source) opts.source = options.source; // Agentic harness: direct storage bypass if (options.infer !== undefined) opts.infer = options.infer; if (options.deduced_memories) - opts.deduced_memories = options.deduced_memories; + opts.deducedMemories = options.deduced_memories; if (options.metadata) opts.metadata = options.metadata; - if (options.expiration_date) opts.expiration_date = options.expiration_date; - if (options.immutable) opts.immutable = options.immutable; const result = await this.client.add(messages, opts); return normalizeAddResult(result); @@ -133,19 +131,15 @@ class PlatformProvider implements Mem0Provider { async search(query: string, options: SearchOptions): Promise { await this.ensureClient(); - const opts: Record = { - api_version: "v2", - 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; + // v3.0.0: SDK uses camelCase options, userId must be in filters + const opts: Record = {}; + if (options.top_k != null) opts.topK = options.top_k; if (options.threshold != null) opts.threshold = options.threshold; - if (options.keyword_search != null) - opts.keyword_search = options.keyword_search; - if (options.reranking != null) opts.rerank = options.reranking; - if (options.filter_memories != null) - opts.filter_memories = options.filter_memories; if (options.categories != null) opts.categories = options.categories; + + // Build filters with user_id/run_id inside (v3.0.0 requirement) + // Filters use snake_case as they're passed directly to the API + // Note: source is NOT a valid filter field - only used when adding const baseFilters: Record = { user_id: options.user_id }; if (options.run_id) baseFilters.run_id = options.run_id; @@ -167,16 +161,14 @@ class PlatformProvider implements Mem0Provider { async getAll(options: ListOptions): Promise { await this.ensureClient(); - 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; + // v3.0.0: SDK uses camelCase options, userId must be in filters + // Filters use snake_case as they're passed directly to the API + // Note: source is NOT a valid filter field - only used when adding + const filters: Record = { user_id: options.user_id }; + if (options.run_id) filters.run_id = options.run_id; + + const opts: Record = { filters }; + if (options.page_size != null) opts.pageSize = options.page_size; const results = await this.client.getAll(opts); if (Array.isArray(results)) return results.map(normalizeMemoryItem); @@ -198,7 +190,8 @@ class PlatformProvider implements Mem0Provider { async deleteAll(userId: string): Promise { await this.ensureClient(); - await this.client.deleteAll({ user_id: userId }); + // v3.0.0: SDK uses camelCase + await this.client.deleteAll({ userId }); } async history(memoryId: string): Promise< @@ -226,7 +219,7 @@ class OSSProvider implements Mem0Provider { constructor( private readonly ossConfig?: Mem0Config["oss"], - private readonly customPrompt?: string, + private readonly customInstructions?: string, private readonly resolvePath?: (p: string) => string, ) {} @@ -241,7 +234,8 @@ class OSSProvider implements Mem0Provider { } private _buildConfig(disableHistory = false): Record { - const config: Record = { version: "v1.1" }; + // v3.0.0: removed version field + const config: Record = {}; const defaultEmbedder = { provider: "openai", @@ -299,7 +293,8 @@ class OSSProvider implements Mem0Provider { config.disableHistory = true; } - if (this.customPrompt) config.customPrompt = this.customPrompt; + // v3.0.0: customPrompt renamed to customInstructions + if (this.customInstructions) config.customInstructions = this.customInstructions; return config; } @@ -347,7 +342,8 @@ class OSSProvider implements Mem0Provider { } } - await mem.getAll({ userId: "__mem0_warmup__" }); + // v3.0.0: entity IDs must be in filters, not top-level + await mem.getAll({ filters: { user_id: "__mem0_warmup__" } }); this.memory = mem; } @@ -364,9 +360,7 @@ class OSSProvider implements Mem0Provider { // Agentic harness: direct storage bypass if (options.infer !== undefined) addOpts.infer = options.infer; if (options.metadata) addOpts.metadata = options.metadata; - if (options.expiration_date) - addOpts.expirationDate = options.expiration_date; - if (options.immutable) addOpts.immutable = options.immutable; + // v3.0.0: removed expiration_date, immutable // OSS SDK doesn't support deduced_memories — when infer=false, it stores // raw message content directly. Rewrite messages to contain the facts so @@ -385,17 +379,24 @@ class OSSProvider implements Mem0Provider { async search(query: string, options: SearchOptions): Promise { await this.ensureMemory(); - // OSS SDK uses camelCase: userId/runId, not user_id/run_id - const opts: Record = { userId: options.user_id }; - if (options.run_id) opts.runId = options.run_id; - if (options.limit != null) opts.limit = options.limit; - else if (options.top_k != null) opts.limit = options.top_k; - if (options.keyword_search != null) - opts.keyword_search = options.keyword_search; - if (options.reranking != null) opts.reranking = options.reranking; - if (options.source) opts.source = options.source; + // v3.0.0: entity IDs must be in filters, not top-level; limit renamed to topK + const opts: Record = {}; + if (options.top_k != null) opts.topK = options.top_k; if (options.threshold != null) opts.threshold = options.threshold; + // Build filters with user_id/run_id inside (v3.0.0 requirement) + // Filters use snake_case as they're passed directly to the vector store + // Note: source is NOT a valid filter field - only used when adding + const baseFilters: Record = { user_id: options.user_id }; + if (options.run_id) baseFilters.run_id = options.run_id; + + // Merge with any additional user-provided filters + if (options.filters) { + opts.filters = { AND: [baseFilters, options.filters] }; + } else { + opts.filters = baseFilters; + } + const results = await this.memory.search(query, opts); const normalized = normalizeSearchResults(results); @@ -417,10 +418,16 @@ class OSSProvider implements Mem0Provider { async getAll(options: ListOptions): Promise { await this.ensureMemory(); - // OSS SDK uses camelCase: userId/runId, not user_id/run_id - const getAllOpts: Record = { userId: options.user_id }; - if (options.run_id) getAllOpts.runId = options.run_id; - if (options.source) getAllOpts.source = options.source; + // v3.0.0: entity IDs must be in filters, not top-level + // Filters use snake_case as they're passed directly to the vector store + // Note: source is NOT a valid filter field - only used when adding + const filters: Record = { user_id: options.user_id }; + if (options.run_id) filters.run_id = options.run_id; + + // OSS SDK uses topK for limiting results (not pageSize like Platform) + const getAllOpts: Record = { filters }; + if (options.page_size != null) getAllOpts.topK = options.page_size; + const results = await this.memory.getAll(getAllOpts); if (Array.isArray(results)) return results.map(normalizeMemoryItem); if (results?.results && Array.isArray(results.results)) @@ -476,7 +483,8 @@ export function createProvider( api: OpenClawPluginApi, ): Mem0Provider { if (cfg.mode === "open-source") { - return new OSSProvider(cfg.oss, cfg.customPrompt, (p) => + // v3.0.0: use customInstructions (was customPrompt) + return new OSSProvider(cfg.oss, cfg.customInstructions, (p) => api.resolvePath(p), ); } @@ -502,6 +510,7 @@ export function providerToBackend( return { async add(content, messages, opts = {}) { const msgs = messages ?? (content ? [{ role: "user", content }] : []); + // v3.0.0: removed immutable, expiration_date const result = await provider.add( msgs as Array<{ role: string; content: string }>, { @@ -509,21 +518,18 @@ export function providerToBackend( source: "OPENCLAW", ...(opts.runId && { run_id: opts.runId }), ...(opts.metadata && { metadata: opts.metadata }), - ...(opts.immutable && { immutable: true }), ...(opts.infer === false && { infer: false }), - ...(opts.expires && { expiration_date: opts.expires }), }, ); return result as unknown as Record; }, async search(query, opts = {}) { + // v3.0.0: removed keyword_search, reranking const results = await provider.search(query, { user_id: opts.userId ?? userId, top_k: opts.topK, threshold: opts.threshold, - keyword_search: opts.keyword, - reranking: opts.rerank, filters: opts.filters, source: "OPENCLAW", }); diff --git a/openclaw/public-artifacts.ts b/openclaw/public-artifacts.ts new file mode 100644 index 000000000..a8ba105f5 --- /dev/null +++ b/openclaw/public-artifacts.ts @@ -0,0 +1,159 @@ +/** + * Public Artifacts Provider for OpenClaw memory-wiki bridge mode. + * + * Exposes Mem0 memories and dream state as artifacts that can be + * consumed by other plugins (e.g., memory-wiki in bridge mode). + */ + +import type { Mem0Provider, MemoryItem, Mem0Config } from "./types.ts"; +import type { MemoryArtifact } from "openclaw/plugin-sdk"; +import { getDreamState } from "./dream-gate.ts"; + +export interface PublicArtifactsContext { + provider: Mem0Provider; + cfg: Mem0Config; + stateDir?: string; + effectiveUserId: (sessionKey?: string) => string; +} + +/** + * Create a publicArtifacts provider that exposes Mem0 data to other plugins. + */ +export function createPublicArtifactsProvider(ctx: PublicArtifactsContext) { + return { + async listArtifacts(options?: { + userId?: string; + types?: string[]; + limit?: number; + }): Promise { + const artifacts: MemoryArtifact[] = []; + const userId = options?.userId ?? ctx.effectiveUserId(); + const types = options?.types ?? ["memory", "dream", "entity"]; + const limit = options?.limit ?? 100; + + try { + // Memory artifacts + if (types.includes("memory")) { + const memories = await ctx.provider.getAll({ + user_id: userId, + page_size: limit, + }); + + for (const mem of memories) { + artifacts.push(memoryToArtifact(mem)); + } + } + + // Dream state artifact (if dream enabled and stateDir available) + if (types.includes("dream") && ctx.stateDir && ctx.cfg.skills?.dream?.enabled) { + const dreamArtifact = getDreamArtifact(ctx.stateDir, userId); + if (dreamArtifact) { + artifacts.push(dreamArtifact); + } + } + + // Entity artifacts (grouped memories by category) + if (types.includes("entity")) { + const entityArtifacts = extractEntityArtifacts(artifacts.filter(a => a.type === "memory")); + artifacts.push(...entityArtifacts); + } + + } catch (err) { + console.warn( + "[mem0] publicArtifacts.listArtifacts failed:", + err instanceof Error ? err.message : err, + ); + } + + return artifacts.slice(0, limit); + }, + }; +} + +/** + * Convert a MemoryItem to a MemoryArtifact. + */ +function memoryToArtifact(mem: MemoryItem): MemoryArtifact { + return { + id: `mem0:memory:${mem.id}`, + type: "memory", + title: mem.memory.slice(0, 80) + (mem.memory.length > 80 ? "..." : ""), + content: mem.memory, + metadata: { + score: mem.score, + categories: mem.categories, + user_id: mem.user_id, + ...mem.metadata, + }, + createdAt: mem.created_at, + updatedAt: mem.updated_at, + }; +} + +/** + * Get dream consolidation state as an artifact. + */ +function getDreamArtifact(stateDir: string, userId: string): MemoryArtifact | null { + try { + const state = getDreamState(stateDir); + if (state.lastConsolidatedAt === 0) { + return null; // No consolidation has occurred yet + } + + const lastDate = new Date(state.lastConsolidatedAt).toISOString(); + return { + id: `mem0:dream:${userId}:state`, + type: "dream", + title: `Dream State (last: ${lastDate.split("T")[0]})`, + content: [ + `Last consolidation: ${lastDate}`, + `Sessions since: ${state.sessionsSince}`, + `Last session: ${state.lastSessionId ?? "none"}`, + ].join("\n"), + metadata: { + lastConsolidatedAt: state.lastConsolidatedAt, + sessionsSince: state.sessionsSince, + lastSessionId: state.lastSessionId, + user_id: userId, + }, + updatedAt: lastDate, + }; + } catch { + return null; + } +} + +/** + * Extract entity artifacts from memories (grouped by category). + */ +function extractEntityArtifacts(memoryArtifacts: MemoryArtifact[]): MemoryArtifact[] { + const byCategory = new Map(); + + for (const artifact of memoryArtifacts) { + const categories = (artifact.metadata?.categories as string[]) ?? ["uncategorized"]; + for (const cat of categories) { + const existing = byCategory.get(cat) ?? []; + existing.push(artifact); + byCategory.set(cat, existing); + } + } + + const entities: MemoryArtifact[] = []; + for (const [category, mems] of byCategory) { + if (mems.length >= 2) { + entities.push({ + id: `mem0:entity:${category}`, + type: "entity", + title: `${category.charAt(0).toUpperCase() + category.slice(1)} (${mems.length} memories)`, + content: mems.map(m => `- ${m.content}`).join("\n"), + metadata: { + category, + memoryCount: mems.length, + memoryIds: mems.map(m => m.id), + }, + }); + } + } + + return entities; +} diff --git a/openclaw/recall.ts b/openclaw/recall.ts index 2f4f77263..90a5c68a7 100644 --- a/openclaw/recall.ts +++ b/openclaw/recall.ts @@ -2,7 +2,7 @@ * Token-budgeted, category-ranked recall engine. * * Replaces naive "dump all search results" with: - * 1. Search with rerank + keyword_search + * 1. Search memories with threshold filtering * 2. Rank by category priority (identity first) * 3. Token-budget the results * 4. Format by category with importance scores @@ -253,18 +253,13 @@ export async function recall( const categoryOrder = recallConfig.categoryOrder ?? DEFAULT_CATEGORY_ORDER; const identityAlwaysInclude = recallConfig.identityAlwaysInclude !== false; - // Build search options with enhanced features + // Build search options (v3.0.0: keyword_search, reranking, filter_memories removed) const searchOpts: SearchOptions = { user_id: userId, top_k: maxMemories * 2, // Over-fetch for ranking threshold, - keyword_search: recallConfig.keywordSearch !== false, // Default on - reranking: recallConfig.rerank !== false, // Default on source: "OPENCLAW", }; - if (recallConfig.filterMemories) { - searchOpts.filter_memories = true; - } // Sanitize query: strip OpenClaw metadata prefix before searching const cleanQuery = sanitizeQuery(query); diff --git a/openclaw/sqlite-resilience.test.ts b/openclaw/sqlite-resilience.test.ts index 026e0b94d..484f75390 100644 --- a/openclaw/sqlite-resilience.test.ts +++ b/openclaw/sqlite-resilience.test.ts @@ -654,9 +654,9 @@ describe("OSSProvider — history error handling", () => { }); // --------------------------------------------------------------------------- -// 9. OSSProvider: customPrompt passthrough +// 9. OSSProvider: customInstructions passthrough (v3.0.0: renamed from customPrompt) // --------------------------------------------------------------------------- -describe("OSSProvider — customPrompt passthrough", () => { +describe("OSSProvider — customInstructions passthrough", () => { let capturedConfig: Record | undefined; beforeEach(() => { @@ -682,12 +682,13 @@ describe("OSSProvider — customPrompt passthrough", () => { vi.restoreAllMocks(); }); - it("passes customPrompt to Memory config when provided", async () => { + // v3.0.0: customPrompt renamed to customInstructions + it("passes customInstructions to Memory config when provided", async () => { const { createProvider } = await import("./index.ts"); const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: { disableHistory: true }, - customPrompt: "Extract only user preferences.", + customInstructions: "Extract only user preferences.", }); const api = { resolvePath: (p: string) => p } as any; const provider = createProvider(cfg, api); @@ -695,6 +696,6 @@ describe("OSSProvider — customPrompt passthrough", () => { await provider.search("test", { user_id: "u1" }); expect(capturedConfig).toBeDefined(); - expect(capturedConfig!.customPrompt).toBe("Extract only user preferences."); + expect(capturedConfig!.customInstructions).toBe("Extract only user preferences."); }); }); diff --git a/openclaw/telemetry.ts b/openclaw/telemetry.ts index ee0522851..93497f2a5 100644 --- a/openclaw/telemetry.ts +++ b/openclaw/telemetry.ts @@ -9,9 +9,9 @@ */ import { createHash, randomUUID } from "node:crypto"; -import { readPluginAuth, writePluginAuth, getBaseUrl } from "./cli/config-file.ts"; +import { readPluginAuth, writePluginAuth, getBaseUrl, clearAnonymousTelemetryId } from "./cli/config-file.ts"; -export const PLUGIN_VERSION = "1.0.6"; +export const PLUGIN_VERSION = "1.0.7"; const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"; const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"; @@ -84,8 +84,9 @@ function maybeBuildIdentifyEvent( $lib: "posthog-node", }, }; + // Clear the anonymous ID from config after aliasing (don't write empty string) try { - writePluginAuth({ anonymousTelemetryId: "" }); + clearAnonymousTelemetryId(); } catch { /* ignore — alias may double-fire next session, harmless */ } diff --git a/openclaw/tests/config-file.test.ts b/openclaw/tests/config-file.test.ts index f2d535757..e059ddff3 100644 --- a/openclaw/tests/config-file.test.ts +++ b/openclaw/tests/config-file.test.ts @@ -113,9 +113,15 @@ describe("readPluginAuth", () => { expect(auth.userId).toBe("user-snake"); }); - it("returns empty object when JSON is invalid", () => { + it("throws error when JSON is invalid (prevents config destruction)", () => { mockExists.mockReturnValue(true); mockReadText.mockReturnValue("not valid json {{{"); + expect(() => readPluginAuth()).toThrow(/Failed to parse[\s\S]*Fix the JSON syntax error/); + }); + + it("returns empty object when config file is empty", () => { + mockExists.mockReturnValue(true); + mockReadText.mockReturnValue(" "); expect(readPluginAuth()).toEqual({}); }); }); diff --git a/openclaw/tests/config.test.ts b/openclaw/tests/config.test.ts index 6e99a0f39..4b630f704 100644 --- a/openclaw/tests/config.test.ts +++ b/openclaw/tests/config.test.ts @@ -74,9 +74,10 @@ describe("mem0ConfigSchema.parse() — defaults", () => { expect(cfg.customCategories).toBe(DEFAULT_CUSTOM_CATEGORIES); }); - it("customPrompt defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => { - const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" }); - expect(cfg.customPrompt).toBe(DEFAULT_CUSTOM_INSTRUCTIONS); + // v3.0.0: customPrompt removed, use customInstructions instead + it("customPrompt input falls back to customInstructions", () => { + const cfg = mem0ConfigSchema.parse({ apiKey: "test-key", customPrompt: "My prompt" }); + expect(cfg.customInstructions).toBe("My prompt"); }); it("oss defaults to undefined", () => { @@ -296,13 +297,14 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => { expect(cfg.customInstructions).toBe(custom); }); - it("custom customPrompt overrides defaults", () => { + // v3.0.0: customPrompt renamed to customInstructions (backwards compat: customPrompt maps to customInstructions) + it("customPrompt input maps to customInstructions output", () => { const custom = "My custom prompt"; const cfg = mem0ConfigSchema.parse({ apiKey: "k", customPrompt: custom, }); - expect(cfg.customPrompt).toBe(custom); + expect(cfg.customInstructions).toBe(custom); }); it("custom customCategories override defaults", () => { diff --git a/openclaw/tests/filtering.test.ts b/openclaw/tests/filtering.test.ts new file mode 100644 index 000000000..501a434f7 --- /dev/null +++ b/openclaw/tests/filtering.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest"; +import { + isNoiseMessage, + isGenericAssistantMessage, + isSessionSpecificContent, + stripNoiseFromContent, + filterMessagesForExtraction, +} from "../filtering.ts"; + +describe("isNoiseMessage", () => { + it("returns true for heartbeat messages", () => { + expect(isNoiseMessage("HEARTBEAT_OK")).toBe(true); + expect(isNoiseMessage("NO_REPLY")).toBe(true); + }); + + it("returns true for single-word acknowledgments", () => { + expect(isNoiseMessage("ok")).toBe(true); + expect(isNoiseMessage("done")).toBe(true); + expect(isNoiseMessage("thanks")).toBe(true); + }); + + it("returns false for substantive content", () => { + expect(isNoiseMessage("My name is John and I live in Tokyo")).toBe(false); + }); +}); + +describe("isGenericAssistantMessage", () => { + it("returns true for generic acknowledgments", () => { + expect(isGenericAssistantMessage("I see you've shared this. How can I help?")).toBe(true); + expect(isGenericAssistantMessage("Got it! How can I assist you?")).toBe(true); + }); + + it("returns false for substantive responses", () => { + expect(isGenericAssistantMessage("Based on the code, the bug is in line 42 where the null check is missing.")).toBe(false); + }); +}); + +describe("isSessionSpecificContent", () => { + it("returns true for tool availability discussions", () => { + expect(isSessionSpecificContent( + "I do not currently see Mem0 write/update/delete tools exposed in this session." + )).toBe(true); + }); + + it("returns true for plugin capability statements", () => { + expect(isSessionSpecificContent( + "As of 2026-04-18, the openclaw-mem0 plugin does not expose a memory wiki capability." + )).toBe(true); + }); + + it("returns true for session-specific tool lists", () => { + expect(isSessionSpecificContent( + "The tools I have access to in this session are memory_search and memory_get." + )).toBe(true); + }); + + it("returns false for user preferences", () => { + expect(isSessionSpecificContent( + "My name is Kartik and I like to watch Pokemon." + )).toBe(false); + }); + + it("returns false for technical facts that span sessions", () => { + expect(isSessionSpecificContent( + "The project uses TypeScript 5.0 and Node.js 20." + )).toBe(false); + }); +}); + +describe("filterMessagesForExtraction", () => { + it("filters out session-specific tool discussions", () => { + const messages = [ + { role: "user", content: "What tools do you have access to?" }, + { role: "assistant", content: "I have memory_search and memory_get tools. The other tools are not exposed in this session." }, + { role: "user", content: "My name is Kartik" }, + { role: "assistant", content: "Got it, I'll remember that!" }, + ]; + + const filtered = filterMessagesForExtraction(messages); + + // Should keep the name statement, filter the tool discussion + expect(filtered.some(m => m.content.includes("Kartik"))).toBe(true); + expect(filtered.some(m => m.content.includes("not exposed in this session"))).toBe(false); + }); + + it("keeps valuable user facts", () => { + const messages = [ + { role: "user", content: "I prefer dark mode and use VS Code for development." }, + { role: "assistant", content: "Noted! Dark mode in VS Code is great for reducing eye strain." }, + ]; + + const filtered = filterMessagesForExtraction(messages); + expect(filtered.length).toBe(2); + expect(filtered[0].content).toContain("dark mode"); + }); +}); diff --git a/openclaw/tests/providers.test.ts b/openclaw/tests/providers.test.ts index f1e54fc25..024bf3d91 100644 --- a/openclaw/tests/providers.test.ts +++ b/openclaw/tests/providers.test.ts @@ -42,6 +42,7 @@ beforeEach(() => { // --------------------------------------------------------------------------- describe("providerToBackend — search", () => { + // v3.0.0: keyword_search, reranking removed from SDK it("delegates to provider.search with correct options", async () => { const provider = createMockProvider(); const backend = providerToBackend(provider as any, DEFAULT_USER); @@ -49,8 +50,6 @@ describe("providerToBackend — search", () => { const results = await backend.search("hello world", { topK: 10, threshold: 0.5, - keyword: true, - rerank: true, filters: { category: "preference" }, }); @@ -58,8 +57,6 @@ describe("providerToBackend — search", () => { user_id: DEFAULT_USER, top_k: 10, threshold: 0.5, - keyword_search: true, - reranking: true, filters: { category: "preference" }, source: "OPENCLAW", }); @@ -73,12 +70,11 @@ describe("providerToBackend — search", () => { await backend.search("query"); + // v3.0.0: keyword_search, reranking removed expect(provider.search).toHaveBeenCalledWith("query", { user_id: DEFAULT_USER, top_k: undefined, threshold: undefined, - keyword_search: undefined, - reranking: undefined, filters: undefined, source: "OPENCLAW", }); @@ -119,6 +115,7 @@ describe("providerToBackend — add", () => { ); }); + // v3.0.0: immutable, expiration_date removed from SDK it("forwards optional add options", async () => { const provider = createMockProvider(); const backend = providerToBackend(provider as any, DEFAULT_USER); @@ -126,9 +123,7 @@ describe("providerToBackend — add", () => { await backend.add("fact", undefined, { runId: "run-1", metadata: { source: "test" }, - immutable: true, infer: false, - expires: "2027-01-01", }); expect(provider.add).toHaveBeenCalledWith( @@ -137,9 +132,7 @@ describe("providerToBackend — add", () => { user_id: DEFAULT_USER, run_id: "run-1", metadata: { source: "test" }, - immutable: true, infer: false, - expiration_date: "2027-01-01", }), ); }); diff --git a/openclaw/tests/telemetry.test.ts b/openclaw/tests/telemetry.test.ts index 893d47d10..20abcaaa2 100644 --- a/openclaw/tests/telemetry.test.ts +++ b/openclaw/tests/telemetry.test.ts @@ -24,7 +24,7 @@ describe("telemetry", () => { }); it("exports PLUGIN_VERSION", () => { - expect(PLUGIN_VERSION).toBe("1.0.6"); + expect(PLUGIN_VERSION).toBe("1.0.7"); }); it("captureEvent does not throw", () => { diff --git a/openclaw/tests/tools.test.ts b/openclaw/tests/tools.test.ts index bbddc29ce..1c6f843ca 100644 --- a/openclaw/tests/tools.test.ts +++ b/openclaw/tests/tools.test.ts @@ -37,7 +37,6 @@ function createMockToolDeps(overrides = {}): ToolDeps { searchThreshold: 0.5, customInstructions: "test", customCategories: {}, - customPrompt: "test", } as any, provider: { search: vi @@ -108,13 +107,14 @@ describe("registerAllTools", () => { ]); }); - it("registers tools without a second argument (required, not optional)", () => { + it("registers tools with optional: false metadata", () => { const ctx = createMockToolDeps(); registerAllTools(ctx); const calls = (ctx.api.registerTool as ReturnType).mock.calls; for (const call of calls) { - expect(call).toHaveLength(1); + expect(call).toHaveLength(2); + expect(call[1]).toEqual({ optional: false }); } }); }); diff --git a/openclaw/tools/index.ts b/openclaw/tools/index.ts index 14219d4de..878bb228b 100644 --- a/openclaw/tools/index.ts +++ b/openclaw/tools/index.ts @@ -28,13 +28,14 @@ export interface ToolDeps { export function registerAllTools(deps: ToolDeps): void { const { api } = deps; + const nonOptional = { optional: false }; - api.registerTool(createMemorySearchTool(deps)); - api.registerTool(createMemoryAddTool(deps)); - api.registerTool(createMemoryGetTool(deps)); - api.registerTool(createMemoryListTool(deps)); - api.registerTool(createMemoryUpdateTool(deps)); - api.registerTool(createMemoryDeleteTool(deps)); - api.registerTool(createMemoryEventListTool(deps)); - api.registerTool(createMemoryEventStatusTool(deps)); + api.registerTool(createMemorySearchTool(deps), nonOptional); + api.registerTool(createMemoryAddTool(deps), nonOptional); + api.registerTool(createMemoryGetTool(deps), nonOptional); + api.registerTool(createMemoryListTool(deps), nonOptional); + api.registerTool(createMemoryUpdateTool(deps), nonOptional); + api.registerTool(createMemoryDeleteTool(deps), nonOptional); + api.registerTool(createMemoryEventListTool(deps), nonOptional); + api.registerTool(createMemoryEventStatusTool(deps), nonOptional); } diff --git a/openclaw/tools/memory-add.ts b/openclaw/tools/memory-add.ts index 46e2f20b2..c021b30ec 100644 --- a/openclaw/tools/memory-add.ts +++ b/openclaw/tools/memory-add.ts @@ -1,11 +1,12 @@ import { Type } from "@sinclair/typebox"; import type { AddOptions } from "../types.ts"; import { isSubagentSession } from "../isolation.ts"; -import { resolveCategories, ttlToExpirationDate } from "../skill-loader.ts"; +import { isNoiseMessage, stripNoiseFromContent } from "../filtering.ts"; +// v3.0.0: resolveCategories/ttlToExpirationDate removed - expiration_date/immutable no longer supported import type { ToolDeps } from "./index.ts"; export function createMemoryAddTool(deps: ToolDeps) { - const { api, cfg, provider, resolveUserId, getCurrentSessionId, buildAddOptions, buildSearchOptions, skillsActive } = deps; + const { api, provider, resolveUserId, getCurrentSessionId, buildAddOptions, buildSearchOptions, skillsActive } = deps; return { name: "memory_add", @@ -28,11 +29,20 @@ export function createMemoryAddTool(deps: ToolDeps) { userId?: string; agentId?: string; metadata?: Record; longTerm?: boolean; }; - const allFacts: string[] = p.facts?.length ? p.facts : (p.text ? [p.text] : []); - if (allFacts.length === 0) { + const rawFacts: string[] = p.facts?.length ? p.facts : (p.text ? [p.text] : []); + if (rawFacts.length === 0) { return { content: [{ type: "text", text: "No facts provided. Pass 'text' or 'facts' array." }], details: { error: "missing_facts" } }; } + // Filter out noise and clean the facts before storing + const allFacts = rawFacts + .map((f) => stripNoiseFromContent(f)) + .filter((f) => f.length > 0 && !isNoiseMessage(f)); + + if (allFacts.length === 0) { + return { content: [{ type: "text", text: "All provided facts were filtered as noise. Nothing stored." }], details: { error: "all_noise" } }; + } + const start = Date.now(); try { const currentSessionId = getCurrentSessionId(); @@ -53,21 +63,12 @@ export function createMemoryAddTool(deps: ToolDeps) { ...(category && { category }), ...(importance !== undefined && { importance }), }; - const categories = resolveCategories(cfg.skills); - const catConfig = category ? categories[category] : undefined; - const expirationDate = catConfig ? ttlToExpirationDate(catConfig.ttl) : undefined; - const isImmutable = catConfig?.immutable ?? false; const addOpts: AddOptions = { user_id: uid, source: "OPENCLAW", infer: false, deduced_memories: allFacts, metadata: parsedMetadata ?? {}, - ...(expirationDate && { expiration_date: expirationDate }), - ...(isImmutable && { immutable: true }), }; if (runId) addOpts.run_id = runId; - if (cfg.mode === "platform") { - addOpts.output_format = "v1.1"; - } const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts); const count = result.results?.length ?? 0; diff --git a/openclaw/tools/memory-search.ts b/openclaw/tools/memory-search.ts index c9f666127..5c8f7f46a 100644 --- a/openclaw/tools/memory-search.ts +++ b/openclaw/tools/memory-search.ts @@ -16,7 +16,7 @@ export function createMemorySearchTool(deps: ToolDeps) { agentId: Type.Optional(Type.String({ description: "Agent ID to search a specific agent's memories" })), scope: Type.Optional( Type.Union([Type.Literal("session"), Type.Literal("long-term"), Type.Literal("all")], { - description: 'Scope: "long-term" (default), "session", or "all"', + description: 'Scope: "all" (default), "session", or "long-term"', }), ), categories: Type.Optional(Type.Array(Type.String(), { description: "Filter by category" })), @@ -25,7 +25,7 @@ export function createMemorySearchTool(deps: ToolDeps) { async execute(_toolCallId: string, params: Record) { const { - query, limit, userId, agentId, scope = "long-term", + query, limit, userId, agentId, scope = "all", categories: filterCategories, filters: agentFilters, } = params as { query: string; limit?: number; userId?: string; agentId?: string; diff --git a/openclaw/types.ts b/openclaw/types.ts index 5aef97a4c..5c7b1ac88 100644 --- a/openclaw/types.ts +++ b/openclaw/types.ts @@ -12,8 +12,7 @@ export type Mem0Config = { baseUrl?: string; customInstructions: string; customCategories: Record; - // OSS-specific - customPrompt?: string; + // OSS-specific (customPrompt renamed to customInstructions in v3.0.0) oss?: { embedder?: { provider: string; config: Record }; vectorStore?: { provider: string; config: Record }; @@ -37,15 +36,12 @@ export interface AddOptions { user_id: string; run_id?: string; custom_instructions?: string; - custom_categories?: Array>; - output_format?: string; + custom_categories?: Record; source?: string; // Agentic harness additions infer?: boolean; deduced_memories?: string[]; metadata?: Record; - expiration_date?: string; - immutable?: boolean; } export interface SearchOptions { @@ -53,10 +49,6 @@ export interface SearchOptions { run_id?: string; top_k?: number; threshold?: number; - limit?: number; - keyword_search?: boolean; - reranking?: boolean; - filter_memories?: boolean; categories?: string[]; filters?: Record; source?: string;