Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0fb194c76 |
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -39,7 +38,7 @@ jobs:
|
||||
# packages_dir: dist/
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.ref, 'refs/tags')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages_dir: dist/
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Publish @mem0/cli 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/cli 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cli/node
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0-cli 📦 to PyPI
|
||||
if: startsWith(github.event.release.tag_name, 'cli-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: cli/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
|
||||
- name: Build a binary wheel and a source tarball
|
||||
run: hatch build --clean
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: cli/python/dist/
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Publish @mem0/openclaw-mem0 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openclaw
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Publish mem0ai 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0ai 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mem0-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -1,46 +0,0 @@
|
||||
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: vercel-ai-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
npx npm@latest publish --provenance --access public
|
||||
fi
|
||||
@@ -61,31 +61,3 @@ make test # After activating a shell with hatch shell test_XX
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
### 🚀 Releasing
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
|
||||
#### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
| `mem0ai` (Python SDK) | PyPI | `v*` | `v0.1.31` |
|
||||
| `mem0-cli` (Python CLI) | PyPI | `cli-v*` | `cli-v0.2.1` |
|
||||
| `mem0ai` (TypeScript SDK) | npm | `ts-v*` | `ts-v2.4.6` |
|
||||
| `@mem0/cli` (Node CLI) | npm | `cli-node-v*` | `cli-node-v0.1.2` |
|
||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||
|
||||
#### How to Release
|
||||
|
||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||
|
||||
#### Publishing Details
|
||||
|
||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||
|
||||
+15
-175
@@ -28,7 +28,7 @@ mem0 CLI is the official command-line interface for [mem0](https://mem0.ai) -- t
|
||||
### Who is it for?
|
||||
|
||||
- Developers integrating mem0 into their workflows
|
||||
- AI agents that need persistent memory (the CLI is designed with `--json`/`--agent` global flags and `help --json` specifically for machine consumption)
|
||||
- AI agents that need persistent memory (the CLI is designed with `--output json` and `help --json` specifically for machine consumption)
|
||||
- DevOps/CI pipelines that need to manage memories programmatically
|
||||
|
||||
### Project Structure
|
||||
@@ -79,7 +79,6 @@ Apache-2.0
|
||||
│ ├── init_cmd.py # run_init (interactive wizard)
|
||||
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
|
||||
│ ├── entities.py # cmd_entities_list, cmd_entities_delete
|
||||
│ ├── events_cmd.py # cmd_event_list, cmd_event_status
|
||||
│ └── utils.py # cmd_status, cmd_version, cmd_import
|
||||
└── node/
|
||||
├── package.json # Node package config (tsup build)
|
||||
@@ -89,7 +88,6 @@ Apache-2.0
|
||||
├── config.ts # Config loading/saving, env var overrides
|
||||
├── branding.ts # Colors, icons, banner, timedStatus, print helpers
|
||||
├── output.ts # Output formatting (text, json, table, quiet)
|
||||
├── state.ts # Agent mode flag (setAgentMode, isAgentMode)
|
||||
├── help.ts # Rich-style help formatter (panels, command ordering)
|
||||
├── backend/
|
||||
│ ├── index.ts # Re-exports
|
||||
@@ -100,7 +98,6 @@ Apache-2.0
|
||||
├── init.ts # runInit (interactive wizard)
|
||||
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
|
||||
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
|
||||
├── events.ts # cmdEventList, cmdEventStatus
|
||||
└── utils.ts # cmdStatus, cmdVersion, cmdImport
|
||||
```
|
||||
|
||||
@@ -158,15 +155,9 @@ Interactive setup wizard for mem0 CLI.
|
||||
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
|
||||
| `--email` | string | No | - | Login via email verification code. |
|
||||
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
|
||||
| `--force` | bool | No | false | Overwrite existing config without confirmation. |
|
||||
|
||||
**Behavior:**
|
||||
|
||||
*Existing config protection:*
|
||||
- If `~/.mem0/config.json` exists with an API key, the CLI warns and asks for confirmation before overwriting.
|
||||
- In non-TTY mode, this is a hard error unless `--force` is passed.
|
||||
- `--force` skips the confirmation in both TTY and non-TTY modes.
|
||||
|
||||
*Email login flow (when `--email` is provided):*
|
||||
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
|
||||
- If `--code` is also provided, verifies immediately (fully non-interactive).
|
||||
@@ -184,7 +175,6 @@ Interactive setup wizard for mem0 CLI.
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
mem0 init --email alice@company.com
|
||||
mem0 init --email alice@company.com --code 482901
|
||||
```
|
||||
@@ -710,114 +700,7 @@ mem0 entity delete --user-id alice --dry-run
|
||||
|
||||
---
|
||||
|
||||
### 3.14 `event list`
|
||||
|
||||
List recent background processing events.
|
||||
|
||||
| Property | Value |
|
||||
|------------------|-------|
|
||||
| Usage | `mem0 event list [OPTIONS]` |
|
||||
| needsBackend | Yes |
|
||||
| needsConfig | Yes |
|
||||
| resolveIds | No |
|
||||
| resolveGraph | No |
|
||||
| confirmDangerous | No |
|
||||
| Output formats | text (table), json |
|
||||
| Default output | table |
|
||||
| API endpoint | `GET /v1/events/` |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Type | Default | Panel | Help |
|
||||
|----------------|--------|---------|------------|------|
|
||||
| `-o, --output` | string | "table" | Output | Output: text, json. |
|
||||
| `--api-key` | string | - | Connection | Override API key. |
|
||||
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||
|
||||
**Behavior:** Fetches all background events for the project. Displays as a table with columns: Event ID (first 8 chars), Type, Status (color-coded), Latency, Created. Status values: `PENDING` (accent), `SUCCEEDED` (green), `FAILED` (red), `PROCESSING` (yellow).
|
||||
|
||||
**JSON output envelope:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "event list",
|
||||
"count": 3,
|
||||
"duration_ms": 87,
|
||||
"data": [
|
||||
{ "id": "evt-abc", "event_type": "ADD", "status": "SUCCEEDED", "latency": 412.0, "created_at": "2026-01-01T10:00:00Z" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
mem0 event list
|
||||
mem0 event list --output json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.15 `event status`
|
||||
|
||||
Get the status and results of a specific background event.
|
||||
|
||||
| Property | Value |
|
||||
|------------------|-------|
|
||||
| Usage | `mem0 event status <event_id> [OPTIONS]` |
|
||||
| needsBackend | Yes |
|
||||
| needsConfig | Yes |
|
||||
| resolveIds | No |
|
||||
| resolveGraph | No |
|
||||
| confirmDangerous | No |
|
||||
| Output formats | text, json |
|
||||
| Default output | text |
|
||||
| API endpoint | `GET /v1/events/{event_id}/` |
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Name | Type | Required | Help |
|
||||
|------------|--------|----------|------|
|
||||
| `event_id` | string | Yes | Event ID to inspect. |
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Type | Default | Panel | Help |
|
||||
|----------------|--------|---------|------------|------|
|
||||
| `-o, --output` | string | "text" | Output | Output: text, json. |
|
||||
| `--api-key` | string | - | Connection | Override API key. |
|
||||
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||
|
||||
**Behavior:** Fetches the event by ID and displays: Event ID, Type, Status (color-coded), Latency, Created, Updated, and a numbered list of result memories (event type, memory text, user_id, truncated memory ID). Displayed in a boxed panel (text) or JSON envelope.
|
||||
|
||||
**JSON output envelope:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "event status",
|
||||
"duration_ms": 65,
|
||||
"data": {
|
||||
"id": "evt-abc",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"latency": 412.0,
|
||||
"created_at": "2026-01-01T10:00:00Z",
|
||||
"updated_at": "2026-01-01T10:00:01Z",
|
||||
"results": [
|
||||
{ "id": "mem-xyz", "event": "ADD", "user_id": "alice", "memory": "User prefers dark mode" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
mem0 event status evt-abc-123
|
||||
mem0 event status evt-abc-123 --output json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.16 `status`
|
||||
### 3.14 `status`
|
||||
|
||||
Check connectivity and authentication.
|
||||
|
||||
@@ -838,19 +721,20 @@ Check connectivity and authentication.
|
||||
| `--api-key` | string | - | Connection | Override API key. |
|
||||
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||
|
||||
**Behavior:** Validates connectivity by calling `GET /v1/ping/`. Displays connection status in a boxed panel (text) or JSON envelope. The ping endpoint is lightweight and does not require any entity scope.
|
||||
**Behavior:** If config has a default `user_id` or `agent_id`, validates by making a minimal `POST /v2/memories/` with `page=1&page_size=1`. Otherwise validates via `GET /v1/entities/`. Displays connection status in a boxed panel (text) or JSON envelope.
|
||||
|
||||
**JSON output:**
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "status",
|
||||
"duration_ms": 112,
|
||||
"data": {
|
||||
"connected": true,
|
||||
"backend": "platform",
|
||||
"base_url": "https://api.mem0.ai"
|
||||
}
|
||||
"base_url": "https://api.mem0.ai",
|
||||
"latency_ms": 245
|
||||
},
|
||||
"duration_ms": 245
|
||||
}
|
||||
```
|
||||
|
||||
@@ -862,7 +746,7 @@ mem0 status -o json
|
||||
|
||||
---
|
||||
|
||||
### 3.17 `help`
|
||||
### 3.15 `help`
|
||||
|
||||
Show help. Use `--json` for machine-readable output (for LLM agents).
|
||||
|
||||
@@ -922,9 +806,6 @@ The auth header name is `Authorization` and the scheme is `Token` (not Bearer).
|
||||
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
|
||||
| List entities | `GET` | `/v1/entities/` | - | - |
|
||||
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params |
|
||||
| List events | `GET` | `/v1/events/` | - | - |
|
||||
| Get event | `GET` | `/v1/events/{event_id}/` | - | - |
|
||||
| Ping (status) | `GET` | `/v1/ping/` | - | - |
|
||||
|
||||
### How Filters Are Built (`_buildFilters` / `_build_filters`)
|
||||
|
||||
@@ -1227,8 +1108,6 @@ For `PENDING` events, displays "Processing in background" with the event ID.
|
||||
|
||||
### 7.1 Supported Modes Per Command
|
||||
|
||||
All commands also support `agent` mode via the global `--json`/`--agent` flag, which wraps output in a structured JSON envelope with sanitized fields.
|
||||
|
||||
| Command | text | json | table | quiet |
|
||||
|----------------|------|------|-------|-------|
|
||||
| add | Y | Y | - | Y |
|
||||
@@ -1243,16 +1122,12 @@ All commands also support `agent` mode via the global `--json`/`--agent` flag, w
|
||||
| config set | (success msg) | - | - | - |
|
||||
| entity list | - | Y | Y (default) | - |
|
||||
| entity delete | Y | Y | - | Y |
|
||||
| event list | Y (table) | Y | - | - |
|
||||
| event status | Y | Y | - | - |
|
||||
| status | Y | Y | - | - |
|
||||
| help | Y | Y (--json) | - | - |
|
||||
|
||||
### 7.2 JSON Envelope Format
|
||||
### 7.2 JSON Envelope Format (`formatJsonEnvelope`)
|
||||
|
||||
There are two related envelope formats:
|
||||
|
||||
**`formatJsonEnvelope`** — used by `config show`, `status`, and `import` for `--output json`:
|
||||
Used by `config show`, `status`, and `import` for structured JSON output:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -1266,39 +1141,14 @@ There are two related envelope formats:
|
||||
}
|
||||
```
|
||||
|
||||
**`formatAgentEnvelope`** — used by all commands in agent mode (`--json`/`--agent`). Same structure, but `data` is passed through `sanitizeAgentData(command, data)` to project only the most relevant fields:
|
||||
|
||||
| Command | Fields in `data` |
|
||||
|---------------|-----------------|
|
||||
| add | `[{id, memory, event}]` or `[{status, event_id}]` for PENDING |
|
||||
| search | `[{id, memory, score, created_at, categories}]` |
|
||||
| list | `[{id, memory, created_at, categories}]` |
|
||||
| get | `{id, memory, created_at, updated_at, categories, metadata}` |
|
||||
| update | `{id, memory}` |
|
||||
| delete | (raw API response) |
|
||||
| entity list | `[{name, type, count}]` |
|
||||
| event list | `[{id, event_type, status, latency, created_at}]` |
|
||||
| event status | `{id, event_type, status, latency, created_at, updated_at, results: [{id, event, user_id, memory}]}` |
|
||||
| status/config/import | (pass-through) |
|
||||
|
||||
Error envelopes (on non-zero exit):
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"command": "<command_name>",
|
||||
"error": "Authentication failed. Your API key may be invalid or expired.",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- `status`: `"success"` or `"error"`.
|
||||
- `command`: The command name.
|
||||
- `status`: Always `"success"` (errors go to stderr before exit).
|
||||
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`).
|
||||
- `duration_ms`: Optional, elapsed time in milliseconds.
|
||||
- `scope`: Optional, active entity scope (omitted if empty).
|
||||
- `scope`: Optional, active entity scope.
|
||||
- `count`: Optional, result count.
|
||||
- `error`: Only present when `status` is `"error"`.
|
||||
- `data`: The primary payload (sanitized in agent mode).
|
||||
- `error`: Optional, error message string.
|
||||
- `data`: The primary payload.
|
||||
|
||||
### 7.3 Text Output
|
||||
|
||||
@@ -1369,16 +1219,6 @@ Destructive commands (`delete --all`, `delete --entity`, `entity delete`) requir
|
||||
- CI/CD pipelines
|
||||
- Scripting
|
||||
|
||||
### Why `--json`/`--agent` global flags exist
|
||||
|
||||
The `--json` and `--agent` flags (aliases of each other) activate agent mode globally. When set:
|
||||
1. All output becomes a structured JSON envelope (`{status, command, duration_ms, scope, count, data}`).
|
||||
2. The `data` field is sanitized via `sanitizeAgentData` — only the most relevant fields are included per command, reducing noise for agents parsing the output.
|
||||
3. All human-readable output (spinners, colors, banners, timing lines) is suppressed.
|
||||
4. Errors are emitted as JSON to stdout with a non-zero exit code, not to stderr as text.
|
||||
|
||||
This is distinct from `--output json`, which returns the raw API response without sanitization.
|
||||
|
||||
### Why `--output json` is on every command
|
||||
|
||||
Every data-returning command supports `--output json` (or `--json` for `help`). This enables machine consumption by AI agents and scripts. JSON output goes to stdout while human-readable spinners/timing go to stderr, so piping `mem0 list -o json | jq .` works cleanly.
|
||||
|
||||
+6
-38
@@ -2,8 +2,6 @@
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
|
||||
|
||||
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -57,45 +55,12 @@ mem0 delete <memory-id>
|
||||
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
||||
| `mem0 import` | Bulk import memories from a JSON file |
|
||||
| `mem0 config` | View or modify CLI configuration |
|
||||
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||
| `mem0 entities` | List or delete entities (users, agents, apps) |
|
||||
| `mem0 status` | Verify API connection and display current project |
|
||||
| `mem0 version` | Print the CLI version |
|
||||
|
||||
Run `mem0 <command> --help` for detailed usage on any command.
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
@@ -103,10 +68,13 @@ Control how results are displayed with `--output`:
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `json` | Structured JSON for piping to `jq` or agent consumption |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
```bash
|
||||
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
|
||||
+1
-2
@@ -515,8 +515,7 @@
|
||||
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
|
||||
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
||||
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
|
||||
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+37
-299
@@ -2,12 +2,10 @@
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
||||
|
||||
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js **18+**
|
||||
- pnpm (`npm install -g pnpm`) — for development only
|
||||
- pnpm (`npm install -g pnpm`)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -15,307 +13,22 @@ The official command-line interface for [mem0](https://mem0.ai) — the memory l
|
||||
npm install -g @mem0/cli
|
||||
```
|
||||
|
||||
## Quick start
|
||||
Or from source:
|
||||
|
||||
```bash
|
||||
# Interactive setup wizard
|
||||
mem0 init
|
||||
cd node
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm link --global
|
||||
|
||||
# Or login via email
|
||||
mem0 init --email alice@company.com
|
||||
|
||||
# Or authenticate with an existing API key
|
||||
mem0 init --api-key m0-xxx
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Get a specific memory
|
||||
mem0 get <memory-id>
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I switched to light mode"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
# Now use it like a normal CLI
|
||||
mem0 --help
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mem0 init`
|
||||
|
||||
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
## Running during development
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
```
|
||||
|
||||
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | API key (skip prompt) |
|
||||
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||
| `--email` | Login via email verification code |
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
|
||||
```bash
|
||||
mem0 add "I prefer dark mode" --user-id alice
|
||||
mem0 add --file conversation.json --user-id alice
|
||||
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Scope to a user |
|
||||
| `--agent-id` | Scope to an agent |
|
||||
| `--messages` | Conversation messages as JSON |
|
||||
| `-f, --file` | Read messages from a JSON file |
|
||||
| `-m, --metadata` | Custom metadata as JSON |
|
||||
| `--categories` | Categories (JSON array or comma-separated) |
|
||||
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||
|
||||
### `mem0 search`
|
||||
|
||||
Search memories using natural language.
|
||||
|
||||
```bash
|
||||
mem0 search "dietary restrictions" --user-id alice
|
||||
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `-k, --top-k` | Number of results (default: 10) |
|
||||
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||
| `--rerank` | Enable reranking |
|
||||
| `--keyword` | Use keyword search instead of semantic |
|
||||
| `--filter` | Advanced filter expression (JSON) |
|
||||
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 list`
|
||||
|
||||
List memories with optional filters and pagination.
|
||||
|
||||
```bash
|
||||
mem0 list --user-id alice
|
||||
mem0 list --user-id alice --category preferences --output json
|
||||
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `--page` | Page number (default: 1) |
|
||||
| `--page-size` | Results per page (default: 100) |
|
||||
| `--category` | Filter by category |
|
||||
| `--after` | Created after date (YYYY-MM-DD) |
|
||||
| `--before` | Created before date (YYYY-MM-DD) |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 get`
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
```bash
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||
```
|
||||
|
||||
### `mem0 update`
|
||||
|
||||
Update the text or metadata of an existing memory.
|
||||
|
||||
```bash
|
||||
mem0 update <memory-id> "Updated preference text"
|
||||
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||
echo "new text" | mem0 update <memory-id>
|
||||
```
|
||||
|
||||
### `mem0 delete`
|
||||
|
||||
Delete a single memory, all memories for a scope, or an entire entity.
|
||||
|
||||
```bash
|
||||
# Delete a single memory
|
||||
mem0 delete <memory-id>
|
||||
|
||||
# Delete all memories for a user
|
||||
mem0 delete --all --user-id alice --force
|
||||
|
||||
# Delete all memories project-wide
|
||||
mem0 delete --all --project --force
|
||||
|
||||
# Preview what would be deleted
|
||||
mem0 delete --all --user-id alice --dry-run
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Delete all memories matching scope filters |
|
||||
| `--entity` | Delete the entity and all its memories |
|
||||
| `--project` | With `--all`: delete all memories project-wide |
|
||||
| `--dry-run` | Preview without deleting |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### `mem0 import`
|
||||
|
||||
Bulk import memories from a JSON file.
|
||||
|
||||
```bash
|
||||
mem0 import data.json --user-id alice
|
||||
```
|
||||
|
||||
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||
|
||||
### `mem0 config`
|
||||
|
||||
View or modify the local CLI configuration.
|
||||
|
||||
```bash
|
||||
mem0 config show # Display current config (secrets redacted)
|
||||
mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entity`
|
||||
|
||||
List or delete entities (users, agents, apps, runs).
|
||||
|
||||
```bash
|
||||
mem0 entity list users
|
||||
mem0 entity list agents --output json
|
||||
mem0 entity delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 event`
|
||||
|
||||
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||
|
||||
```bash
|
||||
# List recent events
|
||||
mem0 event list
|
||||
|
||||
# Check the status of a specific event
|
||||
mem0 event status <event-id>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-o, --output` | Output format: `text`, `json` |
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
|
||||
```bash
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
mem0 --agent delete --all --user-id alice --force
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||
| `--agent` | Alias for `--json` |
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd cli/node
|
||||
cd node
|
||||
pnpm install
|
||||
|
||||
# Development mode (runs TypeScript directly, no build needed)
|
||||
@@ -326,11 +39,36 @@ pnpm dev search "test" --user-id alice
|
||||
# Or build first, then run the compiled JS
|
||||
pnpm build
|
||||
node dist/index.js --help
|
||||
node dist/index.js add "test memory" --user-id alice
|
||||
```
|
||||
|
||||
## Documentation
|
||||
## Quick Start
|
||||
|
||||
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||
```bash
|
||||
# Set up your configuration
|
||||
mem0 init
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories
|
||||
mem0 list --user-id alice
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (true/false) |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.2",
|
||||
"version": "0.1.0",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -20,11 +20,6 @@
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"author": "mem0.ai <founders@mem0.ai>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "cli/node"
|
||||
},
|
||||
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -89,17 +89,11 @@ export interface Backend {
|
||||
|
||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||
|
||||
ping(): Promise<Record<string, unknown>>;
|
||||
|
||||
status(opts?: { userId?: string; agentId?: string }): Promise<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
|
||||
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||
|
||||
listEvents(): Promise<Record<string, unknown>[]>;
|
||||
|
||||
getEvent(eventId: string): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
*/
|
||||
|
||||
import type { PlatformConfig } from "../config.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
import {
|
||||
APIError,
|
||||
type AddOptions,
|
||||
@@ -26,9 +24,6 @@ export class PlatformBackend implements Backend {
|
||||
this.headers = {
|
||||
Authorization: `Token ${config.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
"X-Mem0-Client-Version": CLI_VERSION,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,14 +38,9 @@ export class PlatformBackend implements Backend {
|
||||
url += `?${qs}`;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
...this.headers,
|
||||
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
|
||||
};
|
||||
|
||||
const fetchOpts: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
headers: this.headers,
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
};
|
||||
if (opts?.json) {
|
||||
@@ -274,37 +264,41 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
|
||||
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||
const typeMap: [string, string | undefined][] = [
|
||||
["user", opts.userId],
|
||||
["agent", opts.agentId],
|
||||
["app", opts.appId],
|
||||
["run", opts.runId],
|
||||
];
|
||||
const entities = typeMap.filter(([, v]) => v) as [string, string][];
|
||||
if (entities.length === 0) {
|
||||
const params: Record<string, string> = {};
|
||||
if (opts.userId) params.user_id = opts.userId;
|
||||
if (opts.agentId) params.agent_id = opts.agentId;
|
||||
if (opts.appId) params.app_id = opts.appId;
|
||||
if (opts.runId) params.run_id = opts.runId;
|
||||
if (Object.keys(params).length === 0) {
|
||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||
}
|
||||
// Delete each provided entity via the v2 path-based endpoint
|
||||
let result: Record<string, unknown> = {};
|
||||
for (const [entityType, entityId] of entities) {
|
||||
result = (await this._request(
|
||||
"DELETE",
|
||||
`/v2/entities/${entityType}/${entityId}/`,
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async ping(): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
|
||||
return (await this._request("DELETE", "/v1/entities/", {
|
||||
params,
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async status(
|
||||
opts: { userId?: string; agentId?: string } = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
try {
|
||||
await this.ping();
|
||||
if (opts.userId || opts.agentId) {
|
||||
const payload: Record<string, unknown> = {};
|
||||
const statusParams: Record<string, string> = {
|
||||
page: "1",
|
||||
page_size: "1",
|
||||
};
|
||||
const apiFilters = this._buildFilters({
|
||||
userId: opts.userId,
|
||||
agentId: opts.agentId,
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
await this._request("POST", "/v2/memories/", {
|
||||
json: payload,
|
||||
params: statusParams,
|
||||
});
|
||||
} else {
|
||||
await this._request("GET", "/v1/entities/");
|
||||
}
|
||||
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
||||
} catch (e) {
|
||||
return {
|
||||
@@ -341,20 +335,4 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async listEvents(): Promise<Record<string, unknown>[]> {
|
||||
const result = (await this._request("GET", "/v1/events/")) as unknown;
|
||||
if (Array.isArray(result)) return result;
|
||||
return ((result as Record<string, unknown>).results ?? []) as Record<
|
||||
string,
|
||||
unknown
|
||||
>[];
|
||||
}
|
||||
|
||||
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import chalk from "chalk";
|
||||
import ora, { type Ora } from "ora";
|
||||
import { getCurrentCommand, isAgentMode } from "./state.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
export const LOGO = `
|
||||
@@ -43,7 +42,6 @@ export function sym(fancy: string, plain: string): string {
|
||||
}
|
||||
|
||||
export function printBanner(): void {
|
||||
if (isAgentMode()) return;
|
||||
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||
const logoLines = LOGO.trimEnd().split("\n");
|
||||
const tagline = ` ${TAGLINE}`;
|
||||
@@ -77,29 +75,13 @@ export function printBanner(): void {
|
||||
}
|
||||
|
||||
export function printSuccess(message: string): void {
|
||||
if (isAgentMode()) return;
|
||||
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printError(message: string, hint?: string): void {
|
||||
if (isAgentMode()) {
|
||||
const envelope = {
|
||||
status: "error",
|
||||
command: getCurrentCommand(),
|
||||
error: message,
|
||||
data: null,
|
||||
};
|
||||
console.log(JSON.stringify(envelope));
|
||||
return;
|
||||
}
|
||||
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
|
||||
const resolvedHint =
|
||||
hint ??
|
||||
(message.includes("Authentication failed")
|
||||
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys`
|
||||
: undefined);
|
||||
if (resolvedHint) {
|
||||
console.error(` ${dim(resolvedHint)}`);
|
||||
if (hint) {
|
||||
console.error(` ${dim(hint)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +90,15 @@ export function printWarning(message: string): void {
|
||||
}
|
||||
|
||||
export function printInfo(message: string): void {
|
||||
if (isAgentMode()) return;
|
||||
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printScope(ids: Record<string, string | undefined>): void {
|
||||
if (isAgentMode()) return;
|
||||
const parts: string[] = [];
|
||||
for (const [key, val] of Object.entries(ids)) {
|
||||
if (val) {
|
||||
parts.push(`${key}=${val}`);
|
||||
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
|
||||
parts.push(`${label}=${val}`);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
@@ -138,14 +119,10 @@ export async function timedStatus<T>(
|
||||
message: string,
|
||||
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||
): Promise<T> {
|
||||
if (isAgentMode()) {
|
||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||
return fn(ctx);
|
||||
}
|
||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||
const spinner = ora({
|
||||
text: dim(message),
|
||||
color: "yellow",
|
||||
color: "magenta",
|
||||
stream: process.stderr,
|
||||
}).start();
|
||||
const start = performance.now();
|
||||
@@ -162,7 +139,7 @@ export async function timedStatus<T>(
|
||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||
spinner.stop();
|
||||
if (ctx.errorMsg) {
|
||||
printError(`${ctx.errorMsg} (${elapsed}s)`);
|
||||
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -11,17 +11,15 @@ import {
|
||||
saveConfig,
|
||||
setNestedValue,
|
||||
} from "../config.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
|
||||
const { brand, accent, dim } = colors;
|
||||
|
||||
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||
setCurrentCommand("config show");
|
||||
const config = loadConfig();
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
if (opts.output === "json") {
|
||||
formatJsonEnvelope({
|
||||
command: "config show",
|
||||
data: {
|
||||
defaults: {
|
||||
@@ -68,7 +66,6 @@ export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||
}
|
||||
|
||||
export function cmdConfigGet(key: string): void {
|
||||
setCurrentCommand("config get");
|
||||
const config = loadConfig();
|
||||
const value = getNestedValue(config, key);
|
||||
|
||||
@@ -76,35 +73,20 @@ export function cmdConfigGet(key: string): void {
|
||||
printError(`Unknown config key: ${key}`);
|
||||
} else {
|
||||
// Redact secrets
|
||||
const displayValue =
|
||||
key.includes("api_key") || key.split(".").pop() === "key"
|
||||
? redactKey(String(value))
|
||||
: String(value);
|
||||
if (isAgentMode()) {
|
||||
formatAgentEnvelope({
|
||||
command: "config get",
|
||||
data: { key, value: displayValue },
|
||||
});
|
||||
if (key.includes("api_key") || key.split(".").pop() === "key") {
|
||||
console.log(redactKey(String(value)));
|
||||
} else {
|
||||
console.log(displayValue);
|
||||
console.log(String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function cmdConfigSet(key: string, value: string): void {
|
||||
setCurrentCommand("config set");
|
||||
const config = loadConfig();
|
||||
if (setNestedValue(config, key, value)) {
|
||||
saveConfig(config);
|
||||
const display = key.includes("key") ? redactKey(value) : value;
|
||||
if (isAgentMode()) {
|
||||
formatAgentEnvelope({
|
||||
command: "config set",
|
||||
data: { key, value: display },
|
||||
});
|
||||
} else {
|
||||
printSuccess(`${key} = ${display}`);
|
||||
}
|
||||
printSuccess(`${key} = ${display}`);
|
||||
} else {
|
||||
printError(`Unknown config key: ${key}`);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ import {
|
||||
printSuccess,
|
||||
timedStatus,
|
||||
} from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
import { formatJson } from "../output.js";
|
||||
|
||||
const { brand, accent, dim } = colors;
|
||||
|
||||
@@ -24,7 +23,6 @@ export async function cmdEntitiesList(
|
||||
entityType: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("entity list");
|
||||
if (!VALID_TYPES.has(entityType)) {
|
||||
printError(
|
||||
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||
@@ -47,13 +45,8 @@ export async function cmdEntitiesList(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "entity list",
|
||||
data: results,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
if (opts.output === "json") {
|
||||
formatJson(results);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -93,12 +86,6 @@ export async function cmdEntitiesDelete(
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("entity delete");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
if (isAgentMode() && !opts.force) {
|
||||
printError("Destructive operation requires --force in agent mode.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||
printError(
|
||||
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||
@@ -106,20 +93,27 @@ export async function cmdEntitiesDelete(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scopeParts: string[] = [];
|
||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||
const scope = scopeParts.join(", ");
|
||||
|
||||
if (opts.dryRun) {
|
||||
printInfo(`Would delete entity ${scope} and all its memories.`);
|
||||
const scopeParts: string[] = [];
|
||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||
printInfo(
|
||||
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
|
||||
);
|
||||
printInfo("No changes made.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.force) {
|
||||
const scopeParts: string[] = [];
|
||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||
const scope = scopeParts.join(", ");
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
@@ -154,13 +148,7 @@ export async function cmdEntitiesDelete(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "entity delete",
|
||||
data: { deleted: true },
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
/**
|
||||
* Event commands: list and status.
|
||||
*/
|
||||
|
||||
import boxen from "boxen";
|
||||
import Table from "cli-table3";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printInfo, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
|
||||
const { brand, accent, success, error: errorColor, warning, dim } = colors;
|
||||
|
||||
function statusStyled(status: string): string {
|
||||
switch (status.toUpperCase()) {
|
||||
case "SUCCEEDED":
|
||||
return success("SUCCEEDED");
|
||||
case "PENDING":
|
||||
return accent("PENDING");
|
||||
case "FAILED":
|
||||
return errorColor("FAILED");
|
||||
case "PROCESSING":
|
||||
return warning("PROCESSING");
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdEventList(
|
||||
backend: Backend,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("event list");
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus("Fetching events...", async () => {
|
||||
return backend.listEvents();
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "event list",
|
||||
data: results,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log();
|
||||
printInfo("No events found.");
|
||||
console.log();
|
||||
return;
|
||||
}
|
||||
|
||||
const table = new Table({
|
||||
head: [
|
||||
accent("Event ID"),
|
||||
accent("Type"),
|
||||
accent("Status"),
|
||||
accent("Latency"),
|
||||
accent("Created"),
|
||||
],
|
||||
colWidths: [12, 14, 14, 10, 22],
|
||||
wordWrap: true,
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const ev of results) {
|
||||
const evId = String(ev.id ?? "").slice(0, 8);
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const status = String(ev.status ?? "—");
|
||||
const latency = ev.latency as number | undefined;
|
||||
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(table.toString());
|
||||
console.log(
|
||||
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
export async function cmdEventStatus(
|
||||
backend: Backend,
|
||||
eventId: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("event status");
|
||||
const start = performance.now();
|
||||
let ev: Record<string, unknown>;
|
||||
try {
|
||||
ev = await timedStatus("Fetching event...", async () => {
|
||||
return backend.getEvent(eventId);
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
process.exit(1);
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
command: "event status",
|
||||
data: ev,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const status = String(ev.status ?? "—");
|
||||
const evType = String(ev.event_type ?? "—");
|
||||
const latency = ev.latency as number | undefined;
|
||||
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||
const created = String(ev.created_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const updated = String(ev.updated_at ?? "—")
|
||||
.slice(0, 19)
|
||||
.replace("T", " ");
|
||||
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(` ${dim("Event ID:")} ${eventId}`);
|
||||
lines.push(` ${dim("Type:")} ${evType}`);
|
||||
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
|
||||
lines.push(` ${dim("Latency:")} ${latencyStr}`);
|
||||
lines.push(` ${dim("Created:")} ${created}`);
|
||||
lines.push(` ${dim("Updated:")} ${updated}`);
|
||||
|
||||
if (results && results.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(` ${dim(`Results (${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 ? String(data.memory) : "";
|
||||
const evName = String(r.event ?? "");
|
||||
const user = String(r.user_id ?? "");
|
||||
let detail = `${evName} ${memory}`;
|
||||
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
|
||||
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const content = lines.join("\n");
|
||||
console.log();
|
||||
console.log(
|
||||
boxen(content, {
|
||||
title: brand("Event Status"),
|
||||
titleAlignment: "left",
|
||||
borderColor: "magenta",
|
||||
padding: 1,
|
||||
}),
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
* mem0 init — interactive setup wizard.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import readline from "node:readline";
|
||||
import { PlatformBackend } from "../backend/platform.js";
|
||||
import {
|
||||
@@ -13,12 +12,10 @@ import {
|
||||
printSuccess,
|
||||
} from "../branding.js";
|
||||
import {
|
||||
CONFIG_FILE,
|
||||
DEFAULT_BASE_URL,
|
||||
type Mem0Config,
|
||||
createDefaultConfig,
|
||||
loadConfig,
|
||||
redactKey,
|
||||
saveConfig,
|
||||
} from "../config.js";
|
||||
|
||||
@@ -41,16 +38,10 @@ async function emailLogin(
|
||||
const url = baseUrl.replace(/\/+$/, "");
|
||||
let codeValue = code;
|
||||
|
||||
const sourceHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
};
|
||||
|
||||
if (!codeValue) {
|
||||
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
||||
method: "POST",
|
||||
headers: sourceHeaders,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
@@ -91,7 +82,7 @@ async function emailLogin(
|
||||
|
||||
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
|
||||
method: "POST",
|
||||
headers: sourceHeaders,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, code: codeValue.trim() }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
@@ -202,10 +193,9 @@ async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||
console.log();
|
||||
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||
|
||||
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
const userId = await promptLine(
|
||||
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||
_systemUser,
|
||||
"mem0-cli",
|
||||
);
|
||||
if (userId) config.defaults.userId = userId;
|
||||
}
|
||||
@@ -221,20 +211,10 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
|
||||
});
|
||||
if (status.connected) {
|
||||
printSuccess("Connected to mem0 Platform!");
|
||||
// Cache user_email from ping response for telemetry distinct_id
|
||||
try {
|
||||
const pingData = (await backend.ping()) as Record<string, unknown>;
|
||||
const userEmail = pingData?.user_email as string | undefined;
|
||||
if (userEmail) {
|
||||
config.platform.userEmail = userEmail;
|
||||
}
|
||||
} catch {
|
||||
/* ignore — telemetry ID will fall back to API key hash */
|
||||
}
|
||||
} else {
|
||||
printError(
|
||||
`Could not connect: ${status.error ?? "Unknown error"}`,
|
||||
"Visit https://app.mem0.ai/dashboard/api-keys to get a new key, or run mem0 init again.",
|
||||
"Check your API key and try again.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -248,7 +228,6 @@ export async function runInit(
|
||||
userId?: string;
|
||||
email?: string;
|
||||
code?: string;
|
||||
force?: boolean;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const config = createDefaultConfig();
|
||||
@@ -268,40 +247,6 @@ export async function runInit(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Warn if an existing config with an API key would be overwritten
|
||||
if (
|
||||
!opts.force &&
|
||||
fs.existsSync(CONFIG_FILE) &&
|
||||
savedConfig.platform.apiKey
|
||||
) {
|
||||
console.log(
|
||||
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
|
||||
);
|
||||
if (process.stdin.isTTY) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
" Overwrite existing config? This cannot be undone. [y/N] ",
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
rl.close();
|
||||
if (answer.toLowerCase() !== "y") {
|
||||
printInfo("Cancelled. Use --force to skip this check.");
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
printError(
|
||||
"Existing config would be overwritten.",
|
||||
"Use --force to overwrite.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Email login flow ──────────────────────────────────────────────────────
|
||||
if (opts.email) {
|
||||
const email = opts.email.trim().toLowerCase();
|
||||
@@ -323,9 +268,7 @@ export async function runInit(
|
||||
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
config.defaults.userId = opts.userId || "mem0-cli";
|
||||
|
||||
saveConfig(config);
|
||||
console.log();
|
||||
@@ -340,19 +283,6 @@ export async function runInit(
|
||||
|
||||
// ── API key flow ──────────────────────────────────────────────────────────
|
||||
|
||||
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if (!process.stdin.isTTY) {
|
||||
if (!opts.apiKey) {
|
||||
printError(
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
"Usage: mem0 init --api-key <key> [--user-id <id>]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
opts.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
}
|
||||
|
||||
// Non-interactive: both flags provided
|
||||
if (opts.apiKey && opts.userId) {
|
||||
config.platform.apiKey = opts.apiKey;
|
||||
@@ -363,6 +293,15 @@ export async function runInit(
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-TTY without full flags: error with usage hint
|
||||
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
|
||||
printError(
|
||||
"Non-interactive terminal detected and missing required flags.",
|
||||
"Usage: mem0 init --api-key <key> --user-id <id>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printBanner();
|
||||
console.log();
|
||||
printInfo("Welcome! Let's set up your mem0 CLI.\n");
|
||||
@@ -402,9 +341,7 @@ export async function runInit(
|
||||
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
config.defaults.userId = opts.userId || "mem0-cli";
|
||||
|
||||
saveConfig(config);
|
||||
console.log();
|
||||
|
||||
+27
-136
@@ -13,7 +13,6 @@ import {
|
||||
} from "../branding.js";
|
||||
import {
|
||||
formatAddResult,
|
||||
formatAgentEnvelope,
|
||||
formatJson,
|
||||
formatJsonEnvelope,
|
||||
formatMemoriesTable,
|
||||
@@ -21,18 +20,6 @@ import {
|
||||
formatSingleMemory,
|
||||
printResultSummary,
|
||||
} from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||
function _stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdAdd(
|
||||
backend: Backend,
|
||||
@@ -53,7 +40,6 @@ export async function cmdAdd(
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("add");
|
||||
let msgs: Record<string, unknown>[] | undefined;
|
||||
let content = text;
|
||||
|
||||
@@ -78,8 +64,8 @@ export async function cmdAdd(
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
// Read from stdin only if stdin is an actual pipe or file redirect
|
||||
else if (!content && _stdinIsPiped()) {
|
||||
// Read from stdin if piped
|
||||
else if (!content && !process.stdin.isTTY) {
|
||||
content = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
|
||||
@@ -150,43 +136,8 @@ export async function cmdAdd(
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
// Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||
const rawResults: Record<string, unknown>[] = Array.isArray(result)
|
||||
? result
|
||||
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||
const seenEvents = new Set<string>();
|
||||
const deduped: Record<string, unknown>[] = [];
|
||||
for (const r of rawResults) {
|
||||
if (r.status === "PENDING") {
|
||||
const eid = (r.event_id as string) ?? "";
|
||||
if (eid && seenEvents.has(eid)) continue;
|
||||
if (eid) seenEvents.add(eid);
|
||||
}
|
||||
deduped.push(r);
|
||||
}
|
||||
// Write back so downstream formatters see deduplicated data
|
||||
const dedupedResult: Record<string, unknown> = Array.isArray(result)
|
||||
? (deduped as unknown as Record<string, unknown>)
|
||||
: { ...result, results: deduped };
|
||||
|
||||
if (opts.output === "agent") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
command: "add",
|
||||
data: deduped,
|
||||
scope,
|
||||
count: deduped.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.output === "json") {
|
||||
formatAddResult(dedupedResult, opts.output);
|
||||
formatAddResult(result, opts.output);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,18 +148,14 @@ export async function cmdAdd(
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
});
|
||||
const count = deduped.length;
|
||||
const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
|
||||
if (allPending) {
|
||||
printSuccess(
|
||||
`Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
|
||||
);
|
||||
} else {
|
||||
printSuccess(
|
||||
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||
);
|
||||
}
|
||||
formatAddResult(dedupedResult, opts.output);
|
||||
const results = Array.isArray(result)
|
||||
? result
|
||||
: ((result.results as unknown[]) ?? [result]);
|
||||
const count = results.length;
|
||||
printSuccess(
|
||||
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||
);
|
||||
formatAddResult(result, opts.output);
|
||||
}
|
||||
|
||||
export async function cmdSearch(
|
||||
@@ -229,7 +176,6 @@ export async function cmdSearch(
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("search");
|
||||
if (!query) {
|
||||
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||
process.exit(1);
|
||||
@@ -285,23 +231,6 @@ export async function cmdSearch(
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
command: "search",
|
||||
data: results,
|
||||
scope,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.output === "json") {
|
||||
formatJson(results);
|
||||
} else if (opts.output === "table") {
|
||||
@@ -338,7 +267,6 @@ export async function cmdGet(
|
||||
memoryId: string,
|
||||
opts: { output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("get");
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Fetching memory...", async () => {
|
||||
@@ -349,11 +277,7 @@ export async function cmdGet(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({ command: "get", data: result });
|
||||
} else {
|
||||
formatSingleMemory(result, opts.output);
|
||||
}
|
||||
formatSingleMemory(result, opts.output);
|
||||
}
|
||||
|
||||
export async function cmdList(
|
||||
@@ -372,7 +296,6 @@ export async function cmdList(
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("list");
|
||||
if (opts.pageSize < 1) {
|
||||
printError("--page-size must be >= 1.");
|
||||
process.exit(1);
|
||||
@@ -407,19 +330,12 @@ export async function cmdList(
|
||||
|
||||
if (opts.output === "quiet") return;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
const scope: Record<string, string | undefined> = {
|
||||
user_id: opts.userId,
|
||||
agent_id: opts.agentId,
|
||||
app_id: opts.appId,
|
||||
run_id: opts.runId,
|
||||
};
|
||||
formatAgentEnvelope({
|
||||
if (opts.output === "json") {
|
||||
formatJsonEnvelope({
|
||||
command: "list",
|
||||
data: results,
|
||||
scope,
|
||||
count: results.length,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
scope: { user_id: opts.userId, agent_id: opts.agentId },
|
||||
});
|
||||
} else if (opts.output === "table") {
|
||||
if (results.length > 0) {
|
||||
@@ -458,7 +374,6 @@ export async function cmdUpdate(
|
||||
text: string | undefined,
|
||||
opts: { metadata?: string; output: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("update");
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
if (opts.metadata) {
|
||||
try {
|
||||
@@ -481,13 +396,7 @@ export async function cmdUpdate(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "update",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(
|
||||
@@ -501,7 +410,6 @@ export async function cmdDelete(
|
||||
memoryId: string,
|
||||
opts: { output: string; dryRun?: boolean; force?: boolean },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete");
|
||||
if (opts.dryRun) {
|
||||
let mem: Record<string, unknown>;
|
||||
try {
|
||||
@@ -528,13 +436,7 @@ export async function cmdDelete(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete",
|
||||
data: { id: memoryId, deleted: true },
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
printSuccess(
|
||||
@@ -556,15 +458,16 @@ export async function cmdDeleteAll(
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete-all");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
if (isAgentMode() && !opts.force) {
|
||||
printError("Destructive operation requires --force in agent mode.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (opts.all) {
|
||||
// Project-wide wipe using wildcard entity IDs
|
||||
// Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||
if (opts.dryRun) {
|
||||
printInfo("Would delete ALL memories project-wide.");
|
||||
printInfo(
|
||||
"Run without --dry-run to see the actual count of deleted memories.",
|
||||
);
|
||||
printInfo("No changes made.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opts.force) {
|
||||
const readline = await import("node:readline");
|
||||
@@ -606,13 +509,7 @@ export async function cmdDeleteAll(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete-all",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
if (result.message) {
|
||||
@@ -689,13 +586,7 @@ export async function cmdDeleteAll(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent") {
|
||||
formatAgentEnvelope({
|
||||
command: "delete-all",
|
||||
data: result,
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
} else if (opts.output === "json") {
|
||||
if (opts.output === "json") {
|
||||
formatJson(result);
|
||||
} else if (opts.output !== "quiet") {
|
||||
if (result.message) {
|
||||
|
||||
@@ -6,8 +6,7 @@ import fs from "node:fs";
|
||||
import boxen from "boxen";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const { brand, dim, success, error: errorColor } = colors;
|
||||
@@ -16,7 +15,6 @@ export async function cmdStatus(
|
||||
backend: Backend,
|
||||
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("status");
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
@@ -31,13 +29,14 @@ export async function cmdStatus(
|
||||
}
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
if (opts.output === "json") {
|
||||
formatJsonEnvelope({
|
||||
command: "status",
|
||||
data: {
|
||||
connected: result.connected,
|
||||
backend: result.backend ?? null,
|
||||
base_url: result.base_url ?? null,
|
||||
latency_ms: Math.round(elapsed * 1000),
|
||||
},
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
@@ -57,15 +56,6 @@ export async function cmdStatus(
|
||||
}
|
||||
if (result.error) {
|
||||
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
||||
if (String(result.error).includes("Authentication failed")) {
|
||||
lines.push("");
|
||||
lines.push(
|
||||
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
|
||||
);
|
||||
lines.push(
|
||||
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
|
||||
|
||||
@@ -91,7 +81,6 @@ export async function cmdImport(
|
||||
filePath: string,
|
||||
opts: { userId?: string; agentId?: string; output?: string },
|
||||
): Promise<void> {
|
||||
setCurrentCommand("import");
|
||||
let data: Record<string, unknown>[];
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf-8");
|
||||
@@ -136,12 +125,13 @@ export async function cmdImport(
|
||||
const elapsed = (performance.now() - start) / 1000;
|
||||
console.log(); // Clear progress line
|
||||
|
||||
if (opts.output === "agent" || opts.output === "json") {
|
||||
formatAgentEnvelope({
|
||||
if (opts.output === "json") {
|
||||
formatJsonEnvelope({
|
||||
command: "import",
|
||||
data: {
|
||||
added,
|
||||
failed,
|
||||
duration_s: Number.parseFloat(elapsed.toFixed(2)),
|
||||
},
|
||||
durationMs: Math.round(elapsed * 1000),
|
||||
});
|
||||
|
||||
@@ -20,7 +20,6 @@ export const CONFIG_VERSION = 1;
|
||||
export interface PlatformConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
export interface DefaultsConfig {
|
||||
@@ -50,7 +49,6 @@ export function createDefaultConfig(): Mem0Config {
|
||||
platform: {
|
||||
apiKey: "",
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
userEmail: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -72,7 +70,6 @@ export function loadConfig(): Mem0Config {
|
||||
const plat = data.platform ?? {};
|
||||
config.platform.apiKey = plat.api_key ?? "";
|
||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||
config.platform.userEmail = plat.user_email ?? "";
|
||||
|
||||
const defaults = data.defaults ?? {};
|
||||
config.defaults.userId = defaults.user_id ?? "";
|
||||
@@ -117,7 +114,6 @@ export function saveConfig(config: Mem0Config): void {
|
||||
platform: {
|
||||
api_key: config.platform.apiKey,
|
||||
base_url: config.platform.baseUrl,
|
||||
user_email: config.platform.userEmail,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -135,7 +131,6 @@ export function redactKey(key: string): string {
|
||||
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||
"platform.api_key": ["platform", "apiKey"],
|
||||
"platform.base_url": ["platform", "baseUrl"],
|
||||
"platform.user_email": ["platform", "userEmail"],
|
||||
"defaults.user_id": ["defaults", "userId"],
|
||||
"defaults.agent_id": ["defaults", "agentId"],
|
||||
"defaults.app_id": ["defaults", "appId"],
|
||||
@@ -144,7 +139,6 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||
// Short-form aliases
|
||||
api_key: ["platform", "apiKey"],
|
||||
base_url: ["platform", "baseUrl"],
|
||||
user_email: ["platform", "userEmail"],
|
||||
user_id: ["defaults", "userId"],
|
||||
agent_id: ["defaults", "agentId"],
|
||||
app_id: ["defaults", "appId"],
|
||||
|
||||
@@ -36,7 +36,7 @@ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||
},
|
||||
{
|
||||
panel: "Management",
|
||||
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||
commands: ["init", "status", "import", "help", "entity", "config"],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+49
-213
@@ -8,27 +8,21 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import { AuthError, type Backend, getBackend } from "./backend/index.js";
|
||||
import { colors, printError, printWarning } from "./branding.js";
|
||||
import { type Backend, getBackend } from "./backend/index.js";
|
||||
import { colors, printError } from "./branding.js";
|
||||
import type { Mem0Config } from "./config.js";
|
||||
import { loadConfig, saveConfig } from "./config.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { richFormatHelp } from "./help.js";
|
||||
import { setAgentMode } from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
const program = new Command();
|
||||
|
||||
// ── Validated user identity (set by getBackendAndConfig) ─────────────────
|
||||
|
||||
let _validatedUserEmail: string | undefined;
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function getBackendAndConfig(
|
||||
function getBackendAndConfig(
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
): Promise<{ backend: Backend; config: Mem0Config }> {
|
||||
): { backend: Backend; config: Mem0Config } {
|
||||
const config = loadConfig();
|
||||
|
||||
if (apiKey) config.platform.apiKey = apiKey;
|
||||
@@ -42,58 +36,11 @@ async function getBackendAndConfig(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const backend = getBackend(config);
|
||||
|
||||
// Validate the API key upfront with a fast timeout
|
||||
try {
|
||||
const pingData = (await Promise.race([
|
||||
backend.ping(),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error("timeout")), 5000),
|
||||
),
|
||||
])) as Record<string, unknown>;
|
||||
|
||||
const email = pingData?.user_email as string | undefined;
|
||||
if (email) {
|
||||
_validatedUserEmail = email;
|
||||
if (config.platform.userEmail !== email) {
|
||||
config.platform.userEmail = email;
|
||||
try {
|
||||
saveConfig(config);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof AuthError) {
|
||||
printError(
|
||||
"Invalid or expired API key.",
|
||||
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// Network error / timeout — warn but proceed
|
||||
printWarning(
|
||||
"Could not validate API key (network issue). Proceeding anyway.",
|
||||
);
|
||||
}
|
||||
|
||||
return { backend, config };
|
||||
return { backend: getBackend(config), config };
|
||||
}
|
||||
|
||||
async function getBackendOnly(
|
||||
apiKey?: string,
|
||||
baseUrl?: string,
|
||||
): Promise<Backend> {
|
||||
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
|
||||
}
|
||||
|
||||
function checkAgentMode(): boolean {
|
||||
const rootOpts = program.opts();
|
||||
const isAgent = !!(rootOpts.json || rootOpts.agent);
|
||||
if (isAgent) setAgentMode(true);
|
||||
return isAgent;
|
||||
function getBackendOnly(apiKey?: string, baseUrl?: string): Backend {
|
||||
return getBackendAndConfig(apiKey, baseUrl).backend;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,45 +105,18 @@ program
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
process.exit(0);
|
||||
})
|
||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||
.option(
|
||||
"--agent",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||
)
|
||||
.usage("<command> [options]")
|
||||
.helpOption("--help", "Show this message and exit.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
// ── Telemetry hook ───────────────────────────────────────────────────────
|
||||
|
||||
program.hook("preAction", (_thisCommand, actionCommand) => {
|
||||
try {
|
||||
const commandName = actionCommand.name();
|
||||
const parentName = actionCommand.parent?.name();
|
||||
const fullCommand =
|
||||
parentName && parentName !== "mem0"
|
||||
? `${parentName}.${commandName}`
|
||||
: commandName;
|
||||
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||
captureEvent(
|
||||
`cli.${fullCommand}`,
|
||||
{
|
||||
command: fullCommand,
|
||||
is_agent: isAgent,
|
||||
},
|
||||
_validatedUserEmail,
|
||||
);
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
}
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description("Interactive setup wizard for mem0 CLI.")
|
||||
.description(
|
||||
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||
)
|
||||
.option("--api-key <key>", "API key (skip prompt).")
|
||||
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||
.option("--email <email>", "Login via email verification code.")
|
||||
@@ -204,7 +124,6 @@ program
|
||||
"--code <code>",
|
||||
"Verification code (use with --email for non-interactive login).",
|
||||
)
|
||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
|
||||
@@ -216,7 +135,6 @@ program
|
||||
userId: opts.userId,
|
||||
email: opts.email,
|
||||
code: opts.code,
|
||||
force: opts.force,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,24 +165,17 @@ program
|
||||
)
|
||||
.action(async (text, opts) => {
|
||||
const { cmdAdd } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||
const ids = resolveIds(config, opts);
|
||||
const enableGraph = resolveGraph(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph, output });
|
||||
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph });
|
||||
});
|
||||
|
||||
// ── Memory: search ────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("search [query]")
|
||||
.description(
|
||||
"Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||
)
|
||||
.description("Search memories by semantic query.")
|
||||
.option("-u, --user-id <id>", "Filter by user.")
|
||||
.option("--agent-id <id>", "Filter by agent.")
|
||||
.option("--app-id <id>", "Filter by app.")
|
||||
@@ -304,14 +215,9 @@ program
|
||||
process.exit(1);
|
||||
}
|
||||
const { cmdSearch } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||
const ids = resolveIds(config, opts);
|
||||
const enableGraph = resolveGraph(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdSearch(backend, resolvedQuery, {
|
||||
...ids,
|
||||
topK: opts.topK,
|
||||
@@ -321,7 +227,7 @@ program
|
||||
filterJson: opts.filter,
|
||||
fields: opts.fields,
|
||||
enableGraph,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -339,10 +245,8 @@ program
|
||||
)
|
||||
.action(async (memoryId, opts) => {
|
||||
const { cmdGet } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdGet(backend, memoryId, { output });
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdGet(backend, memoryId, { output: opts.output });
|
||||
});
|
||||
|
||||
// ── Memory: list ──────────────────────────────────────────────────────────
|
||||
@@ -375,14 +279,9 @@ program
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdList } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||
const ids = resolveIds(config, opts);
|
||||
const enableGraph = resolveGraph(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdList(backend, {
|
||||
...ids,
|
||||
page: opts.page,
|
||||
@@ -391,7 +290,7 @@ program
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
enableGraph,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -414,12 +313,10 @@ program
|
||||
resolvedText = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
const { cmdUpdate } = await import("./commands/memory.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||
metadata: opts.metadata,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -455,8 +352,6 @@ program
|
||||
].join("\n"),
|
||||
)
|
||||
.action(async (memoryId, opts) => {
|
||||
const isAgent = checkAgentMode();
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
// ── Mutual-exclusion checks ──
|
||||
if (memoryId && opts.all) {
|
||||
printError("Cannot combine <memoryId> with --all. Use one or the other.");
|
||||
@@ -485,9 +380,9 @@ program
|
||||
// ── Dispatch: single memory ──
|
||||
if (memoryId) {
|
||||
const { cmdDelete } = await import("./commands/memory.js");
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdDelete(backend, memoryId, {
|
||||
output,
|
||||
output: opts.output,
|
||||
dryRun: opts.dryRun,
|
||||
force: opts.force,
|
||||
});
|
||||
@@ -497,7 +392,7 @@ program
|
||||
// ── Dispatch: --all ──
|
||||
if (opts.all) {
|
||||
const { cmdDeleteAll } = await import("./commands/memory.js");
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
const { backend, config } = getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
@@ -514,7 +409,7 @@ program
|
||||
dryRun: opts.dryRun,
|
||||
all: opts.project,
|
||||
...ids,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -522,8 +417,8 @@ program
|
||||
// ── Dispatch: --entity ──
|
||||
if (opts.entity) {
|
||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdEntitiesDelete(backend, opts);
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -545,9 +440,7 @@ configCmd
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdConfigShow } = await import("./commands/config.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
cmdConfigShow({ output });
|
||||
cmdConfigShow({ output: opts.output });
|
||||
});
|
||||
|
||||
configCmd
|
||||
@@ -559,7 +452,6 @@ configCmd
|
||||
)
|
||||
.action(async (key) => {
|
||||
const { cmdConfigGet } = await import("./commands/config.js");
|
||||
checkAgentMode();
|
||||
cmdConfigGet(key);
|
||||
});
|
||||
|
||||
@@ -572,7 +464,6 @@ configCmd
|
||||
)
|
||||
.action(async (key, value) => {
|
||||
const { cmdConfigSet } = await import("./commands/config.js");
|
||||
checkAgentMode();
|
||||
cmdConfigSet(key, value);
|
||||
});
|
||||
|
||||
@@ -596,10 +487,8 @@ entityCmd
|
||||
)
|
||||
.action(async (entityType, opts) => {
|
||||
const { cmdEntitiesList } = await import("./commands/entities.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEntitiesList(backend, entityType, { output });
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdEntitiesList(backend, entityType, { output: opts.output });
|
||||
});
|
||||
|
||||
entityCmd
|
||||
@@ -620,54 +509,8 @@ entityCmd
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||
});
|
||||
|
||||
// ── Event subcommands ─────────────────────────────────────────────────────
|
||||
|
||||
const eventCmd = program
|
||||
.command("event")
|
||||
.description("Inspect background processing events.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
eventCmd
|
||||
.command("list")
|
||||
.description("List recent background processing events.")
|
||||
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const { cmdEventList } = await import("./commands/events.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEventList(backend, { output });
|
||||
});
|
||||
|
||||
eventCmd
|
||||
.command("status <eventId>")
|
||||
.description("Check the status of a specific background event.")
|
||||
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
|
||||
)
|
||||
.action(async (eventId, opts) => {
|
||||
const { cmdEventStatus } = await import("./commands/events.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdEventStatus(backend, eventId, { output });
|
||||
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||
await cmdEntitiesDelete(backend, opts);
|
||||
});
|
||||
|
||||
// ── Utility commands ──────────────────────────────────────────────────────
|
||||
@@ -681,16 +524,11 @@ program
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json")
|
||||
.action(async (opts) => {
|
||||
const { cmdStatus } = await import("./commands/utils.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||
await cmdStatus(backend, {
|
||||
userId: config.defaults.userId || undefined,
|
||||
agentId: config.defaults.agentId || undefined,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -708,17 +546,12 @@ program
|
||||
)
|
||||
.action(async (filePath, opts) => {
|
||||
const { cmdImport } = await import("./commands/utils.js");
|
||||
const isAgent = checkAgentMode();
|
||||
const { backend, config } = await getBackendAndConfig(
|
||||
opts.apiKey,
|
||||
opts.baseUrl,
|
||||
);
|
||||
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||
const ids = resolveIds(config, opts);
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdImport(backend, filePath, {
|
||||
userId: ids.userId,
|
||||
agentId: ids.agentId,
|
||||
output,
|
||||
output: opts.output,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -732,9 +565,7 @@ program
|
||||
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||
.action((opts) => {
|
||||
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||
// program.opts().json is set when the root --json global flag was used first.
|
||||
if (opts.json || program.opts().json) {
|
||||
if (opts.json) {
|
||||
// Load spec from parent directory
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||
@@ -764,9 +595,7 @@ program
|
||||
console.log(
|
||||
" add Add a memory from text, messages, file, or stdin",
|
||||
);
|
||||
console.log(
|
||||
" search Query your memory store (semantic, keyword, hybrid)",
|
||||
);
|
||||
console.log(" search Search memories by semantic query");
|
||||
console.log(" get Get a specific memory by ID");
|
||||
console.log(" list List memories with optional filters");
|
||||
console.log(" update Update a memory's text or metadata");
|
||||
@@ -776,9 +605,6 @@ program
|
||||
console.log(" import Import memories from a JSON file");
|
||||
console.log(" config Manage configuration (show, get, set)");
|
||||
console.log(" entity Manage entities (list, delete)");
|
||||
console.log(
|
||||
" event Inspect background events (list, status)",
|
||||
);
|
||||
console.log(" init Interactive setup wizard");
|
||||
console.log(" status Check connectivity and authentication");
|
||||
console.log();
|
||||
@@ -790,6 +616,16 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// ── Version ───────────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("version")
|
||||
.description("Show version.")
|
||||
.action(async () => {
|
||||
const { cmdVersion } = await import("./commands/utils.js");
|
||||
cmdVersion();
|
||||
});
|
||||
|
||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
program.parse();
|
||||
|
||||
+3
-124
@@ -169,26 +169,17 @@ export function formatAddResult(
|
||||
}
|
||||
|
||||
console.log();
|
||||
const seenPendingEvents = new Set<string>();
|
||||
for (const r of results) {
|
||||
// Detect async PENDING response
|
||||
if (r.status === "PENDING") {
|
||||
const eventId = (r.event_id as string) ?? "";
|
||||
// Deduplicate PENDING entries with the same event_id
|
||||
if (eventId && seenPendingEvents.has(eventId)) continue;
|
||||
if (eventId) seenPendingEvents.add(eventId);
|
||||
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
||||
const icon = accent(sym("⧗", "..."));
|
||||
const parts = [
|
||||
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||
"Processing in background",
|
||||
];
|
||||
if (eventId) parts.push(dim(`(event ${eventId})`));
|
||||
console.log(parts.join(" "));
|
||||
if (eventId) {
|
||||
console.log(` ${dim(` event_id: ${eventId}`)}`);
|
||||
console.log(
|
||||
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -247,118 +238,6 @@ export function formatJsonEnvelope(opts: {
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
function pick(
|
||||
obj: Record<string, unknown>,
|
||||
keys: string[],
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key of keys) {
|
||||
if (key in obj) result[key] = obj[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
if (data === null || data === undefined) return data;
|
||||
|
||||
switch (command) {
|
||||
case "add": {
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
return items.map((item) => {
|
||||
const r = item as Record<string, unknown>;
|
||||
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
|
||||
return pick(r, ["id", "memory", "event"]);
|
||||
});
|
||||
}
|
||||
case "search":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||
);
|
||||
case "list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||
);
|
||||
case "get": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
]);
|
||||
}
|
||||
case "update": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, ["id", "memory"]);
|
||||
}
|
||||
case "delete":
|
||||
case "delete-all":
|
||||
case "entity delete":
|
||||
return data;
|
||||
case "entity list":
|
||||
return (data as Record<string, unknown>[]).map((r) => ({
|
||||
name: (r.name ?? r.id) as string,
|
||||
...pick(r, ["type", "count"]),
|
||||
}));
|
||||
case "event list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
|
||||
);
|
||||
case "event status": {
|
||||
const ev = data as Record<string, unknown>;
|
||||
const rawResults =
|
||||
(ev.results as Record<string, unknown>[] | undefined) ?? [];
|
||||
const sanitizedResults = rawResults.map((r) => {
|
||||
const nested = r.data as Record<string, unknown> | undefined;
|
||||
return {
|
||||
id: r.id,
|
||||
event: r.event,
|
||||
user_id: r.user_id,
|
||||
memory: nested?.memory ?? null,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...pick(ev, [
|
||||
"id",
|
||||
"event_type",
|
||||
"status",
|
||||
"latency",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]),
|
||||
results: sanitizedResults,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatAgentEnvelope(opts: {
|
||||
command: string;
|
||||
data: unknown;
|
||||
durationMs?: number;
|
||||
scope?: Record<string, string | undefined>;
|
||||
count?: number;
|
||||
}): void {
|
||||
const envelope: Record<string, unknown> = {
|
||||
status: "success",
|
||||
command: opts.command,
|
||||
};
|
||||
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||
if (opts.scope) {
|
||||
const filtered = Object.fromEntries(
|
||||
Object.entries(opts.scope).filter(([, v]) => v),
|
||||
);
|
||||
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
|
||||
}
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
export function printResultSummary(opts: {
|
||||
count: number;
|
||||
durationSecs?: number;
|
||||
@@ -370,7 +249,7 @@ export function printResultSummary(opts: {
|
||||
if (opts.scopeIds) {
|
||||
const scopeParts = Object.entries(opts.scopeIds)
|
||||
.filter(([, v]) => v)
|
||||
.map(([k, v]) => `${k}=${v}`);
|
||||
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
||||
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||
}
|
||||
if (opts.durationSecs !== undefined)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Agent mode state — set by the root program option handler,
|
||||
* read by commands and branding functions.
|
||||
*/
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
|
||||
export function isAgentMode(): boolean {
|
||||
return _agentMode;
|
||||
}
|
||||
|
||||
export function setAgentMode(val: boolean): void {
|
||||
_agentMode = val;
|
||||
}
|
||||
|
||||
export function getCurrentCommand(): string {
|
||||
return _currentCommand;
|
||||
}
|
||||
|
||||
export function setCurrentCommand(name: string): void {
|
||||
_currentCommand = name;
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/**
|
||||
* CLI telemetry — anonymous usage tracking via PostHog.
|
||||
*
|
||||
* Sends fire-and-forget events by spawning a detached child process
|
||||
* (telemetry-sender.cjs). The parent CLI process exits immediately;
|
||||
* the child handles email resolution, caching, and the HTTP POST.
|
||||
*
|
||||
* Disable with: MEM0_TELEMETRY=false
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { CONFIG_FILE, loadConfig } from "./config.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
|
||||
|
||||
function isTelemetryEnabled(): boolean {
|
||||
try {
|
||||
return process.env.MEM0_TELEMETRY !== "false";
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a stable anonymous identifier for the current user.
|
||||
*
|
||||
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
|
||||
* Matches the SDK pattern in mem0-ts/src/client/mem0.ts.
|
||||
*/
|
||||
function getDistinctId(): string {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
if (config.platform.userEmail) {
|
||||
return config.platform.userEmail;
|
||||
}
|
||||
if (config.platform.apiKey) {
|
||||
return createHash("md5").update(config.platform.apiKey).digest("hex");
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return "anonymous-cli";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire a PostHog event (non-blocking, returns void, never throws).
|
||||
* Spawns telemetry-sender.cjs as a detached subprocess.
|
||||
*
|
||||
* When `preResolvedEmail` is provided (e.g. from an upfront ping
|
||||
* validation), it is used directly as the PostHog distinct ID and the
|
||||
* subprocess skips its own `/v1/ping/` call.
|
||||
*/
|
||||
export function captureEvent(
|
||||
eventName: string,
|
||||
properties: Record<string, unknown> = {},
|
||||
preResolvedEmail?: string,
|
||||
): void {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const distinctId = preResolvedEmail || getDistinctId();
|
||||
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
event: eventName,
|
||||
properties: {
|
||||
source: "CLI",
|
||||
language: "node",
|
||||
cli_version: CLI_VERSION,
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
...properties,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
payload,
|
||||
posthogHost: POSTHOG_HOST,
|
||||
needsEmail: !distinctId || !distinctId.includes("@"),
|
||||
mem0ApiKey: config.platform.apiKey || "",
|
||||
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
|
||||
configPath: CONFIG_FILE,
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||
{ detached: true, stdio: "ignore" },
|
||||
);
|
||||
child.unref();
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Standalone telemetry sender — runs as a detached child process.
|
||||
*
|
||||
* Usage: node telemetry-sender.cjs '<json context>'
|
||||
*
|
||||
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||
* of the parent CLI process. It:
|
||||
*
|
||||
* 1. Resolves the user's email via /v1/ping/ if not already cached
|
||||
* 2. Caches the email in ~/.mem0/config.json for future runs
|
||||
* 3. Sends the PostHog event
|
||||
*
|
||||
* All errors are silently swallowed — this process must never produce output
|
||||
* or affect the user experience.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
|
||||
function httpsRequest(url, method, headers, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
const opts = {
|
||||
hostname: u.hostname,
|
||||
path: u.pathname + u.search,
|
||||
method,
|
||||
headers,
|
||||
timeout: 10000,
|
||||
};
|
||||
const req = https.request(opts, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch {
|
||||
resolve({});
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
req.destroy();
|
||||
reject(new Error("timeout"));
|
||||
});
|
||||
if (body) {
|
||||
req.end(body);
|
||||
} else {
|
||||
req.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveAndCacheEmail(ctx, payload) {
|
||||
try {
|
||||
const pingUrl = ctx.mem0BaseUrl.replace(/\/+$/, "") + "/v1/ping/";
|
||||
const data = await httpsRequest(pingUrl, "GET", {
|
||||
Authorization: "Token " + ctx.mem0ApiKey,
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
if (data.user_email) {
|
||||
payload.distinct_id = data.user_email;
|
||||
cacheEmail(ctx.configPath, data.user_email);
|
||||
}
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
function cacheEmail(configPath, email) {
|
||||
if (!configPath) return;
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, "utf-8");
|
||||
const cfg = JSON.parse(raw);
|
||||
if (!cfg.platform) cfg.platform = {};
|
||||
cfg.platform.user_email = email;
|
||||
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPosthogEvent(posthogHost, payload) {
|
||||
try {
|
||||
const body = JSON.stringify(payload);
|
||||
await httpsRequest(posthogHost, "POST", {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(body),
|
||||
}, body);
|
||||
} catch {
|
||||
// silently swallow
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = JSON.parse(process.argv[2]);
|
||||
const payload = ctx.payload;
|
||||
|
||||
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||
await resolveAndCacheEmail(ctx, payload);
|
||||
}
|
||||
|
||||
await sendPosthogEvent(ctx.posthogHost, payload);
|
||||
}
|
||||
|
||||
main().catch(() => {});
|
||||
@@ -44,6 +44,13 @@ describe("CLI Integration — help and version", () => {
|
||||
expect(result.stdout).toContain("search");
|
||||
});
|
||||
|
||||
it("shows version with --version", () => {
|
||||
const result = run(["--version"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("0.1.0");
|
||||
});
|
||||
|
||||
|
||||
it("help --json produces valid JSON", () => {
|
||||
const result = run(["help", "--json"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { createMockBackend } from "./setup.js";
|
||||
import type { Backend } from "../src/backend/base.js";
|
||||
import { setAgentMode } from "../src/state.js";
|
||||
|
||||
let mockBackend: Backend;
|
||||
|
||||
@@ -32,7 +31,6 @@ import { afterEach } from "vitest";
|
||||
afterEach(() => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
setAgentMode(false);
|
||||
});
|
||||
|
||||
describe("cmdAdd", () => {
|
||||
@@ -86,59 +84,6 @@ describe("cmdAdd", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdAdd deduplicates PENDING", () => {
|
||||
const DUPLICATE_PENDING = {
|
||||
results: [
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
],
|
||||
};
|
||||
|
||||
it("text shows one pending block", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
enableGraph: false,
|
||||
output: "text",
|
||||
});
|
||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||
});
|
||||
|
||||
it("json shows one pending entry", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
enableGraph: false,
|
||||
output: "json",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
|
||||
expect(pending).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("agent shows one pending entry", async () => {
|
||||
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||
setAgentMode(true);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
enableGraph: false,
|
||||
output: "agent",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
expect(data.count).toBe(1);
|
||||
expect(data.data).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdSearch", () => {
|
||||
it("searches and shows results in text mode", async () => {
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
@@ -253,6 +198,13 @@ describe("cmdDeleteAll", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdVersion", () => {
|
||||
it("shows version", async () => {
|
||||
const { cmdVersion } = await import("../src/commands/utils.js");
|
||||
cmdVersion();
|
||||
expect(output).toContain("0.1.0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdEntitiesList", () => {
|
||||
it("lists users in table mode", async () => {
|
||||
@@ -267,168 +219,3 @@ describe("cmdEntitiesList", () => {
|
||||
expect(output).toContain("alice");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdEventList", () => {
|
||||
it("lists events in table mode", async () => {
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "table" });
|
||||
expect(output).toContain("evt-abc-");
|
||||
expect(output).toContain("ADD");
|
||||
expect(output).toContain("SUCCEEDED");
|
||||
});
|
||||
|
||||
it("lists events in json mode", async () => {
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "json" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("evt-def-456-ghi-789");
|
||||
});
|
||||
|
||||
it("shows empty message when no events", async () => {
|
||||
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "table" });
|
||||
expect((output + errOutput).toLowerCase()).toContain("no events");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdEventStatus", () => {
|
||||
it("shows event details in text mode", async () => {
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("SUCCEEDED");
|
||||
});
|
||||
|
||||
it("shows event details in json mode", async () => {
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
|
||||
expect(output).toContain("evt-abc-123-def-456");
|
||||
expect(output).toContain("ADD");
|
||||
});
|
||||
});
|
||||
|
||||
describe("agent mode", () => {
|
||||
it("cmdAdd outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "test preference", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
enableGraph: false,
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("add");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.scope).toMatchObject({ user_id: "alice" });
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
|
||||
});
|
||||
|
||||
it("cmdSearch outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||
await cmdSearch(mockBackend, "preferences", {
|
||||
userId: "alice",
|
||||
topK: 10,
|
||||
threshold: 0.3,
|
||||
rerank: false,
|
||||
keyword: false,
|
||||
enableGraph: false,
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("search");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
const keys = Object.keys(parsed.data[0]);
|
||||
expect(keys).toContain("id");
|
||||
expect(keys).toContain("memory");
|
||||
expect(keys).toContain("score");
|
||||
expect(keys).toContain("created_at");
|
||||
expect(keys).toContain("categories");
|
||||
expect(keys).not.toContain("user_id");
|
||||
expect(keys).not.toContain("agent_id");
|
||||
});
|
||||
|
||||
it("cmdList outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdList } = await import("../src/commands/memory.js");
|
||||
await cmdList(mockBackend, {
|
||||
userId: "alice",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
enableGraph: false,
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("list");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||
});
|
||||
|
||||
it("cmdGet outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdGet } = await import("../src/commands/memory.js");
|
||||
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("get");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
|
||||
expect(Object.keys(parsed.data)).not.toContain("user_id");
|
||||
});
|
||||
|
||||
it("cmdUpdate outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("update");
|
||||
expect(parsed.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("cmdDelete outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("delete");
|
||||
expect(parsed.data).toBeDefined();
|
||||
});
|
||||
|
||||
it("cmdEventList outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdEventList } = await import("../src/commands/events.js");
|
||||
await cmdEventList(mockBackend, { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("event list");
|
||||
expect(Array.isArray(parsed.data)).toBe(true);
|
||||
expect(parsed.count).toBe(2);
|
||||
expect(Object.keys(parsed.data[0]).sort()).toEqual(
|
||||
["created_at", "event_type", "id", "latency", "status"],
|
||||
);
|
||||
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
|
||||
});
|
||||
|
||||
it("cmdEventStatus outputs JSON envelope", async () => {
|
||||
setAgentMode(true);
|
||||
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
|
||||
const parsed = JSON.parse(output.trim());
|
||||
expect(parsed.status).toBe("success");
|
||||
expect(parsed.command).toBe("event status");
|
||||
expect(parsed.data).toBeDefined();
|
||||
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
|
||||
expect(parsed.data.results[0]).toHaveProperty("memory");
|
||||
expect(parsed.data.results[0]).not.toHaveProperty("data");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
formatSingleMemory,
|
||||
formatAddResult,
|
||||
printResultSummary,
|
||||
sanitizeAgentData,
|
||||
} from "../src/output.js";
|
||||
|
||||
let output: string;
|
||||
@@ -99,18 +98,6 @@ describe("formatAddResult", () => {
|
||||
});
|
||||
expect(output).toContain("Queued");
|
||||
});
|
||||
|
||||
it("deduplicates PENDING entries with same event_id", () => {
|
||||
formatAddResult({
|
||||
results: [
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
{ status: "PENDING", event_id: "evt-dup" },
|
||||
],
|
||||
});
|
||||
// Should show only one PENDING block despite two entries with same event_id
|
||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
|
||||
});
|
||||
});
|
||||
|
||||
describe("printResultSummary", () => {
|
||||
@@ -126,73 +113,3 @@ describe("printResultSummary", () => {
|
||||
expect(output).not.toContain("results");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeAgentData", () => {
|
||||
it("projects add results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
|
||||
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
|
||||
});
|
||||
|
||||
it("passes through PENDING add items", () => {
|
||||
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
|
||||
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
|
||||
});
|
||||
|
||||
it("projects search results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
|
||||
expect(result[0]).not.toHaveProperty("user_id");
|
||||
expect(result[0]).toHaveProperty("score");
|
||||
});
|
||||
|
||||
it("projects list results", () => {
|
||||
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
|
||||
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||
});
|
||||
|
||||
it("projects get result", () => {
|
||||
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
|
||||
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
|
||||
expect(result).not.toHaveProperty("user_id");
|
||||
expect(result).toHaveProperty("metadata");
|
||||
});
|
||||
|
||||
it("projects update result", () => {
|
||||
const raw = { id: "abc", memory: "updated", extra: "noise" };
|
||||
const result = sanitizeAgentData("update", raw);
|
||||
expect(result).toEqual({ id: "abc", memory: "updated" });
|
||||
});
|
||||
|
||||
it("projects event list results", () => {
|
||||
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
|
||||
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
|
||||
expect(result[0]).not.toHaveProperty("updated_at");
|
||||
expect(result[0]).not.toHaveProperty("graph_status");
|
||||
});
|
||||
|
||||
it("flattens event status results", () => {
|
||||
const raw = {
|
||||
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
|
||||
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
|
||||
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
|
||||
};
|
||||
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
|
||||
const firstResult = (result.results as Record<string, unknown>[])[0];
|
||||
expect(firstResult).toHaveProperty("memory", "dark mode");
|
||||
expect(firstResult).not.toHaveProperty("data");
|
||||
});
|
||||
|
||||
it("passes through status/config/import commands unchanged", () => {
|
||||
const data = { key: "value", other: "stuff" };
|
||||
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
|
||||
expect(sanitizeAgentData(cmd, data)).toEqual(data);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles null data", () => {
|
||||
expect(sanitizeAgentData("add", null)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,42 +71,5 @@ export function createMockBackend(): Backend {
|
||||
{ name: "alice", count: 5 },
|
||||
{ name: "bob", count: 3 },
|
||||
]),
|
||||
listEvents: vi.fn().mockResolvedValue([
|
||||
{
|
||||
id: "evt-abc-123-def-456",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
graph_status: null,
|
||||
latency: 1234.5,
|
||||
created_at: "2026-04-01T10:00:00Z",
|
||||
updated_at: "2026-04-01T10:00:01Z",
|
||||
},
|
||||
{
|
||||
id: "evt-def-456-ghi-789",
|
||||
event_type: "SEARCH",
|
||||
status: "PENDING",
|
||||
graph_status: null,
|
||||
latency: null,
|
||||
created_at: "2026-04-01T10:01:00Z",
|
||||
updated_at: "2026-04-01T10:01:00Z",
|
||||
},
|
||||
]),
|
||||
getEvent: vi.fn().mockResolvedValue({
|
||||
id: "evt-abc-123-def-456",
|
||||
event_type: "ADD",
|
||||
status: "SUCCEEDED",
|
||||
graph_status: "SUCCEEDED",
|
||||
latency: 1234.5,
|
||||
created_at: "2026-04-01T10:00:00Z",
|
||||
updated_at: "2026-04-01T10:00:01Z",
|
||||
results: [
|
||||
{
|
||||
id: "mem-abc-123",
|
||||
event: "ADD",
|
||||
user_id: "alice",
|
||||
data: { memory: "User prefers dark mode" },
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
+5
-315
@@ -1,12 +1,6 @@
|
||||
# mem0 CLI (Python)
|
||||
# mem0 CLI
|
||||
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
|
||||
|
||||
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python **3.10+**
|
||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -24,326 +18,22 @@ pip install mem0-cli
|
||||
|
||||
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
|
||||
|
||||
## Quick start
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Interactive setup wizard
|
||||
# Set up your configuration
|
||||
mem0 init
|
||||
|
||||
# Or login via email
|
||||
mem0 init --email alice@company.com
|
||||
|
||||
# Or authenticate with an existing API key
|
||||
mem0 init --api-key m0-xxx
|
||||
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
# List all memories
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Get a specific memory
|
||||
mem0 get <memory-id>
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I switched to light mode"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mem0 init`
|
||||
|
||||
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
```
|
||||
|
||||
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | API key (skip prompt) |
|
||||
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||
| `--email` | Login via email verification code |
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
|
||||
```bash
|
||||
mem0 add "I prefer dark mode" --user-id alice
|
||||
mem0 add --file conversation.json --user-id alice
|
||||
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Scope to a user |
|
||||
| `--agent-id` | Scope to an agent |
|
||||
| `--messages` | Conversation messages as JSON |
|
||||
| `-f, --file` | Read messages from a JSON file |
|
||||
| `-m, --metadata` | Custom metadata as JSON |
|
||||
| `--categories` | Categories (JSON array or comma-separated) |
|
||||
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||
|
||||
### `mem0 search`
|
||||
|
||||
Search memories using natural language.
|
||||
|
||||
```bash
|
||||
mem0 search "dietary restrictions" --user-id alice
|
||||
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `-k, --top-k` | Number of results (default: 10) |
|
||||
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||
| `--rerank` | Enable reranking |
|
||||
| `--keyword` | Use keyword search instead of semantic |
|
||||
| `--filter` | Advanced filter expression (JSON) |
|
||||
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 list`
|
||||
|
||||
List memories with optional filters and pagination.
|
||||
|
||||
```bash
|
||||
mem0 list --user-id alice
|
||||
mem0 list --user-id alice --category preferences --output json
|
||||
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `--page` | Page number (default: 1) |
|
||||
| `--page-size` | Results per page (default: 100) |
|
||||
| `--category` | Filter by category |
|
||||
| `--after` | Created after date (YYYY-MM-DD) |
|
||||
| `--before` | Created before date (YYYY-MM-DD) |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 get`
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
```bash
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||
```
|
||||
|
||||
### `mem0 update`
|
||||
|
||||
Update the text or metadata of an existing memory.
|
||||
|
||||
```bash
|
||||
mem0 update <memory-id> "Updated preference text"
|
||||
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||
echo "new text" | mem0 update <memory-id>
|
||||
```
|
||||
|
||||
### `mem0 delete`
|
||||
|
||||
Delete a single memory, all memories for a scope, or an entire entity.
|
||||
|
||||
```bash
|
||||
# Delete a single memory
|
||||
mem0 delete <memory-id>
|
||||
|
||||
# Delete all memories for a user
|
||||
mem0 delete --all --user-id alice --force
|
||||
|
||||
# Delete all memories project-wide
|
||||
mem0 delete --all --project --force
|
||||
|
||||
# Preview what would be deleted
|
||||
mem0 delete --all --user-id alice --dry-run
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Delete all memories matching scope filters |
|
||||
| `--entity` | Delete the entity and all its memories |
|
||||
| `--project` | With `--all`: delete all memories project-wide |
|
||||
| `--dry-run` | Preview without deleting |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### `mem0 import`
|
||||
|
||||
Bulk import memories from a JSON file.
|
||||
|
||||
```bash
|
||||
mem0 import data.json --user-id alice
|
||||
```
|
||||
|
||||
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||
|
||||
### `mem0 config`
|
||||
|
||||
View or modify the local CLI configuration.
|
||||
|
||||
```bash
|
||||
mem0 config show # Display current config (secrets redacted)
|
||||
mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entity`
|
||||
|
||||
List or delete entities (users, agents, apps, runs).
|
||||
|
||||
```bash
|
||||
mem0 entity list users
|
||||
mem0 entity list agents --output json
|
||||
mem0 entity delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 event`
|
||||
|
||||
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||
|
||||
```bash
|
||||
# List recent events
|
||||
mem0 event list
|
||||
|
||||
# Check the status of a specific event
|
||||
mem0 event status <event-id>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-o, --output` | Output format: `text`, `json` |
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
|
||||
```bash
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
|
||||
```bash
|
||||
mem0 --agent search "user preferences" --user-id alice
|
||||
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||
mem0 --agent list --user-id alice
|
||||
mem0 --agent delete --all --user-id alice --force
|
||||
```
|
||||
|
||||
Every command returns the same envelope shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
What agent mode does differently from `--output json`:
|
||||
|
||||
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||
|
||||
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||
|
||||
## Output formats
|
||||
|
||||
Control how results are displayed with `--output`:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable with colors and formatting (default) |
|
||||
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||
| `--agent` | Alias for `--json` |
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cd cli/python
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run during development
|
||||
python -m mem0_cli --help
|
||||
mem0 add "test memory" --user-id alice
|
||||
```
|
||||
|
||||
## Releasing
|
||||
|
||||
1. Update `version` in `pyproject.toml`
|
||||
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
|
||||
|
||||
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.2"
|
||||
version = "0.1.0"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.2"
|
||||
__version__ = "0.1.0"
|
||||
|
||||
+19
-215
@@ -2,10 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json as _json
|
||||
import os
|
||||
import stat as _stat_mod
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -13,7 +10,7 @@ import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
|
||||
from mem0_cli.branding import BRAND_COLOR, print_error
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
@@ -46,52 +43,7 @@ entity_app = typer.Typer(
|
||||
no_args_is_help=True,
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
event_app = typer.Typer(
|
||||
name="event",
|
||||
help="Inspect background processing events.",
|
||||
no_args_is_help=True,
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
# entity_app and event_app registered after Memory commands to control panel ordering
|
||||
|
||||
|
||||
# ── Validated user identity (set by _get_backend_and_config) ──────────────
|
||||
|
||||
_validated_user_email: str | None = None
|
||||
|
||||
# ── Telemetry helper ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _fire_telemetry(command_name: str, extra: dict | None = None) -> None:
|
||||
"""Fire a PostHog telemetry event (non-blocking, never fails)."""
|
||||
try:
|
||||
from mem0_cli.telemetry import capture_event
|
||||
|
||||
props = {"command": command_name}
|
||||
if extra:
|
||||
props.update(extra)
|
||||
capture_event(f"cli.{command_name}", props, pre_resolved_email=_validated_user_email)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@config_app.callback(invoke_without_command=True)
|
||||
def _config_callback(ctx: typer.Context) -> None:
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(f"config.{ctx.invoked_subcommand}")
|
||||
|
||||
|
||||
@entity_app.callback(invoke_without_command=True)
|
||||
def _entity_callback(ctx: typer.Context) -> None:
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(f"entity.{ctx.invoked_subcommand}")
|
||||
|
||||
|
||||
@event_app.callback(invoke_without_command=True)
|
||||
def _event_callback(ctx: typer.Context) -> None:
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(f"event.{ctx.invoked_subcommand}")
|
||||
# entity_app registered after Memory commands to control panel ordering
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||
@@ -101,16 +53,9 @@ def _get_backend_and_config(
|
||||
api_key: str | None = None,
|
||||
base_url: str | None = None,
|
||||
):
|
||||
"""Build and return the Platform backend plus the loaded config.
|
||||
|
||||
Validates the API key upfront via ``/v1/ping/`` and caches the
|
||||
resolved user email for telemetry.
|
||||
"""
|
||||
global _validated_user_email
|
||||
|
||||
"""Build and return the Platform backend plus the loaded config."""
|
||||
from mem0_cli.backend import get_backend
|
||||
from mem0_cli.backend.platform import AuthError
|
||||
from mem0_cli.config import load_config, save_config
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
config = load_config()
|
||||
|
||||
@@ -127,29 +72,7 @@ def _get_backend_and_config(
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
backend = get_backend(config)
|
||||
|
||||
# Validate the API key upfront with a fast timeout
|
||||
try:
|
||||
ping_data = backend.ping(timeout=5.0)
|
||||
email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
|
||||
if email:
|
||||
_validated_user_email = email
|
||||
if config.platform.user_email != email:
|
||||
config.platform.user_email = email
|
||||
with contextlib.suppress(Exception):
|
||||
save_config(config)
|
||||
except AuthError:
|
||||
print_error(
|
||||
err_console,
|
||||
"Invalid or expired API key.",
|
||||
hint="Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||
)
|
||||
raise typer.Exit(1) from None
|
||||
except Exception:
|
||||
print_warning(err_console, "Could not validate API key (network issue). Proceeding anyway.")
|
||||
|
||||
return backend, config
|
||||
return get_backend(config), config
|
||||
|
||||
|
||||
def _get_backend(
|
||||
@@ -191,22 +114,9 @@ def _resolve_ids(
|
||||
}
|
||||
|
||||
|
||||
def _stdin_is_piped() -> bool:
|
||||
"""Return True only when stdin is an actual pipe or file redirect — not a bare open fd."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return False
|
||||
try:
|
||||
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _read_stdin() -> str | None:
|
||||
"""Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
|
||||
if _stdin_is_piped():
|
||||
"""Read from stdin if it is piped (not a TTY)."""
|
||||
if not sys.stdin.isatty():
|
||||
return sys.stdin.read().strip() or None
|
||||
return None
|
||||
|
||||
@@ -218,26 +128,12 @@ def _read_stdin() -> str | None:
|
||||
def main_callback(
|
||||
ctx: typer.Context,
|
||||
version: bool = typer.Option(False, "--version", help="Show version and exit."),
|
||||
json_agent: bool = typer.Option(
|
||||
False,
|
||||
"--json",
|
||||
"--agent",
|
||||
help="Output as JSON for agent/programmatic use.",
|
||||
is_eager=False,
|
||||
),
|
||||
) -> None:
|
||||
if json_agent:
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
if version:
|
||||
from mem0_cli.commands.utils import cmd_version
|
||||
|
||||
_fire_telemetry("version")
|
||||
cmd_version()
|
||||
raise typer.Exit()
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(ctx.invoked_subcommand)
|
||||
|
||||
|
||||
# ── Memory: add ───────────────────────────────────────────────────────────
|
||||
@@ -377,7 +273,7 @@ def search(
|
||||
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||
),
|
||||
) -> None:
|
||||
"""Query your memory store — semantic, keyword, or hybrid retrieval.
|
||||
"""Search memories by semantic query.
|
||||
|
||||
Examples:
|
||||
mem0 search "preferences" --user-id alice
|
||||
@@ -642,14 +538,12 @@ def delete(
|
||||
|
||||
# ── Dispatch ─────────────────────────────────────────────────────
|
||||
if memory_id is not None:
|
||||
_fire_telemetry("delete", {"delete_mode": "single"})
|
||||
from mem0_cli.commands.memory import cmd_delete
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
|
||||
|
||||
elif all_:
|
||||
_fire_telemetry("delete", {"delete_mode": "all"})
|
||||
from mem0_cli.commands.memory import cmd_delete_all
|
||||
|
||||
backend, config = _get_backend_and_config(api_key, base_url)
|
||||
@@ -657,7 +551,6 @@ def delete(
|
||||
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
|
||||
|
||||
else: # --entity
|
||||
_fire_telemetry("delete", {"delete_mode": "entity"})
|
||||
from mem0_cli.commands.entities import cmd_entities_delete
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
@@ -809,70 +702,6 @@ def entity_delete(
|
||||
app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
||||
|
||||
|
||||
# ── Event subcommands ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@event_app.command("list")
|
||||
def event_list(
|
||||
output: str = typer.Option(
|
||||
"table", "--output", "-o", help="Output: table, json.", rich_help_panel="Output"
|
||||
),
|
||||
api_key: str | None = typer.Option(
|
||||
None,
|
||||
"--api-key",
|
||||
help="Override API key.",
|
||||
envvar="MEM0_API_KEY",
|
||||
rich_help_panel="Connection",
|
||||
),
|
||||
base_url: str | None = typer.Option(
|
||||
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||
),
|
||||
) -> None:
|
||||
"""List recent background processing events.
|
||||
|
||||
Examples:
|
||||
mem0 event list
|
||||
mem0 event list -o json
|
||||
"""
|
||||
from mem0_cli.commands.events_cmd import cmd_event_list
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_event_list(backend, output=output)
|
||||
|
||||
|
||||
@event_app.command("status")
|
||||
def event_status(
|
||||
event_id: str = typer.Argument(..., help="Event ID to inspect."),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json.", rich_help_panel="Output"
|
||||
),
|
||||
api_key: str | None = typer.Option(
|
||||
None,
|
||||
"--api-key",
|
||||
help="Override API key.",
|
||||
envvar="MEM0_API_KEY",
|
||||
rich_help_panel="Connection",
|
||||
),
|
||||
base_url: str | None = typer.Option(
|
||||
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||
),
|
||||
) -> None:
|
||||
"""Check the status of a specific background event.
|
||||
|
||||
Examples:
|
||||
mem0 event status <event-id>
|
||||
mem0 event status <event-id> -o json
|
||||
"""
|
||||
from mem0_cli.commands.events_cmd import cmd_event_status
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_event_status(backend, event_id, output=output)
|
||||
|
||||
|
||||
# ── Event subgroup ──
|
||||
app.add_typer(event_app, name="event", rich_help_panel="Management")
|
||||
|
||||
|
||||
# ── Management commands ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -886,9 +715,6 @@ def init(
|
||||
code: str | None = typer.Option(
|
||||
None, "--code", help="Verification code (use with --email for non-interactive login)."
|
||||
),
|
||||
force: bool = typer.Option(
|
||||
False, "--force", help="Overwrite existing config without confirmation."
|
||||
),
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
@@ -900,7 +726,7 @@ def init(
|
||||
"""
|
||||
from mem0_cli.commands.init_cmd import run_init
|
||||
|
||||
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
||||
run_init(api_key=api_key, user_id=user_id, email=email, code=code)
|
||||
|
||||
|
||||
# (entity_app registered at module level, below sub-group definitions)
|
||||
@@ -1005,7 +831,7 @@ def _build_help_json() -> dict:
|
||||
},
|
||||
},
|
||||
"search": {
|
||||
"description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||
"description": "Search memories by semantic query.",
|
||||
"usage": "mem0 search <query> [OPTIONS]",
|
||||
"arguments": {"query": {"description": "Search query.", "required": False}},
|
||||
"options": {
|
||||
@@ -1109,24 +935,6 @@ def _build_help_json() -> dict:
|
||||
"value": {"description": "Value to set.", "required": True},
|
||||
},
|
||||
},
|
||||
"event": {
|
||||
"description": "Inspect background processing events.",
|
||||
"subcommands": {
|
||||
"list": {
|
||||
"description": "List recent background processing events.",
|
||||
"usage": "mem0 event list [OPTIONS]",
|
||||
"options": {"--output, -o": "Output format: table, json."},
|
||||
},
|
||||
"status": {
|
||||
"description": "Check the status of a specific background event.",
|
||||
"usage": "mem0 event status <event_id> [OPTIONS]",
|
||||
"arguments": {
|
||||
"event_id": {"description": "Event ID to inspect.", "required": True}
|
||||
},
|
||||
"options": {"--output, -o": "Output format: text, json."},
|
||||
},
|
||||
},
|
||||
},
|
||||
"entity": {
|
||||
"description": "Manage entities.",
|
||||
"subcommands": {
|
||||
@@ -1178,7 +986,6 @@ def _build_help_json() -> dict:
|
||||
"global_options": {
|
||||
"--api-key": "Override API key (env: MEM0_API_KEY).",
|
||||
"--base-url": "Override API base URL.",
|
||||
"--json / --agent": "Output as JSON for agent/programmatic use.",
|
||||
"--help": "Show help for a command.",
|
||||
"--version": "Show version and exit.",
|
||||
},
|
||||
@@ -1208,7 +1015,7 @@ def help(
|
||||
console.print("Usage: mem0 <command> [OPTIONS]\n")
|
||||
console.print("[bold]Commands:[/]")
|
||||
console.print(" add Add a memory from text, messages, file, or stdin")
|
||||
console.print(" search Query your memory store (semantic, keyword, hybrid)")
|
||||
console.print(" search Search memories by semantic query")
|
||||
console.print(" get Get a specific memory by ID")
|
||||
console.print(" list List memories with optional filters")
|
||||
console.print(" update Update a memory's text or metadata")
|
||||
@@ -1216,7 +1023,6 @@ def help(
|
||||
console.print(" import Import memories from a JSON file")
|
||||
console.print(" config Manage configuration (show, get, set)")
|
||||
console.print(" entity Manage entities (list, delete)")
|
||||
console.print(" event Inspect background events (list, status)")
|
||||
console.print(" init Interactive setup wizard")
|
||||
console.print(" status Check connectivity and authentication")
|
||||
console.print()
|
||||
@@ -1225,6 +1031,14 @@ def help(
|
||||
console.print()
|
||||
|
||||
|
||||
@app.command(rich_help_panel="Utility")
|
||||
def version() -> None:
|
||||
"""Show version and exit."""
|
||||
from mem0_cli.commands.utils import cmd_version
|
||||
|
||||
cmd_version()
|
||||
|
||||
|
||||
# Register config subgroup here so it appears after help in Management panel
|
||||
app.add_typer(config_app, name="config", rich_help_panel="Management")
|
||||
|
||||
@@ -1233,14 +1047,4 @@ app.add_typer(config_app, name="config", rich_help_panel="Management")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
import sys
|
||||
|
||||
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||
_json_flags = {"--json", "--agent"}
|
||||
if any(a in _json_flags for a in sys.argv[1:]):
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
||||
|
||||
app()
|
||||
|
||||
@@ -104,12 +104,6 @@ class Backend(ABC):
|
||||
@abstractmethod
|
||||
def entities(self, entity_type: str) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def list_events(self) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_event(self, event_id: str) -> dict: ...
|
||||
|
||||
|
||||
def get_backend(config: Mem0Config) -> Backend:
|
||||
"""Return the Platform backend."""
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
@@ -22,17 +21,11 @@ class PlatformBackend(Backend):
|
||||
headers={
|
||||
"Authorization": f"Token {config.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
"X-Mem0-Client-Version": __version__,
|
||||
},
|
||||
timeout=30.0,
|
||||
)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||
resp = self._client.request(method, path, **kwargs)
|
||||
if resp.status_code == 401:
|
||||
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||
@@ -272,35 +265,18 @@ class PlatformBackend(Backend):
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> dict:
|
||||
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||
type_map = {
|
||||
"user": user_id,
|
||||
"agent": agent_id,
|
||||
"app": app_id,
|
||||
"run": run_id,
|
||||
}
|
||||
entities = {t: v for t, v in type_map.items() if v}
|
||||
if not entities:
|
||||
params: dict[str, str] = {}
|
||||
if user_id:
|
||||
params["user_id"] = user_id
|
||||
if agent_id:
|
||||
params["agent_id"] = agent_id
|
||||
if app_id:
|
||||
params["app_id"] = app_id
|
||||
if run_id:
|
||||
params["run_id"] = run_id
|
||||
if not params:
|
||||
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||
# Delete each provided entity via the v2 path-based endpoint
|
||||
result: dict = {}
|
||||
for entity_type, entity_id in entities.items():
|
||||
result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
|
||||
return result
|
||||
|
||||
def ping(self, timeout: float | None = None) -> dict:
|
||||
"""Call the ping endpoint and return the raw response.
|
||||
|
||||
When *timeout* is given it overrides the client-level timeout so that
|
||||
validation pings can fail fast without blocking the user.
|
||||
"""
|
||||
if timeout is not None:
|
||||
resp = self._client.get("/v1/ping/", timeout=timeout)
|
||||
if resp.status_code == 401:
|
||||
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
return self._request("GET", "/v1/ping/")
|
||||
return self._request("DELETE", "/v1/entities/", params=params)
|
||||
|
||||
def status(
|
||||
self,
|
||||
@@ -308,9 +284,19 @@ class PlatformBackend(Backend):
|
||||
user_id: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Check connectivity using the ping endpoint."""
|
||||
"""Check connectivity by making a lightweight API call."""
|
||||
try:
|
||||
self.ping()
|
||||
# If entity IDs are available, validate with a minimal memories list
|
||||
if user_id or agent_id:
|
||||
payload: dict[str, Any] = {}
|
||||
params = {"page": "1", "page_size": "1"}
|
||||
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
|
||||
if api_filters:
|
||||
payload["filters"] = api_filters
|
||||
self._request("POST", "/v2/memories/", json=payload, params=params)
|
||||
else:
|
||||
# No entity IDs — use entities endpoint to validate API key
|
||||
self._request("GET", "/v1/entities/")
|
||||
return {"connected": True, "backend": "platform", "base_url": self.base_url}
|
||||
except Exception as e:
|
||||
return {"connected": False, "backend": "platform", "error": str(e)}
|
||||
@@ -325,13 +311,6 @@ class PlatformBackend(Backend):
|
||||
items = [e for e in items if e.get("type", "").lower() == target_type]
|
||||
return items
|
||||
|
||||
def list_events(self) -> list[dict]:
|
||||
result = self._request("GET", "/v1/events/")
|
||||
return result if isinstance(result, list) else result.get("results", [])
|
||||
|
||||
def get_event(self, event_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/event/{event_id}/")
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
pass
|
||||
|
||||
@@ -43,10 +43,6 @@ def _sym(fancy: str, plain: str) -> str:
|
||||
|
||||
def print_banner(console: Console) -> None:
|
||||
"""Print the mem0 welcome banner."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
||||
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
||||
|
||||
@@ -65,28 +61,11 @@ def print_banner(console: Console) -> None:
|
||||
|
||||
|
||||
def print_success(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("✓", "[ok]")
|
||||
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
||||
|
||||
|
||||
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
||||
from mem0_cli.state import get_current_command, is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
import json as _json
|
||||
|
||||
envelope = {
|
||||
"status": "error",
|
||||
"command": get_current_command(),
|
||||
"error": message,
|
||||
"data": None,
|
||||
}
|
||||
print(_json.dumps(envelope))
|
||||
return
|
||||
sym = _sym("✗", "[error]")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||
if hint:
|
||||
@@ -94,19 +73,11 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
||||
|
||||
|
||||
def print_warning(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("⚠", "[warn]")
|
||||
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
||||
|
||||
|
||||
def print_info(console: Console, message: str) -> None:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
sym = _sym("◆", "*")
|
||||
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
||||
|
||||
@@ -118,9 +89,7 @@ def timed_status(console: Console, message: str):
|
||||
The spinner and timing output are sent to stderr (via ``_err``) so they
|
||||
never contaminate machine-readable stdout. The *console* parameter is
|
||||
kept for backward compatibility but is not used for spinner output.
|
||||
In agent mode the spinner is suppressed entirely.
|
||||
"""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
class _Ctx:
|
||||
def __init__(self):
|
||||
@@ -128,13 +97,6 @@ def timed_status(console: Console, message: str):
|
||||
self.error_msg = ""
|
||||
|
||||
ctx = _Ctx()
|
||||
if is_agent_mode():
|
||||
try:
|
||||
yield ctx
|
||||
except Exception:
|
||||
raise
|
||||
return
|
||||
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
||||
@@ -143,11 +105,6 @@ def timed_status(console: Console, message: str):
|
||||
elapsed = time.perf_counter() - start
|
||||
if ctx.error_msg:
|
||||
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
||||
if "Authentication failed" in ctx.error_msg:
|
||||
_err.print(
|
||||
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
||||
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||
)
|
||||
raise
|
||||
else:
|
||||
elapsed = time.perf_counter() - start
|
||||
@@ -157,14 +114,11 @@ def timed_status(console: Console, message: str):
|
||||
|
||||
def print_scope(console: Console, **ids: str | None) -> None:
|
||||
"""Show active entity scope if any IDs are set."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return
|
||||
parts = []
|
||||
for key, val in ids.items():
|
||||
if val:
|
||||
parts.append(f"{key}={val}")
|
||||
label = key.replace("_", " ").replace("id", "ID").strip()
|
||||
parts.append(f"{label}={val}")
|
||||
if parts:
|
||||
scope_str = ", ".join(parts)
|
||||
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
||||
|
||||
@@ -20,17 +20,12 @@ err_console = Console(stderr=True)
|
||||
|
||||
def cmd_config_show(*, output: str = "text") -> None:
|
||||
"""Display current configuration (secrets redacted)."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config show")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
config = load_config()
|
||||
|
||||
if output in ("json", "agent"):
|
||||
format_agent_envelope(
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="config show",
|
||||
data={
|
||||
@@ -89,44 +84,25 @@ def cmd_config_show(*, output: str = "text") -> None:
|
||||
|
||||
def cmd_config_get(key: str) -> None:
|
||||
"""Get a config value."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config get")
|
||||
config = load_config()
|
||||
value = get_nested_value(config, key)
|
||||
|
||||
if value is None:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
return
|
||||
|
||||
display_value = (
|
||||
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
|
||||
)
|
||||
|
||||
if is_agent_mode():
|
||||
format_agent_envelope(
|
||||
console, command="config get", data={"key": key, "value": display_value}
|
||||
)
|
||||
else:
|
||||
console.print(display_value)
|
||||
# Redact secrets
|
||||
if "api_key" in key or "key" in key.split(".")[-1:]:
|
||||
console.print(redact_key(str(value)))
|
||||
else:
|
||||
console.print(str(value))
|
||||
|
||||
|
||||
def cmd_config_set(key: str, value: str) -> None:
|
||||
"""Set a config value."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("config set")
|
||||
config = load_config()
|
||||
if set_nested_value(config, key, value):
|
||||
save_config(config)
|
||||
display = redact_key(value) if "key" in key else value
|
||||
if is_agent_mode():
|
||||
format_agent_envelope(
|
||||
console, command="config set", data={"key": key, "value": display}
|
||||
)
|
||||
else:
|
||||
print_success(console, f"{key} = {display}")
|
||||
print_success(console, f"{key} = {display}")
|
||||
else:
|
||||
print_error(err_console, f"Unknown config key: {key}")
|
||||
|
||||
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
|
||||
print_success,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import format_agent_envelope, format_json
|
||||
from mem0_cli.output import format_json
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
@@ -26,11 +26,6 @@ err_console = Console(stderr=True)
|
||||
|
||||
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
||||
"""List entities of a given type."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("entity list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
valid_types = {"users", "agents", "apps", "runs"}
|
||||
if entity_type not in valid_types:
|
||||
print_error(
|
||||
@@ -47,16 +42,6 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="entity list",
|
||||
data=results,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
return
|
||||
@@ -92,39 +77,41 @@ def cmd_entities_delete(
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete an entity and all its memories (cascade delete)."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("entity delete")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if not force:
|
||||
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||
raise typer.Exit(1)
|
||||
if not any([user_id, agent_id, app_id, run_id]):
|
||||
print_error(
|
||||
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope_str = ", ".join(scope_parts)
|
||||
|
||||
if dry_run:
|
||||
print_info(console, f"Would delete entity {scope_str} and all its memories.")
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts)
|
||||
print_info(console, f"Would delete entity {scope} and all its memories.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
scope_parts = []
|
||||
if user_id:
|
||||
scope_parts.append(f"user={user_id}")
|
||||
if agent_id:
|
||||
scope_parts.append(f"agent={agent_id}")
|
||||
if app_id:
|
||||
scope_parts.append(f"app={app_id}")
|
||||
if run_id:
|
||||
scope_parts.append(f"run={run_id}")
|
||||
scope = ", ".join(scope_parts)
|
||||
|
||||
confirm = typer.confirm(
|
||||
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
|
||||
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
|
||||
)
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled.")
|
||||
@@ -144,25 +131,7 @@ def cmd_entities_delete(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="entity delete",
|
||||
data={"deleted": True},
|
||||
scope=scope or None,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"""Event commands: list and status."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.branding import (
|
||||
ACCENT_COLOR,
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
ERROR_COLOR,
|
||||
SUCCESS_COLOR,
|
||||
WARNING_COLOR,
|
||||
print_info,
|
||||
timed_status,
|
||||
)
|
||||
from mem0_cli.output import format_agent_envelope, format_json
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_STATUS_STYLE = {
|
||||
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
|
||||
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
|
||||
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
|
||||
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
|
||||
}
|
||||
|
||||
|
||||
def _status_styled(status: str) -> str:
|
||||
return _STATUS_STYLE.get(status.upper(), status)
|
||||
|
||||
|
||||
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
|
||||
"""List recent background events."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("event list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
import time as _time
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Fetching events...") as _ts:
|
||||
try:
|
||||
results = backend.list_events()
|
||||
except Exception as e:
|
||||
_ts.error_msg = str(e)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="event list",
|
||||
data=results,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
return
|
||||
|
||||
if not results:
|
||||
console.print()
|
||||
print_info(console, "No events found.")
|
||||
console.print()
|
||||
return
|
||||
|
||||
table = Table(
|
||||
border_style=BRAND_COLOR,
|
||||
header_style=f"bold {ACCENT_COLOR}",
|
||||
row_styles=["", "dim"],
|
||||
padding=(0, 1),
|
||||
)
|
||||
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
|
||||
table.add_column("Type", max_width=14)
|
||||
table.add_column("Status", max_width=12)
|
||||
table.add_column("Latency", max_width=10, justify="right")
|
||||
table.add_column("Created", max_width=20)
|
||||
|
||||
for ev in results:
|
||||
ev_id = str(ev.get("id", ""))[:8]
|
||||
ev_type = str(ev.get("event_type", "—"))
|
||||
status = str(ev.get("status", "—"))
|
||||
latency = ev.get("latency")
|
||||
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
|
||||
|
||||
console.print()
|
||||
console.print(table)
|
||||
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
|
||||
console.print()
|
||||
|
||||
|
||||
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
|
||||
"""Get the status of a specific background event."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("event status")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
import time as _time
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Fetching event...") as _ts:
|
||||
try:
|
||||
ev = backend.get_event(event_id)
|
||||
except Exception as e:
|
||||
_ts.error_msg = str(e)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="event status",
|
||||
data=ev,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, ev)
|
||||
return
|
||||
|
||||
status = str(ev.get("status", "—"))
|
||||
ev_type = str(ev.get("event_type", "—"))
|
||||
latency = ev.get("latency")
|
||||
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
|
||||
results = ev.get("results")
|
||||
|
||||
lines = []
|
||||
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
|
||||
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
|
||||
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
|
||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
|
||||
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||
|
||||
if results:
|
||||
lines.append("")
|
||||
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
|
||||
for r in results:
|
||||
mem_id = str(r.get("id", ""))[:8]
|
||||
data = r.get("data", {})
|
||||
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
|
||||
ev_name = str(r.get("event", ""))
|
||||
user = str(r.get("user_id", ""))
|
||||
detail = f"{ev_name} {memory}"
|
||||
if user:
|
||||
detail += f" [{DIM_COLOR}](user_id={user})[/]"
|
||||
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
|
||||
|
||||
content = "\n".join(lines)
|
||||
panel = Panel(
|
||||
content,
|
||||
title=f"[{BRAND_COLOR}]Event Status[/]",
|
||||
title_align="left",
|
||||
border_style=BRAND_COLOR,
|
||||
padding=(1, 1),
|
||||
)
|
||||
console.print()
|
||||
console.print(panel)
|
||||
console.print()
|
||||
@@ -19,7 +19,7 @@ from mem0_cli.branding import (
|
||||
print_info,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
|
||||
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
@@ -108,10 +108,6 @@ def _email_login(
|
||||
The caller expects at minimum an ``api_key`` field.
|
||||
"""
|
||||
url = base_url.rstrip("/")
|
||||
_source_headers = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
# If code is already provided, skip sending — user already has a code
|
||||
@@ -120,7 +116,6 @@ def _email_login(
|
||||
resp = client.post(
|
||||
f"{url}/api/v1/auth/email_code/",
|
||||
json={"email": email},
|
||||
headers=_source_headers,
|
||||
)
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
@@ -153,7 +148,6 @@ def _email_login(
|
||||
resp = client.post(
|
||||
f"{url}/api/v1/auth/email_code/verify/",
|
||||
json={"email": email, "code": code.strip()},
|
||||
headers=_source_headers,
|
||||
)
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
@@ -175,7 +169,6 @@ def run_init(
|
||||
user_id: str | None = None,
|
||||
email: str | None = None,
|
||||
code: str | None = None,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
@@ -191,29 +184,6 @@ def run_init(
|
||||
print_error(err_console, "--code requires --email.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Warn if an existing config with an API key would be overwritten
|
||||
if not force and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
if existing.platform.api_key:
|
||||
from mem0_cli.config import redact_key
|
||||
|
||||
console.print(
|
||||
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
|
||||
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
|
||||
)
|
||||
if sys.stdin.isatty():
|
||||
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
|
||||
if not confirm:
|
||||
print_info(console, "Cancelled. Use --force to skip this check.")
|
||||
raise typer.Exit(0)
|
||||
else:
|
||||
print_error(
|
||||
err_console,
|
||||
"Existing config would be overwritten.",
|
||||
hint="Use --force to overwrite.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# ── Email login flow ──────────────────────────────────────────────
|
||||
if email:
|
||||
if api_key:
|
||||
@@ -235,10 +205,7 @@ def run_init(
|
||||
raise typer.Exit(1)
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
config.defaults.user_id = user_id or "mem0-cli"
|
||||
|
||||
save_config(config)
|
||||
|
||||
@@ -253,17 +220,6 @@ def run_init(
|
||||
|
||||
# ── API key flow (existing) ───────────────────────────────────────
|
||||
|
||||
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if not sys.stdin.isatty():
|
||||
if not api_key:
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
|
||||
# Fully non-interactive when both flags provided
|
||||
if api_key and user_id:
|
||||
config.platform.api_key = api_key
|
||||
@@ -273,6 +229,15 @@ def run_init(
|
||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||
return
|
||||
|
||||
# Non-TTY without full flags -> error
|
||||
if not sys.stdin.isatty() and (not api_key or not user_id):
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and required flags missing.",
|
||||
hint="Run: mem0 init --api-key <key> --user-id <id>",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_banner(console)
|
||||
console.print()
|
||||
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||
@@ -306,10 +271,7 @@ def run_init(
|
||||
raise typer.Exit(1)
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email_addr
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
config.defaults.user_id = user_id or "mem0-cli"
|
||||
|
||||
save_config(config)
|
||||
|
||||
@@ -369,10 +331,9 @@ def _setup_defaults(config: Mem0Config) -> None:
|
||||
console.print()
|
||||
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
||||
|
||||
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
user_id = Prompt.ask(
|
||||
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||
default=_default_user,
|
||||
default="mem0-cli",
|
||||
)
|
||||
if user_id:
|
||||
config.defaults.user_id = user_id
|
||||
@@ -392,19 +353,11 @@ def _validate_platform(config: Mem0Config) -> None:
|
||||
)
|
||||
if status.get("connected"):
|
||||
print_success(console, "Connected to mem0 Platform!")
|
||||
# Cache user_email from ping response for telemetry distinct_id
|
||||
try:
|
||||
ping_data = backend.ping()
|
||||
user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
|
||||
if user_email:
|
||||
config.platform.user_email = user_email
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Could not connect: {status.get('error', 'Unknown error')}",
|
||||
hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
|
||||
hint="Check your API key and try again.",
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, f"Connection test failed: {e}")
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import stat as _stat_mod
|
||||
import sys
|
||||
import time as _time
|
||||
from pathlib import Path
|
||||
@@ -22,7 +20,6 @@ from mem0_cli.branding import (
|
||||
)
|
||||
from mem0_cli.output import (
|
||||
format_add_result,
|
||||
format_agent_envelope,
|
||||
format_json,
|
||||
format_memories_table,
|
||||
format_memories_text,
|
||||
@@ -34,19 +31,6 @@ console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _stdin_is_piped() -> bool:
|
||||
"""Return True only when stdin is an actual pipe or file redirect."""
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if is_agent_mode():
|
||||
return False
|
||||
try:
|
||||
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def cmd_add(
|
||||
backend: Backend,
|
||||
text: str | None,
|
||||
@@ -66,11 +50,6 @@ def cmd_add(
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Add a memory."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("add")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
msgs = None
|
||||
content = text
|
||||
|
||||
@@ -91,8 +70,8 @@ def cmd_add(
|
||||
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Read from stdin only if stdin is an actual pipe or file redirect
|
||||
elif not content and _stdin_is_piped():
|
||||
# Read from stdin if no text and stdin is piped
|
||||
elif not content and not sys.stdin.isatty():
|
||||
content = sys.stdin.read().strip()
|
||||
|
||||
if not content and not msgs:
|
||||
@@ -154,61 +133,18 @@ def cmd_add(
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
# Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||
results_list = result if isinstance(result, list) else result.get("results", [result])
|
||||
seen_events: set[str] = set()
|
||||
deduped: list[dict] = []
|
||||
for r in results_list:
|
||||
if r.get("status") == "PENDING":
|
||||
eid = r.get("event_id", "")
|
||||
if eid and eid in seen_events:
|
||||
continue
|
||||
if eid:
|
||||
seen_events.add(eid)
|
||||
deduped.append(r)
|
||||
# Write back so downstream formatters see deduplicated data
|
||||
if isinstance(result, dict) and "results" in result:
|
||||
result = {**result, "results": deduped}
|
||||
else:
|
||||
result = deduped
|
||||
|
||||
if output == "agent":
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="add",
|
||||
data=deduped,
|
||||
scope=scope or None,
|
||||
count=len(deduped),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_add_result(console, result, output)
|
||||
return
|
||||
|
||||
console.print()
|
||||
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||
count = len(deduped)
|
||||
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
|
||||
if all_pending:
|
||||
print_success(
|
||||
console,
|
||||
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
|
||||
)
|
||||
else:
|
||||
print_success(
|
||||
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||
)
|
||||
# Count results
|
||||
results = result if isinstance(result, list) else result.get("results", [result])
|
||||
count = len(results) if results else 0
|
||||
print_success(
|
||||
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||
)
|
||||
format_add_result(console, result, output)
|
||||
|
||||
|
||||
@@ -230,11 +166,6 @@ def cmd_search(
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Search memories."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("search")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
filters = None
|
||||
if filter_json:
|
||||
try:
|
||||
@@ -279,27 +210,6 @@ def cmd_search(
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output == "agent":
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="search",
|
||||
data=results,
|
||||
scope=scope or None,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
if output == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
@@ -326,11 +236,6 @@ def cmd_search(
|
||||
|
||||
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||
"""Get a specific memory by ID."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("get")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||
try:
|
||||
result = backend.get(memory_id)
|
||||
@@ -338,10 +243,7 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(console, command="get", data=result)
|
||||
else:
|
||||
format_single_memory(console, result, output)
|
||||
format_single_memory(console, result, output)
|
||||
|
||||
|
||||
def cmd_list(
|
||||
@@ -360,11 +262,6 @@ def cmd_list(
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("list")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if page_size < 1:
|
||||
print_error(err_console, "--page-size must be >= 1.")
|
||||
raise typer.Exit(1)
|
||||
@@ -395,24 +292,15 @@ def cmd_list(
|
||||
if output == "quiet":
|
||||
return
|
||||
|
||||
if output in ("json", "agent"):
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
format_agent_envelope(
|
||||
if output == "json":
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="list",
|
||||
data=results,
|
||||
scope=scope or None,
|
||||
count=len(results),
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
|
||||
)
|
||||
elif output == "table":
|
||||
if results:
|
||||
@@ -455,11 +343,6 @@ def cmd_update(
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Update a memory."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("update")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
meta = None
|
||||
if metadata:
|
||||
try:
|
||||
@@ -477,14 +360,7 @@ def cmd_update(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="update",
|
||||
data=result,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||
@@ -499,11 +375,6 @@ def cmd_delete(
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete a single memory by ID."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("delete")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if dry_run:
|
||||
# Fetch and display what would be deleted
|
||||
try:
|
||||
@@ -524,14 +395,7 @@ def cmd_delete(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete",
|
||||
data={"id": memory_id, "deleted": True},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||
@@ -550,17 +414,13 @@ def cmd_delete_all(
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete all memories matching a scope."""
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("delete-all")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
if not force:
|
||||
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||
raise typer.Exit(1)
|
||||
if all_:
|
||||
# Project-wide wipe using wildcard entity IDs
|
||||
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||
if dry_run:
|
||||
print_info(console, "Would delete ALL memories project-wide.")
|
||||
print_info(console, "Run without --dry-run to see the actual count.")
|
||||
print_info(console, "No changes made (dry run).")
|
||||
return
|
||||
|
||||
if not force:
|
||||
confirm = typer.confirm(
|
||||
@@ -585,14 +445,7 @@ def cmd_delete_all(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete-all",
|
||||
data={"deleted": True, "scope": "project"},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
@@ -650,25 +503,7 @@ def cmd_delete_all(
|
||||
raise typer.Exit(1) from None
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
scope = {
|
||||
k: v
|
||||
for k, v in {
|
||||
"user_id": user_id,
|
||||
"agent_id": agent_id,
|
||||
"app_id": app_id,
|
||||
"run_id": run_id,
|
||||
}.items()
|
||||
if v
|
||||
}
|
||||
if output == "agent":
|
||||
format_agent_envelope(
|
||||
console,
|
||||
command="delete-all",
|
||||
data={"deleted": True},
|
||||
scope=scope or None,
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
elif output == "json":
|
||||
if output == "json":
|
||||
format_json(console, result)
|
||||
elif output != "quiet":
|
||||
if isinstance(result, dict) and "message" in result:
|
||||
|
||||
@@ -35,26 +35,22 @@ def cmd_status(
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Check connectivity and auth."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("status")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Checking connection...") as _ts:
|
||||
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output in ("json", "agent"):
|
||||
format_agent_envelope(
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="status",
|
||||
data={
|
||||
"connected": result.get("connected", False),
|
||||
"backend": result.get("backend", "?"),
|
||||
"base_url": result.get("base_url", ""),
|
||||
"latency_ms": int(_elapsed * 1000),
|
||||
},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
@@ -71,14 +67,6 @@ def cmd_status(
|
||||
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
||||
if result.get("error"):
|
||||
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
||||
if "Authentication failed" in str(result["error"]):
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
|
||||
)
|
||||
lines.append(
|
||||
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||
)
|
||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||
|
||||
content = "\n".join(lines)
|
||||
@@ -108,12 +96,7 @@ def cmd_import(
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Import memories from a JSON file."""
|
||||
from mem0_cli.output import format_agent_envelope
|
||||
from mem0_cli.state import is_agent_mode, set_current_command
|
||||
|
||||
set_current_command("import")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
from mem0_cli.output import format_json_envelope
|
||||
|
||||
try:
|
||||
data = json.loads(Path(file_path).read_text())
|
||||
@@ -146,13 +129,11 @@ def cmd_import(
|
||||
failed += 1
|
||||
_elapsed = _time.perf_counter() - _start
|
||||
|
||||
if output in ("json", "agent"):
|
||||
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
|
||||
format_agent_envelope(
|
||||
if output == "json":
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="import",
|
||||
data={"added": added, "failed": failed},
|
||||
scope=scope or None,
|
||||
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
|
||||
duration_ms=int(_elapsed * 1000),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -27,7 +27,6 @@ CONFIG_VERSION = 1
|
||||
class PlatformConfig:
|
||||
api_key: str = ""
|
||||
base_url: str = DEFAULT_BASE_URL
|
||||
user_email: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -49,7 +48,6 @@ class Mem0Config:
|
||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||
"api_key": "platform.api_key",
|
||||
"base_url": "platform.base_url",
|
||||
"user_email": "platform.user_email",
|
||||
"user_id": "defaults.user_id",
|
||||
"agent_id": "defaults.agent_id",
|
||||
"app_id": "defaults.app_id",
|
||||
@@ -78,7 +76,6 @@ def load_config() -> Mem0Config:
|
||||
plat = data.get("platform", {})
|
||||
config.platform.api_key = plat.get("api_key", "")
|
||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||
config.platform.user_email = plat.get("user_email", "")
|
||||
|
||||
defaults = data.get("defaults", {})
|
||||
config.defaults.user_id = defaults.get("user_id", "")
|
||||
@@ -135,7 +132,6 @@ def save_config(config: Mem0Config) -> None:
|
||||
"platform": {
|
||||
"api_key": config.platform.api_key,
|
||||
"base_url": config.platform.base_url,
|
||||
"user_email": config.platform.user_email,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -155,23 +155,16 @@ def format_add_result(console: Console, result: dict | list, output: str = "text
|
||||
return
|
||||
|
||||
console.print()
|
||||
seen_pending_events: set[str] = set()
|
||||
for r in results:
|
||||
# Detect async PENDING response from Platform API
|
||||
if r.get("status") == "PENDING":
|
||||
event_id = r.get("event_id", "")
|
||||
# Deduplicate PENDING entries with the same event_id
|
||||
if event_id and event_id in seen_pending_events:
|
||||
continue
|
||||
if event_id:
|
||||
seen_pending_events.add(event_id)
|
||||
event_id = r.get("event_id", "")[:8]
|
||||
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
||||
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
||||
parts.append("[white]Processing in background[/]")
|
||||
console.print(" ".join(parts))
|
||||
if event_id:
|
||||
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
|
||||
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
|
||||
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
|
||||
console.print(" ".join(parts))
|
||||
continue
|
||||
|
||||
event = r.get("event", "ADD")
|
||||
@@ -232,100 +225,6 @@ def format_json_envelope(
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||
"""Project API response data to minimal relevant fields for agent consumption."""
|
||||
|
||||
def pick(obj: dict, keys: list) -> dict:
|
||||
return {k: obj[k] for k in keys if k in obj}
|
||||
|
||||
if data is None:
|
||||
return data
|
||||
|
||||
if command == "add":
|
||||
items = data if isinstance(data, list) else [data]
|
||||
result = []
|
||||
for item in items:
|
||||
if item.get("status") == "PENDING":
|
||||
result.append(pick(item, ["status", "event_id"]))
|
||||
else:
|
||||
result.append(pick(item, ["id", "memory", "event"]))
|
||||
return result
|
||||
|
||||
if command == "search":
|
||||
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||
|
||||
if command == "list":
|
||||
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||
|
||||
if command == "get":
|
||||
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||
|
||||
if command == "update":
|
||||
return pick(data, ["id", "memory"])
|
||||
|
||||
if command in ("delete", "delete-all", "entity delete"):
|
||||
return data
|
||||
|
||||
if command == "entity list":
|
||||
result = []
|
||||
for r in data:
|
||||
item = pick(r, ["type", "count"])
|
||||
item["name"] = r.get("name") or r.get("id", "")
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
if command == "event list":
|
||||
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
|
||||
|
||||
if command == "event status":
|
||||
ev = data
|
||||
raw_results = ev.get("results") or []
|
||||
sanitized_results = []
|
||||
for r in raw_results:
|
||||
nested = r.get("data") or {}
|
||||
memory = nested.get("memory") if isinstance(nested, dict) else None
|
||||
sanitized_results.append(
|
||||
{
|
||||
"id": r.get("id"),
|
||||
"event": r.get("event"),
|
||||
"user_id": r.get("user_id"),
|
||||
"memory": memory,
|
||||
}
|
||||
)
|
||||
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
|
||||
result["results"] = sanitized_results
|
||||
return result
|
||||
|
||||
# Pass-through: status, import, config show/get/set
|
||||
return data
|
||||
|
||||
|
||||
def format_agent_envelope(
|
||||
console: Console,
|
||||
*,
|
||||
command: str,
|
||||
data: Any,
|
||||
duration_ms: int | None = None,
|
||||
scope: dict | None = None,
|
||||
count: int | None = None,
|
||||
) -> None:
|
||||
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
|
||||
envelope: dict[str, Any] = {
|
||||
"status": "success",
|
||||
"command": command,
|
||||
}
|
||||
if duration_ms is not None:
|
||||
envelope["duration_ms"] = duration_ms
|
||||
if scope:
|
||||
filtered = {k: v for k, v in scope.items() if v}
|
||||
if filtered:
|
||||
envelope["scope"] = filtered
|
||||
if count is not None:
|
||||
envelope["count"] = count
|
||||
envelope["data"] = sanitize_agent_data(command, data)
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
def print_result_summary(
|
||||
console: Console,
|
||||
count: int,
|
||||
@@ -338,7 +237,7 @@ def print_result_summary(
|
||||
parts = [f"{count} result{'s' if count != 1 else ''}"]
|
||||
if page is not None:
|
||||
parts.append(f"page {page}")
|
||||
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
|
||||
scope_parts = [f"{k.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
|
||||
if scope_parts:
|
||||
parts.append(", ".join(scope_parts))
|
||||
if duration_secs is not None:
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Agent mode state — set by the root callback, read by commands and branding."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
_agent_mode: bool = False
|
||||
_current_command: str = ""
|
||||
|
||||
|
||||
def is_agent_mode() -> bool:
|
||||
return _agent_mode
|
||||
|
||||
|
||||
def set_agent_mode(val: bool) -> None:
|
||||
global _agent_mode
|
||||
_agent_mode = val
|
||||
|
||||
|
||||
def get_current_command() -> str:
|
||||
return _current_command
|
||||
|
||||
|
||||
def set_current_command(name: str) -> None:
|
||||
global _current_command
|
||||
_current_command = name
|
||||
@@ -1,105 +0,0 @@
|
||||
"""CLI telemetry — anonymous usage tracking via PostHog.
|
||||
|
||||
Sends fire-and-forget events to PostHog by spawning a detached subprocess
|
||||
(telemetry_sender.py). The parent CLI process exits immediately; the
|
||||
subprocess handles email resolution, caching, and the HTTP POST.
|
||||
|
||||
Disable with: MEM0_TELEMETRY=false
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
|
||||
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
|
||||
|
||||
|
||||
def _is_telemetry_enabled() -> bool:
|
||||
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
|
||||
return val not in ("false", "0", "no")
|
||||
|
||||
|
||||
def _get_distinct_id() -> str:
|
||||
"""Return a stable anonymous identifier for the current user.
|
||||
|
||||
Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
|
||||
Matches the SDK pattern in mem0/client/main.py.
|
||||
"""
|
||||
try:
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
config = load_config()
|
||||
if config.platform.user_email:
|
||||
return config.platform.user_email
|
||||
if config.platform.api_key:
|
||||
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
|
||||
except Exception:
|
||||
pass
|
||||
return "anonymous-cli"
|
||||
|
||||
|
||||
def capture_event(
|
||||
event_name: str,
|
||||
properties: dict[str, Any] | None = None,
|
||||
pre_resolved_email: str | None = None,
|
||||
) -> None:
|
||||
"""Fire a PostHog event via a detached subprocess (non-blocking).
|
||||
|
||||
When *pre_resolved_email* is provided (e.g. from an upfront ping
|
||||
validation), it is used directly as the PostHog distinct ID and the
|
||||
subprocess skips its own ``/v1/ping/`` call.
|
||||
"""
|
||||
if not _is_telemetry_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
from mem0_cli import __version__
|
||||
from mem0_cli.config import CONFIG_FILE, load_config
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
config = load_config()
|
||||
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||
|
||||
payload = {
|
||||
"api_key": POSTHOG_API_KEY,
|
||||
"distinct_id": distinct_id,
|
||||
"event": event_name,
|
||||
"properties": {
|
||||
"source": "CLI",
|
||||
"language": "python",
|
||||
"cli_version": __version__,
|
||||
"agent_mode": is_agent_mode(),
|
||||
"python_version": sys.version,
|
||||
"os": sys.platform,
|
||||
"os_version": platform.version(),
|
||||
"$process_person_profile": False,
|
||||
"$lib": "posthog-python",
|
||||
**(properties or {}),
|
||||
},
|
||||
}
|
||||
|
||||
context = {
|
||||
"payload": payload,
|
||||
"posthog_host": POSTHOG_HOST,
|
||||
"needs_email": not distinct_id or "@" not in distinct_id,
|
||||
"mem0_api_key": config.platform.api_key or "",
|
||||
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
|
||||
"config_path": str(CONFIG_FILE),
|
||||
}
|
||||
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,86 +0,0 @@
|
||||
"""Standalone telemetry sender — runs as a detached subprocess.
|
||||
|
||||
Usage: python -m mem0_cli.telemetry_sender '<json context>'
|
||||
|
||||
This module is spawned by telemetry.capture_event() and runs independently
|
||||
of the parent CLI process. It:
|
||||
|
||||
1. Resolves the user's email via /v1/ping/ if not already cached
|
||||
2. Caches the email in ~/.mem0/config.json for future runs
|
||||
3. Sends the PostHog event
|
||||
|
||||
All errors are silently swallowed — this process must never produce output
|
||||
or affect the user experience.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ctx = json.loads(sys.argv[1])
|
||||
payload = ctx["payload"]
|
||||
|
||||
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
||||
_resolve_and_cache_email(ctx, payload)
|
||||
|
||||
_send_posthog_event(ctx["posthog_host"], payload)
|
||||
|
||||
|
||||
def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
|
||||
"""Call /v1/ping/ to get the user's email, update the payload, and cache it."""
|
||||
try:
|
||||
ping_url = ctx["mem0_base_url"].rstrip("/") + "/v1/ping/"
|
||||
req = urllib.request.Request(
|
||||
ping_url,
|
||||
headers={
|
||||
"Authorization": "Token " + ctx["mem0_api_key"],
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(resp.read())
|
||||
email = data.get("user_email")
|
||||
if email:
|
||||
payload["distinct_id"] = email
|
||||
_cache_email(ctx.get("config_path"), email)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _cache_email(config_path: str | None, email: str) -> None:
|
||||
"""Write user_email into the config file for future runs."""
|
||||
if not config_path:
|
||||
return
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
cfg = json.load(f)
|
||||
cfg.setdefault("platform", {})["user_email"] = email
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _send_posthog_event(posthog_host: str, payload: dict) -> None:
|
||||
"""POST the event to PostHog."""
|
||||
try:
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
posthog_host,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
main()
|
||||
@@ -96,43 +96,6 @@ def mock_backend():
|
||||
{"name": "alice", "count": 5},
|
||||
{"name": "bob", "count": 3},
|
||||
]
|
||||
backend.list_events.return_value = [
|
||||
{
|
||||
"id": "evt-abc-123-def-456",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": None,
|
||||
"latency": 1234.5,
|
||||
"created_at": "2026-04-01T10:00:00Z",
|
||||
"updated_at": "2026-04-01T10:00:01Z",
|
||||
},
|
||||
{
|
||||
"id": "evt-def-456-ghi-789",
|
||||
"event_type": "SEARCH",
|
||||
"status": "PENDING",
|
||||
"graph_status": None,
|
||||
"latency": None,
|
||||
"created_at": "2026-04-01T10:01:00Z",
|
||||
"updated_at": "2026-04-01T10:01:00Z",
|
||||
},
|
||||
]
|
||||
backend.get_event.return_value = {
|
||||
"id": "evt-abc-123-def-456",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": "SUCCEEDED",
|
||||
"latency": 1234.5,
|
||||
"created_at": "2026-04-01T10:00:00Z",
|
||||
"updated_at": "2026-04-01T10:00:01Z",
|
||||
"results": [
|
||||
{
|
||||
"id": "mem-abc-123",
|
||||
"event": "ADD",
|
||||
"user_id": "alice",
|
||||
"data": {"memory": "User prefers dark mode"},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
return backend
|
||||
|
||||
|
||||
@@ -83,6 +83,11 @@ class TestCLIIntegration:
|
||||
assert "add" in result.stdout
|
||||
assert "search" in result.stdout
|
||||
|
||||
def test_version_flag(self):
|
||||
result = _run(["--version"])
|
||||
assert result.returncode == 0
|
||||
assert "0.1.0" in result.stdout
|
||||
|
||||
def test_add_help(self):
|
||||
result = _run(["add", "--help"])
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import typing
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -17,7 +16,6 @@ from mem0_cli.commands.config_cmd import (
|
||||
cmd_config_show,
|
||||
)
|
||||
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list
|
||||
from mem0_cli.commands.events_cmd import cmd_event_list, cmd_event_status
|
||||
from mem0_cli.commands.memory import (
|
||||
cmd_add,
|
||||
cmd_delete,
|
||||
@@ -30,6 +28,7 @@ from mem0_cli.commands.memory import (
|
||||
from mem0_cli.commands.utils import (
|
||||
cmd_import,
|
||||
cmd_status,
|
||||
cmd_version,
|
||||
)
|
||||
|
||||
|
||||
@@ -177,28 +176,30 @@ class TestAddCommand:
|
||||
def test_add_no_content_exits(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
# Patch stdin.isatty to return True so it doesn't try to read stdin
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
patch("mem0_cli.commands.memory.sys") as mock_sys,
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
None,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
mock_sys.stdin.isatty.return_value = True
|
||||
with pytest.raises((SystemExit, ClickExit)):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
None,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
|
||||
def test_add_invalid_metadata_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
@@ -278,66 +279,6 @@ class TestAddCommand:
|
||||
mock_backend.add.assert_called_once()
|
||||
|
||||
|
||||
class TestAddDeduplicatesPending:
|
||||
"""Ensure duplicate PENDING entries with the same event_id are collapsed."""
|
||||
|
||||
DUPLICATE_PENDING: typing.ClassVar[dict] = {
|
||||
"results": [
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
]
|
||||
}
|
||||
|
||||
def _run_add(self, mock_backend, output):
|
||||
mock_backend.add.return_value = self.DUPLICATE_PENDING
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output=output,
|
||||
)
|
||||
return buf.getvalue()
|
||||
|
||||
def test_text_shows_one_pending(self, mock_backend):
|
||||
raw = self._run_add(mock_backend, "text")
|
||||
assert raw.count("Queued") == 1
|
||||
|
||||
def test_json_shows_one_pending(self, mock_backend):
|
||||
raw = self._run_add(mock_backend, "json")
|
||||
data = json.loads(raw)
|
||||
results = data.get("results", data)
|
||||
pending = [r for r in results if r.get("status") == "PENDING"]
|
||||
assert len(pending) == 1
|
||||
|
||||
def test_agent_shows_one_pending(self, mock_backend):
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
try:
|
||||
raw = self._run_add(mock_backend, "agent")
|
||||
finally:
|
||||
set_agent_mode(False)
|
||||
data = json.loads(raw)
|
||||
assert data["count"] == 1
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
|
||||
class TestSearchCommand:
|
||||
def test_search_text(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
@@ -677,6 +618,28 @@ class TestDeleteAllCommand:
|
||||
run_id="*",
|
||||
)
|
||||
|
||||
def test_delete_all_project_wide_dry_run(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete_all(
|
||||
mock_backend,
|
||||
force=True,
|
||||
all_=True,
|
||||
dry_run=True,
|
||||
user_id=None,
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
output="text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "project-wide" in output.lower()
|
||||
mock_backend.delete.assert_not_called()
|
||||
|
||||
def test_delete_all_project_wide_async_response(self, mock_backend):
|
||||
mock_backend.delete.return_value = {"message": "Memories deletion started..."}
|
||||
console, buf = _make_console()
|
||||
@@ -741,6 +704,15 @@ class TestStatusCommand:
|
||||
assert '"status"' in output
|
||||
|
||||
|
||||
class TestVersionCommand:
|
||||
def test_version(self):
|
||||
console, buf = _make_console()
|
||||
with patch("mem0_cli.commands.utils.console", console):
|
||||
cmd_version()
|
||||
output = buf.getvalue()
|
||||
assert "0.1.0" in output
|
||||
|
||||
|
||||
class TestImportCommand:
|
||||
def test_import_json(self, mock_backend, tmp_path):
|
||||
file_path = tmp_path / "import.json"
|
||||
@@ -916,28 +888,6 @@ class TestEntitiesDeleteCommand:
|
||||
output = buf.getvalue()
|
||||
assert "deleted" in output.lower()
|
||||
|
||||
def test_delete_entity_agent_id(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
):
|
||||
cmd_entities_delete(
|
||||
mock_backend,
|
||||
user_id=None,
|
||||
agent_id="bot1",
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
force=True,
|
||||
output="text",
|
||||
)
|
||||
mock_backend.delete_entities.assert_called_once_with(
|
||||
user_id=None, agent_id="bot1", app_id=None, run_id=None
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "deleted" in output.lower()
|
||||
|
||||
def test_delete_entity_no_id_exits(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
@@ -1074,388 +1024,3 @@ class TestEnableGraph:
|
||||
)
|
||||
call_kwargs = mock_backend.list_memories.call_args
|
||||
assert call_kwargs.kwargs.get("enable_graph") is True
|
||||
|
||||
|
||||
class TestEventCommands:
|
||||
def test_event_list_table(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-" in out
|
||||
assert "ADD" in out
|
||||
assert "SUCCEEDED" in out
|
||||
|
||||
def test_event_list_json(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="json")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "evt-def-456-ghi-789" in out
|
||||
|
||||
def test_event_list_empty(self, mock_backend):
|
||||
mock_backend.list_events.return_value = []
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
out = buf.getvalue()
|
||||
assert "No events" in out
|
||||
|
||||
def test_event_status_text(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "SUCCEEDED" in out
|
||||
|
||||
def test_event_status_json(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="json")
|
||||
out = buf.getvalue()
|
||||
assert "evt-abc-123-def-456" in out
|
||||
assert "ADD" in out
|
||||
|
||||
|
||||
class TestAgentMode:
|
||||
"""Tests for --json/--agent mode: structured JSON envelope output."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Enable agent mode before each test."""
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
|
||||
def teardown_method(self):
|
||||
"""Reset agent mode after each test."""
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(False)
|
||||
|
||||
# ── add ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_add_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"I prefer dark mode",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text", # will be overridden to "agent"
|
||||
)
|
||||
raw = buf.getvalue()
|
||||
data = json.loads(raw)
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "add"
|
||||
assert "data" in data
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 1
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "event"}
|
||||
|
||||
def test_add_agent_mode_scope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="bob",
|
||||
agent_id="agent1",
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["scope"]["user_id"] == "bob"
|
||||
assert data["scope"]["agent_id"] == "agent1"
|
||||
|
||||
# ── search ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_search_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"dark mode",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=10,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
output="text",
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "search"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "score", "created_at", "categories"}
|
||||
|
||||
# ── list ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_list_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_list(
|
||||
mock_backend,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
page=1,
|
||||
page_size=100,
|
||||
category=None,
|
||||
after=None,
|
||||
before=None,
|
||||
output="table", # will be overridden to "agent"
|
||||
)
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "list"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert data["scope"]["user_id"] == "alice"
|
||||
assert set(data["data"][0].keys()) == {"id", "memory", "created_at", "categories"}
|
||||
|
||||
# ── get ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def test_get_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_get(mock_backend, "abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "get"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert data["data"]["id"] == "abc-123-def-456"
|
||||
assert "memory" in data["data"]
|
||||
assert set(data["data"].keys()) >= {"id", "memory"}
|
||||
|
||||
# ── update ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_update_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_update(mock_backend, "abc-123", "Updated content", metadata=None, output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "update"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert "memory" in data["data"]
|
||||
assert "duration_ms" in data
|
||||
|
||||
# ── delete ───────────────────────────────────────────────────────────────
|
||||
|
||||
def test_delete_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete(mock_backend, "abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "delete"
|
||||
assert data["data"]["id"] == "abc-123-def-456"
|
||||
assert data["data"]["deleted"] is True
|
||||
assert "duration_ms" in data
|
||||
|
||||
# ── event list ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_event_list_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_list(mock_backend, output="table")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "event list"
|
||||
assert isinstance(data["data"], list)
|
||||
assert data["count"] == 2
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"][0].keys()) == {
|
||||
"id",
|
||||
"event_type",
|
||||
"status",
|
||||
"latency",
|
||||
"created_at",
|
||||
}
|
||||
|
||||
# ── event status ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_event_status_agent_mode_envelope(self, mock_backend):
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.events_cmd.console", console),
|
||||
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||
):
|
||||
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||
data = json.loads(buf.getvalue())
|
||||
assert data["status"] == "success"
|
||||
assert data["command"] == "event status"
|
||||
assert isinstance(data["data"], dict)
|
||||
assert data["data"]["id"] == "evt-abc-123-def-456"
|
||||
assert "duration_ms" in data
|
||||
assert set(data["data"]["results"][0].keys()) == {"id", "event", "user_id", "memory"}
|
||||
assert "data" not in data["data"]["results"][0]
|
||||
|
||||
# ── error handling ───────────────────────────────────────────────────────
|
||||
|
||||
def test_error_in_agent_mode_produces_json_to_stdout(self, mock_backend):
|
||||
"""Errors in agent mode must emit a JSON envelope to stdout, not stderr."""
|
||||
from io import StringIO
|
||||
|
||||
mock_backend.get.side_effect = Exception("Memory not found")
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
|
||||
captured_stdout = StringIO()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("sys.stdout", captured_stdout),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_get(mock_backend, "bad-id", output="text")
|
||||
|
||||
stdout_output = captured_stdout.getvalue()
|
||||
# The error JSON envelope must be on stdout
|
||||
error_data = json.loads(stdout_output)
|
||||
assert error_data["status"] == "error"
|
||||
assert "error" in error_data
|
||||
assert error_data["data"] is None
|
||||
|
||||
def test_branding_suppressed_in_agent_mode(self, mock_backend):
|
||||
"""Scope line and success message must be absent in agent mode output."""
|
||||
console, buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"branding test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
output="text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
# Must be valid JSON only — no human-readable branding
|
||||
data = json.loads(output)
|
||||
assert data["status"] == "success"
|
||||
# "Scope:" and "Memory processed" must NOT appear in the raw output
|
||||
assert "Scope:" not in output
|
||||
assert "Memory processed" not in output
|
||||
assert "spinner" not in output.lower()
|
||||
|
||||
def test_no_spinner_in_agent_mode(self, mock_backend):
|
||||
"""timed_status must not emit spinner output in agent mode."""
|
||||
err_buf = StringIO()
|
||||
err_console_buf = Console(file=err_buf, force_terminal=False, no_color=True, width=120)
|
||||
console, _buf = _make_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console_buf),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"query",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=5,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
output="text",
|
||||
)
|
||||
# The err_buf captures what would have been spinner/timing noise
|
||||
# In agent mode it should be empty (no status lines printed)
|
||||
err_output = err_buf.getvalue()
|
||||
assert "Searching" not in err_output
|
||||
|
||||
@@ -11,7 +11,6 @@ from mem0_cli.output import (
|
||||
format_memories_table,
|
||||
format_memories_text,
|
||||
format_single_memory,
|
||||
sanitize_agent_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -130,153 +129,8 @@ class TestAddResult:
|
||||
output = buf.getvalue()
|
||||
assert output.strip() == ""
|
||||
|
||||
def test_format_add_result_deduplicates_pending_by_event_id(self):
|
||||
console, buf = _make_console()
|
||||
result = {
|
||||
"results": [
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
{"status": "PENDING", "event_id": "evt-dup"},
|
||||
]
|
||||
}
|
||||
format_add_result(console, result, "text")
|
||||
output = buf.getvalue()
|
||||
# Should show only one PENDING block despite two entries with same event_id
|
||||
assert output.count("evt-dup") == 2 # event_id line + status hint line
|
||||
assert output.count("Queued") == 1
|
||||
|
||||
def test_format_add_result_empty(self):
|
||||
console, buf = _make_console()
|
||||
format_add_result(console, {"results": []}, "text")
|
||||
output = buf.getvalue()
|
||||
assert "No memories extracted" in output
|
||||
|
||||
|
||||
class TestSanitizeAgentData:
|
||||
def test_add_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"event": "ADD",
|
||||
"metadata": {"x": 1},
|
||||
"categories": ["a"],
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("add", raw)
|
||||
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
|
||||
|
||||
def test_add_pending_passthrough(self):
|
||||
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
|
||||
result = sanitize_agent_data("add", raw)
|
||||
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
|
||||
|
||||
def test_search_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"score": 0.9,
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
"user_id": "u1",
|
||||
"agent_id": None,
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("search", raw)
|
||||
assert result == [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"score": 0.9,
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
}
|
||||
]
|
||||
|
||||
def test_list_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"created_at": "2026-01-01",
|
||||
"categories": ["a"],
|
||||
"user_id": "u1",
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("list", raw)
|
||||
assert result == [
|
||||
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
|
||||
]
|
||||
|
||||
def test_get_projects_fields(self):
|
||||
raw = {
|
||||
"id": "abc",
|
||||
"memory": "test",
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
"categories": ["a"],
|
||||
"metadata": {"k": "v"},
|
||||
"user_id": "u1",
|
||||
}
|
||||
result = sanitize_agent_data("get", raw)
|
||||
assert "user_id" not in result
|
||||
assert "id" in result and "memory" in result
|
||||
|
||||
def test_update_projects_fields(self):
|
||||
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
|
||||
result = sanitize_agent_data("update", raw)
|
||||
assert result == {"id": "abc", "memory": "updated"}
|
||||
|
||||
def test_event_list_projects_fields(self):
|
||||
raw = [
|
||||
{
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"graph_status": None,
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
}
|
||||
]
|
||||
result = sanitize_agent_data("event list", raw)
|
||||
assert result == [
|
||||
{
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
}
|
||||
]
|
||||
assert "updated_at" not in result[0]
|
||||
assert "graph_status" not in result[0]
|
||||
|
||||
def test_event_status_flattens_results(self):
|
||||
raw = {
|
||||
"id": "evt-1",
|
||||
"event_type": "ADD",
|
||||
"status": "SUCCEEDED",
|
||||
"latency": 100.0,
|
||||
"created_at": "2026-01-01",
|
||||
"updated_at": "2026-01-02",
|
||||
"results": [
|
||||
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
|
||||
],
|
||||
}
|
||||
result = sanitize_agent_data("event status", raw)
|
||||
assert result["results"][0] == {
|
||||
"id": "mem-1",
|
||||
"event": "ADD",
|
||||
"user_id": "alice",
|
||||
"memory": "dark mode",
|
||||
}
|
||||
assert "data" not in result["results"][0]
|
||||
|
||||
def test_passthrough_commands(self):
|
||||
for cmd in ("status", "import", "config show", "config get", "config set"):
|
||||
data = {"key": "value", "other": "stuff"}
|
||||
assert sanitize_agent_data(cmd, data) == data
|
||||
|
||||
def test_none_data(self):
|
||||
assert sanitize_agent_data("add", None) is None
|
||||
|
||||
@@ -82,7 +82,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
|
||||
Modify project configuration including custom instructions, categories, and graph settings:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
@@ -101,9 +101,6 @@ client.project.update(
|
||||
# Enable graph memory for the project
|
||||
client.project.update(enable_graph=True)
|
||||
|
||||
# Use the input language for memory storage and retrieval
|
||||
client.project.update(multilingual=True)
|
||||
|
||||
# Update multiple settings at once
|
||||
client.project.update(
|
||||
custom_instructions="...",
|
||||
@@ -111,8 +108,7 @@ client.project.update(
|
||||
{"personal_info": "User personal information and preferences"},
|
||||
{"work_context": "Professional context and work-related information"}
|
||||
],
|
||||
enable_graph=True,
|
||||
multilingual=True
|
||||
enable_graph=True
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -8,34 +8,6 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-04-04" description="v1.0.11">
|
||||
|
||||
**New Features & Updates:**
|
||||
- **SDK:** Added `multilingual` parameter to project update ([#4314](https://github.com/mem0ai/mem0/pull/4314))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **LLMs:** Fixed Groq model configuration ([#4700](https://github.com/mem0ai/mem0/pull/4700))
|
||||
- **Core:** Prevented thread and memory leaks from PostHog telemetry ([#4535](https://github.com/mem0ai/mem0/pull/4535))
|
||||
- **Vector Stores:** Used `DatetimeRange` for datetime string values in Qdrant range filters ([#4659](https://github.com/mem0ai/mem0/pull/4659))
|
||||
- **Configs:** Added missing `ConfigDict` to vector store configs (Elasticsearch, MongoDB, Neptune, OpenSearch, PGVector, Supabase, Valkey) ([#4656](https://github.com/mem0ai/mem0/pull/4656))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-01" description="v1.0.10">
|
||||
|
||||
**New Features & Updates:**
|
||||
- **LLMs:** Added MiniMax provider support for AWS Bedrock ([#4609](https://github.com/mem0ai/mem0/pull/4609))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Configs:** Migrated CassandraConfig and AzureMySQLConfig to pydantic v2 ConfigDict ([#4646](https://github.com/mem0ai/mem0/pull/4646))
|
||||
- **LLMs:** Forward `response_format` to OpenAI-compatible API for DeepSeek ([#4635](https://github.com/mem0ai/mem0/pull/4635))
|
||||
- **LLMs:** Forward `response_format` to OpenAI-compatible API for vLLM ([#4608](https://github.com/mem0ai/mem0/pull/4608))
|
||||
- **Vector Stores:** Only list authorized collections when listing MongoDB collections ([#3888](https://github.com/mem0ai/mem0/pull/3888))
|
||||
- **Core:** Reset graph database in `Memory.reset()` ([#4185](https://github.com/mem0ai/mem0/pull/4185))
|
||||
- **Core:** Make `AsyncMemory.from_config` a regular classmethod ([#4183](https://github.com/mem0ai/mem0/pull/4183))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-28" description="v1.0.9">
|
||||
|
||||
**New Features & Updates:**
|
||||
@@ -845,22 +817,6 @@ mode: "wide"
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-04-04" description="v2.4.6">
|
||||
|
||||
**New Features & Updates:**
|
||||
- **Client:** Added `multilingual` parameter to project update types ([#4314](https://github.com/mem0ai/mem0/pull/4314))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-01" description="v2.4.5">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **OSS:** Replace `.single()` with `.maybeSingle()` in SupabaseDB.get() to handle missing rows ([#4599](https://github.com/mem0ai/mem0/pull/4599))
|
||||
- **Embeddings:** Pass dimensions parameter to OpenAI embeddings API ([#4632](https://github.com/mem0ai/mem0/pull/4632))
|
||||
- **OSS:** Extract JSON from chatty LLM responses in fact retrieval ([#4533](https://github.com/mem0ai/mem0/pull/4533))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-28" description="v2.4.4">
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
@@ -24,7 +24,6 @@ config = {
|
||||
"provider": "gemini",
|
||||
"config": {
|
||||
"model": "gemini-2.0-flash-001",
|
||||
"api_key": "your-gemini-api-key",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 2000,
|
||||
"top_p": 1.0
|
||||
@@ -53,7 +52,6 @@ const config = {
|
||||
provider: "gemini",
|
||||
config: {
|
||||
model: "gemini-2.0-flash-001",
|
||||
apiKey: process.env.GOOGLE_API_KEY || '',
|
||||
temperature: 0.1
|
||||
}
|
||||
}
|
||||
|
||||
+13
-42
@@ -133,26 +133,6 @@
|
||||
"pages": [
|
||||
"platform/contribute"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Release Notes",
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"changelog"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "OpenClaw",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Agent Harness",
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -413,6 +393,7 @@
|
||||
"integrations/autogen",
|
||||
"integrations/agno",
|
||||
"integrations/camel-ai",
|
||||
"integrations/openclaw",
|
||||
"integrations/openai-agents-sdk",
|
||||
"integrations/google-ai-adk",
|
||||
"integrations/mastra",
|
||||
@@ -449,28 +430,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Agent Plugins",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Coding Agents",
|
||||
"icon": "terminal",
|
||||
"pages": [
|
||||
"integrations/claude-code",
|
||||
"integrations/cursor",
|
||||
"integrations/codex"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Agent Harness",
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "API Reference",
|
||||
"groups": [
|
||||
@@ -558,6 +517,18 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Release Notes",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Changelog",
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"changelog"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -381,32 +381,4 @@ Here are the available integrations for Mem0:
|
||||
>
|
||||
Build AI agents with persistent memory using Mastra's framework and tools.
|
||||
</Card>
|
||||
<Card
|
||||
title="OpenAI Agents SDK"
|
||||
icon="robot"
|
||||
href="/integrations/openai-agents-sdk"
|
||||
>
|
||||
Integrate Mem0 with the OpenAI Agents SDK for persistent memory across multi-agent workflows.
|
||||
</Card>
|
||||
<Card
|
||||
title="Google ADK"
|
||||
icon="google"
|
||||
href="/integrations/google-ai-adk"
|
||||
>
|
||||
Integrate Mem0 with Google Agent Development Kit for persistent memory across multi-agent workflows.
|
||||
</Card>
|
||||
<Card
|
||||
title="Flowise"
|
||||
icon="diagram-project"
|
||||
href="/integrations/flowise"
|
||||
>
|
||||
Add persistent Mem0 memory to Flowise chatflows for context-aware conversations in the low-code builder.
|
||||
</Card>
|
||||
<Card
|
||||
title="AWS Bedrock"
|
||||
icon="cloud"
|
||||
href="/integrations/aws-bedrock"
|
||||
>
|
||||
Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
---
|
||||
title: Claude Code
|
||||
description: "Add persistent memory to Claude Code and Claude Cowork with the Mem0 plugin — MCP server, lifecycle hooks, and SDK skill."
|
||||
---
|
||||
|
||||
Add persistent memory to [**Claude Code**](https://docs.anthropic.com/en/docs/claude-code) (CLI) and **Claude Cowork** (desktop app) with the Mem0 plugin. Your agent forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Lifecycle Hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
|
||||
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Claude Code, ensure you have:
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||
|
||||
2. Claude Code CLI or Claude Cowork desktop app installed
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — Plugin Marketplace (Recommended)
|
||||
|
||||
Install the full plugin including MCP server, lifecycle hooks, and SDK skill:
|
||||
|
||||
```
|
||||
/plugin marketplace add mem0ai/mem0
|
||||
/plugin install mem0@mem0-plugins
|
||||
```
|
||||
|
||||
**Claude Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
|
||||
|
||||
### Option B — MCP Only
|
||||
|
||||
Add the Mem0 MCP server directly with a single command:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude code"
|
||||
```
|
||||
|
||||
This gives you the MCP tools but not the lifecycle hooks or SDK skill.
|
||||
|
||||
### Option C — Manual MCP Configuration
|
||||
|
||||
Add to your Claude Code MCP config (`.mcp.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Info icon="check">
|
||||
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Plugin Install | MCP Only |
|
||||
|-----------|:--------------:|:--------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
Once installed, the following tools are available in every Claude Code session:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecycle to automatically manage memory:
|
||||
|
||||
### Session Start
|
||||
On every new session, the plugin prompts Claude to call `search_memories` to load relevant context from prior sessions. On resumed or post-compaction sessions, it adjusts the prompt accordingly.
|
||||
|
||||
### User Prompt
|
||||
Before processing each user message, the plugin searches Mem0 for memories relevant to the current prompt and injects them into context. Short prompts (< 20 characters) are skipped to minimize latency.
|
||||
|
||||
### Pre-Compaction
|
||||
Before context compaction, the plugin prompts Claude to store a comprehensive session summary — including goals, accomplishments, decisions, modified files, and current state — so nothing is lost.
|
||||
|
||||
### Task Completed
|
||||
After each task completion, the plugin prompts Claude to extract and store key learnings: successful strategies, failed approaches, architectural decisions, and new conventions.
|
||||
|
||||
### Session End
|
||||
When Claude finishes responding, the plugin prompts for any unstored learnings and captures transcript state via the Mem0 REST API as a background safety net.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
# Session 1: Working on a feature
|
||||
You: Let's refactor the auth module to use JWT tokens instead of sessions.
|
||||
|
||||
# Claude searches memories, finds nothing relevant, proceeds with the work.
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Decision: "Migrated auth from sessions to JWT tokens"
|
||||
# - Files modified: auth/middleware.ts, auth/token.ts
|
||||
# - User preference: "Prefers TypeScript, uses ESLint"
|
||||
|
||||
# Session 2 (days later): Related work
|
||||
You: Add refresh token rotation to the auth system.
|
||||
|
||||
# Claude searches memories, retrieves the JWT migration context.
|
||||
# Knows the file structure, decisions made, and user preferences.
|
||||
# Continues seamlessly without re-explaining the codebase.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Claude Code session after installation
|
||||
- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
Detailed MCP configuration for all clients
|
||||
</Card>
|
||||
<Card title="Codex Integration" icon={<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20.5565 10.6338C21.0009 9.27575 20.8528 7.76958 20.1367 6.53501C19.0503 4.63378 16.8528 3.67081 14.7046 4.11526C13.7663 3.05353 12.3836 2.46094 10.9515 2.46094C8.75399 2.46094 6.82807 3.86835 6.13671 5.94242C4.7293 6.23872 3.51943 7.10291 2.80338 8.36217C1.71696 10.2634 1.96387 12.6338 3.42066 14.2634C2.97622 15.6461 3.14906 17.1276 3.8651 18.3622C4.95152 20.2634 7.14906 21.2511 9.2972 20.7819C10.2602 21.8437 11.6182 22.4609 13.0503 22.4609C15.2478 22.4609 17.1737 21.0535 17.8651 18.9795C19.2725 18.6832 20.4824 17.819 21.1984 16.5597C22.2849 14.6585 22.0379 12.2634 20.5565 10.6338ZM13.0503 21.1523C12.1614 21.1523 11.3219 20.856 10.6552 20.2881C10.6799 20.2634 10.754 20.2387 10.7787 20.214L14.754 17.9177C14.9515 17.7943 15.075 17.5967 15.075 17.3498V11.7449L16.754 12.7079C16.7787 12.7079 16.7787 12.7325 16.7787 12.7572V17.3992C16.8034 19.4733 15.1244 21.1523 13.0503 21.1523ZM5.00091 17.7202C4.55646 16.9548 4.40831 16.0659 4.55646 15.2017C4.58115 15.2264 4.63054 15.2511 4.67992 15.2758L8.65523 17.572C8.85276 17.6955 9.09967 17.6955 9.2972 17.572L14.1614 14.7572V16.7079C14.1614 16.7325 14.1614 16.7572 14.1367 16.7572L10.112 19.0782C8.33424 20.1153 6.03794 19.498 5.00091 17.7202ZM3.96387 9.02884C4.40831 8.26341 5.09967 7.69551 5.91449 7.37452V12.1153C5.91449 12.3375 6.03794 12.5597 6.23548 12.6832L11.0997 15.498L9.42066 16.4609C9.39597 16.4609 9.37128 16.4856 9.37128 16.4609L5.34659 14.1399C3.51943 13.1029 2.92683 10.8066 3.96387 9.02884ZM17.791 12.2387L12.9268 9.4239L14.6058 8.46094C14.6305 8.46094 14.6552 8.43625 14.6552 8.46094L18.6799 10.7819C20.4824 11.819 21.075 14.1153 20.0379 15.893C19.5935 16.6585 18.9021 17.2264 18.0873 17.5227V12.8066C18.112 12.5844 17.9886 12.3622 17.791 12.2387ZM19.4454 9.7202C19.4207 9.69551 19.3713 9.67081 19.3219 9.64612L15.3466 7.34983C15.1491 7.22637 14.9021 7.22637 14.7046 7.34983L9.84041 10.1646V8.21402C9.84041 8.18933 9.84041 8.16464 9.86511 8.16464L13.8898 5.84365C15.6923 4.80662 17.9639 5.4239 19.0009 7.22637C19.4454 7.96711 19.5935 8.856 19.4454 9.7202ZM8.92683 13.177L7.24782 12.214C7.22313 12.214 7.22313 12.1893 7.22313 12.1646V7.52267C7.22313 5.44859 8.90214 3.76958 10.9762 3.76958C11.8651 3.76958 12.7046 4.06588 13.3713 4.63378C13.3466 4.65847 13.2972 4.68316 13.2478 4.70785L9.27251 7.00415C9.07498 7.1276 8.95152 7.32514 8.95152 7.57205V13.177H8.92683ZM9.84041 11.2017L12.0133 9.94242L14.1861 11.2017V13.6955L12.0133 14.9548L9.84041 13.6955V11.2017Z" fill="currentColor"/></svg>} href="/integrations/codex">
|
||||
Add Mem0 memory to OpenAI Codex workflows
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,212 +0,0 @@
|
||||
---
|
||||
title: Codex
|
||||
description: "Add persistent memory to OpenAI Codex with the Mem0 plugin — MCP server, memory protocol skill, and plugin marketplace support."
|
||||
---
|
||||
|
||||
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP and using a skill-based memory protocol to automatically retrieve context and store learnings.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Memory Protocol Skill** — Instructs the agent to retrieve memories at task start, store learnings on completion, and capture session state before context loss
|
||||
3. **Plugin Marketplace** — Install via Codex's repo-level or personal plugin marketplace
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Codex, ensure you have:
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||
|
||||
2. OpenAI Codex access
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — Repo Marketplace (Recommended for Teams)
|
||||
|
||||
Add a `.agents/plugins/marketplace.json` to your repository root:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/mem0"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
|
||||
### Option B — Personal Marketplace
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Option C — Manual MCP Configuration
|
||||
|
||||
Add to your Codex MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<Info icon="check">
|
||||
Start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Plugin Install | MCP Only |
|
||||
|-----------|:--------------:|:--------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Memory Protocol Skill | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
Once installed, the following tools are available in every Codex session:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## Memory Protocol Skill
|
||||
|
||||
Codex uses a skill-based approach instead of lifecycle hooks. When installed via the plugin marketplace, the memory protocol skill instructs the agent to:
|
||||
|
||||
### On Every New Task
|
||||
1. Call `search_memories` with a query related to the current task to load relevant context
|
||||
2. Review returned memories to understand what was learned in prior sessions
|
||||
3. Optionally call `get_memories` to browse all stored memories
|
||||
|
||||
### After Completing Significant Work
|
||||
Store key learnings using `add_memory` with structured metadata:
|
||||
|
||||
| What to store | Metadata type |
|
||||
|--------------|---------------|
|
||||
| Architectural decisions | `{"type": "decision"}` |
|
||||
| Strategies that worked | `{"type": "task_learning"}` |
|
||||
| Failed approaches | `{"type": "anti_pattern"}` |
|
||||
| User preferences observed | `{"type": "user_preference"}` |
|
||||
| Environment discoveries | `{"type": "environmental"}` |
|
||||
| Conventions established | `{"type": "convention"}` |
|
||||
|
||||
### Before Losing Context
|
||||
Store a comprehensive session summary including goals, accomplishments, decisions, files modified, and current state with metadata `{"type": "session_state"}`.
|
||||
|
||||
## Plugin Manifest
|
||||
|
||||
The Codex plugin manifest (`.codex-plugin/plugin.json`) follows the Codex plugin specification:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications.",
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.codex-mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Mem0",
|
||||
"shortDescription": "Persistent memory layer for AI coding workflows",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Read", "Write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
# Task 1: Setting up a new service
|
||||
You: Create a REST API for the notifications service using Express and TypeScript.
|
||||
|
||||
# Codex searches memories, finds user preferences from prior tasks.
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
|
||||
# - Convention: "All API routes follow /api/v1/{resource} pattern"
|
||||
# - Preference: "User prefers explicit error types over generic catch-all"
|
||||
|
||||
# Task 2 (days later): Extending the service
|
||||
You: Add WebSocket support for real-time notification delivery.
|
||||
|
||||
# Codex searches memories, retrieves the architecture decisions and conventions.
|
||||
# Follows the same patterns established in the first task.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Codex session after plugin installation
|
||||
- **Plugin not found** — Ensure `.agents/plugins/marketplace.json` is at the repository root and `source.path` points to the correct plugin directory
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
Detailed MCP configuration for all clients
|
||||
</Card>
|
||||
<Card title="Claude Code Integration" icon={<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/></svg>} href="/integrations/claude-code">
|
||||
Add Mem0 memory to Claude Code workflows
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,148 +0,0 @@
|
||||
---
|
||||
title: Cursor
|
||||
description: "Add persistent memory to Cursor with the Mem0 plugin — MCP server, lifecycle hooks, and SDK skill for context-aware coding."
|
||||
---
|
||||
|
||||
Add persistent memory to [**Cursor**](https://cursor.com) with the Mem0 plugin. Your AI assistant forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Lifecycle Hooks** — Automatic memory capture at session start, compaction, and user prompts (Marketplace install)
|
||||
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Cursor, ensure you have:
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||
|
||||
2. Cursor installed ([cursor.com](https://cursor.com))
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Already have `mem0` configured as an MCP server in Cursor? Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
</Warning>
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — One-Click Deeplink (MCP Only)
|
||||
|
||||
The fastest way to get started. Click the link below to install the Mem0 MCP server directly in Cursor:
|
||||
|
||||
[Install Mem0 MCP in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=mem0&config=eyJtY3BTZXJ2ZXJzIjp7Im1lbTAiOnsidXJsIjoiaHR0cHM6Ly9tY3AubWVtMC5haS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiVG9rZW4gJHtlbnY6TUVNMF9BUElfS0VZfSJ9fX19)
|
||||
|
||||
### Option B — npx (MCP Only)
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "cursor"
|
||||
```
|
||||
|
||||
### Option C — Manual Configuration (MCP Only)
|
||||
|
||||
Add the following to your `.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option D — Cursor Marketplace (Full Plugin)
|
||||
|
||||
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks, the Mem0 SDK skill, and automatic memory capture.
|
||||
|
||||
<Info icon="check">
|
||||
Start a new Cursor session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Marketplace Install | Deeplink / Manual / npx |
|
||||
|-----------|:-------------------:|:-----------------------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
Once installed, the following tools are available in every Cursor session:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## Lifecycle Hooks (Marketplace Install)
|
||||
|
||||
When installed via the Cursor Marketplace, Mem0 hooks into Cursor's lifecycle:
|
||||
|
||||
### Session Start
|
||||
On every new session, the plugin prompts the agent to call `search_memories` to load relevant context from prior sessions.
|
||||
|
||||
### User Prompt
|
||||
Before processing each user message, the plugin searches Mem0 for relevant memories and injects them into context. Short prompts are skipped to minimize latency.
|
||||
|
||||
### Pre-Compaction
|
||||
Before context compaction, the plugin captures a comprehensive session summary so nothing is lost when the context window resets.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
# Session 1: Debugging a performance issue
|
||||
You: The API endpoint /users is taking 3 seconds. Help me optimize it.
|
||||
|
||||
# Cursor agent searches memories, proceeds with investigation.
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Learning: "N+1 query in UserService.getAll() — fixed with eager loading"
|
||||
# - Decision: "Added database index on users.email column"
|
||||
# - Preference: "User prefers query-level fixes over caching"
|
||||
|
||||
# Session 2 (next week): Similar issue
|
||||
You: The /orders endpoint is also slow, same pattern as before.
|
||||
|
||||
# Agent searches memories, retrieves the optimization learnings.
|
||||
# Immediately checks for N+1 queries and missing indexes.
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
|
||||
- **Duplicate tools** — If you had a previous MCP config for `mem0`, remove it before installing the plugin
|
||||
- **No tools appearing** — Go to Cursor Settings > MCP and verify the `mem0` server shows as connected
|
||||
- **Hooks not running** — Hooks require the Marketplace install (Option D). Deeplink/manual installs only provide MCP tools.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
Detailed MCP configuration for all clients
|
||||
</Card>
|
||||
<Card title="Claude Code Integration" icon={<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/></svg>} href="/integrations/claude-code">
|
||||
Add Mem0 memory to Claude Code workflows
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -1,106 +0,0 @@
|
||||
---
|
||||
title: Hermes Agent
|
||||
description: "Add long-term memory to Hermes agents using Mem0 as a pluggable memory provider with automatic background sync and zero-latency prefetch."
|
||||
---
|
||||
|
||||
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent) — a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 automatically learns facts from your conversations and surfaces relevant ones before each turn — all without slowing down the chat.
|
||||
|
||||
## Overview
|
||||
|
||||
Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alongside one external provider. When Mem0 is active, it works additively with the built-in system at three key moments in every conversation turn:
|
||||
|
||||
### 1. Before the Agent Responds (Prefetch)
|
||||
|
||||
When you send a message, Hermes checks if it already has cached Mem0 search results from the previous turn. If so, those memories are injected into the system prompt so the LLM can see them. This is **zero-latency** — no waiting for an API call.
|
||||
|
||||
### 2. After the Agent Responds (Sync)
|
||||
|
||||
Once the LLM finishes responding, Hermes sends the `(user message, assistant response)` pair to Mem0's API in a **background thread**. Mem0's server-side LLM automatically extracts facts (e.g., "user prefers Python", "user works at Acme Corp") — you don't have to tell it what to remember.
|
||||
|
||||
### 3. Background Prefetch for Next Turn
|
||||
|
||||
At the same time as sync, Hermes kicks off a background search on Mem0 to pre-load relevant memories for the next turn. By the time you type your next message, the memories are already cached.
|
||||
|
||||
## Agent Tools
|
||||
|
||||
When Mem0 is active, the LLM gets three extra tools it can call during conversations:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `mem0_profile` | Fetch all stored memories about the user |
|
||||
| `mem0_search` | Semantic search through memories (supports optional reranking via `rerank` and `top_k` parameters) |
|
||||
| `mem0_conclude` | Store a specific fact verbatim — uses `infer=False` so no server-side LLM extraction happens |
|
||||
|
||||
## Installation
|
||||
|
||||
Install Hermes Agent:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
The `mem0ai` Python package is automatically installed when you enable the Mem0 provider — no manual pip install needed.
|
||||
|
||||
## Setup
|
||||
|
||||
### Option 1: Interactive Setup Wizard (Recommended)
|
||||
|
||||
```bash
|
||||
hermes memory setup
|
||||
```
|
||||
|
||||
Select **mem0** as the provider and enter your Mem0 API key when prompted. The wizard writes your config to `~/.hermes/mem0.json`.
|
||||
|
||||
<Note>Get your API key from [app.mem0.ai](https://app.mem0.ai).</Note>
|
||||
|
||||
### Option 2: Manual Configuration
|
||||
|
||||
```bash
|
||||
hermes config set memory.provider mem0
|
||||
echo "MEM0_API_KEY=your-api-key" >> ~/.hermes/.env
|
||||
```
|
||||
|
||||
Then in your `config.yaml`:
|
||||
|
||||
```yaml
|
||||
memory:
|
||||
provider: mem0
|
||||
```
|
||||
|
||||
That's it — Mem0 runs automatically from this point.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
Configuration is stored in `~/.hermes/mem0.json`. Values can also be set via environment variables.
|
||||
|
||||
| Key | Env Variable | Default | Description |
|
||||
|-----|-------------|---------|-------------|
|
||||
| `api_key` | `MEM0_API_KEY` | — | **Required.** Mem0 Platform API key |
|
||||
| `user_id` | `MEM0_USER_ID` | `hermes-user` | User identifier for scoping memories |
|
||||
| `agent_id` | `MEM0_AGENT_ID` | `hermes` | Agent identifier |
|
||||
| `rerank` | — | `true` | Enable reranking for memory recall |
|
||||
|
||||
|
||||
## Reliability
|
||||
|
||||
- **Circuit Breaker** — If Mem0's API fails 5 times in a row, Hermes stops calling it for 2 minutes, then retries. The agent keeps working fine without memory during that time.
|
||||
- **Non-blocking** — All Mem0 API calls happen in background daemon threads. A slow or failed API call never blocks your conversation.
|
||||
- **Thread-safe** — The Mem0 client uses lazy initialization with locking, safe for concurrent access.
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Zero-Latency Recall** — Memories are prefetched in the background and cached, ready before you type
|
||||
2. **Server-side Extraction** — Mem0's API automatically extracts and deduplicates facts from each exchange
|
||||
3. **Non-blocking** — All API calls run in background daemon threads
|
||||
4. **Fault Tolerant** — Circuit breaker ensures the agent works even if Mem0 is temporarily unreachable
|
||||
5. **Additive Memory** — Works alongside Hermes' built-in file-based memory system (MEMORY.md, USER.md)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="OpenClaw Integration" icon={<svg width="24" height="24" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="m153.5 173.5q24.62 1.46 46 13.5 12.11 8.1 17.5 21.5 0.74 2.45 0.5 5 0.09 0.81 1 1 1.48-4.9 1-10 5.04 10.48 1.5 22-9.81 27.86-35.5 42.5-26.17 14.97-56 19.5-2.77-0.4-2 1 2.86 1.27 6 1 25.64 1.53 48.5-10 0.34 10.08 2 20 1.08 5.76 5 10 1 1.5 0 3-31.11 20.84-68.5 17.5-23.7-5.7-32.5-28.5-4.39-9.18-3.5-19 15.41 6.23 32 4.5-20.68-6.39-39-18-34.81-27.22-12.5-65.5 11.84-14.83 29-23 4.21 7.66 11.5 12.5 3 1 6 0-26.04-34.62-29-78-0.13-8.46 2-16.5 1 6.5 2 13 3.43 39.53 24.5 73 2.03 2.28 4.5 4 0.5-1.25 1-2.5-1.27-6.54-5-12 0.5-0.75 1-1.5 9.72-3.43 20-4 0.55 10.34 8 17.5 1.94 0.74 4 0.5-17.8-64.6 16.5-122 0.98-1.79 1.5 0-28.21 56.64-13.5 118 1.08 1.43 2.5 0.5 2.21-4.98 2-10.5z" fill="currentColor"/><path fill-rule="evenodd" d="m454.5 97.5q-1.33 11.18-8.5 20-21.81 26.28-55.5 32-1.11-0.2-2 0.5 2.31 2.82 5.5 4.5 1 2 0 4-9.56 11.3-19.5 20 19.71-8.72 31-27 2.68-0.43 5 1-14.24 30.97-48 36.5-9.93 1.71-20 1.5-6.8-0.48-13 1 5.81 6.92 14 11-10.78 16.03-27 26.5 27.16-7.4 38-33.5 4.34 1.35 9 1-9.08 23.84-33 33.5-18.45 6.41-38 7 22.59 8.92 45-1 12.05-5.52 24-11 9.01-1.79 17 2.5 5.28-4.38 11-8 12.8-6.07 27-5 0 0.5 0 1-19.34 2.69-34 15.5 0.5 0.25 1 0.5 17.79-8.09 36-15 2.71-0.79 5-2 2.5-1 5-2 5.53-4.04 11-8 11.7-4.18 24-6.5 7.78-1.36 15 1.5-2.97 18.45-13.5 34-34.92 49.37-94.5 62.5-59.27 12.45-108-23-15.53-12.52-21.5-31.5-2.47-14.26 4-27-3.15 24.41 14 42-4.92-10.28-7-22-1.97-17.63 7-33 47.28-69.5 125.5-100 15.86-3.42 32-5.5 18.63-1.47 37 1.5z" fill="currentColor"/><path fill-rule="evenodd" d="m231.5 238.5q1.31-0.2 2 1-3.13 28.62 15 51-16.25 6.75-27-7.5-1-1-2 0 14.73 29.34 46 18.5 1.79 0.52 0 1.5-37.63 16.82-50.5-22.5-5.1-26.48 16.5-42z" fill="currentColor"/><path fill-rule="evenodd" d="m203.5 266.5q1.31-0.2 2 1-2.48 22.08 12 39-6.99 1.35-14 0.5 4.59 4.08 10 7-8.71 0.28-14.5-6.5-16.98-22.76 4.5-41z" fill="currentColor"/><path fill-rule="evenodd" d="m58.5 284.5q9.6-2.17 14.5 6 5.15 14.18-1 28-11.05-13.14-27.5-17.5 5.15-9.9 14-16.5z" fill="currentColor"/><path fill-rule="evenodd" d="m56.5 313.5q3.43 5.43 8 10-4.88 0.44-8 4-1.11-0.2-2 0.5 28.91 1.65 38 28.5 0.45 3.16-1 6-11.02-7.01-23-12.5-4.75-3.75-9.5-7.5 1.47 7.42 7 13 8.34 27.18 32 43 0.99 2.41-1.5 3.5-40.25 5.58-66.5-25.5-15.67-22.01-8-48 10.46-23.87 34.5-15z" fill="currentColor"/><path fill-rule="evenodd" d="m198.5 319.5q1.44 0.68 2.5 2 2.41 8.23 6 16 1.2 2.64-0.5 5-30.65 21.41-68 18.5-25.16-6.17-32.5-30.5 6.96 4.99 15.5 6.5 8.99 0.75 18 0.5 16.25 2.38 32-2.5 15.9-3.94 27-15.5z" fill="currentColor"/><path fill-rule="evenodd" d="m239.5 342.5q7.02-0.25 14 0.5 4.46 1.06 8 3.5-5.2 2.35-10 5.5-3.88 4.65-9 7.5-9.89-3.09-9.5-13 2.36-3.63 6.5-4z" fill="currentColor"/><path fill-rule="evenodd" d="m214.5 349.5q5.96 7.2 13.5 13 1 1 0 2-28.58 23.34-65.5 20.5-18.15-4.24-27.5-19.5 1.13 0.94 2.5 1.5 14.7 1.42 29-1.5 26.57-0.52 48-16z" fill="currentColor"/><path fill-rule="evenodd" d="m302.5 373.5q0.21 2.44-2 3.5-28.69 7.6-50.5-12.5-0.06-6.71 6.5-9 4.45-0.75 9-1 22.26 2.27 37 19z" fill="currentColor"/><path fill-rule="evenodd" d="m232.5 365.5q17.6 6.19 10.5 23-10.6 10.42-25.5 11.5-25.94 3.21-49-9 36.75-1.65 64-25.5z" fill="currentColor"/><path fill-rule="evenodd" d="m113.5 367.5q7.7-0.01 9.5 7-9.69 7.19-18.5 15.5-7.23 5.76-5.5-3.5 3.12-12.84 14.5-19z" fill="currentColor"/><path fill-rule="evenodd" d="m126.5 380.5q7.88-0.4 12 6.5-8.5 7.25-17 14.5-5.62-12.55 5-21z" fill="currentColor"/><path fill-rule="evenodd" d="m283.5 385.5q3.22 2.95 7 5.5 2.8 4.03 6 7.5 0.42 2.77-2 4-15.5-9.75-31-19.5-1.79-0.98 0-1.5 9.96 2.49 20 4z" fill="currentColor"/></svg>} href="/integrations/openclaw">
|
||||
Add memory to OpenClaw agents with auto-recall and auto-capture
|
||||
</Card>
|
||||
<Card title="Mem0 Platform" icon="rocket" href="/platform/overview">
|
||||
Get your API key and explore the Mem0 dashboard
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -3686,10 +3686,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"description": "List of custom categories to be used for memory categorization."
|
||||
},
|
||||
"multilingual": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to use the input language for memory storage and retrieval."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-88
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: CLI
|
||||
description: "Manage memories from your terminal — built for both humans and AI agents."
|
||||
description: "Manage memories from your terminal. Available for Node.js and Python."
|
||||
icon: "terminal"
|
||||
iconType: "solid"
|
||||
---
|
||||
@@ -9,10 +9,6 @@ The mem0 CLI lets you add, search, list, update, and delete memories directly fr
|
||||
|
||||
Both implementations provide identical behavior — same commands, same options, same output formats.
|
||||
|
||||
<Tip>
|
||||
**Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too. Drop it into any agent tool loop with zero extra parsing.
|
||||
</Tip>
|
||||
|
||||
## Installation
|
||||
|
||||
<CodeGroup>
|
||||
@@ -78,23 +74,8 @@ Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
```
|
||||
|
||||
If an existing configuration is detected, the CLI will ask for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD pipelines).
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice --force
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | API key (skip prompt) |
|
||||
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||
| `--email` | Login via email verification code |
|
||||
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||
| `--force` | Overwrite existing config without confirmation |
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
@@ -221,32 +202,16 @@ mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entity`
|
||||
### `mem0 entities`
|
||||
|
||||
List or delete entities (users, agents, apps, runs).
|
||||
List or delete entities (users, agents, apps).
|
||||
|
||||
```bash
|
||||
mem0 entity list users
|
||||
mem0 entity list agents --output json
|
||||
mem0 entity delete --user-id alice --force
|
||||
mem0 entities list
|
||||
mem0 entities list --type agent --output json
|
||||
mem0 entities delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 event`
|
||||
|
||||
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||
|
||||
```bash
|
||||
# List recent events
|
||||
mem0 event list
|
||||
|
||||
# Check the status of a specific event
|
||||
mem0 event status <event-id>
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-o, --output` | Output format: `text`, `json` |
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
@@ -273,7 +238,6 @@ All commands support the `--output` flag to control how results are displayed:
|
||||
| `json` | Structured JSON, suitable for piping to `jq` or consumption by AI agents |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal output — just IDs or status codes |
|
||||
| `agent` | Structured JSON envelope with sanitized fields — set automatically by `--json`/`--agent` |
|
||||
|
||||
Example with JSON output:
|
||||
|
||||
@@ -283,54 +247,20 @@ mem0 search "user preferences" --user-id alice --output json | jq '.data.results
|
||||
|
||||
## Use with AI agents
|
||||
|
||||
The CLI is purpose-built for use inside AI agent tool loops. Pass `--agent` or `--json` as a global flag on **any** command to activate agent mode:
|
||||
The CLI is designed to be used by AI agents and automation tools. Two features make this straightforward:
|
||||
|
||||
- Every command outputs a consistent JSON envelope: `{"status", "command", "duration_ms", "scope", "count", "data"}`
|
||||
- The `data` field contains only the fields that matter — IDs, memory text, scores, categories. Noisy API fields are stripped.
|
||||
- All human-readable output is suppressed: no spinners, no colors, no banners.
|
||||
- Errors are returned as JSON to stdout with a non-zero exit code, so your agent can catch them the same way as successes.
|
||||
- **`--output json`** returns structured data that agents can parse directly
|
||||
- **`mem0 help --json`** returns the full command tree as JSON, so agents can discover available commands programmatically
|
||||
|
||||
```bash
|
||||
# Drop --agent on any command and get clean, parseable JSON
|
||||
mem0 --agent search "response preferences" --user-id user-42
|
||||
mem0 --agent add "User prefers concise responses" --user-id user-42
|
||||
mem0 --agent list --user-id user-42
|
||||
mem0 --agent delete --all --user-id user-42 --force
|
||||
# Agent adds a memory
|
||||
mem0 add "User prefers concise responses" --user-id user-42 --output quiet
|
||||
|
||||
# Agent searches memories and parses results
|
||||
mem0 search "response preferences" --user-id user-42 --output json
|
||||
```
|
||||
|
||||
<CodeGroup>
|
||||
```json Output: mem0 --agent search "dark mode" --user-id alice
|
||||
{
|
||||
"status": "success",
|
||||
"command": "search",
|
||||
"duration_ms": 134,
|
||||
"scope": { "user_id": "alice" },
|
||||
"count": 2,
|
||||
"data": [
|
||||
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] },
|
||||
{ "id": "def-456", "memory": "User uses vim keybindings", "score": 0.81, "created_at": "2026-01-10", "categories": ["tools"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
```json Output: mem0 --agent add "Likes concise answers" --user-id alice
|
||||
{
|
||||
"status": "success",
|
||||
"command": "add",
|
||||
"duration_ms": 210,
|
||||
"data": [
|
||||
{ "id": "ghi-789", "memory": "Likes concise answers", "event": "ADD" }
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Two other agent-friendly features:
|
||||
|
||||
- **`--output json`** returns structured data without sanitization — useful when you want the full raw API response
|
||||
- **`mem0 help --json`** returns the complete command tree as JSON, so agents can self-discover available commands and options
|
||||
|
||||
For non-interactive environments (CI, agent runtimes), set credentials via `mem0 init --api-key m0-xxx --user-id alice --force` or the `MEM0_API_KEY` environment variable.
|
||||
For non-interactive environments (CI, agent runtimes), use `mem0 init --api-key --user-id` or set the `MEM0_API_KEY` environment variable to skip interactive prompts.
|
||||
|
||||
## Environment variables
|
||||
|
||||
@@ -348,12 +278,10 @@ Environment variables take precedence over values in the config file, which take
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands:
|
||||
These flags are available on all commands that interact with the API:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||
| `--agent` | Alias for `--json` |
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"bearer_token_env_var": "MEM0_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Codex workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"homepage": "https://mem0.ai",
|
||||
"repository": "https://github.com/mem0ai/mem0",
|
||||
"license": "Apache-2.0",
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.codex-mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Mem0",
|
||||
"shortDescription": "Persistent memory layer for AI coding workflows",
|
||||
"longDescription": "Mem0 adds long-term memory to Codex. Store decisions, user preferences, project context, and session state across conversations. Memories are automatically retrieved via semantic search so Codex always has the right context.",
|
||||
"developerName": "Mem0",
|
||||
"category": "Productivity",
|
||||
"capabilities": [
|
||||
"Read",
|
||||
"Write"
|
||||
],
|
||||
"websiteURL": "https://mem0.ai",
|
||||
"privacyPolicyURL": "https://mem0.ai/privacy",
|
||||
"termsOfServiceURL": "https://mem0.ai/terms",
|
||||
"defaultPrompt": [
|
||||
"Search my memories for recent project decisions",
|
||||
"Remember that I prefer TypeScript over JavaScript",
|
||||
"What do you know about my coding preferences?"
|
||||
],
|
||||
"brandColor": "#FBBF24",
|
||||
"logo": "./logo.svg"
|
||||
}
|
||||
}
|
||||
+8
-69
@@ -1,6 +1,6 @@
|
||||
# Mem0 Plugin for Claude Code, Claude Cowork, Cursor & Codex
|
||||
# Mem0 Plugin for Claude Code, Claude Cowork & Cursor
|
||||
|
||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, and **Codex**.
|
||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), and **Cursor**.
|
||||
|
||||
## Step 1: Set your API key
|
||||
|
||||
@@ -47,65 +47,6 @@ Claude Code and Claude Cowork share the same plugin system.
|
||||
|
||||
This installs the full plugin including the MCP server, lifecycle hooks (automatic memory capture), and the Mem0 SDK skill.
|
||||
|
||||
### Codex
|
||||
|
||||
**Option A — Repo marketplace** (recommended for teams):
|
||||
|
||||
Add the plugin marketplace to your repo root (already included in this repository):
|
||||
|
||||
```
|
||||
.agents/plugins/marketplace.json
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
|
||||
**Option B — Personal marketplace**:
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Option C — Manual MCP configuration**:
|
||||
|
||||
Add to your Codex MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This installs the MCP server and the Mem0 SDK skill. Codex uses the skill-based memory protocol instead of lifecycle hooks.
|
||||
|
||||
### Cursor
|
||||
|
||||
> **Already have `mem0` configured as an MCP server?** Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
@@ -145,17 +86,15 @@ After installing, confirm the MCP server is connected:
|
||||
|
||||
## What's included
|
||||
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|:-----:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No | Yes |
|
||||
| Memory Protocol Skill | No | No | No | Yes |
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|
|
||||
| MCP Server | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No |
|
||||
|
||||
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end. (Claude Code/Cursor only)
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end.
|
||||
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
|
||||
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Replaces lifecycle hooks on platforms that don't support them.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
---
|
||||
name: mem0-codex
|
||||
description: >
|
||||
Mem0 persistent memory integration for Codex. Automatically retrieve relevant
|
||||
memories at the start of each task, store key learnings when tasks complete,
|
||||
and capture session state before context is lost. Use the mem0 MCP tools
|
||||
(add_memory, search_memories, get_memories, etc.) for all memory operations.
|
||||
---
|
||||
|
||||
# Mem0 Memory Protocol for Codex
|
||||
|
||||
You have access to persistent memory via the mem0 MCP tools. Follow this protocol to maintain context across sessions.
|
||||
|
||||
## On every new task
|
||||
|
||||
1. Call `search_memories` with a query related to the current task or project to load relevant context.
|
||||
2. Review returned memories to understand what has been learned in prior sessions.
|
||||
3. If appropriate, call `get_memories` to browse all stored memories for this user.
|
||||
|
||||
## After completing significant work
|
||||
|
||||
Extract key learnings and store them using the `add_memory` tool:
|
||||
|
||||
- **Decisions made** -> Include metadata `{"type": "decision"}`
|
||||
- **Strategies that worked** -> Include metadata `{"type": "task_learning"}`
|
||||
- **Failed approaches** -> Include metadata `{"type": "anti_pattern"}`
|
||||
- **User preferences observed** -> Include metadata `{"type": "user_preference"}`
|
||||
- **Environment/setup discoveries** -> Include metadata `{"type": "environmental"}`
|
||||
- **Conventions established** -> Include metadata `{"type": "convention"}`
|
||||
|
||||
Memories can be as detailed as needed -- include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
## Before losing context
|
||||
|
||||
If context is about to be compacted or the session is ending, store a comprehensive session summary:
|
||||
|
||||
```
|
||||
## Session Summary
|
||||
|
||||
### User's Goal
|
||||
[What the user originally asked for]
|
||||
|
||||
### What Was Accomplished
|
||||
[Numbered list of tasks completed]
|
||||
|
||||
### Key Decisions Made
|
||||
[Architectural choices, trade-offs discussed]
|
||||
|
||||
### Files Created or Modified
|
||||
[Important file paths with what changed]
|
||||
|
||||
### Current State
|
||||
[What is in progress, pending items, next steps]
|
||||
```
|
||||
|
||||
Include metadata: `{"type": "session_state"}`
|
||||
|
||||
## Memory hygiene
|
||||
|
||||
- Do NOT write to MEMORY.md or any file-based memory. Use mem0 MCP tools exclusively.
|
||||
- Only store genuinely useful learnings. Skip trivial interactions.
|
||||
- Use specific, searchable language in memory content.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "2.4.6",
|
||||
"version": "2.4.4",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -68,11 +68,6 @@
|
||||
"!src/community/**"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "mem0-ts"
|
||||
},
|
||||
"keywords": [
|
||||
"mem0",
|
||||
"api",
|
||||
|
||||
@@ -167,7 +167,6 @@ export interface PromptUpdatePayload {
|
||||
exclusion_prompt?: string;
|
||||
memory_depth?: string | null;
|
||||
usecase_setting?: string | number;
|
||||
multilingual?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types";
|
||||
export class AzureOpenAIEmbedder implements Embedder {
|
||||
private client: AzureOpenAI;
|
||||
private model: string;
|
||||
private embeddingDims: number | undefined;
|
||||
private embeddingDims?: number;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
if (!config.apiKey || !config.modelProperties?.endpoint) {
|
||||
@@ -20,16 +20,13 @@ export class AzureOpenAIEmbedder implements Embedder {
|
||||
...rest,
|
||||
});
|
||||
this.model = config.model || "text-embedding-3-small";
|
||||
this.embeddingDims = config.embeddingDims;
|
||||
this.embeddingDims = config.embeddingDims || 1536;
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
const response = await this.client.embeddings.create({
|
||||
model: this.model,
|
||||
input: text,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
dimensions: this.embeddingDims,
|
||||
}),
|
||||
});
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
@@ -38,9 +35,6 @@ export class AzureOpenAIEmbedder implements Embedder {
|
||||
const response = await this.client.embeddings.create({
|
||||
model: this.model,
|
||||
input: texts,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
dimensions: this.embeddingDims,
|
||||
}),
|
||||
});
|
||||
return response.data.map((item) => item.embedding);
|
||||
}
|
||||
|
||||
@@ -5,23 +5,21 @@ import { EmbeddingConfig } from "../types";
|
||||
export class GoogleEmbedder implements Embedder {
|
||||
private google: GoogleGenAI;
|
||||
private model: string;
|
||||
private embeddingDims: number | undefined;
|
||||
private embeddingDims?: number;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.google = new GoogleGenAI({
|
||||
apiKey: config.apiKey || process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
this.model = config.model || "gemini-embedding-001";
|
||||
this.embeddingDims = config.embeddingDims;
|
||||
this.embeddingDims = config.embeddingDims || 1536;
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
const response = await this.google.models.embedContent({
|
||||
model: this.model,
|
||||
contents: text,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
config: { outputDimensionality: this.embeddingDims },
|
||||
}),
|
||||
config: { outputDimensionality: this.embeddingDims },
|
||||
});
|
||||
return response.embeddings![0].values!;
|
||||
}
|
||||
@@ -30,9 +28,7 @@ export class GoogleEmbedder implements Embedder {
|
||||
const response = await this.google.models.embedContent({
|
||||
model: this.model,
|
||||
contents: texts,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
config: { outputDimensionality: this.embeddingDims },
|
||||
}),
|
||||
config: { outputDimensionality: this.embeddingDims },
|
||||
});
|
||||
return response.embeddings!.map((item) => item.values!);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types";
|
||||
export class OpenAIEmbedder implements Embedder {
|
||||
private openai: OpenAI;
|
||||
private model: string;
|
||||
private embeddingDims: number | undefined;
|
||||
private embeddingDims?: number;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.openai = new OpenAI({
|
||||
@@ -13,16 +13,13 @@ export class OpenAIEmbedder implements Embedder {
|
||||
baseURL: config.baseURL || config.url,
|
||||
});
|
||||
this.model = config.model || "text-embedding-3-small";
|
||||
this.embeddingDims = config.embeddingDims;
|
||||
this.embeddingDims = config.embeddingDims || 1536;
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
const response = await this.openai.embeddings.create({
|
||||
model: this.model,
|
||||
input: text,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
dimensions: this.embeddingDims,
|
||||
}),
|
||||
});
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
@@ -31,9 +28,6 @@ export class OpenAIEmbedder implements Embedder {
|
||||
const response = await this.openai.embeddings.create({
|
||||
model: this.model,
|
||||
input: texts,
|
||||
...(this.embeddingDims !== undefined && {
|
||||
dimensions: this.embeddingDims,
|
||||
}),
|
||||
});
|
||||
return response.data.map((item) => item.embedding);
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ See the SQL migration instructions in the code comments.`,
|
||||
.from(this.tableName)
|
||||
.select("*")
|
||||
.eq("id", vectorId)
|
||||
.maybeSingle();
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
if (!data) return null;
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
/**
|
||||
* Azure OpenAI Embedder — unit tests (mocked Azure OpenAI client).
|
||||
* Verifies that the `dimensions` parameter is only passed to the API
|
||||
* when the user explicitly configures `embeddingDims`.
|
||||
*/
|
||||
|
||||
const mockEmbeddingsCreate = jest.fn();
|
||||
|
||||
jest.mock("openai", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
AzureOpenAI: jest.fn().mockImplementation(() => ({
|
||||
embeddings: { create: mockEmbeddingsCreate },
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { AzureOpenAIEmbedder } from "../src/embeddings/azure";
|
||||
|
||||
const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5];
|
||||
|
||||
const baseConfig = {
|
||||
apiKey: "test-key",
|
||||
modelProperties: { endpoint: "https://test.openai.azure.com" },
|
||||
};
|
||||
|
||||
describe("AzureOpenAIEmbedder (unit)", () => {
|
||||
beforeEach(() => {
|
||||
mockEmbeddingsCreate.mockReset();
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("dimensions parameter", () => {
|
||||
it("does NOT pass dimensions when embeddingDims is not set", async () => {
|
||||
const embedder = new AzureOpenAIEmbedder(baseConfig);
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("dimensions");
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes dimensions when embeddingDims is explicitly set", async () => {
|
||||
const embedder = new AzureOpenAIEmbedder({
|
||||
...baseConfig,
|
||||
embeddingDims: 1024,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: "hello",
|
||||
dimensions: 1024,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes dimensions=1536 when embeddingDims is explicitly set to 1536", async () => {
|
||||
const embedder = new AzureOpenAIEmbedder({
|
||||
...baseConfig,
|
||||
embeddingDims: 1536,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toHaveProperty("dimensions", 1536);
|
||||
});
|
||||
|
||||
it("does NOT pass dimensions in embedBatch when embeddingDims is not set", async () => {
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new AzureOpenAIEmbedder(baseConfig);
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("dimensions");
|
||||
});
|
||||
|
||||
it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => {
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new AzureOpenAIEmbedder({
|
||||
...baseConfig,
|
||||
embeddingDims: 512,
|
||||
});
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: ["hello", "world"],
|
||||
dimensions: 512,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("basic functionality", () => {
|
||||
it("embed() returns the embedding vector", async () => {
|
||||
const embedder = new AzureOpenAIEmbedder(baseConfig);
|
||||
|
||||
const result = await embedder.embed("hello");
|
||||
expect(result).toEqual(mockEmbedding);
|
||||
});
|
||||
|
||||
it("embedBatch() returns vectors for multiple inputs", async () => {
|
||||
const batch = [
|
||||
[0.1, 0.2],
|
||||
[0.3, 0.4],
|
||||
];
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: batch.map((embedding) => ({ embedding })),
|
||||
});
|
||||
|
||||
const embedder = new AzureOpenAIEmbedder(baseConfig);
|
||||
|
||||
const result = await embedder.embedBatch(["text1", "text2"]);
|
||||
expect(result).toEqual(batch);
|
||||
});
|
||||
|
||||
it("uses custom model when provided", async () => {
|
||||
const embedder = new AzureOpenAIEmbedder({
|
||||
...baseConfig,
|
||||
model: "text-embedding-3-large",
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs.model).toBe("text-embedding-3-large");
|
||||
});
|
||||
|
||||
it("throws when API key is missing", () => {
|
||||
expect(() => {
|
||||
new AzureOpenAIEmbedder({
|
||||
modelProperties: { endpoint: "https://test.openai.azure.com" },
|
||||
});
|
||||
}).toThrow("Azure OpenAI requires both API key and endpoint");
|
||||
});
|
||||
|
||||
it("throws when endpoint is missing", () => {
|
||||
expect(() => {
|
||||
new AzureOpenAIEmbedder({ apiKey: "test-key" });
|
||||
}).toThrow("Azure OpenAI requires both API key and endpoint");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,153 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
/**
|
||||
* Google Embedder — unit tests (mocked Google GenAI client).
|
||||
* Verifies that the `outputDimensionality` config is only passed to the API
|
||||
* when the user explicitly configures `embeddingDims`.
|
||||
*/
|
||||
|
||||
const mockEmbedContent = jest.fn();
|
||||
|
||||
jest.mock("@google/genai", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
GoogleGenAI: jest.fn().mockImplementation(() => ({
|
||||
models: { embedContent: mockEmbedContent },
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { GoogleEmbedder } from "../src/embeddings/google";
|
||||
|
||||
const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5];
|
||||
|
||||
describe("GoogleEmbedder (unit)", () => {
|
||||
beforeEach(() => {
|
||||
mockEmbedContent.mockReset();
|
||||
mockEmbedContent.mockResolvedValue({
|
||||
embeddings: [{ values: mockEmbedding }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("outputDimensionality parameter", () => {
|
||||
it("does NOT pass config when embeddingDims is not set", async () => {
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbedContent).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("config");
|
||||
expect(callArgs).toEqual({
|
||||
model: "gemini-embedding-001",
|
||||
contents: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes outputDimensionality when embeddingDims is explicitly set", async () => {
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 768,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbedContent).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "gemini-embedding-001",
|
||||
contents: "hello",
|
||||
config: { outputDimensionality: 768 },
|
||||
});
|
||||
});
|
||||
|
||||
it("passes outputDimensionality=1536 when embeddingDims is explicitly set to 1536", async () => {
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 1536,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs).toHaveProperty("config");
|
||||
expect(callArgs.config).toEqual({ outputDimensionality: 1536 });
|
||||
});
|
||||
|
||||
it("does NOT pass config in embedBatch when embeddingDims is not set", async () => {
|
||||
mockEmbedContent.mockResolvedValue({
|
||||
embeddings: [{ values: mockEmbedding }, { values: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("config");
|
||||
});
|
||||
|
||||
it("passes outputDimensionality in embedBatch when embeddingDims is explicitly set", async () => {
|
||||
mockEmbedContent.mockResolvedValue({
|
||||
embeddings: [{ values: mockEmbedding }, { values: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 512,
|
||||
});
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "gemini-embedding-001",
|
||||
contents: ["hello", "world"],
|
||||
config: { outputDimensionality: 512 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("basic functionality", () => {
|
||||
it("embed() returns the embedding vector", async () => {
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
const result = await embedder.embed("hello");
|
||||
expect(result).toEqual(mockEmbedding);
|
||||
});
|
||||
|
||||
it("embedBatch() returns vectors for multiple inputs", async () => {
|
||||
const batch = [
|
||||
[0.1, 0.2],
|
||||
[0.3, 0.4],
|
||||
];
|
||||
mockEmbedContent.mockResolvedValue({
|
||||
embeddings: batch.map((values) => ({ values })),
|
||||
});
|
||||
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
const result = await embedder.embedBatch(["text1", "text2"]);
|
||||
expect(result).toEqual(batch);
|
||||
});
|
||||
|
||||
it("uses custom model when provided", async () => {
|
||||
const embedder = new GoogleEmbedder({
|
||||
apiKey: "test-key",
|
||||
model: "text-embedding-004",
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbedContent.mock.calls[0][0];
|
||||
expect(callArgs.model).toBe("text-embedding-004");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
/// <reference types="jest" />
|
||||
/**
|
||||
* OpenAI Embedder — unit tests (mocked OpenAI client).
|
||||
* Verifies that the `dimensions` parameter is only passed to the API
|
||||
* when the user explicitly configures `embeddingDims`.
|
||||
*/
|
||||
|
||||
const mockEmbeddingsCreate = jest.fn();
|
||||
|
||||
jest.mock("openai", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
embeddings: { create: mockEmbeddingsCreate },
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
import { OpenAIEmbedder } from "../src/embeddings/openai";
|
||||
|
||||
const mockEmbedding = [0.1, 0.2, 0.3, 0.4, 0.5];
|
||||
|
||||
describe("OpenAIEmbedder (unit)", () => {
|
||||
beforeEach(() => {
|
||||
mockEmbeddingsCreate.mockReset();
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }],
|
||||
});
|
||||
});
|
||||
|
||||
describe("dimensions parameter", () => {
|
||||
it("does NOT pass dimensions when embeddingDims is not set", async () => {
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("dimensions");
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes dimensions when embeddingDims is explicitly set", async () => {
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 1024,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledTimes(1);
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: "hello",
|
||||
dimensions: 1024,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes dimensions=1536 when embeddingDims is explicitly set to 1536", async () => {
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 1536,
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toHaveProperty("dimensions", 1536);
|
||||
});
|
||||
|
||||
it("does NOT pass dimensions in embedBatch when embeddingDims is not set", async () => {
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).not.toHaveProperty("dimensions");
|
||||
});
|
||||
|
||||
it("passes dimensions in embedBatch when embeddingDims is explicitly set", async () => {
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: [{ embedding: mockEmbedding }, { embedding: mockEmbedding }],
|
||||
});
|
||||
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
embeddingDims: 512,
|
||||
});
|
||||
|
||||
await embedder.embedBatch(["hello", "world"]);
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs).toEqual({
|
||||
model: "text-embedding-3-small",
|
||||
input: ["hello", "world"],
|
||||
dimensions: 512,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("basic functionality", () => {
|
||||
it("embed() returns the embedding vector", async () => {
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
const result = await embedder.embed("hello");
|
||||
expect(result).toEqual(mockEmbedding);
|
||||
});
|
||||
|
||||
it("embedBatch() returns vectors for multiple inputs", async () => {
|
||||
const batch = [
|
||||
[0.1, 0.2],
|
||||
[0.3, 0.4],
|
||||
];
|
||||
mockEmbeddingsCreate.mockResolvedValue({
|
||||
data: batch.map((embedding) => ({ embedding })),
|
||||
});
|
||||
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
});
|
||||
|
||||
const result = await embedder.embedBatch(["text1", "text2"]);
|
||||
expect(result).toEqual(batch);
|
||||
});
|
||||
|
||||
it("uses custom model when provided", async () => {
|
||||
const embedder = new OpenAIEmbedder({
|
||||
apiKey: "test-key",
|
||||
model: "text-embedding-3-large",
|
||||
});
|
||||
|
||||
await embedder.embed("hello");
|
||||
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0];
|
||||
expect(callArgs.model).toBe("text-embedding-3-large");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -676,7 +676,6 @@ class MemoryClient:
|
||||
exclusion_prompt: Optional[str] = None,
|
||||
memory_depth: Optional[str] = None,
|
||||
usecase_setting: Optional[str] = None,
|
||||
multilingual: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the project settings.
|
||||
|
||||
@@ -690,7 +689,6 @@ class MemoryClient:
|
||||
exclusion_prompt: Exclusion prompt for the project
|
||||
memory_depth: Memory depth for the project
|
||||
usecase_setting: Usecase setting for the project
|
||||
multilingual: Whether to use the input language for memory storage and retrieval
|
||||
|
||||
Returns:
|
||||
Dictionary containing the API response.
|
||||
@@ -720,7 +718,6 @@ class MemoryClient:
|
||||
and exclusion_prompt is None
|
||||
and memory_depth is None
|
||||
and usecase_setting is None
|
||||
and multilingual is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Currently we only support updating custom_instructions or "
|
||||
@@ -739,7 +736,6 @@ class MemoryClient:
|
||||
"exclusion_prompt": exclusion_prompt,
|
||||
"memory_depth": memory_depth,
|
||||
"usecase_setting": usecase_setting,
|
||||
"multilingual": multilingual,
|
||||
}
|
||||
)
|
||||
response = self.client.patch(
|
||||
@@ -760,7 +756,6 @@ class MemoryClient:
|
||||
"exclusion_prompt": exclusion_prompt,
|
||||
"memory_depth": memory_depth,
|
||||
"usecase_setting": usecase_setting,
|
||||
"multilingual": multilingual,
|
||||
"sync_type": "sync",
|
||||
},
|
||||
)
|
||||
@@ -1559,7 +1554,6 @@ class AsyncMemoryClient:
|
||||
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
|
||||
enable_graph: Optional[bool] = None,
|
||||
version: Optional[str] = None,
|
||||
multilingual: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the project settings.
|
||||
|
||||
@@ -1569,7 +1563,6 @@ class AsyncMemoryClient:
|
||||
retrieval_criteria: New retrieval criteria for the project
|
||||
enable_graph: Enable or disable the graph for the project
|
||||
version: Version of the project
|
||||
multilingual: Whether to use the input language for memory storage and retrieval
|
||||
|
||||
Returns:
|
||||
Dictionary containing the API response.
|
||||
@@ -1595,7 +1588,6 @@ class AsyncMemoryClient:
|
||||
and retrieval_criteria is None
|
||||
and enable_graph is None
|
||||
and version is None
|
||||
and multilingual is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Currently we only support updating custom_instructions or custom_categories or retrieval_criteria, so you must provide at least one of them"
|
||||
@@ -1608,7 +1600,6 @@ class AsyncMemoryClient:
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"version": version,
|
||||
"multilingual": multilingual,
|
||||
}
|
||||
)
|
||||
response = await self.async_client.patch(
|
||||
@@ -1625,7 +1616,6 @@ class AsyncMemoryClient:
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"version": version,
|
||||
"multilingual": multilingual,
|
||||
"sync_type": "async",
|
||||
},
|
||||
)
|
||||
|
||||
+2
-12
@@ -399,7 +399,6 @@ class Project(BaseProject):
|
||||
custom_categories: Optional[List[str]] = None,
|
||||
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
|
||||
enable_graph: Optional[bool] = None,
|
||||
multilingual: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update project settings.
|
||||
@@ -409,7 +408,6 @@ class Project(BaseProject):
|
||||
custom_categories: New categories for the project
|
||||
retrieval_criteria: New retrieval criteria for the project
|
||||
enable_graph: Enable or disable the graph for the project
|
||||
multilingual: Whether to use the input language for memory storage and retrieval
|
||||
|
||||
Returns:
|
||||
Dictionary containing the API response.
|
||||
@@ -426,12 +424,11 @@ class Project(BaseProject):
|
||||
and custom_categories is None
|
||||
and retrieval_criteria is None
|
||||
and enable_graph is None
|
||||
and multilingual is None
|
||||
):
|
||||
raise ValueError(
|
||||
"At least one parameter must be provided for update: "
|
||||
"custom_instructions, custom_categories, retrieval_criteria, "
|
||||
"enable_graph, multilingual"
|
||||
"enable_graph"
|
||||
)
|
||||
|
||||
payload = self._prepare_params(
|
||||
@@ -440,7 +437,6 @@ class Project(BaseProject):
|
||||
"custom_categories": custom_categories,
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"multilingual": multilingual,
|
||||
}
|
||||
)
|
||||
response = self._client.patch(
|
||||
@@ -456,7 +452,6 @@ class Project(BaseProject):
|
||||
"custom_categories": custom_categories,
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"multilingual": multilingual,
|
||||
"sync_type": "sync",
|
||||
},
|
||||
)
|
||||
@@ -721,7 +716,6 @@ class AsyncProject(BaseProject):
|
||||
custom_categories: Optional[List[str]] = None,
|
||||
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
|
||||
enable_graph: Optional[bool] = None,
|
||||
multilingual: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update project settings.
|
||||
@@ -731,7 +725,6 @@ class AsyncProject(BaseProject):
|
||||
custom_categories: New categories for the project
|
||||
retrieval_criteria: New retrieval criteria for the project
|
||||
enable_graph: Enable or disable the graph for the project
|
||||
multilingual: Whether to use the input language for memory storage and retrieval
|
||||
|
||||
Returns:
|
||||
Dictionary containing the API response.
|
||||
@@ -748,12 +741,11 @@ class AsyncProject(BaseProject):
|
||||
and custom_categories is None
|
||||
and retrieval_criteria is None
|
||||
and enable_graph is None
|
||||
and multilingual is None
|
||||
):
|
||||
raise ValueError(
|
||||
"At least one parameter must be provided for update: "
|
||||
"custom_instructions, custom_categories, retrieval_criteria, "
|
||||
"enable_graph, multilingual"
|
||||
"enable_graph"
|
||||
)
|
||||
|
||||
payload = self._prepare_params(
|
||||
@@ -762,7 +754,6 @@ class AsyncProject(BaseProject):
|
||||
"custom_categories": custom_categories,
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"multilingual": multilingual,
|
||||
}
|
||||
)
|
||||
response = await self._client.patch(
|
||||
@@ -778,7 +769,6 @@ class AsyncProject(BaseProject):
|
||||
"custom_categories": custom_categories,
|
||||
"retrieval_criteria": retrieval_criteria,
|
||||
"enable_graph": enable_graph,
|
||||
"multilingual": multilingual,
|
||||
"sync_type": "async",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class AzureMySQLConfig(BaseModel):
|
||||
@@ -80,4 +80,5 @@ class AzureMySQLConfig(BaseModel):
|
||||
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class CassandraConfig(BaseModel):
|
||||
@@ -72,5 +72,6 @@ class CassandraConfig(BaseModel):
|
||||
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class ElasticsearchConfig(BaseModel):
|
||||
@@ -63,5 +63,3 @@ class ElasticsearchConfig(BaseModel):
|
||||
f"Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class MongoDBConfig(BaseModel):
|
||||
@@ -23,5 +23,3 @@ class MongoDBConfig(BaseModel):
|
||||
f"Please provide only the following fields: {', '.join(allowed_fields)}."
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=False)
|
||||
|
||||
@@ -5,7 +5,7 @@ This module provides configuration settings for integrating with Amazon Neptune
|
||||
as a vector store backend for Mem0's memory layer.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class NeptuneAnalyticsConfig(BaseModel):
|
||||
@@ -22,4 +22,6 @@ class NeptuneAnalyticsConfig(BaseModel):
|
||||
collection_name: str = Field("mem0", description="Default name for the collection")
|
||||
endpoint: str = Field("endpoint", description="Graph ID for the runtime")
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=False)
|
||||
model_config = {
|
||||
"arbitrary_types_allowed": False,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, Optional, Type, Union
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class OpenSearchConfig(BaseModel):
|
||||
@@ -39,5 +39,3 @@ class OpenSearchConfig(BaseModel):
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Allowed fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class PGVectorConfig(BaseModel):
|
||||
@@ -50,5 +50,3 @@ class PGVectorConfig(BaseModel):
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
class IndexMethod(str, Enum):
|
||||
@@ -42,5 +42,3 @@ class SupabaseConfig(BaseModel):
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=False)
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ValkeyConfig(BaseModel):
|
||||
"""Configuration for Valkey vector store."""
|
||||
|
||||
valkey_url: str = Field(..., description="Valkey server URL (e.g., redis://localhost:6379)")
|
||||
collection_name: str = Field(..., description="Name of the index / collection")
|
||||
embedding_model_dims: int = Field(..., description="Dimensions of the embedding model")
|
||||
timezone: str = Field("UTC", description="Timezone for timestamp handling")
|
||||
index_type: str = Field("hnsw", description="Index type: 'hnsw' (default) or 'flat'")
|
||||
hnsw_m: int = Field(16, description="HNSW: number of connections per layer")
|
||||
hnsw_ef_construction: int = Field(200, description="HNSW: search width during index construction")
|
||||
hnsw_ef_runtime: int = Field(10, description="HNSW: search width during queries")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
input_fields = set(values.keys())
|
||||
extra_fields = input_fields - allowed_fields
|
||||
if extra_fields:
|
||||
raise ValueError(
|
||||
f"Extra fields not allowed: {', '.join(extra_fields)}. "
|
||||
f"Please input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=False)
|
||||
valkey_url: str
|
||||
collection_name: str
|
||||
embedding_model_dims: int
|
||||
timezone: str = "UTC"
|
||||
index_type: str = "hnsw" # Default to HNSW, can be 'hnsw' or 'flat'
|
||||
# HNSW specific parameters with recommended defaults
|
||||
hnsw_m: int = 16 # Number of connections per layer (default from Valkey docs)
|
||||
hnsw_ef_construction: int = 200 # Search width during construction
|
||||
hnsw_ef_runtime: int = 10 # Search width during queries
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ class GroqLLM(LLMBase):
|
||||
super().__init__(config)
|
||||
|
||||
if not self.config.model:
|
||||
self.config.model = "llama-3.3-70b-versatile"
|
||||
self.config.model = "llama3-70b-8192"
|
||||
|
||||
api_key = self.config.api_key or os.getenv("GROQ_API_KEY")
|
||||
self.client = Groq(api_key=api_key)
|
||||
|
||||
@@ -304,23 +304,6 @@ class Memory(MemoryBase):
|
||||
)
|
||||
capture_event("mem0.init", self, {"sync_type": "sync"})
|
||||
|
||||
def close(self):
|
||||
"""Release resources held by this Memory instance (SQLite connections, etc.).
|
||||
|
||||
The global telemetry singleton is intentionally *not* shut down here
|
||||
because it is shared across all Memory instances in the process. It is
|
||||
cleaned up automatically at process exit via an ``atexit`` handler.
|
||||
"""
|
||||
if hasattr(self, "db") and self.db is not None:
|
||||
self.db.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config_dict: Dict[str, Any]):
|
||||
try:
|
||||
@@ -1442,18 +1425,6 @@ class AsyncMemory(MemoryBase):
|
||||
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
|
||||
capture_event("mem0.init", self, {"sync_type": "async"})
|
||||
|
||||
def close(self):
|
||||
"""Release resources held by this AsyncMemory instance."""
|
||||
if hasattr(self, "db") and self.db is not None:
|
||||
self.db.close()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config_dict: Dict[str, Any]):
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from posthog import Posthog
|
||||
|
||||
@@ -57,62 +55,20 @@ class AnonymousTelemetry:
|
||||
def close(self):
|
||||
if self.posthog is not None:
|
||||
self.posthog.shutdown()
|
||||
self.posthog = None
|
||||
|
||||
|
||||
# Thread-safe lazy singleton for OSS telemetry.
|
||||
# A single AnonymousTelemetry instance (and its underlying PostHog client /
|
||||
# background thread) is reused for the lifetime of the process instead of
|
||||
# creating a new one on every capture_event() call. The singleton is shut down
|
||||
# once at process exit via an atexit handler.
|
||||
_oss_telemetry_instance = None
|
||||
_oss_telemetry_lock = threading.Lock()
|
||||
_oss_telemetry_shutting_down = False
|
||||
|
||||
|
||||
def _get_oss_telemetry():
|
||||
"""Return the process-wide AnonymousTelemetry singleton, creating it on first call.
|
||||
|
||||
Returns None after _shutdown_oss_telemetry() has run (interpreter exit).
|
||||
"""
|
||||
global _oss_telemetry_instance
|
||||
if _oss_telemetry_shutting_down:
|
||||
return None
|
||||
if _oss_telemetry_instance is not None:
|
||||
return _oss_telemetry_instance
|
||||
|
||||
with _oss_telemetry_lock:
|
||||
if _oss_telemetry_shutting_down:
|
||||
return None
|
||||
# Double-checked locking
|
||||
if _oss_telemetry_instance is not None:
|
||||
return _oss_telemetry_instance
|
||||
_oss_telemetry_instance = AnonymousTelemetry()
|
||||
atexit.register(_shutdown_oss_telemetry)
|
||||
return _oss_telemetry_instance
|
||||
|
||||
|
||||
def _shutdown_oss_telemetry():
|
||||
global _oss_telemetry_instance, _oss_telemetry_shutting_down
|
||||
with _oss_telemetry_lock:
|
||||
_oss_telemetry_shutting_down = True
|
||||
if _oss_telemetry_instance is not None:
|
||||
_oss_telemetry_instance.close()
|
||||
_oss_telemetry_instance = None
|
||||
|
||||
|
||||
# Module-level client telemetry singleton (used by capture_client_event).
|
||||
client_telemetry = AnonymousTelemetry()
|
||||
atexit.register(client_telemetry.close)
|
||||
|
||||
|
||||
def capture_event(event_name, memory_instance, additional_data=None):
|
||||
if not MEM0_TELEMETRY:
|
||||
return
|
||||
|
||||
oss_telemetry = _get_oss_telemetry()
|
||||
if oss_telemetry is None:
|
||||
return
|
||||
oss_telemetry = AnonymousTelemetry(
|
||||
vector_store=memory_instance._telemetry_vector_store
|
||||
if hasattr(memory_instance, "_telemetry_vector_store")
|
||||
else None,
|
||||
)
|
||||
|
||||
event_data = {
|
||||
"collection": memory_instance.collection_name,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client.models import (
|
||||
DatetimeRange,
|
||||
Distance,
|
||||
FieldCondition,
|
||||
Filter,
|
||||
@@ -141,23 +139,6 @@ class Qdrant(VectorStoreBase):
|
||||
]
|
||||
self.client.upsert(collection_name=self.collection_name, points=points)
|
||||
|
||||
# ISO 8601 datetime pattern for detecting datetime strings in range filters
|
||||
_ISO_DATETIME_RE = re.compile(
|
||||
r"^\d{4}-\d{2}-\d{2}" # date part
|
||||
r"([T ]\d{2}:\d{2}(:\d{2})?" # optional time part
|
||||
r"(\.\d+)?" # optional fractional seconds
|
||||
r"(Z|[+-]\d{2}:?\d{2})?" # optional timezone
|
||||
r")?$"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_datetime_range(range_kwargs: dict) -> bool:
|
||||
"""Check if all values in range kwargs are ISO datetime strings."""
|
||||
return all(
|
||||
isinstance(v, str) and Qdrant._ISO_DATETIME_RE.match(v)
|
||||
for v in range_kwargs.values()
|
||||
)
|
||||
|
||||
def _build_field_condition(self, key: str, value) -> Optional[FieldCondition]:
|
||||
"""
|
||||
Build a single FieldCondition from a key-value filter pair.
|
||||
@@ -196,13 +177,6 @@ class Qdrant(VectorStoreBase):
|
||||
f"Use AND to combine them as separate conditions."
|
||||
)
|
||||
range_kwargs = {op: value[op] for op in range_ops if op in value}
|
||||
if self._is_datetime_range(range_kwargs):
|
||||
try:
|
||||
return FieldCondition(key=key, range=DatetimeRange(**range_kwargs))
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ValueError(
|
||||
f"Invalid datetime value in range filter for field '{key}': {e}"
|
||||
) from e
|
||||
return FieldCondition(key=key, range=Range(**range_kwargs))
|
||||
elif "eq" in value:
|
||||
return FieldCondition(key=key, match=MatchValue(value=value["eq"]))
|
||||
|
||||
@@ -2,84 +2,6 @@
|
||||
|
||||
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
|
||||
|
||||
## [1.0.4] - 2026-04-04
|
||||
|
||||
### Added
|
||||
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
|
||||
- **`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`
|
||||
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
|
||||
- **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 (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`
|
||||
- **`mem0ai` dependency**: Updated from `2.3.0` to `2.4.5`
|
||||
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race` — if the LLM takes too long, recall is skipped instead of stalling the gateway
|
||||
- **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
|
||||
|
||||
### Fixed
|
||||
- **Path traversal vulnerability**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal via `config.domain` or the exported `loadSkill` API
|
||||
- **Noise filter regression**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction` so the filter correctly matches real upstream compaction audit messages
|
||||
- **Cosmetic revert**: Restored `// Over-fetch for ranking` comment in `recall.ts` (was changed to work around a false-positive scanner match on the substring `fetch`)
|
||||
|
||||
### Changed
|
||||
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
|
||||
|
||||
### Added
|
||||
- **Path traversal tests**: 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
|
||||
|
||||
## [1.0.2] - 2026-04-02
|
||||
|
||||
### Fixed
|
||||
- **Security scanner warning**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — OpenClaw already resolves `${VAR}` in `openclaw.json` before passing config to the plugin, so plugin-side env resolution was redundant and triggered the "credential harvesting" static analysis warning ([#4676](https://github.com/mem0ai/mem0/pull/4676))
|
||||
|
||||
## [1.0.1] - 2026-04-02
|
||||
|
||||
### Added
|
||||
- **CD workflow**: Added continuous deployment workflow for `@mem0/openclaw-mem0` with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
|
||||
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` specifying minimum gateway version and OpenClaw SDK compatibility (`>=2026.3.24-beta.2`) ([#4667](https://github.com/mem0ai/mem0/pull/4667))
|
||||
- **LICENSE**: Added Apache-2.0 license file to the package ([#4667](https://github.com/mem0ai/mem0/pull/4667))
|
||||
|
||||
### Fixed
|
||||
- **Dream gate correctness**: Fixed cheap-first ordering, session isolation, and verified completion in the dream gate memory consolidation pipeline ([#4666](https://github.com/mem0ai/mem0/pull/4666))
|
||||
- **Graceful startup without API key**: Plugin now starts gracefully when no API key is configured instead of crashing on init ([#4669](https://github.com/mem0ai/mem0/pull/4669))
|
||||
|
||||
## [1.0.0] - 2026-04-01
|
||||
|
||||
### Added
|
||||
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction for higher quality memory capture ([#4624](https://github.com/mem0ai/mem0/pull/4624))
|
||||
- **Dream gate**: Added `dream-gate.ts` for memory consolidation and dream-cycle processing
|
||||
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
|
||||
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
|
||||
- **Memory dream skill**: Skill for memory consolidation during idle periods
|
||||
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
|
||||
|
||||
### Changed
|
||||
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
|
||||
|
||||
## [0.4.1] - 2026-03-26
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2023] [Taranjeet Singh]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,175 +0,0 @@
|
||||
# ============================================================================
|
||||
# Agentic Memory Skills — OpenClaw Plugin Installer
|
||||
# ============================================================================
|
||||
#
|
||||
# Usage:
|
||||
# make install # Interactive — prompts for API key
|
||||
# make install MEM0_API_KEY=m0-xxx # Non-interactive
|
||||
# make uninstall # Revert to stock plugin
|
||||
# make restart # Rebuild + restart gateway
|
||||
# make status # Check everything is working
|
||||
# make logs # Tail gateway logs filtered to mem0
|
||||
# make clean # Full teardown
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
SHELL := /bin/bash
|
||||
.PHONY: install uninstall restart status logs clean build check-deps configure help
|
||||
|
||||
# Defaults
|
||||
MEM0_API_KEY ?=
|
||||
MEM0_USER_ID ?= $(shell whoami)
|
||||
OPENCLAW_CONFIG := $(HOME)/.openclaw/openclaw.json
|
||||
PLUGIN_DIR := $(shell pwd)
|
||||
|
||||
help: ## Show this help
|
||||
@echo ""
|
||||
@echo " Agentic Memory Skills — OpenClaw Plugin"
|
||||
@echo " ========================================"
|
||||
@echo ""
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Dependency checks
|
||||
# ============================================================================
|
||||
|
||||
check-deps:
|
||||
@echo "Checking dependencies..."
|
||||
@command -v node >/dev/null 2>&1 || { echo "Error: Node.js is required. Install from https://nodejs.org"; exit 1; }
|
||||
@NODE_MAJOR=$$(node -v | sed 's/v//' | cut -d. -f1); \
|
||||
if [ "$$NODE_MAJOR" -lt 18 ]; then echo "Error: Node.js >= 18 required (found $$(node -v))"; exit 1; fi
|
||||
@command -v npm >/dev/null 2>&1 || { echo "Error: npm is required"; exit 1; }
|
||||
@if ! command -v openclaw >/dev/null 2>&1; then \
|
||||
echo "OpenClaw not found. Installing..."; \
|
||||
npm install -g openclaw || { echo "Error: Failed to install OpenClaw. Try: sudo npm install -g openclaw"; exit 1; }; \
|
||||
echo "OpenClaw installed: $$(openclaw --version)"; \
|
||||
else \
|
||||
echo "OpenClaw: $$(openclaw --version 2>/dev/null || echo 'installed')"; \
|
||||
fi
|
||||
@echo "Node.js: $$(node -v)"
|
||||
@echo "All dependencies OK."
|
||||
|
||||
# ============================================================================
|
||||
# Build
|
||||
# ============================================================================
|
||||
|
||||
build: ## Build the plugin from source
|
||||
@echo "Installing npm dependencies..."
|
||||
@npm install --silent 2>/dev/null
|
||||
@echo "Building plugin..."
|
||||
@npm run build --silent 2>/dev/null
|
||||
@echo "Build OK: $$(ls -lh dist/index.js | awk '{print $$5}') ESM bundle"
|
||||
|
||||
# ============================================================================
|
||||
# Install
|
||||
# ============================================================================
|
||||
|
||||
install: check-deps build ## Full install — build, link, configure, restart
|
||||
@# Remove existing plugin (suppress errors if not installed)
|
||||
@echo "Removing existing openclaw-mem0 plugin (if any)..."
|
||||
@echo "y" | openclaw plugins uninstall openclaw-mem0 >/dev/null 2>&1 || true
|
||||
@# Link local build
|
||||
@echo "Linking local plugin build..."
|
||||
@openclaw plugins install "$(PLUGIN_DIR)" --link 2>&1 | grep -v "plugins.allow"
|
||||
@# Configure AFTER link (link may overwrite config)
|
||||
@$(MAKE) --no-print-directory configure
|
||||
@# Restart gateway
|
||||
@echo "Restarting gateway..."
|
||||
@kill -9 $$(lsof -ti:18789) 2>/dev/null || true
|
||||
@sleep 2
|
||||
@openclaw gateway >/dev/null 2>&1 &
|
||||
@sleep 4
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Installation complete!"
|
||||
@echo "============================================"
|
||||
@echo ""
|
||||
@echo " Plugin: openclaw-mem0 (skills mode)"
|
||||
@echo " User: $(MEM0_USER_ID)"
|
||||
@echo " Web UI: http://127.0.0.1:18789"
|
||||
@echo ""
|
||||
@echo " Verify with: make status"
|
||||
@echo " View logs: make logs"
|
||||
@echo ""
|
||||
|
||||
# ============================================================================
|
||||
# Configure — patch openclaw.json with required settings
|
||||
# ============================================================================
|
||||
|
||||
configure:
|
||||
@if [ -z "$(MEM0_API_KEY)" ]; then \
|
||||
echo ""; \
|
||||
echo " Enter your Mem0 API key (from https://app.mem0.ai/dashboard/api-keys):"; \
|
||||
echo -n " > "; \
|
||||
read -r key; \
|
||||
if [ -z "$$key" ]; then echo "Error: API key is required."; exit 1; fi; \
|
||||
MEM0_API_KEY="$$key" MEM0_USER_ID="$(MEM0_USER_ID)" python3 scripts/configure.py; \
|
||||
else \
|
||||
MEM0_API_KEY="$(MEM0_API_KEY)" MEM0_USER_ID="$(MEM0_USER_ID)" python3 scripts/configure.py; \
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Status & Logs
|
||||
# ============================================================================
|
||||
|
||||
status: ## Check plugin health and skills registration
|
||||
@echo "Plugin health:"
|
||||
@openclaw plugins doctor 2>&1 | grep -v "plugins.allow"
|
||||
@echo ""
|
||||
@echo "Skills:"
|
||||
@openclaw skills list 2>&1 | grep -E "memory|Status|---" | head -10
|
||||
@echo ""
|
||||
@echo "Last gateway registration:"
|
||||
@tail -50 /tmp/openclaw/openclaw-$$(date +%Y-%m-%d).log 2>/dev/null | grep "openclaw-mem0: registered" | tail -1 | grep -o '"1":"[^"]*"' || echo " No gateway log found. Is the gateway running?"
|
||||
|
||||
logs: ## Tail gateway logs (mem0 activity only)
|
||||
@tail -f /tmp/openclaw/openclaw-$$(date +%Y-%m-%d).log 2>/dev/null | grep --line-buffered -o '"1":"[^"]*"' | grep --line-buffered -i "mem0\|skills-mode\|stored\|inject\|recall"
|
||||
|
||||
# ============================================================================
|
||||
# Restart
|
||||
# ============================================================================
|
||||
|
||||
restart: build ## Rebuild plugin and restart gateway
|
||||
@echo "Restarting gateway..."
|
||||
@kill -9 $$(lsof -ti:18789) 2>/dev/null || true
|
||||
@sleep 2
|
||||
@openclaw gateway >/dev/null 2>&1 &
|
||||
@sleep 4
|
||||
@echo "Gateway restarted. Verify: make status"
|
||||
|
||||
# ============================================================================
|
||||
# Uninstall — revert to stock plugin
|
||||
# ============================================================================
|
||||
|
||||
uninstall: ## Revert to stock openclaw-mem0 from npm
|
||||
@echo "Uninstalling local plugin..."
|
||||
@echo "y" | openclaw plugins uninstall openclaw-mem0 >/dev/null 2>&1 || true
|
||||
@echo "Installing stock plugin from npm..."
|
||||
@openclaw plugins install @mem0/openclaw-mem0 2>&1 | grep -v "plugins.allow"
|
||||
@# Restore config backup if it exists
|
||||
@if [ -f "$(OPENCLAW_CONFIG).pre-skills-backup" ]; then \
|
||||
cp "$(OPENCLAW_CONFIG).pre-skills-backup" "$(OPENCLAW_CONFIG)"; \
|
||||
echo "Restored config from backup."; \
|
||||
else \
|
||||
echo "Note: No config backup found. You may need to manually revert openclaw.json changes."; \
|
||||
fi
|
||||
@echo "Restarting gateway..."
|
||||
@kill -9 $$(lsof -ti:18789) 2>/dev/null || true
|
||||
@sleep 2
|
||||
@openclaw gateway >/dev/null 2>&1 &
|
||||
@sleep 4
|
||||
@echo "Reverted to stock openclaw-mem0."
|
||||
|
||||
# ============================================================================
|
||||
# Clean
|
||||
# ============================================================================
|
||||
|
||||
clean: ## Full teardown — stop gateway, remove plugin, clean build
|
||||
@echo "Stopping gateway..."
|
||||
@kill -9 $$(lsof -ti:18789) 2>/dev/null || true
|
||||
@echo "Removing plugin..."
|
||||
@echo "y" | openclaw plugins uninstall openclaw-mem0 >/dev/null 2>&1 || true
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf dist/ node_modules/
|
||||
@echo "Clean complete."
|
||||
+151
-103
@@ -2,175 +2,223 @@
|
||||
|
||||
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [Mem0](https://mem0.ai).
|
||||
|
||||
Your agent forgets everything between sessions. This plugin fixes that — it watches conversations, extracts what matters, and brings it back when relevant. Automatically.
|
||||
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
|
||||
## How it works
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
||||
</p>
|
||||
|
||||
**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for memories that match the current message and injects them into context.
|
||||
|
||||
**Auto-Capture** — After the agent responds, the plugin filters the conversation through a noise-removal pipeline, then sends the cleaned exchange to Mem0. Mem0 decides what's worth keeping — new facts get stored, stale ones updated, duplicates merged.
|
||||
|
||||
Both run silently. No prompting, no configuration, no manual calls.
|
||||
|
||||
### Message filtering
|
||||
|
||||
Before extraction, messages pass through a multi-stage filtering pipeline:
|
||||
|
||||
1. **Noise detection** — Drops entire messages that are system noise: heartbeats (`HEARTBEAT_OK`, `NO_REPLY`), timestamps, single-word acknowledgments (`ok`, `sure`, `done`), system routing metadata, and compaction audit logs.
|
||||
2. **Generic assistant detection** — Drops short assistant messages that are boilerplate acknowledgments with no extractable facts (e.g. "I see you've shared an update. How can I help?").
|
||||
3. **Content stripping** — Removes embedded noise fragments (media boilerplate, routing metadata, compaction blocks) from otherwise useful messages.
|
||||
4. **Truncation** — Caps messages at 2000 characters to avoid sending excessive context.
|
||||
|
||||
### Short-term vs long-term memory
|
||||
|
||||
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 and automatically recalled alongside long-term memories.
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
The agent tools (`memory_search`, `memory_list`) 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.
|
||||
|
||||
All new parameters are optional and backward-compatible — existing configurations work without changes.
|
||||
|
||||
### Per-agent memory isolation
|
||||
|
||||
In multi-agent setups, each agent automatically gets its own memory namespace. Session keys following the pattern `agent:<agentId>:<uuid>` are parsed to derive isolated namespaces (`${userId}:agent:${agentId}`). Single-agent deployments are unaffected — plain session keys and `agent:main:*` keys resolve to the configured `userId`.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- The agent's session key is inspected on every recall/capture cycle
|
||||
- If the key matches `agent:<name>:<uuid>`, memories are stored under `userId:agent:<name>`
|
||||
- Different agents never see each other's memories unless explicitly queried
|
||||
|
||||
**Subagent handling:**
|
||||
|
||||
Ephemeral subagents (session keys like `agent:main:subagent:<uuid>`) are handled specially:
|
||||
- **Recall** is routed to the parent (main user) namespace — subagents get the user's long-term context instead of searching their empty ephemeral namespace
|
||||
- **Capture** is skipped entirely — the main agent's `agent_end` hook captures the consolidated result including subagent output, preventing orphaned memories
|
||||
- A **subagent-specific preamble** is used: "You are a subagent — use these memories for context but do not assume you are this user"
|
||||
|
||||
**Explicit cross-agent queries:**
|
||||
|
||||
All memory tools (`memory_search`, `memory_store`, `memory_list`, `memory_forget`) accept an optional `agentId` parameter to query another agent's namespace:
|
||||
|
||||
```
|
||||
memory_search({ query: "user's tech stack", agentId: "researcher" })
|
||||
```
|
||||
|
||||
The `agentId` is always namespaced under the configured `userId` (e.g. `agentId: "researcher"` → `utkarsh:agent:researcher`), so it cannot be used to access other users' namespaces.
|
||||
|
||||
### Concurrency safety
|
||||
|
||||
Lifecycle hooks (`before_agent_start`, `agent_end`) use `ctx.sessionKey` directly from the event context rather than shared mutable state. This prevents race conditions when multiple sessions run concurrently (e.g. multiple Telegram users chatting simultaneously).
|
||||
|
||||
Tools still read from a best-effort `currentSessionId` variable (since tools don't receive `ctx`), but hooks — where the critical recall and capture logic runs — are fully concurrency-safe.
|
||||
|
||||
### Non-interactive trigger filtering
|
||||
|
||||
The plugin automatically skips recall and capture for non-interactive triggers: `cron`, `heartbeat`, `automation`, and `schedule`. Detection works via both `ctx.trigger` and session key patterns (`:cron:`, `:heartbeat:`). This prevents system-generated noise from polluting long-term memory.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
openclaw plugins install @mem0/openclaw-mem0
|
||||
```
|
||||
|
||||
### Understanding `userId`
|
||||
|
||||
The `userId` field is a **string you choose** to uniquely identify the user whose memories are being stored. It is **not** something you look up in the Mem0 dashboard — you define it yourself.
|
||||
|
||||
Pick any stable, unique identifier for the user. Common choices:
|
||||
|
||||
- Your application's internal user ID (e.g. `"user_123"`, `"alice@example.com"`)
|
||||
- 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.
|
||||
|
||||
> **Tip:** In a multi-user application, set `userId` dynamically per user (e.g. from your auth system) rather than hardcoding a single value.
|
||||
|
||||
### Platform (Mem0 Cloud)
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
Or configure manually in `openclaw.json`:
|
||||
Get an API key from [app.mem0.ai](https://app.mem0.ai), then add to your `openclaw.json`:
|
||||
|
||||
```json5
|
||||
// plugins.entries
|
||||
"openclaw-mem0": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiKey": "${MEM0_API_KEY}",
|
||||
"userId": "alice"
|
||||
"userId": "alice" // any unique identifier you choose for this user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Open-Source (Self-hosted)
|
||||
|
||||
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.
|
||||
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM.
|
||||
|
||||
```json5
|
||||
"openclaw-mem0": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"mode": "open-source",
|
||||
"userId": "alice"
|
||||
"userId": "alice" // any unique identifier you choose for this user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Customize the embedder, vector store, or LLM via the `oss` block:
|
||||
Sensible defaults out of the box. To customize the embedder, vector store, or LLM:
|
||||
|
||||
```json5
|
||||
"config": {
|
||||
"mode": "open-source",
|
||||
"userId": "alice",
|
||||
"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-5.4" } }
|
||||
"llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All `oss` fields are optional. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for supported providers.
|
||||
All `oss` fields are optional. See [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for providers.
|
||||
|
||||
## How It Works
|
||||
## Agent tools
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
||||
</p>
|
||||
|
||||
**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
|
||||
|
||||
**Auto-Capture** — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
|
||||
|
||||
Both run silently. No prompting, no manual calls required.
|
||||
|
||||
### Memory Scopes
|
||||
|
||||
- **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
|
||||
|
||||
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
|
||||
|
||||
Eight tools are registered for agent use:
|
||||
The agent gets five tools it can call during conversations:
|
||||
|
||||
| Tool | Description |
|
||||
| ---- | ----------- |
|
||||
| `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. |
|
||||
|------|-------------|
|
||||
| `memory_search` | Search memories by natural language. Optional `agentId` to scope to a specific agent, `scope` to filter by session/long-term. |
|
||||
| `memory_list` | List all stored memories. Optional `agentId` to scope to a specific agent, `scope` to filter. |
|
||||
| `memory_store` | Explicitly save a fact. Optional `agentId` to store under a specific agent's namespace, `longTerm` to choose scope. |
|
||||
| `memory_get` | Retrieve a memory by ID. |
|
||||
| `memory_forget` | Delete by ID or by query. Optional `agentId` to scope deletion to a specific agent. |
|
||||
|
||||
## CLI
|
||||
|
||||
All commands: `openclaw mem0 <command>`.
|
||||
|
||||
```bash
|
||||
# Memory operations
|
||||
openclaw mem0 add "User prefers TypeScript over JavaScript"
|
||||
# Search all memories (long-term + session)
|
||||
openclaw mem0 search "what languages does the user know"
|
||||
openclaw mem0 search "preferences" --scope long-term
|
||||
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
|
||||
|
||||
# Management
|
||||
openclaw mem0 init
|
||||
openclaw mem0 init --api-key <key> --user-id alice
|
||||
openclaw mem0 status
|
||||
openclaw mem0 config show
|
||||
openclaw mem0 config get api_key
|
||||
openclaw mem0 config set user_id alice
|
||||
# Search only long-term memories
|
||||
openclaw mem0 search "what languages does the user know" --scope long-term
|
||||
|
||||
# Events (platform only)
|
||||
openclaw mem0 event list
|
||||
openclaw mem0 event status <event_id>
|
||||
# Search only session/short-term memories
|
||||
openclaw mem0 search "what languages does the user know" --scope session
|
||||
|
||||
# Memory consolidation
|
||||
openclaw mem0 dream
|
||||
openclaw mem0 dream --dry-run
|
||||
# Stats
|
||||
openclaw mem0 stats
|
||||
|
||||
# Search a specific agent's memories
|
||||
openclaw mem0 search "user preferences" --agent researcher
|
||||
|
||||
# Stats for a specific agent
|
||||
openclaw mem0 stats --agent researcher
|
||||
```
|
||||
|
||||
## Configuration Reference
|
||||
## Options
|
||||
|
||||
### General
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
||||
| `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 |
|
||||
| `searchThreshold` | `number` | `0.5` | Minimum similarity score (0-1) |
|
||||
| Key | Type | Default | |
|
||||
|-----|------|---------|---|
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
|
||||
| `userId` | `string` | `"default"` | Any unique identifier you choose for the user (e.g. `"alice"`, `"user_123"`). All memories are scoped to this value. Not found in any dashboard — you define it yourself. |
|
||||
| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
|
||||
| `autoCapture` | `boolean` | `true` | Store facts after each turn |
|
||||
| `topK` | `number` | `5` | Max memories per recall |
|
||||
| `searchThreshold` | `number` | `0.5` | Min similarity (0–1) |
|
||||
|
||||
### Platform Mode
|
||||
### Platform mode
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| Key | Type | Default | |
|
||||
|-----|------|---------|---|
|
||||
| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
|
||||
| `orgId` | `string` | — | Organization ID |
|
||||
| `projectId` | `string` | — | Project ID |
|
||||
| `enableGraph` | `boolean` | `false` | Entity graph for relationship tracking |
|
||||
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules |
|
||||
| `customCategories` | `object` | *(12 defaults)* | Category name to description map |
|
||||
| `enableGraph` | `boolean` | `false` | Entity graph for relationships |
|
||||
| `customInstructions` | `string` | *(built-in)* | Extraction rules — what to store, how to format. Built-in instructions include temporal anchoring, conciseness, outcome-over-intent, deduplication, and language preservation guidelines. |
|
||||
| `customCategories` | `object` | *(12 defaults)* | Category name → description map for tagging |
|
||||
|
||||
### Open-Source Mode
|
||||
### Open-source mode
|
||||
|
||||
All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM.
|
||||
Works with zero extra config. The `oss` block lets you swap out any component:
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `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 (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 edit history |
|
||||
| `oss.disableHistory` | `boolean` | `false` | Skip history DB |
|
||||
| Key | Type | Default | |
|
||||
|-----|------|---------|---|
|
||||
| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
|
||||
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider (`"openai"`, `"ollama"`, `"lmstudio"`, etc.) |
|
||||
| `oss.embedder.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL` |
|
||||
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store (`"memory"`, `"qdrant"`, `"chroma"`, etc.) |
|
||||
| `oss.vectorStore.config` | `object` | — | Provider config: `host`, `port`, `collectionName`, `dimension` |
|
||||
| `oss.llm.provider` | `string` | `"openai"` | LLM provider (`"openai"`, `"anthropic"`, `"ollama"`, `"lmstudio"`, etc.) |
|
||||
| `oss.llm.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL`, `temperature` |
|
||||
| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
|
||||
| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization (useful when native SQLite bindings fail) |
|
||||
|
||||
Everything inside `oss` is optional — defaults use OpenAI embeddings (`text-embedding-3-small`), in-memory vector store, and OpenAI LLM. Override only what you need.
|
||||
|
||||
> **SQLite resilience:** If the history DB fails to initialize (e.g. native binding resolution under jiti), the plugin automatically retries with history disabled. Core memory operations (add, search, get, delete) work without the history DB.
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](LICENSE)
|
||||
Apache 2.0
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Mirrored from cli/node/src/backend/base.ts — DO NOT DIVERGE
|
||||
|
||||
/**
|
||||
* Abstract backend interface and error classes.
|
||||
*/
|
||||
|
||||
export interface AddOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
immutable?: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string[];
|
||||
enableGraph?: boolean;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
topK?: number;
|
||||
threshold?: number;
|
||||
rerank?: boolean;
|
||||
keyword?: boolean;
|
||||
filters?: Record<string, unknown>;
|
||||
fields?: string[];
|
||||
enableGraph?: boolean;
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
enableGraph?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteOptions {
|
||||
all?: boolean;
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
}
|
||||
|
||||
export interface EntityIds {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
}
|
||||
|
||||
export interface Backend {
|
||||
add(
|
||||
content?: string,
|
||||
messages?: Record<string, unknown>[],
|
||||
opts?: AddOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
search(
|
||||
query: string,
|
||||
opts?: SearchOptions,
|
||||
): Promise<Record<string, unknown>[]>;
|
||||
|
||||
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||
|
||||
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
|
||||
|
||||
update(
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
delete(
|
||||
memoryId?: string,
|
||||
opts?: DeleteOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||
|
||||
status(opts?: {
|
||||
userId?: string;
|
||||
agentId?: string;
|
||||
}): Promise<Record<string, unknown>>;
|
||||
|
||||
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||
|
||||
listEvents(): Promise<Record<string, unknown>[]>;
|
||||
|
||||
getEvent(eventId: string): Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export class AuthError extends Error {
|
||||
constructor(
|
||||
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||
) {
|
||||
super(message);
|
||||
this.name = "AuthError";
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends Error {
|
||||
constructor(path: string) {
|
||||
super(`Resource not found: ${path}`);
|
||||
this.name = "NotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export class APIError extends Error {
|
||||
constructor(path: string, detail: string) {
|
||||
super(`Bad request to ${path}: ${detail}`);
|
||||
this.name = "APIError";
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export { PlatformBackend } from "./platform.ts";
|
||||
export {
|
||||
type Backend,
|
||||
type AddOptions,
|
||||
type SearchOptions,
|
||||
type ListOptions,
|
||||
type DeleteOptions,
|
||||
type EntityIds,
|
||||
AuthError,
|
||||
NotFoundError,
|
||||
APIError,
|
||||
} from "./base.ts";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user