Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 566bbeb018 | |||
| f8c6ccf5fe | |||
| 8b9f11df39 | |||
| 87157c1fe7 | |||
| 458576dd0c | |||
| 5c14b4c594 | |||
| 6f43487c34 |
@@ -9,6 +9,8 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
|
||||
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
|
||||
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
|
||||
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status` (renamed from `stats`), `openclaw mem0 config show`, `openclaw mem0 config set`
|
||||
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
|
||||
- **`event list` / `event status` CLI commands**: Monitor background processing events
|
||||
- **`fs-safe.ts` module**: Isolated filesystem wrappers (sync read/write/exists/mkdir/unlink) in a separate entry point — keeps file I/O out of the main bundle
|
||||
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
|
||||
- **`cli/config-file.ts`**: Persistent plugin auth storage in `~/.openclaw/openclaw.json`
|
||||
@@ -16,7 +18,7 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
|
||||
- **Test suite**: 329 tests across 10 test files covering tools, CLI, config, dream gate, providers, and skill-loader
|
||||
|
||||
### Changed
|
||||
- **Modular architecture**: Extracted tools into `tools/` directory (7 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines
|
||||
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts` — `index.ts` down from ~1700 to ~890 lines
|
||||
- **Code splitting**: tsup builds with `splitting: true` and two entry points (`index.ts`, `fs-safe.ts`), separating filesystem I/O from the main bundle
|
||||
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`) matching the plugin manifest
|
||||
- **WRITE_TOOLS updated**: Dream gate tracks `memory_delete` and `memory_add` instead of `memory_forget` and `memory_store`
|
||||
@@ -25,11 +27,15 @@ All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in th
|
||||
- **Auto-capture fire-and-forget**: `provider.add()` now runs in the background via `.then()/.catch()` — the `agent_end` hook returns immediately, zero event loop blocking
|
||||
- **Auto-capture minimum content gate**: Skips extraction when total user content is <50 chars after filtering — trivial conversations ("ok", "thanks") no longer trigger LLM calls
|
||||
- **CLI search**: Lowered threshold to 0.3 so explicit searches are more permissive than auto-recall
|
||||
- **Init defaults**: Choice defaults to `1` (email login) on Enter, User ID defaults to OS username — no more empty values
|
||||
- **Init no longer stores `baseUrl`**: Uses `https://api.mem0.ai` directly instead of persisting it to config
|
||||
- **Help output reordered**: `openclaw mem0 help` now shows high-value commands first (search, add) instead of alphabetical
|
||||
|
||||
### Removed
|
||||
- `memory_store` tool — replaced by `memory_add`
|
||||
- `memory_forget` tool — replaced by `memory_delete`
|
||||
- `memory_delete_all` tool — merged into `memory_delete`
|
||||
- `memory_history` tool and `history` CLI command — deprecated
|
||||
- **`custom_instructions` / `custom_categories` in `buildAddOptions`**: No longer injected into every auto-capture API call. Config fields (`customInstructions`, `customCategories`) remain as user-configurable options.
|
||||
|
||||
## [1.0.3] - 2026-04-03
|
||||
|
||||
+41
-63
@@ -12,7 +12,7 @@ openclaw plugins install @mem0/openclaw-mem0
|
||||
|
||||
### Platform (Mem0 Cloud)
|
||||
|
||||
Get an API key from [app.mem0.ai](https://app.mem0.ai):
|
||||
Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys):
|
||||
|
||||
```bash
|
||||
openclaw mem0 init --api-key <your-key> --user-id <your-user-id>
|
||||
@@ -32,7 +32,9 @@ Or configure manually in `openclaw.json`:
|
||||
|
||||
### Open-Source (Self-hosted)
|
||||
|
||||
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM.
|
||||
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Vectors are stored locally in SQLite at `~/.mem0/vector_store.db` — no external database required.
|
||||
|
||||
Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction.
|
||||
|
||||
```json5
|
||||
"openclaw-mem0": {
|
||||
@@ -53,7 +55,7 @@ Customize the embedder, vector store, or LLM via the `oss` block:
|
||||
"oss": {
|
||||
"embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } },
|
||||
"vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } },
|
||||
"llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
|
||||
"llm": { "provider": "openai", "config": { "model": "gpt-5.4" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -74,79 +76,57 @@ Both run silently. No prompting, no manual calls required.
|
||||
|
||||
### Memory Scopes
|
||||
|
||||
| Scope | Description |
|
||||
|-------|-------------|
|
||||
| **Session (short-term)** | Memories scoped to the current conversation via `run_id`. Automatically recalled alongside long-term memories. |
|
||||
| **User (long-term)** | Persistent memories that span all sessions. Stored via `memory_add` with `longTerm: true` (the default). |
|
||||
|
||||
During auto-recall, both scopes are searched and presented separately — long-term first, then session — so the agent has full context.
|
||||
- **Session (short-term)** — Scoped to the current conversation via `run_id`. Recalled alongside long-term memories.
|
||||
- **User (long-term)** — Persistent across all sessions. Default for `memory_add`.
|
||||
|
||||
### Multi-Agent Isolation
|
||||
|
||||
In multi-agent setups, each agent gets its own memory namespace automatically. Session keys matching `agent:<name>:<uuid>` route memories to `userId:agent:<name>`. Single-agent deployments are unaffected.
|
||||
|
||||
All memory tools accept an optional `agentId` parameter for cross-agent queries:
|
||||
|
||||
```
|
||||
memory_search({ query: "user's tech stack", agentId: "researcher" })
|
||||
```
|
||||
Each agent gets its own memory namespace automatically via session key routing (`agent:<name>:<uuid>` maps to `userId:agent:<name>`). Single-agent setups are unaffected.
|
||||
|
||||
## Agent Tools
|
||||
|
||||
Seven tools are available to the agent during conversations:
|
||||
Eight tools are registered for agent use:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| **`memory_search`** | Search memories by natural language query. Supports `scope` (`session`, `long-term`, `all`) and `agentId` filtering. |
|
||||
| **`memory_add`** | Save a fact to memory. Supports `category`, `importance`, `longTerm`, and `agentId`. |
|
||||
| **`memory_get`** | Retrieve a specific memory by ID. |
|
||||
| **`memory_list`** | List stored memories with optional `userId`, `agentId`, and `limit` filters. |
|
||||
| **`memory_update`** | Update an existing memory's text in place. Preserves edit history. |
|
||||
| **`memory_delete`** | Delete by ID, search query, or bulk (`all: true`). Requires `confirm: true` for bulk. |
|
||||
| **`memory_history`** | View the edit history of a specific memory. |
|
||||
| ---- | ----------- |
|
||||
| `memory_search` | Search by natural language query. Supports `scope` (`session`, `long-term`, `all`), `categories`, `filters`, and `agentId`. |
|
||||
| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `longTerm`, `metadata`. |
|
||||
| `memory_get` | Retrieve a single memory by ID. |
|
||||
| `memory_list` | List all memories. Filter by `userId`, `agentId`, `scope`. |
|
||||
| `memory_update` | Update a memory's text in place. Preserves history. |
|
||||
| `memory_delete` | Delete by `memoryId`, `query` (search-and-delete), or `all: true` (requires `confirm: true`). |
|
||||
| `memory_event_list` | List recent background processing events. Platform mode only. |
|
||||
| `memory_event_status` | Get status of a specific event by ID. Platform mode only. |
|
||||
|
||||
## CLI
|
||||
|
||||
All commands follow the pattern `openclaw mem0 <command>`.
|
||||
|
||||
### Memory Operations
|
||||
All commands: `openclaw mem0 <command>`.
|
||||
|
||||
```bash
|
||||
# Add a memory
|
||||
# Memory operations
|
||||
openclaw mem0 add "User prefers TypeScript over JavaScript"
|
||||
|
||||
# Search memories
|
||||
openclaw mem0 search "what languages does the user know"
|
||||
openclaw mem0 search "preferences" --scope long-term
|
||||
openclaw mem0 search "context" --scope session
|
||||
|
||||
# Get, list, update, delete
|
||||
openclaw mem0 get <memory_id>
|
||||
openclaw mem0 list --user-id alice --top-k 20
|
||||
openclaw mem0 update <memory_id> "Updated preference text"
|
||||
openclaw mem0 delete <memory_id>
|
||||
openclaw mem0 delete --all --user-id alice --confirm
|
||||
openclaw mem0 import memories.json
|
||||
|
||||
# View edit history
|
||||
openclaw mem0 history <memory_id>
|
||||
```
|
||||
|
||||
### Management
|
||||
|
||||
```bash
|
||||
# Authenticate and configure
|
||||
# Management
|
||||
openclaw mem0 init
|
||||
openclaw mem0 init --api-key <key> --user-id alice
|
||||
|
||||
# Check connectivity
|
||||
openclaw mem0 status
|
||||
|
||||
# Manage configuration
|
||||
openclaw mem0 config show
|
||||
openclaw mem0 config get api_key
|
||||
openclaw mem0 config set user_id alice
|
||||
|
||||
# Memory consolidation (review, merge, prune)
|
||||
# Events (platform only)
|
||||
openclaw mem0 event list
|
||||
openclaw mem0 event status <event_id>
|
||||
|
||||
# Memory consolidation
|
||||
openclaw mem0 dream
|
||||
openclaw mem0 dream --dry-run
|
||||
```
|
||||
@@ -156,9 +136,9 @@ openclaw mem0 dream --dry-run
|
||||
### General
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
||||
| `userId` | `string` | `"default"` | Unique identifier for the user. You define this — it's not found in any dashboard. All memories are scoped to this value. |
|
||||
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
|
||||
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn |
|
||||
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn |
|
||||
| `topK` | `number` | `5` | Max memories returned per recall |
|
||||
@@ -167,32 +147,30 @@ openclaw mem0 dream --dry-run
|
||||
### Platform Mode
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
|
||||
| `orgId` | `string` | — | Organization ID |
|
||||
| `projectId` | `string` | — | Project ID |
|
||||
| `enableGraph` | `boolean` | `false` | Enable entity graph for relationship tracking |
|
||||
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules for what to store and how to format |
|
||||
| `customCategories` | `object` | *(12 defaults)* | Category name to description map for memory tagging |
|
||||
| `enableGraph` | `boolean` | `false` | Entity graph for relationship tracking |
|
||||
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules |
|
||||
| `customCategories` | `object` | *(12 defaults)* | Category name to description map |
|
||||
|
||||
### Open-Source Mode
|
||||
|
||||
All fields below are optional. Defaults use OpenAI embeddings, in-memory vector store, and OpenAI LLM.
|
||||
All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `customPrompt` | `string` | *(built-in)* | Extraction prompt |
|
||||
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider |
|
||||
| `oss.embedder.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
|
||||
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider |
|
||||
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`) |
|
||||
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider (see list above) |
|
||||
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`, `dbPath`) |
|
||||
| `oss.llm.provider` | `string` | `"openai"` | LLM provider |
|
||||
| `oss.llm.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
|
||||
| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
|
||||
| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization |
|
||||
|
||||
Supported providers: `openai`, `anthropic`, `ollama`, `lmstudio`, `qdrant`, `chroma`, and more. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for the full list.
|
||||
| `oss.historyDbPath` | `string` | — | SQLite path for edit history |
|
||||
| `oss.disableHistory` | `boolean` | `false` | Skip history DB |
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
[Apache 2.0](LICENSE)
|
||||
|
||||
+254
-77
@@ -10,7 +10,7 @@
|
||||
* - list : List memories with optional filters (--user-id, --agent-id, --top-k)
|
||||
* - update : Update a memory's text
|
||||
* - delete : Delete a memory or all memories (--all, --confirm)
|
||||
* - history : View edit history of a memory
|
||||
* - import : Import memories from a JSON file
|
||||
*
|
||||
* Management:
|
||||
* - init : Authenticate with Mem0 Platform (email or API key)
|
||||
@@ -18,6 +18,8 @@
|
||||
* - config show : Display current plugin configuration
|
||||
* - config get : Get a single config value
|
||||
* - config set : Update a plugin config field
|
||||
* - event list : List recent background events
|
||||
* - event status: Get status of a specific event
|
||||
* - dream : Run memory consolidation
|
||||
*
|
||||
* Naming conventions match the Python CLI (`mem0 init`, `mem0 search`, etc.)
|
||||
@@ -35,12 +37,12 @@ import type {
|
||||
SearchOptions,
|
||||
} from "../types.ts";
|
||||
import { loadDreamPrompt } from "../skill-loader.ts";
|
||||
import { readText } from "../fs-safe.ts";
|
||||
import type { PluginAuthConfig } from "./config-file.ts";
|
||||
import {
|
||||
readPluginAuth,
|
||||
writePluginAuth,
|
||||
writePluginConfigField,
|
||||
getBaseUrl,
|
||||
OPENCLAW_CONFIG_FILE,
|
||||
} from "./config-file.ts";
|
||||
|
||||
@@ -49,13 +51,14 @@ import {
|
||||
// ============================================================================
|
||||
|
||||
/** Prompt user for input on stderr (keeps stdout clean for piping). */
|
||||
function promptInput(question: string): Promise<string> {
|
||||
function promptInput(question: string, prefill?: string): Promise<string> {
|
||||
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
if (prefill) rl.write(prefill);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,14 +199,13 @@ async function verifyEmailCode(
|
||||
*/
|
||||
function saveLoginConfig(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
userIdFlag?: string,
|
||||
userEmail?: string,
|
||||
): void {
|
||||
const existingAuth = readPluginAuth();
|
||||
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
||||
|
||||
writePluginAuth({ apiKey, baseUrl, userId, mode: "platform", ...(userEmail && { userEmail }) });
|
||||
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
|
||||
|
||||
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
||||
console.log(` Mode: platform`);
|
||||
@@ -245,7 +247,8 @@ export function registerCliCommands(
|
||||
({ program }) => {
|
||||
const mem0 = program
|
||||
.command("mem0")
|
||||
.description("Mem0 memory plugin commands");
|
||||
.description("Mem0 memory plugin commands")
|
||||
.configureHelp({ sortSubcommands: false, subcommandTerm: (cmd) => cmd.name() });
|
||||
|
||||
// Telemetry: fire event for each CLI subcommand
|
||||
if (captureCliEvent) {
|
||||
@@ -278,7 +281,7 @@ export function registerCliCommands(
|
||||
userId?: string;
|
||||
}) => {
|
||||
try {
|
||||
const baseUrl = getBaseUrl();
|
||||
const baseUrl = "https://api.mem0.ai";
|
||||
const existingAuth = readPluginAuth();
|
||||
const hasExistingConfig = !!(existingAuth.apiKey || existingAuth.mode);
|
||||
|
||||
@@ -290,7 +293,7 @@ export function registerCliCommands(
|
||||
}
|
||||
|
||||
const check = await validateApiKey(baseUrl, opts.apiKey);
|
||||
saveLoginConfig(opts.apiKey, baseUrl, opts.userId, check.userEmail);
|
||||
saveLoginConfig(opts.apiKey, opts.userId, check.userEmail);
|
||||
if (hasExistingConfig) {
|
||||
console.log(
|
||||
" Existing configuration detected — updated API key (other settings preserved).",
|
||||
@@ -322,7 +325,7 @@ export function registerCliCommands(
|
||||
const apiKey = await verifyEmailCode(baseUrl, email, opts.code);
|
||||
if (!apiKey) return;
|
||||
|
||||
saveLoginConfig(apiKey, baseUrl, opts.userId, email);
|
||||
saveLoginConfig(apiKey, opts.userId, email);
|
||||
if (hasExistingConfig) {
|
||||
console.log(
|
||||
" Existing configuration detected — updated API key (other settings preserved).",
|
||||
@@ -390,7 +393,7 @@ export function registerCliCommands(
|
||||
// Validate existing key before asking
|
||||
if (existingAuth.apiKey) {
|
||||
const check = await validateApiKey(
|
||||
existingAuth.baseUrl || baseUrl,
|
||||
baseUrl,
|
||||
existingAuth.apiKey,
|
||||
);
|
||||
if (check.ok) {
|
||||
@@ -429,7 +432,7 @@ export function registerCliCommands(
|
||||
console.log(" 2. Enter API key manually");
|
||||
console.log(" 3. Open-source mode (self-hosted)\n");
|
||||
|
||||
const choice = await promptInput(" Choice (1/2/3): ");
|
||||
const choice = (await promptInput(" Choice: ", "1")) || "1";
|
||||
|
||||
if (choice === "1") {
|
||||
// --- Email interactive flow ---
|
||||
@@ -461,13 +464,13 @@ export function registerCliCommands(
|
||||
if (!userIdValue) {
|
||||
const defaultUid = resolveUserId(undefined, existingAuth.userId);
|
||||
const uidInput = await promptInput(
|
||||
` User ID (${defaultUid}): `,
|
||||
` User ID: `, defaultUid,
|
||||
);
|
||||
userIdValue = uidInput || undefined;
|
||||
userIdValue = uidInput || defaultUid;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
saveLoginConfig(apiKey, baseUrl, userIdValue, email);
|
||||
saveLoginConfig(apiKey, userIdValue, email);
|
||||
console.log(" Authenticated!");
|
||||
console.log(
|
||||
" Restart the gateway: openclaw gateway restart\n",
|
||||
@@ -485,14 +488,14 @@ export function registerCliCommands(
|
||||
if (!userIdValue2) {
|
||||
const defaultUid = resolveUserId(undefined, existingAuth.userId);
|
||||
const uidInput = await promptInput(
|
||||
` User ID (${defaultUid}): `,
|
||||
` User ID: `, defaultUid,
|
||||
);
|
||||
userIdValue2 = uidInput || undefined;
|
||||
userIdValue2 = uidInput || defaultUid;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
const check = await validateApiKey(baseUrl, key);
|
||||
saveLoginConfig(key, baseUrl, userIdValue2, check.userEmail);
|
||||
saveLoginConfig(key, userIdValue2, check.userEmail);
|
||||
|
||||
if (check.ok) {
|
||||
console.log(
|
||||
@@ -545,7 +548,7 @@ export function registerCliCommands(
|
||||
"\n Skipped. You can add it later via:",
|
||||
);
|
||||
console.log(
|
||||
" openclaw mem0 config set oss.embedder.config.apiKey <key>",
|
||||
" openclaw mem0 config set embedder_key <key>",
|
||||
);
|
||||
console.log(
|
||||
" Or set OPENAI_API_KEY in your environment.\n",
|
||||
@@ -557,9 +560,9 @@ export function registerCliCommands(
|
||||
if (!userIdValue3) {
|
||||
const defaultUid = resolveUserId(undefined, existingAuth.userId);
|
||||
const uidInput = await promptInput(
|
||||
` User ID (${defaultUid}): `,
|
||||
` User ID: `, defaultUid,
|
||||
);
|
||||
userIdValue3 = uidInput || undefined;
|
||||
userIdValue3 = uidInput || defaultUid;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
@@ -901,27 +904,6 @@ export function registerCliCommands(
|
||||
},
|
||||
);
|
||||
|
||||
// ====================================================================
|
||||
// history (matches: mem0 history <memory_id>)
|
||||
// ====================================================================
|
||||
|
||||
mem0
|
||||
.command("history")
|
||||
.description("View edit history of a memory")
|
||||
.argument("<memory_id>", "Memory ID to view history for")
|
||||
.action(async (memoryId: string) => {
|
||||
try {
|
||||
const entries = await provider.history(memoryId);
|
||||
if (!entries.length) {
|
||||
console.log("No history found for this memory.");
|
||||
return;
|
||||
}
|
||||
console.log(JSON.stringify(entries, null, 2));
|
||||
} catch (err) {
|
||||
console.error(`History failed: ${String(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// status (matches: mem0 status)
|
||||
// ====================================================================
|
||||
@@ -967,6 +949,7 @@ export function registerCliCommands(
|
||||
const CONFIG_KEYS: Record<string, string> = {
|
||||
// Short aliases (matches Python CLI)
|
||||
api_key: "apiKey",
|
||||
email: "userEmail",
|
||||
base_url: "baseUrl",
|
||||
user_id: "userId",
|
||||
org_id: "orgId",
|
||||
@@ -976,30 +959,34 @@ export function registerCliCommands(
|
||||
auto_capture: "autoCapture",
|
||||
top_k: "topK",
|
||||
mode: "mode",
|
||||
// Dot notation (matches Python CLI: platform.api_key, defaults.user_id)
|
||||
"platform.api_key": "apiKey",
|
||||
"platform.base_url": "baseUrl",
|
||||
"defaults.user_id": "userId",
|
||||
"defaults.org_id": "orgId",
|
||||
"defaults.project_id": "projectId",
|
||||
"defaults.enable_graph": "enableGraph",
|
||||
"defaults.auto_recall": "autoRecall",
|
||||
"defaults.auto_capture": "autoCapture",
|
||||
"defaults.top_k": "topK",
|
||||
embedder_provider: "oss.embedder.provider",
|
||||
embedder_model: "oss.embedder.config.model",
|
||||
embedder_key: "oss.embedder.config.apiKey",
|
||||
llm_provider: "oss.llm.provider",
|
||||
llm_model: "oss.llm.config.model",
|
||||
llm_key: "oss.llm.config.apiKey",
|
||||
vector_provider: "oss.vectorStore.provider",
|
||||
vector_host: "oss.vectorStore.config.host",
|
||||
vector_port: "oss.vectorStore.config.port",
|
||||
collection_name: "oss.vectorStore.config.collectionName",
|
||||
vector_db_path: "oss.vectorStore.config.dbPath",
|
||||
history_db_path: "oss.historyDbPath",
|
||||
disable_history: "oss.disableHistory",
|
||||
};
|
||||
|
||||
// Keys that contain secrets — redact in show/get output
|
||||
const SECRET_KEYS = new Set(["apiKey"]);
|
||||
const SECRET_KEYS = new Set(["apiKey", "oss.embedder.config.apiKey", "oss.llm.config.apiKey"]);
|
||||
|
||||
// Boolean config fields — coerce "true"/"1"/"yes" on set
|
||||
const BOOLEAN_KEYS = new Set([
|
||||
"enableGraph",
|
||||
"autoRecall",
|
||||
"autoCapture",
|
||||
"oss.disableHistory",
|
||||
]);
|
||||
|
||||
// Integer config fields — coerce to number on set
|
||||
const INTEGER_KEYS = new Set(["topK"]);
|
||||
const INTEGER_KEYS = new Set(["topK", "oss.vectorStore.config.port"]);
|
||||
|
||||
/** Resolve a user-facing key to the internal camelCase field name. */
|
||||
function resolveConfigKey(key: string): string | null {
|
||||
@@ -1008,6 +995,15 @@ export function registerCliCommands(
|
||||
|
||||
/** Read a config value by internal field name. */
|
||||
function getConfigValue(field: string): unknown {
|
||||
if (field.startsWith("oss.")) {
|
||||
const parts = field.split(".");
|
||||
let current: unknown = cfg.oss;
|
||||
for (let i = 1; i < parts.length && current != null; i++) {
|
||||
current = (current as Record<string, unknown>)[parts[i]];
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
const auth = readPluginAuth();
|
||||
const values: Record<string, unknown> = {
|
||||
apiKey: auth.apiKey ?? cfg.apiKey,
|
||||
@@ -1016,6 +1012,7 @@ export function registerCliCommands(
|
||||
orgId: auth.orgId ?? cfg.orgId,
|
||||
projectId: auth.projectId ?? cfg.projectId,
|
||||
mode: auth.mode ?? cfg.mode,
|
||||
userEmail: auth.userEmail,
|
||||
enableGraph: cfg.enableGraph,
|
||||
autoRecall: cfg.autoRecall,
|
||||
autoCapture: cfg.autoCapture,
|
||||
@@ -1045,20 +1042,37 @@ export function registerCliCommands(
|
||||
.command("show")
|
||||
.description("Show current configuration")
|
||||
.action(() => {
|
||||
// Display order matching Python CLI: platform first, then defaults
|
||||
const entries: Array<[string, string, string]> = [
|
||||
["platform.api_key", "apiKey", ""],
|
||||
["platform.base_url", "baseUrl", ""],
|
||||
["defaults.user_id", "userId", ""],
|
||||
["defaults.org_id", "orgId", ""],
|
||||
["defaults.project_id", "projectId", ""],
|
||||
["defaults.enable_graph", "enableGraph", ""],
|
||||
["defaults.auto_recall", "autoRecall", ""],
|
||||
["defaults.auto_capture", "autoCapture", ""],
|
||||
["defaults.top_k", "topK", ""],
|
||||
["mode", "mode", ""],
|
||||
// Display order: general first, then mode-specific
|
||||
const entries: Array<[string, string]> = [
|
||||
["mode", "mode"],
|
||||
["user_id", "userId"],
|
||||
["auto_recall", "autoRecall"],
|
||||
["auto_capture", "autoCapture"],
|
||||
["top_k", "topK"],
|
||||
];
|
||||
|
||||
if (cfg.mode === "platform") {
|
||||
entries.push(
|
||||
["api_key", "apiKey"],
|
||||
["email", "userEmail"],
|
||||
["org_id", "orgId"],
|
||||
["project_id", "projectId"],
|
||||
["enable_graph", "enableGraph"],
|
||||
);
|
||||
} else {
|
||||
entries.push(
|
||||
["embedder_provider", "oss.embedder.provider"],
|
||||
["embedder_model", "oss.embedder.config.model"],
|
||||
["embedder_key", "oss.embedder.config.apiKey"],
|
||||
["llm_provider", "oss.llm.provider"],
|
||||
["llm_model", "oss.llm.config.model"],
|
||||
["llm_key", "oss.llm.config.apiKey"],
|
||||
["vector_provider", "oss.vectorStore.provider"],
|
||||
["history_db_path", "oss.historyDbPath"],
|
||||
["disable_history", "oss.disableHistory"],
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
const maxKeyLen = Math.max(
|
||||
...entries.map(([k]) => k.length),
|
||||
@@ -1086,17 +1100,21 @@ export function registerCliCommands(
|
||||
console.log(" openclaw mem0 config set <key> <value>");
|
||||
console.log("");
|
||||
console.log(" Examples:");
|
||||
console.log(" openclaw mem0 config set mode open-source");
|
||||
console.log(" openclaw mem0 config set mode platform");
|
||||
console.log(" openclaw mem0 config set auto_recall false");
|
||||
console.log(" openclaw mem0 config set top_k 10");
|
||||
if (cfg.mode === "platform") {
|
||||
console.log(" openclaw mem0 config set mode open-source");
|
||||
console.log(" openclaw mem0 config set auto_recall false");
|
||||
} else {
|
||||
console.log(" openclaw mem0 config set vector_provider qdrant");
|
||||
console.log(" openclaw mem0 config set llm_model gpt-4o");
|
||||
console.log(" openclaw mem0 config set embedder_provider openai");
|
||||
}
|
||||
console.log("");
|
||||
});
|
||||
|
||||
configCmd
|
||||
.command("get")
|
||||
.description("Get a config value")
|
||||
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
|
||||
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
|
||||
.action((key: string) => {
|
||||
const field = resolveConfigKey(key);
|
||||
if (!field) {
|
||||
@@ -1112,7 +1130,7 @@ export function registerCliCommands(
|
||||
configCmd
|
||||
.command("set")
|
||||
.description("Set a config value")
|
||||
.argument("<key>", "Config key (e.g. user_id, platform.api_key)")
|
||||
.argument("<key>", "Config key (e.g. user_id, api_key, llm_model)")
|
||||
.argument("<value>", "New value")
|
||||
.action((key: string, rawValue: string) => {
|
||||
const field = resolveConfigKey(key);
|
||||
@@ -1139,12 +1157,171 @@ export function registerCliCommands(
|
||||
value = parsed;
|
||||
}
|
||||
|
||||
writePluginAuth({ [field]: value } as PluginAuthConfig);
|
||||
// Nested OSS fields use dot-path writer; flat fields use auth writer
|
||||
if (field.startsWith("oss.")) {
|
||||
writePluginConfigField(field.split("."), value);
|
||||
} else {
|
||||
writePluginAuth({ [field]: value } as PluginAuthConfig);
|
||||
}
|
||||
console.log(
|
||||
`${key} = ${displayValue(field, value)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// import (matches: mem0 import <file>)
|
||||
// ====================================================================
|
||||
|
||||
mem0
|
||||
.command("import")
|
||||
.description("Import memories from a JSON file")
|
||||
.argument("<file>", "Path to JSON file containing memories")
|
||||
.option("--user-id <userId>", "Override user ID for all imported memories")
|
||||
.option("--agent-id <agentId>", "Override agent ID for all imported memories")
|
||||
.action(
|
||||
async (
|
||||
file: string,
|
||||
opts: { userId?: string; agentId?: string },
|
||||
) => {
|
||||
try {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(readText(file));
|
||||
} catch (err) {
|
||||
console.error(`Failed to read file: ${String(err)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
let added = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const item of items) {
|
||||
const content =
|
||||
item?.memory ?? item?.text ?? item?.content ?? "";
|
||||
if (!content) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await backend.add(content, undefined, {
|
||||
userId: opts.userId ?? item?.user_id ?? cfg.userId,
|
||||
agentId: opts.agentId ?? item?.agent_id,
|
||||
metadata: item?.metadata,
|
||||
});
|
||||
added++;
|
||||
} catch {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Imported ${added} memories.`);
|
||||
if (failed) {
|
||||
console.error(`${failed} memories failed to import.`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Import failed: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ====================================================================
|
||||
// event (matches: mem0 event list, mem0 event status <id>)
|
||||
// ====================================================================
|
||||
|
||||
const eventCmd = mem0
|
||||
.command("event")
|
||||
.description("Manage background processing events");
|
||||
|
||||
eventCmd
|
||||
.command("list")
|
||||
.description("List recent background events")
|
||||
.action(async () => {
|
||||
try {
|
||||
const results = await backend.listEvents();
|
||||
if (!results.length) {
|
||||
console.log("No events found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Table header
|
||||
const header = [
|
||||
"Event ID".padEnd(36),
|
||||
"Type".padEnd(14),
|
||||
"Status".padEnd(12),
|
||||
"Latency".padStart(10),
|
||||
"Created".padEnd(20),
|
||||
].join(" ");
|
||||
console.log(header);
|
||||
console.log("-".repeat(header.length));
|
||||
|
||||
for (const ev of results) {
|
||||
const evId = String(ev.id ?? "");
|
||||
const evType = String(ev.event_type ?? "—").padEnd(14);
|
||||
const status = String(ev.status ?? "—").padEnd(12);
|
||||
const latency = typeof ev.latency === "number"
|
||||
? `${Math.round(ev.latency as number)}ms`
|
||||
: "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
|
||||
console.log(
|
||||
`${evId.padEnd(36)} ${evType} ${status} ${latency.padStart(10)} ${created}`,
|
||||
);
|
||||
}
|
||||
console.log(`\n${results.length} event${results.length !== 1 ? "s" : ""}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to list events: ${String(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
eventCmd
|
||||
.command("status")
|
||||
.description("Get status of a specific background event")
|
||||
.argument("<event_id>", "Event ID to check")
|
||||
.action(async (eventId: string) => {
|
||||
try {
|
||||
const ev = await backend.getEvent(eventId);
|
||||
|
||||
const status = String(ev.status ?? "—");
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const latency = typeof ev.latency === "number"
|
||||
? `${Math.round(ev.latency as number)}ms`
|
||||
: "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const updated = String(ev.updated_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
|
||||
console.log(`Event ID: ${eventId}`);
|
||||
console.log(`Type: ${evType}`);
|
||||
console.log(`Status: ${status}`);
|
||||
console.log(`Latency: ${latency}`);
|
||||
console.log(`Created: ${created}`);
|
||||
console.log(`Updated: ${updated}`);
|
||||
|
||||
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||
if (results && Array.isArray(results) && results.length) {
|
||||
console.log(`\nResults (${results.length}):`);
|
||||
for (const r of results) {
|
||||
const memId = String(r.id ?? "").slice(0, 8);
|
||||
const data = r.data as Record<string, unknown> | undefined;
|
||||
const memory = data?.memory ?? "";
|
||||
const evName = String(r.event ?? "");
|
||||
const user = String(r.user_id ?? "");
|
||||
let detail = `${evName} ${memory}`;
|
||||
if (user) detail += ` (user_id=${user})`;
|
||||
console.log(` · ${detail} (${memId})`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to get event: ${String(err)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ====================================================================
|
||||
// help (matches: mem0 help, mem0 help --json)
|
||||
// ====================================================================
|
||||
@@ -1156,20 +1333,21 @@ export function registerCliCommands(
|
||||
.action((opts: { json?: boolean }) => {
|
||||
const commands = {
|
||||
memory: {
|
||||
add: "Add a memory from text, messages, or stdin",
|
||||
search: "Query your memory store — semantic, keyword, or hybrid retrieval",
|
||||
add: "Add a memory from text, messages, or stdin",
|
||||
get: "Get a specific memory by ID",
|
||||
list: "List memories with optional filters",
|
||||
update: "Update a memory's text or metadata",
|
||||
delete: "Delete a memory, all memories, or an entity",
|
||||
history: "View edit history of a memory",
|
||||
import: "Import memories from a JSON file",
|
||||
},
|
||||
management: {
|
||||
init: "Interactive setup wizard for mem0 CLI",
|
||||
status: "Check connectivity and authentication",
|
||||
help: "Show help. Use --json for machine-readable output (for LLM agents)",
|
||||
config: "Manage mem0 configuration (show, get, set)",
|
||||
event: "Manage background processing events (list, status)",
|
||||
dream: "Run memory consolidation (review, merge, prune)",
|
||||
help: "Show help. Use --json for machine-readable output (for LLM agents)",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1283,9 +1461,8 @@ export function registerCliCommands(
|
||||
});
|
||||
},
|
||||
{
|
||||
commands: ["mem0"],
|
||||
descriptors: [
|
||||
{ name: "mem0", description: "Mem0 memory plugin commands" },
|
||||
{ name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true },
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -151,6 +151,7 @@ const ALLOWED_KEYS = [
|
||||
"apiKey",
|
||||
"baseUrl",
|
||||
"userId",
|
||||
"userEmail",
|
||||
"orgId",
|
||||
"projectId",
|
||||
"autoCapture",
|
||||
|
||||
@@ -34,3 +34,10 @@ export function mkdirp(dirPath: string, mode?: number): void {
|
||||
export function unlink(filePath: string): void {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
export function bootstrapTelemetryFlag(): void {
|
||||
const val = process.env.MEM0_TELEMETRY;
|
||||
if (val !== undefined) {
|
||||
(globalThis as any).__mem0_telemetry_override = val;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-8
@@ -5,8 +5,8 @@
|
||||
* and the open-source self-hosted SDK. Uses the official `mem0ai` package.
|
||||
*
|
||||
* Features:
|
||||
* - 7 core tools: memory_search, memory_add, memory_get, memory_list,
|
||||
* memory_update, memory_delete, memory_history
|
||||
* - 6 core tools: memory_search, memory_add, memory_get, memory_list,
|
||||
* memory_update, memory_delete
|
||||
* - Short-term (session-scoped) and long-term (user-scoped) memory
|
||||
* - Auto-recall: injects relevant memories (both scopes) before each agent turn
|
||||
* - Auto-capture: stores key facts scoped to the current session after each agent turn
|
||||
@@ -16,6 +16,7 @@
|
||||
* - Dual mode: platform or open-source (self-hosted)
|
||||
*/
|
||||
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
|
||||
import type {
|
||||
@@ -56,6 +57,9 @@ import { readPluginAuth } from "./cli/config-file.ts";
|
||||
import { registerAllTools } from "./tools/index.ts";
|
||||
import type { ToolDeps } from "./tools/index.ts";
|
||||
import { captureEvent } from "./telemetry.ts";
|
||||
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
|
||||
|
||||
bootstrapTelemetryFlag();
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports (for tests and external consumers)
|
||||
@@ -87,12 +91,10 @@ export { createProvider } from "./providers.ts";
|
||||
// Plugin Definition
|
||||
// ============================================================================
|
||||
|
||||
const memoryPlugin = {
|
||||
const memoryPlugin = definePluginEntry({
|
||||
id: "openclaw-mem0",
|
||||
name: "Memory (Mem0)",
|
||||
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
|
||||
kind: "memory" as const,
|
||||
configSchema: mem0ConfigSchema,
|
||||
|
||||
register(api: OpenClawPluginApi) {
|
||||
// Read auth from openclaw.json plugin config (picks up post-startup login).
|
||||
@@ -231,6 +233,7 @@ const memoryPlugin = {
|
||||
api,
|
||||
provider,
|
||||
cfg,
|
||||
backend,
|
||||
resolveUserId: _resolveUserId,
|
||||
effectiveUserId: _effectiveUserId,
|
||||
agentUserId: _agentUserId,
|
||||
@@ -298,7 +301,7 @@ const memoryPlugin = {
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Lifecycle Hook Registration
|
||||
@@ -579,11 +582,11 @@ function registerHooks(
|
||||
// Track last seen session ID to detect actual new sessions (not every turn)
|
||||
let lastRecallSessionId: string | undefined;
|
||||
|
||||
// Auto-recall: inject relevant memories before agent starts
|
||||
// Auto-recall: inject relevant memories before prompt is built
|
||||
if (cfg.autoRecall) {
|
||||
const RECALL_TIMEOUT_MS = 8_000;
|
||||
|
||||
api.on("before_agent_start", async (event: any, ctx: any) => {
|
||||
api.on("before_prompt_build", async (event: any, ctx: any) => {
|
||||
if (!event.prompt || event.prompt.length < 5) return;
|
||||
|
||||
// Skip non-interactive triggers (cron, heartbeat, automation)
|
||||
@@ -772,6 +775,20 @@ function registerHooks(
|
||||
// Update shared state for tools (best-effort — tools don't have ctx)
|
||||
if (sessionId) session.setCurrentSessionId(sessionId);
|
||||
|
||||
const MEMORY_MUTATE_TOOLS = new Set(["memory_add", "memory_update", "memory_delete"]);
|
||||
const agentUsedMemoryTool = event.messages.some((msg: any) => {
|
||||
if (msg?.role !== "assistant" || !Array.isArray(msg?.content)) return false;
|
||||
return msg.content.some(
|
||||
(block: any) => block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name),
|
||||
);
|
||||
});
|
||||
if (agentUsedMemoryTool) {
|
||||
api.logger.info(
|
||||
"openclaw-mem0: skipping auto-capture — agent already used memory tools this turn",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Build capture payload synchronously (cheap), then fire-and-forget ---
|
||||
|
||||
// Patterns indicating an assistant message contains a summary of
|
||||
|
||||
Vendored
+30
-3
@@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" {
|
||||
};
|
||||
resolvePath(p: string): string;
|
||||
registerTool(
|
||||
definition: Record<string, unknown>,
|
||||
metadata?: Record<string, unknown>,
|
||||
definition: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: unknown;
|
||||
execute: (
|
||||
toolCallId: string,
|
||||
params: Record<string, unknown>,
|
||||
) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
|
||||
[key: string]: unknown;
|
||||
},
|
||||
metadata?: { optional?: boolean; [key: string]: unknown },
|
||||
): void;
|
||||
on(event: string, handler: (event: any, ctx: any) => any): void;
|
||||
registerCli(
|
||||
handler: (context: { program: any }) => void,
|
||||
options?: Record<string, unknown>,
|
||||
): void;
|
||||
registerCommand?(definition: Record<string, unknown>): void;
|
||||
registerService(service: {
|
||||
id: string;
|
||||
start: () => void;
|
||||
start: (...args: any[]) => void;
|
||||
stop: () => void;
|
||||
}): void;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "openclaw/plugin-sdk/plugin-entry" {
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
|
||||
export interface PluginEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
register(api: OpenClawPluginApi): void;
|
||||
}
|
||||
|
||||
export function definePluginEntry<T extends PluginEntry>(entry: T): T;
|
||||
}
|
||||
|
||||
declare module "openclaw/plugin-sdk/core" {
|
||||
export * from "openclaw/plugin-sdk";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"contracts": {
|
||||
"tools": [
|
||||
"memory_search", "memory_add", "memory_get", "memory_list",
|
||||
"memory_update", "memory_delete", "memory_history"
|
||||
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
|
||||
]
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
@@ -26,12 +26,6 @@
|
||||
"placeholder": "m0-...",
|
||||
"help": "Platform mode only. Use a SecretRef ({\"source\":\"env\",\"provider\":\"default\",\"id\":\"MEM0_API_KEY\"}) or ${MEM0_API_KEY} instead of storing the key directly."
|
||||
},
|
||||
"baseUrl": {
|
||||
"label": "API Base URL",
|
||||
"placeholder": "https://api.mem0.ai",
|
||||
"advanced": true,
|
||||
"help": "Override the Mem0 API base URL. Only needed for custom deployments."
|
||||
},
|
||||
"userId": {
|
||||
"label": "Default User ID",
|
||||
"placeholder": "default",
|
||||
@@ -109,10 +103,10 @@
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"baseUrl": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"userEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"orgId": {
|
||||
|
||||
+60
-20
@@ -81,6 +81,7 @@ class PlatformProvider implements Mem0Provider {
|
||||
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
private readonly baseUrl?: string,
|
||||
private readonly orgId?: string,
|
||||
private readonly projectId?: string,
|
||||
) {}
|
||||
@@ -97,11 +98,17 @@ class PlatformProvider implements Mem0Provider {
|
||||
|
||||
private async _init(): Promise<void> {
|
||||
const { default: MemoryClient } = await import("mem0ai");
|
||||
const opts: { apiKey: string; org_id?: string; project_id?: string } = {
|
||||
const opts: {
|
||||
apiKey: string;
|
||||
host?: string;
|
||||
organizationId?: string;
|
||||
projectId?: string;
|
||||
} = {
|
||||
apiKey: this.apiKey,
|
||||
};
|
||||
if (this.orgId) opts.org_id = this.orgId;
|
||||
if (this.projectId) opts.project_id = this.projectId;
|
||||
if (this.baseUrl) opts.host = this.baseUrl;
|
||||
if (this.orgId) opts.organizationId = this.orgId;
|
||||
if (this.projectId) opts.projectId = this.projectId;
|
||||
this.client = new MemoryClient(opts);
|
||||
}
|
||||
|
||||
@@ -133,20 +140,11 @@ class PlatformProvider implements Mem0Provider {
|
||||
|
||||
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
|
||||
await this.ensureClient();
|
||||
// Base filters: always scope by user_id, optionally by run_id
|
||||
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
|
||||
if (options.run_id) baseFilters.run_id = options.run_id;
|
||||
|
||||
// Merge agent-provided filters (created_at ranges, metadata, etc.)
|
||||
// with base filters. Agent filters extend, never override user scoping.
|
||||
const mergedFilters = options.filters
|
||||
? { AND: [baseFilters, options.filters] }
|
||||
: baseFilters;
|
||||
|
||||
const opts: Record<string, unknown> = {
|
||||
api_version: "v2",
|
||||
filters: mergedFilters,
|
||||
user_id: options.user_id,
|
||||
};
|
||||
if (options.run_id) opts.run_id = options.run_id;
|
||||
if (options.top_k != null) opts.top_k = options.top_k;
|
||||
if (options.threshold != null) opts.threshold = options.threshold;
|
||||
if (options.keyword_search != null)
|
||||
@@ -155,6 +153,14 @@ class PlatformProvider implements Mem0Provider {
|
||||
if (options.filter_memories != null)
|
||||
opts.filter_memories = options.filter_memories;
|
||||
if (options.categories != null) opts.categories = options.categories;
|
||||
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
|
||||
if (options.run_id) baseFilters.run_id = options.run_id;
|
||||
|
||||
if (options.filters) {
|
||||
opts.filters = { AND: [baseFilters, options.filters] };
|
||||
} else {
|
||||
opts.filters = baseFilters;
|
||||
}
|
||||
|
||||
const results = await this.client.search(query, opts);
|
||||
return normalizeSearchResults(results);
|
||||
@@ -168,10 +174,16 @@ class PlatformProvider implements Mem0Provider {
|
||||
|
||||
async getAll(options: ListOptions): Promise<MemoryItem[]> {
|
||||
await this.ensureClient();
|
||||
const opts: Record<string, unknown> = { user_id: options.user_id };
|
||||
if (options.run_id) opts.run_id = options.run_id;
|
||||
const opts: Record<string, unknown> = {
|
||||
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<string, unknown>).run_id = options.run_id;
|
||||
}
|
||||
if (options.page_size != null) opts.page_size = options.page_size;
|
||||
if (options.source) opts.source = options.source;
|
||||
|
||||
const results = await this.client.getAll(opts);
|
||||
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
|
||||
@@ -242,10 +254,38 @@ class OSSProvider implements Mem0Provider {
|
||||
|
||||
const config: Record<string, unknown> = { version: "v1.1" };
|
||||
|
||||
if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
|
||||
const defaultEmbedder = { provider: "openai", config: { model: "text-embedding-3-small" } };
|
||||
const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
|
||||
|
||||
// Helper: strip empty-string values so they don't clobber defaults
|
||||
const stripEmpty = (obj: Record<string, unknown>) => {
|
||||
const out = { ...obj };
|
||||
for (const k of Object.keys(out)) { if (out[k] === "") delete out[k]; }
|
||||
return out;
|
||||
};
|
||||
|
||||
if (this.ossConfig?.embedder) {
|
||||
const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
|
||||
config.embedder = {
|
||||
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
|
||||
config: { ...defaultEmbedder.config, ...ec },
|
||||
};
|
||||
} else {
|
||||
config.embedder = defaultEmbedder;
|
||||
}
|
||||
|
||||
if (this.ossConfig?.llm) {
|
||||
const lc = stripEmpty(this.ossConfig.llm.config ?? {});
|
||||
config.llm = {
|
||||
provider: this.ossConfig.llm.provider || defaultLlm.provider,
|
||||
config: { ...defaultLlm.config, ...lc },
|
||||
};
|
||||
} else {
|
||||
config.llm = defaultLlm;
|
||||
}
|
||||
|
||||
if (this.ossConfig?.vectorStore)
|
||||
config.vectorStore = this.ossConfig.vectorStore;
|
||||
if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
|
||||
|
||||
if (this.ossConfig?.historyDbPath) {
|
||||
const dbPath = this.resolvePath
|
||||
@@ -419,7 +459,7 @@ export function createProvider(
|
||||
);
|
||||
}
|
||||
|
||||
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
|
||||
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -34,9 +34,6 @@ Identify which memories need action. Use the tools to investigate.
|
||||
**Search for recent additions:**
|
||||
Call `memory_search` with a `created_at` filter to find memories added since the last consolidation. These are the most likely to need merging or cleanup.
|
||||
|
||||
**Check edit history on suspicious entries:**
|
||||
If a memory looks like it was updated multiple times or has contradictory content, call `memory_history` on it to understand how it evolved.
|
||||
|
||||
**Classify each target into one of these actions:**
|
||||
- DELETE: contains credentials, expired by TTL, pure noise, raw tool output, standalone timestamps
|
||||
- MERGE: two or more memories express the same fact in different words, or a series tracks incremental changes to the same entity
|
||||
|
||||
+14
-2
@@ -22,9 +22,21 @@ const FLUSH_THRESHOLD = 10;
|
||||
let eventQueue: Record<string, unknown>[] = [];
|
||||
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
let _telemetryEnabled: boolean | undefined;
|
||||
function isTelemetryEnabled(): boolean {
|
||||
const val = (process.env.MEM0_TELEMETRY ?? "true").toLowerCase();
|
||||
return val !== "false" && val !== "0" && val !== "no";
|
||||
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
|
||||
try {
|
||||
const val = (globalThis as any).__mem0_telemetry_override;
|
||||
if (val !== undefined) {
|
||||
const s = String(val).toLowerCase();
|
||||
_telemetryEnabled = s !== "false" && s !== "0" && s !== "no";
|
||||
} else {
|
||||
_telemetryEnabled = true;
|
||||
}
|
||||
} catch {
|
||||
_telemetryEnabled = true;
|
||||
}
|
||||
return _telemetryEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Test shim for openclaw/plugin-sdk/plugin-entry.
|
||||
* At runtime this is resolved from the OpenClaw gateway.
|
||||
*/
|
||||
export function definePluginEntry<T>(entry: T): T {
|
||||
return entry;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Test shim for openclaw/plugin-sdk.
|
||||
* At runtime this is resolved from the OpenClaw gateway.
|
||||
*/
|
||||
export interface OpenClawPluginApi {
|
||||
pluginConfig: Record<string, unknown>;
|
||||
logger: {
|
||||
info(msg: string): void;
|
||||
warn(msg: string): void;
|
||||
error(msg: string): void;
|
||||
debug(msg: string): void;
|
||||
};
|
||||
resolvePath(p: string): string;
|
||||
registerTool(definition: Record<string, unknown>, metadata?: Record<string, unknown>): void;
|
||||
on(event: string, handler: (event: any, ctx: any) => any): void;
|
||||
registerCli(handler: (context: { program: any }) => void, options?: Record<string, unknown>): void;
|
||||
registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -58,6 +58,8 @@ interface MockCommand {
|
||||
_args: Array<{ name: string; desc: string }>;
|
||||
command(name: string): MockCommand;
|
||||
description(desc: string): MockCommand;
|
||||
configureHelp(opts: any): MockCommand;
|
||||
hook(event: string, fn: (...args: any[]) => any): MockCommand;
|
||||
option(flags: string, desc: string, defaultVal?: string): MockCommand;
|
||||
argument(name: string, desc: string): MockCommand;
|
||||
action(fn: (...args: any[]) => any): MockCommand;
|
||||
@@ -80,6 +82,12 @@ function createMockCommand(name: string): MockCommand {
|
||||
cmd._description = desc;
|
||||
return cmd;
|
||||
},
|
||||
configureHelp(_opts: any) {
|
||||
return cmd;
|
||||
},
|
||||
hook(_event: string, _fn: (...args: any[]) => any) {
|
||||
return cmd;
|
||||
},
|
||||
option(flags: string, desc: string, defaultVal?: string) {
|
||||
cmd._options.push({ flags, desc, defaultVal });
|
||||
return cmd;
|
||||
@@ -145,6 +153,18 @@ function createMockBackend() {
|
||||
connected: true,
|
||||
url: "https://api.mem0.ai",
|
||||
}),
|
||||
add: vi.fn().mockResolvedValue({ id: "new-1" }),
|
||||
listEvents: vi.fn().mockResolvedValue([
|
||||
{ id: "evt-1111-2222-3333-4444", event_type: "ADD", status: "SUCCEEDED", latency: 1500, created_at: "2026-04-01T12:00:00Z" },
|
||||
]),
|
||||
getEvent: vi.fn().mockResolvedValue({
|
||||
id: "evt-1111-2222-3333-4444",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
latency: 1500,
|
||||
created_at: "2026-04-01T12:00:00Z",
|
||||
updated_at: "2026-04-01T12:00:01Z",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,7 +304,6 @@ describe("registerCliCommands", () => {
|
||||
expect(names).toContain("list");
|
||||
expect(names).toContain("update");
|
||||
expect(names).toContain("delete");
|
||||
expect(names).toContain("history");
|
||||
expect(names).toContain("status");
|
||||
expect(names).toContain("config");
|
||||
expect(names).toContain("dream");
|
||||
@@ -321,7 +340,6 @@ describe("registerCliCommands", () => {
|
||||
expect(writePluginAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
apiKey: "m0-my-key-1234",
|
||||
baseUrl: "https://api.mem0.ai",
|
||||
mode: "platform",
|
||||
}),
|
||||
);
|
||||
@@ -857,48 +875,6 @@ describe("registerCliCommands", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// history subcommand
|
||||
// ========================================================================
|
||||
|
||||
describe("history subcommand", () => {
|
||||
it("calls provider.history and prints entries as JSON", async () => {
|
||||
const { mem0, provider } = setup();
|
||||
const historyCmd = findCommand(mem0, "history")!;
|
||||
|
||||
await historyCmd._action!("m1");
|
||||
|
||||
expect(provider.history).toHaveBeenCalledWith("m1");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("UPDATE"),
|
||||
);
|
||||
});
|
||||
|
||||
it("prints message when no history found", async () => {
|
||||
const { mem0, provider } = setup();
|
||||
provider.history.mockResolvedValueOnce([]);
|
||||
const historyCmd = findCommand(mem0, "history")!;
|
||||
|
||||
await historyCmd._action!("m1");
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
"No history found for this memory.",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles history errors gracefully", async () => {
|
||||
const { mem0, provider } = setup();
|
||||
provider.history.mockRejectedValueOnce(new Error("history boom"));
|
||||
const historyCmd = findCommand(mem0, "history")!;
|
||||
|
||||
await historyCmd._action!("m1");
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("History failed"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// status subcommand
|
||||
// ========================================================================
|
||||
@@ -964,9 +940,9 @@ describe("registerCliCommands", () => {
|
||||
const allOutput = consoleSpy.log.mock.calls.map((c) => c[0]).join("\n");
|
||||
expect(allOutput).toContain("Key");
|
||||
expect(allOutput).toContain("Value");
|
||||
expect(allOutput).toContain("platform.api_key");
|
||||
expect(allOutput).toContain("platform.base_url");
|
||||
expect(allOutput).toContain("defaults.user_id");
|
||||
expect(allOutput).toContain("api_key");
|
||||
expect(allOutput).toContain("email");
|
||||
expect(allOutput).toContain("user_id");
|
||||
expect(allOutput).toContain("Config file:");
|
||||
});
|
||||
});
|
||||
@@ -1026,14 +1002,14 @@ describe("registerCliCommands", () => {
|
||||
expect(logged).not.toContain("supersecret");
|
||||
});
|
||||
|
||||
it("supports dot-notation keys like platform.api_key", () => {
|
||||
it("supports short alias keys like email", () => {
|
||||
const { mem0 } = setup();
|
||||
const configCmd = findCommand(mem0, "config")!;
|
||||
const getCmd = findCommand(configCmd, "get")!;
|
||||
|
||||
getCmd._action!("platform.base_url");
|
||||
getCmd._action!("email");
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("https://api.mem0.ai");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("(not set)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1143,12 +1119,12 @@ describe("registerCliCommands", () => {
|
||||
expect(writePluginAuth).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("supports dot-notation keys for set", () => {
|
||||
it("supports short alias keys for set", () => {
|
||||
const { mem0 } = setup();
|
||||
const configCmd = findCommand(mem0, "config")!;
|
||||
const setCmd = findCommand(configCmd, "set")!;
|
||||
|
||||
setCmd._action!("defaults.user_id", "bob");
|
||||
setCmd._action!("user_id", "bob");
|
||||
|
||||
expect(writePluginAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userId: "bob" }),
|
||||
@@ -1268,4 +1244,173 @@ describe("registerCliCommands", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// import subcommand
|
||||
// ========================================================================
|
||||
|
||||
describe("import subcommand", () => {
|
||||
it("imports memories from a JSON array file", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([
|
||||
{ memory: "fact one" },
|
||||
{ memory: "fact two" },
|
||||
]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("memories.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(2);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 2 memories.");
|
||||
});
|
||||
|
||||
it("imports a single JSON object", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify({ memory: "single fact" }),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("single.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(1);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 1 memories.");
|
||||
});
|
||||
|
||||
it("skips items with no extractable content", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "valid" }, { nofield: true }]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("mixed.json", {});
|
||||
|
||||
expect(backend.add).toHaveBeenCalledTimes(1);
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
|
||||
});
|
||||
|
||||
it("uses --user-id and --agent-id overrides", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "fact" }]),
|
||||
);
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("f.json", { userId: "override-user", agentId: "agent-1" });
|
||||
|
||||
expect(backend.add).toHaveBeenCalledWith("fact", undefined, expect.objectContaining({
|
||||
userId: "override-user",
|
||||
agentId: "agent-1",
|
||||
}));
|
||||
});
|
||||
|
||||
it("handles file read errors gracefully", async () => {
|
||||
const { mem0 } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("ENOENT");
|
||||
});
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("missing.json", {});
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to read file"),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles backend.add failures gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const { readText } = await import("../fs-safe.ts");
|
||||
(readText as ReturnType<typeof vi.fn>).mockReturnValueOnce(
|
||||
JSON.stringify([{ memory: "will fail" }]),
|
||||
);
|
||||
backend.add.mockRejectedValueOnce(new Error("API error"));
|
||||
const importCmd = findCommand(mem0, "import")!;
|
||||
|
||||
await importCmd._action!("fail.json", {});
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Imported 0 memories.");
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith("1 memories failed to import.");
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// event subcommand
|
||||
// ========================================================================
|
||||
|
||||
describe("event subcommand", () => {
|
||||
it("lists events in table format", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(backend.listEvents).toHaveBeenCalled();
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("evt-1111-2222-3333-4444"),
|
||||
);
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("1 event"),
|
||||
);
|
||||
});
|
||||
|
||||
it("prints message when no events found", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.listEvents.mockResolvedValueOnce([]);
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("No events found.");
|
||||
});
|
||||
|
||||
it("handles event list errors gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.listEvents.mockRejectedValueOnce(new Error("event boom"));
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const listCmd = findCommand(eventCmd, "list")!;
|
||||
|
||||
await listCmd._action!();
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to list events"),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows event status details", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const statusCmd = findCommand(eventCmd, "status")!;
|
||||
|
||||
await statusCmd._action!("evt-1111-2222-3333-4444");
|
||||
|
||||
expect(backend.getEvent).toHaveBeenCalledWith("evt-1111-2222-3333-4444");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Event ID: evt-1111-2222-3333-4444");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Type: ADD");
|
||||
expect(consoleSpy.log).toHaveBeenCalledWith("Status: SUCCEEDED");
|
||||
});
|
||||
|
||||
it("handles event status errors gracefully", async () => {
|
||||
const { mem0, backend } = setup();
|
||||
backend.getEvent.mockRejectedValueOnce(new Error("not found"));
|
||||
const eventCmd = findCommand(mem0, "event")!;
|
||||
const statusCmd = findCommand(eventCmd, "status")!;
|
||||
|
||||
await statusCmd._action!("bad-id");
|
||||
|
||||
expect(consoleSpy.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to get event"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { bootstrapTelemetryFlag } from "../fs-safe.ts";
|
||||
|
||||
describe("bootstrapTelemetryFlag", () => {
|
||||
const originalEnv = process.env.MEM0_TELEMETRY;
|
||||
|
||||
beforeEach(() => {
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.MEM0_TELEMETRY = originalEnv;
|
||||
} else {
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
}
|
||||
});
|
||||
|
||||
it("sets globalThis override when MEM0_TELEMETRY is set", () => {
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBe("false");
|
||||
});
|
||||
|
||||
it("does not set globalThis override when MEM0_TELEMETRY is unset", () => {
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes through truthy values", () => {
|
||||
process.env.MEM0_TELEMETRY = "true";
|
||||
bootstrapTelemetryFlag();
|
||||
expect((globalThis as any).__mem0_telemetry_override).toBe("true");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mock config-file before importing telemetry
|
||||
vi.mock("../cli/config-file.ts", () => ({
|
||||
readPluginAuth: vi.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts";
|
||||
import { readPluginAuth } from "../cli/config-file.ts";
|
||||
|
||||
describe("telemetry", () => {
|
||||
let fetchSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset telemetry enabled state
|
||||
(globalThis as any).__mem0_telemetry_override = undefined;
|
||||
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
delete (globalThis as any).__mem0_telemetry_override;
|
||||
});
|
||||
|
||||
it("exports PLUGIN_VERSION", () => {
|
||||
expect(PLUGIN_VERSION).toBe("1.0.4");
|
||||
});
|
||||
|
||||
it("captureEvent does not throw", () => {
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("captureEvent accepts properties and context", () => {
|
||||
expect(() =>
|
||||
captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("captureEvent is silent when telemetry disabled", () => {
|
||||
(globalThis as any).__mem0_telemetry_override = "false";
|
||||
// Force re-evaluation by resetting cached value
|
||||
// Since _telemetryEnabled is module-level, we test indirectly
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("uses userEmail as distinct ID when available", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({
|
||||
userEmail: "test@example.com",
|
||||
});
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
|
||||
it("falls back to anonymous-openclaw when no apiKey", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
|
||||
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
|
||||
});
|
||||
|
||||
it("handles readPluginAuth errors gracefully", () => {
|
||||
(readPluginAuth as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
|
||||
throw new Error("config read failed");
|
||||
});
|
||||
expect(() => captureEvent("test_event")).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts";
|
||||
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
|
||||
import { createMemoryListTool } from "../tools/memory-list.ts";
|
||||
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
|
||||
import { createMemoryHistoryTool } from "../tools/memory-history.ts";
|
||||
import { createMemoryEventListTool } from "../tools/memory-event-list.ts";
|
||||
import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock helper
|
||||
@@ -80,10 +81,10 @@ function createMockToolDeps(overrides = {}): ToolDeps {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("registerAllTools", () => {
|
||||
it("calls api.registerTool exactly 7 times", () => {
|
||||
it("calls api.registerTool exactly 8 times", () => {
|
||||
const ctx = createMockToolDeps();
|
||||
registerAllTools(ctx);
|
||||
expect(ctx.api.registerTool).toHaveBeenCalledTimes(7);
|
||||
expect(ctx.api.registerTool).toHaveBeenCalledTimes(8);
|
||||
});
|
||||
|
||||
it("registers tools with the correct names", () => {
|
||||
@@ -103,7 +104,8 @@ describe("registerAllTools", () => {
|
||||
"memory_list",
|
||||
"memory_update",
|
||||
"memory_delete",
|
||||
"memory_history",
|
||||
"memory_event_list",
|
||||
"memory_event_status",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -129,6 +131,8 @@ describe("tool factory shape", () => {
|
||||
{ fn: createMemoryGetTool, expectedName: "memory_get" },
|
||||
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
|
||||
{ fn: createMemoryListTool, expectedName: "memory_list" },
|
||||
{ fn: createMemoryEventListTool, expectedName: "memory_event_list" },
|
||||
{ fn: createMemoryEventStatusTool, expectedName: "memory_event_status" },
|
||||
];
|
||||
|
||||
for (const { fn, expectedName } of factories) {
|
||||
@@ -883,115 +887,3 @@ describe("memory_update execute", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// memory_history execute
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("memory_history execute", () => {
|
||||
it("returns formatted history", async () => {
|
||||
const historyEntries = [
|
||||
{
|
||||
event: "ADD",
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
old_memory: null,
|
||||
new_memory: "Initial memory text",
|
||||
},
|
||||
{
|
||||
event: "UPDATE",
|
||||
created_at: "2026-01-02T00:00:00Z",
|
||||
old_memory: "Initial memory text",
|
||||
new_memory: "Updated memory text",
|
||||
},
|
||||
];
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue(historyEntries),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
|
||||
|
||||
expect(ctx.provider!.history).toHaveBeenCalledWith("mem-abc");
|
||||
expect(result.content[0].text).toContain("History for memory mem-abc");
|
||||
expect(result.content[0].text).toContain("2 entries");
|
||||
expect(result.content[0].text).toContain("[ADD]");
|
||||
expect(result.content[0].text).toContain("[UPDATE]");
|
||||
expect(result.content[0].text).toContain("Initial memory text");
|
||||
expect(result.content[0].text).toContain("Updated memory text");
|
||||
expect(result.content[0].text).toContain("Old: (none)");
|
||||
expect(result.details.count).toBe(2);
|
||||
expect(result.details.history).toEqual(historyEntries);
|
||||
});
|
||||
|
||||
it("handles empty history", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-2", { memoryId: "mem-empty" });
|
||||
|
||||
expect(result.content[0].text).toBe(
|
||||
"No history found for memory mem-empty.",
|
||||
);
|
||||
expect(result.details.count).toBe(0);
|
||||
});
|
||||
|
||||
it("handles null history", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-3", { memoryId: "mem-null" });
|
||||
|
||||
expect(result.content[0].text).toBe(
|
||||
"No history found for memory mem-null.",
|
||||
);
|
||||
expect(result.details.count).toBe(0);
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
const ctx = createMockToolDeps({
|
||||
provider: {
|
||||
search: vi.fn(),
|
||||
add: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
get: vi.fn(),
|
||||
history: vi.fn().mockRejectedValue(new Error("history unavailable")),
|
||||
},
|
||||
});
|
||||
const tool = createMemoryHistoryTool(ctx);
|
||||
|
||||
const result = await tool.execute("call-4", { memoryId: "mem-err" });
|
||||
|
||||
expect(result.content[0].text).toContain("Memory history failed");
|
||||
expect(result.content[0].text).toContain("history unavailable");
|
||||
expect(result.details.error).toContain("history unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts";
|
||||
import type { Backend } from "../backend/base.ts";
|
||||
|
||||
import { createMemorySearchTool } from "./memory-search.ts";
|
||||
import { createMemoryAddTool } from "./memory-add.ts";
|
||||
@@ -7,12 +8,14 @@ import { createMemoryGetTool } from "./memory-get.ts";
|
||||
import { createMemoryListTool } from "./memory-list.ts";
|
||||
import { createMemoryUpdateTool } from "./memory-update.ts";
|
||||
import { createMemoryDeleteTool } from "./memory-delete.ts";
|
||||
import { createMemoryHistoryTool } from "./memory-history.ts";
|
||||
import { createMemoryEventListTool } from "./memory-event-list.ts";
|
||||
import { createMemoryEventStatusTool } from "./memory-event-status.ts";
|
||||
|
||||
export interface ToolDeps {
|
||||
api: OpenClawPluginApi;
|
||||
provider: Mem0Provider;
|
||||
cfg: Mem0Config;
|
||||
backend?: Backend;
|
||||
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
|
||||
effectiveUserId: (sessionKey?: string) => string;
|
||||
agentUserId: (id: string) => string;
|
||||
@@ -32,5 +35,6 @@ export function registerAllTools(deps: ToolDeps): void {
|
||||
api.registerTool(createMemoryListTool(deps));
|
||||
api.registerTool(createMemoryUpdateTool(deps));
|
||||
api.registerTool(createMemoryDeleteTool(deps));
|
||||
api.registerTool(createMemoryHistoryTool(deps));
|
||||
api.registerTool(createMemoryEventListTool(deps));
|
||||
api.registerTool(createMemoryEventStatusTool(deps));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolDeps } from "./index.ts";
|
||||
|
||||
export function createMemoryEventListTool(deps: ToolDeps) {
|
||||
return {
|
||||
name: "memory_event_list",
|
||||
label: "Memory Event List",
|
||||
description:
|
||||
"List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.",
|
||||
parameters: Type.Object({}),
|
||||
|
||||
async execute(_toolCallId: string, _params: Record<string, unknown>) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
if (!deps.backend) {
|
||||
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" });
|
||||
return {
|
||||
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
|
||||
details: { error: "not_platform" },
|
||||
};
|
||||
}
|
||||
|
||||
const results = await deps.backend.listEvents();
|
||||
if (!results.length) {
|
||||
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 });
|
||||
return {
|
||||
content: [{ type: "text", text: "No events found." }],
|
||||
details: { count: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const rows = results.map((ev) => {
|
||||
const evId = String(ev.id ?? "");
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const status = String(ev.status ?? "—");
|
||||
const latency =
|
||||
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
|
||||
return { id: evId, type: evType, status, latency, created };
|
||||
});
|
||||
|
||||
const text = rows
|
||||
.map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`)
|
||||
.join("\n");
|
||||
|
||||
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length });
|
||||
return {
|
||||
content: [{ type: "text", text: `${results.length} event(s):\n${text}` }],
|
||||
details: { count: results.length, events: rows },
|
||||
};
|
||||
} catch (err) {
|
||||
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed to list events: ${String(err)}` }],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolDeps } from "./index.ts";
|
||||
|
||||
export function createMemoryEventStatusTool(deps: ToolDeps) {
|
||||
return {
|
||||
name: "memory_event_status",
|
||||
label: "Memory Event Status",
|
||||
description:
|
||||
"Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.",
|
||||
parameters: Type.Object({
|
||||
event_id: Type.String({ description: "The event ID to check" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { event_id: eventId } = params as { event_id: string };
|
||||
const start = Date.now();
|
||||
try {
|
||||
if (!deps.backend) {
|
||||
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" });
|
||||
return {
|
||||
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
|
||||
details: { error: "not_platform" },
|
||||
};
|
||||
}
|
||||
|
||||
const ev = await deps.backend.getEvent(eventId);
|
||||
|
||||
const status = String(ev.status ?? "—");
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const latency =
|
||||
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
|
||||
const updated = String(ev.updated_at ?? "—").slice(0, 19).replace("T", " ");
|
||||
|
||||
let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`;
|
||||
|
||||
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||
if (results && Array.isArray(results) && results.length) {
|
||||
const resultLines = results.map((r) => {
|
||||
const memId = String(r.id ?? "").slice(0, 8);
|
||||
const data = r.data as Record<string, unknown> | undefined;
|
||||
const memory = data?.memory ?? "";
|
||||
const evName = String(r.event ?? "");
|
||||
return `- [${evName}] ${memory} (${memId})`;
|
||||
});
|
||||
text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`;
|
||||
}
|
||||
|
||||
deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start });
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: { event: ev },
|
||||
};
|
||||
} catch (err) {
|
||||
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Failed to get event: ${String(err)}` }],
|
||||
details: { error: String(err) },
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { ToolDeps } from "./index.ts";
|
||||
|
||||
export function createMemoryHistoryTool(deps: ToolDeps) {
|
||||
const { provider } = deps;
|
||||
|
||||
return {
|
||||
name: "memory_history",
|
||||
label: "Memory History",
|
||||
description: "View the edit history of a specific memory.",
|
||||
parameters: Type.Object({
|
||||
memoryId: Type.String({ description: "The memory ID to view history for" }),
|
||||
}),
|
||||
|
||||
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
||||
const { memoryId } = params as { memoryId: string };
|
||||
const start = Date.now();
|
||||
try {
|
||||
const history = await provider.history(memoryId);
|
||||
deps.captureToolEvent("memory_history", { success: true, latency_ms: Date.now() - start, result_count: history?.length ?? 0 });
|
||||
if (!history || history.length === 0) {
|
||||
return { content: [{ type: "text", text: `No history found for memory ${memoryId}.` }], details: { count: 0 } };
|
||||
}
|
||||
const text = history.map((h, i) =>
|
||||
`${i + 1}. [${h.event}] ${h.created_at}\n Old: ${h.old_memory || "(none)"}\n New: ${h.new_memory || "(none)"}`
|
||||
).join("\n\n");
|
||||
return {
|
||||
content: [{ type: "text", text: `History for memory ${memoryId} (${history.length} entries):\n\n${text}` }],
|
||||
details: { count: history.length, history },
|
||||
};
|
||||
} catch (err) {
|
||||
deps.captureToolEvent("memory_history", { success: false, latency_ms: Date.now() - start, error: String(err) });
|
||||
return { content: [{ type: "text", text: `Memory history failed: ${String(err)}` }], details: { error: String(err) } };
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,6 @@
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"],
|
||||
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ export default defineConfig({
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: [/^node:/, "fs", "os", "path", "url", "readline", "module"],
|
||||
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
alias: {
|
||||
// OpenClaw SDK modules are resolved from the gateway at runtime.
|
||||
// During unit tests we provide lightweight stubs.
|
||||
"openclaw/plugin-sdk/plugin-entry": new URL(
|
||||
"./test-shims/plugin-entry.ts",
|
||||
import.meta.url,
|
||||
).pathname,
|
||||
"openclaw/plugin-sdk": new URL(
|
||||
"./test-shims/plugin-sdk.ts",
|
||||
import.meta.url,
|
||||
).pathname,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user