Compare commits

..
Author SHA1 Message Date
KartikandGitHub 144627c4ce fix(docs): change the position of the openclaw to agnet plugin and fix the integrations overview and sidebar list view (#4722) 2026-04-06 16:57:24 +05:30
KartikandGitHub 6984958138 chore: sat release (#4702) 2026-04-06 16:55:08 +05:30
KartikandGitHub b13748c446 feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704) 2026-04-06 16:54:27 +05:30
Saket AryanandGitHub 4642a1d6e3 feat(cli): validate API key upfront via ping and unify telemetry identity resolution (#4701) 2026-04-04 23:03:27 +05:30
686d5e987d fix: openclaw plugin and fix the login section there (#4696)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-04 22:21:46 +05:30
DEVAN CHAUHANandGitHub c55447c1e4 [fix] groq model (#4700) 2026-04-04 21:36:17 +05:30
Saket AryanandGitHub ee67602c58 feat(cli): add PostHog telemetry and source tracking to Python & Node CLIs (#4699) 2026-04-04 20:47:38 +05:30
0daa5d7d03 fix(ci): handle npm prerelease publish across Node.js CD workflows (#4690)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:17:11 +05:30
KartikandGitHub cfb3f58e4a fix: adding login and fixing the plugin to follow the openclaw plugin standards (#4686) 2026-04-03 20:59:23 +05:30
KartikandGitHub 66230b3f1f docs: update integration docs with new SVG icons and links (#4684) 2026-04-03 20:54:46 +05:30
1941cae031 fix(server): add missing psycopg-pool dependency (#4374)
Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
2026-04-03 20:04:05 +05:30
fcbb70ab3b fix: prevent thread and memory leaks from PostHog telemetry (#4535)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-03 20:01:04 +05:30
33d2bc495d fix(openclaw): clear security scanner exfiltration warning (#4678)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-03 00:34:49 +05:30
c0cae68646 feat(plugin): add Codex plugin support and integration docs (#4665)
Co-authored-by: Gabriel Stein <gabrielstein416@gmail.com>
2026-04-03 00:18:02 +05:30
3b2f01796e feat(cli): comprehensive docs, version bump, and purple branding (#4680)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:52:00 +05:30
9cd3d2cca8 fix(openclaw): remove process.env access to clear security scanner warning (#4676)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:19:12 +05:30
Saket AryanandGitHub c53f1f126d docs(openclaw): add v1.0.1 changelog and release notes (#4675) 2026-04-02 20:25:49 +05:30
Patel TirthandGitHub 0b7615fa87 docs: add api_key parameter to Google AI LLM provider config examples (#4626) 2026-04-02 19:37:23 +05:30
Shaik Faizan Roshan AliandGitHub 66d34fab3c fix: update_memory endpoint passing dict instead of str (#3933) (#4595) 2026-04-02 19:35:09 +05:30
868b63af63 fix: use DatetimeRange for datetime string values in Qdrant range filters (#4659)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-02 18:43:58 +05:30
6cc1c15320 feat(sdk): add multilingual param to project update (#4314)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:38:26 +05:30
7a20da59ee fix(configs): add missing ConfigDict to vector store configs (#4656)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-02 18:05:54 +05:30
Saket AryanandGitHub f89f7c7c81 ci: add CD workflow for @mem0/openclaw-mem0 with OIDC trusted publishing (#4672) 2026-04-02 16:32:31 +05:30
Saket AryanandGitHub 5723136bed fix: add repository field to Node packages for npm provenance (#4671) 2026-04-02 16:20:38 +05:30
Saket AryanandGitHub b5345f8498 ci: add CD workflows for Node SDK packages with OIDC trusted publishing (#4670) 2026-04-02 16:11:06 +05:30
6577ae7616 fix(openclaw): graceful startup without API key (#4669)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:10:49 +05:30
Saket AryanandGitHub 5e00d5c452 chore(cli): bump version for Python CLI to 0.2.0 and Node CLI to 0.1.1 (#4668) 2026-04-02 13:36:21 +05:30
KartikandGitHub 3b152a3e85 fix(openclaw): updating the config of the openclaw plugin here (#4667) 2026-04-02 13:30:03 +05:30
beca7cc873 fix(openclaw): dream gate correctness — cheap-first ordering, session isolation, verified completion (#4666)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:29:49 +05:30
Saket AryanandGitHub 30f242dc4c feat(cli): update brand color palette from purple to golden (#4664) 2026-04-02 04:03:28 +05:30
KartikandGitHub 1bfaaf8750 chore: release (#4657) 2026-04-01 23:44:09 +05:30
c250ccfb5c feat(openclaw): skills-based memory architecture with batched extraction (#4624)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 23:29:15 +05:30
Saket AryanandGitHub e2b439c42a fix(cd): restrict PyPI publish to main SDK tags only (#4654) 2026-04-01 21:42:50 +05:30
Saket AryanandGitHub c788d771d3 feat(cli): add CD workflow and bump version to 0.2.0b1 (#4653) 2026-04-01 21:29:10 +05:30
2acf9571b3 feat(cli): add event commands, --json/--agent flag, agent output sanitization, and edge-case hardening for CLI SDKs (#4649)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 21:04:59 +05:30
Saharsh PatelandGitHub 713dba5d0a fix: replace .single() with .maybeSingle() in SupabaseDB.get() to handle missing rows (#4599) 2026-04-01 18:41:24 +05:30
8ae7a06220 fix: pass dimensions parameter to OpenAI embeddings API (#4632)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-01 18:40:17 +05:30
Prithvi MonangiandGitHub f94ea06588 fix(configs): migrate CassandraConfig and AzureMySQLConfig to pydantic v2 ConfigDict (#4646) 2026-04-01 15:02:22 +05:30
Noah StappandGitHub 215d8b5a71 fix: only list authorized collections when listing MongoDB collections (#3888) 2026-03-31 23:09:36 +05:30
Genaro SanchezandGitHub 82525dbf0f docs: update Twitter references to X (formerly Twitter) (#4432) 2026-03-31 22:44:04 +05:30
32c1ccba5b ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:30:31 +05:30
Saket AryanandGitHub 9bebcf45f7 fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636) 2026-03-31 20:28:48 +05:30
Rakhee SinghandGitHub 93bd4e248c fix(deepseek): forward response_format to OpenAI-compatible API (#4635) 2026-03-31 18:06:53 +05:30
af19495f66 feat(cli): add email verification code login to mem0 init (#4623)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-03-31 16:57:02 +05:30
dbac83218f fix(vllm): forward response_format to OpenAI-compatible API (#4608)
Co-authored-by: rasingh5 <rasingh@demandbase.com>
2026-03-31 13:25:45 +05:30
PrathameshandGitHub 3618aeff22 Link fix (#4631) 2026-03-30 21:36:00 -07:00
171 changed files with 20847 additions and 3798 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "mem0-plugins",
"interface": {
"displayName": "Mem0 Plugins"
},
"plugins": [
{
"name": "mem0",
"source": {
"source": "local",
"path": "./mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+2 -1
View File
@@ -7,6 +7,7 @@ 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
@@ -38,7 +39,7 @@ jobs:
# packages_dir: dist/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: dist/
+49
View File
@@ -0,0 +1,49 @@
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: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- 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]")
npm publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
fi
+100
View File
@@ -0,0 +1,100 @@
name: CLI Node CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: cli/node
run: pnpm run lint
- name: Type check
working-directory: cli/node
run: pnpm run typecheck
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Run tests
working-directory: cli/node
run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Build
working-directory: cli/node
run: pnpm run build
- name: Verify dist output
run: |
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
+34
View File
@@ -0,0 +1,34 @@
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/
+79
View File
@@ -0,0 +1,79 @@
name: CLI Python CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
pull_request:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: cli/python
run: ruff check .
- name: Check formatting
working-directory: cli/python
run: ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: cli/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: cli/python
run: hatch build --clean
- name: Verify dist output
run: |
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+49
View File
@@ -0,0 +1,49 @@
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: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- 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]")
npm publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
fi
+49
View File
@@ -0,0 +1,49 @@
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: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- 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]")
npm publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
fi
+49
View File
@@ -0,0 +1,49 @@
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: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- 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]")
npm publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
fi
+28
View File
@@ -61,3 +61,31 @@ 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
+2 -2
View File
@@ -162,7 +162,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
## 📚 Documentation & Support
- Full docs: https://docs.mem0.ai
- Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
- Contact: founders@mem0.ai
## Citation
@@ -180,4 +180,4 @@ We now have a paper you can cite:
## ⚖️ License
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
+188 -15
View File
@@ -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 `--output json` and `help --json` specifically for machine consumption)
- AI agents that need persistent memory (the CLI is designed with `--json`/`--agent` global flags and `help --json` specifically for machine consumption)
- DevOps/CI pipelines that need to manage memories programmatically
### Project Structure
@@ -79,6 +79,7 @@ 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)
@@ -88,6 +89,7 @@ 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
@@ -98,6 +100,7 @@ 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
```
@@ -153,8 +156,25 @@ Interactive setup wizard for mem0 CLI.
|-----------------|--------|----------|---------|------|
| `--api-key` | string | No | - | API key (skip prompt). |
| `-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).
- If `--code` is not provided, prompts for the code interactively.
- On success: receives API key, org_id, project_id. Saves to config. Creates account if email is new.
- Cannot be combined with `--api-key`.
*API key flow (existing behavior):*
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
- If running in a non-TTY without both flags, prints an error with usage hint and exits.
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
@@ -164,6 +184,9 @@ 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
```
---
@@ -687,7 +710,114 @@ mem0 entity delete --user-id alice --dry-run
---
### 3.14 `status`
### 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`
Check connectivity and authentication.
@@ -708,20 +838,19 @@ Check connectivity and authentication.
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**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.
**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.
**JSON output:**
```json
{
"status": "success",
"command": "status",
"duration_ms": 112,
"data": {
"connected": true,
"backend": "platform",
"base_url": "https://api.mem0.ai",
"latency_ms": 245
},
"duration_ms": 245
"base_url": "https://api.mem0.ai"
}
}
```
@@ -733,7 +862,7 @@ mem0 status -o json
---
### 3.15 `help`
### 3.17 `help`
Show help. Use `--json` for machine-readable output (for LLM agents).
@@ -793,6 +922,9 @@ 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`)
@@ -1095,6 +1227,8 @@ 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 |
@@ -1109,12 +1243,16 @@ For `PENDING` events, displays "Processing in background" with the event ID.
| 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 (`formatJsonEnvelope`)
### 7.2 JSON Envelope Format
Used by `config show`, `status`, and `import` for structured JSON output:
There are two related envelope formats:
**`formatJsonEnvelope`** — used by `config show`, `status`, and `import` for `--output json`:
```json
{
@@ -1128,14 +1266,39 @@ Used by `config show`, `status`, and `import` for structured JSON output:
}
```
**`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`: Always `"success"` (errors go to stderr before exit).
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`).
- `status`: `"success"` or `"error"`.
- `command`: The command name.
- `duration_ms`: Optional, elapsed time in milliseconds.
- `scope`: Optional, active entity scope.
- `scope`: Optional, active entity scope (omitted if empty).
- `count`: Optional, result count.
- `error`: Optional, error message string.
- `data`: The primary payload.
- `error`: Only present when `status` is `"error"`.
- `data`: The primary payload (sanitized in agent mode).
### 7.3 Text Output
@@ -1206,6 +1369,16 @@ 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.
+46 -8
View File
@@ -2,6 +2,8 @@
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
@@ -17,9 +19,15 @@ Both packages install a `mem0` binary with identical behavior.
## Quick start
```bash
# Authenticate and save config
# Interactive setup wizard
mem0 init
# Or login via email (get a new API key)
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
@@ -40,7 +48,7 @@ mem0 delete <memory-id>
| Command | Description |
|---------|-------------|
| `mem0 init` | Interactive setup wizard — configures API key and default user ID |
| `mem0 init` | Setup wizard — login via email or configure API key manually |
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
| `mem0 search` | Search memories using natural language |
| `mem0 list` | List memories with optional filters and pagination |
@@ -49,12 +57,45 @@ 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 entities` | List or delete entities (users, agents, apps) |
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
| `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`:
@@ -62,13 +103,10 @@ Control how results are displayed with `--output`:
| Format | Description |
|--------|-------------|
| `text` | Human-readable with colors and formatting (default) |
| `json` | Structured JSON for piping to `jq` or agent consumption |
| `json` | Structured JSON for piping to `jq` (raw API response) |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
```bash
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
```
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
## Environment variables
+9 -3
View File
@@ -503,15 +503,21 @@
},
{
"name": "init",
"description": "Interactive setup wizard for mem0 CLI.",
"usage": "mem0 init",
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
"usage": "mem0 init [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": []
"options": [
{ "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": "status",
+299 -37
View File
@@ -2,10 +2,12 @@
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`)
- pnpm (`npm install -g pnpm`) — for development only
## Installation
@@ -13,22 +15,307 @@ The official command-line interface for [mem0](https://mem0.ai) — the memory l
npm install -g @mem0/cli
```
Or from source:
## Quick start
```bash
cd node
pnpm install
pnpm build
pnpm link --global
# Interactive setup wizard
mem0 init
# Now use it like a normal CLI
mem0 --help
# 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>
```
## Running during development
## Commands
### `mem0 init`
Interactive setup wizard. Prompts for your API key and default user ID.
```bash
cd node
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
pnpm install
# Development mode (runs TypeScript directly, no build needed)
@@ -39,36 +326,11 @@ 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
```
## Quick Start
## Documentation
```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) |
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
## License
+7 -2
View File
@@ -1,13 +1,13 @@
{
"name": "@mem0/cli",
"version": "0.1.0",
"version": "0.2.2",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
"mem0": "./dist/index.js"
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"build": "tsup",
"dev": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
@@ -20,6 +20,11 @@
},
"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"
+86 -71
View File
@@ -6,110 +6,125 @@ import type { Mem0Config } from "../config.js";
import { PlatformBackend } from "./platform.js";
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;
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;
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;
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;
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface EntityIds {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface Backend {
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
search(
query: string,
opts?: SearchOptions,
): Promise<Record<string, unknown>[]>;
get(memoryId: string): Promise<Record<string, unknown>>;
get(memoryId: string): Promise<Record<string, unknown>>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): 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>>;
delete(
memoryId?: string,
opts?: DeleteOptions,
): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
ping(): Promise<Record<string, unknown>>;
entities(entityType: string): 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";
}
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";
}
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";
}
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
}
export function getBackend(config: Mem0Config): Backend {
return new PlatformBackend(config.platform);
return new PlatformBackend(config.platform);
}
+8 -1
View File
@@ -3,5 +3,12 @@
*/
export { getBackend } from "./base.js";
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
export type {
Backend,
AddOptions,
SearchOptions,
ListOptions,
DeleteOptions,
EntityIds,
} from "./base.js";
export { AuthError, NotFoundError, APIError } from "./base.js";
+323 -266
View File
@@ -3,301 +3,358 @@
*/
import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
type AddOptions,
APIError,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
APIError,
type AddOptions,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
} from "./base.js";
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
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,
};
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
const fetchOpts: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const headers = {
...this.headers,
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
};
const resp = await fetch(url, fetchOpts);
const fetchOpts: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
const body = await resp.json();
detail = (body as Record<string, string>).detail ?? resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
if (resp.status === 204) {
return {};
}
return resp.json();
}
const resp = await fetch(url, fetchOpts);
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail =
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
let detail: string = resp.statusText;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.detail ?? body.message ?? resp.statusText) as string;
} catch {
/* ignore */
}
throw new Error(`HTTP ${resp.status}: ${detail}`);
}
if (resp.status === 204) {
return {};
}
return resp.json();
}
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
string,
unknown
>;
}
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
return opts.extraFilters;
}
return (await this._request("POST", "/v1/memories/", {
json: payload,
})) as Record<string, unknown>;
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (
opts.extraFilters &&
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters;
}
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
async search(
query: string,
opts: SearchOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
}
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
}
if (opts.before) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
}
async listMemories(
opts: ListOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
gte: opts.after,
};
}
if (opts.before) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
}
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
params,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
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;
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
string,
unknown
>;
}
if (memoryId) {
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
throw new Error("Either memoryId or --all is required");
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
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.");
}
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
string,
unknown
>;
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
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;
return (await this._request("DELETE", "/v1/memories/", {
params,
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request(
"DELETE",
`/v1/memories/${memoryId}/`,
)) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
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 {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
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) {
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 entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
await this.ping();
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
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
>;
}
}
+96 -69
View File
@@ -4,10 +4,8 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { createRequire } from "node:module";
const _require = createRequire(import.meta.url);
const PKG_VERSION: string = _require("../package.json").version;
import { getCurrentCommand, isAgentMode } from "./state.js";
import { CLI_VERSION } from "./version.js";
export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -40,75 +38,96 @@ const dim = chalk.hex(DIM_COLOR);
* plain-text for piped/non-TTY or NO_COLOR environments.
*/
export function sym(fancy: string, plain: string): string {
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
}
export function printBanner(): void {
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${PKG_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
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}`;
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
);
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] =
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
}
export function printSuccess(message: string): void {
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
if (isAgentMode()) return;
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
}
export function printError(message: string, hint?: string): void {
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
}
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)}`);
}
}
export function printWarning(message: string): void {
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
}
export function printInfo(message: string): void {
console.log(`${brand(sym("◆", "*"))} ${message}`);
if (isAgentMode()) return;
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
parts.push(`${label}=${val}`);
}
}
if (parts.length > 0) {
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
if (isAgentMode()) return;
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
parts.push(`${key}=${val}`);
}
}
if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
}
export interface TimedStatusContext {
successMsg: string;
errorMsg: string;
successMsg: string;
errorMsg: string;
}
/**
@@ -116,29 +135,37 @@ export interface TimedStatusContext {
* Equivalent to Python's timed_status context manager.
*/
export async function timedStatus<T>(
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
const start = performance.now();
if (isAgentMode()) {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
return fn(ctx);
}
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({
text: dim(message),
color: "yellow",
stream: process.stderr,
}).start();
const start = performance.now();
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
printError(`${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
}
/** Format helpers using brand colors for external use. */
+86 -65
View File
@@ -3,88 +3,109 @@
*/
import Table from "cli-table3";
import { printError, printSuccess, colors } from "../branding.js";
import { colors, printError, printSuccess } from "../branding.js";
import {
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig();
setCurrentCommand("config show");
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push([
"defaults.agent_id",
config.defaults.agentId || dim("(not set)"),
]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString());
console.log();
console.log(table.toString());
console.log();
}
export function cmdConfigGet(key: string): void {
const config = loadConfig();
const value = getNestedValue(config, key);
setCurrentCommand("config get");
const config = loadConfig();
const value = getNestedValue(config, key);
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
if (value === undefined) {
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 },
});
} else {
console.log(displayValue);
}
}
}
export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
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}`);
}
} else {
printError(`Unknown config key: ${key}`);
}
}
+138 -109
View File
@@ -4,136 +4,165 @@
import readline from "node:readline";
import Table from "cli-table3";
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJson } from "../output.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList(
backend: Backend,
entityType: string,
opts: { output: string },
backend: Backend,
entityType: string,
opts: { output: string },
): Promise<void> {
if (!VALID_TYPES.has(entityType)) {
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
process.exit(1);
}
setCurrentCommand("entity list");
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "entity list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
console.log();
console.log(table.toString());
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
console.log();
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
}
export async function cmdEntitiesDelete(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
process.exit(1);
}
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.",
);
process.exit(1);
}
if (opts.dryRun) {
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;
}
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.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(", ");
if (opts.dryRun) {
printInfo(`Would delete entity ${scope} and all its memories.`);
printInfo("No changes made.");
return;
}
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
if (!opts.force) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
if (opts.output === "agent") {
formatAgentEnvelope({
command: "entity delete",
data: { deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
}
+169
View File
@@ -0,0 +1,169 @@
/**
* 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();
}
+407 -143
View File
@@ -2,181 +2,445 @@
* mem0 init — interactive setup wizard.
*/
import fs from "node:fs";
import readline from "node:readline";
import {
printBanner,
printError,
printInfo,
printSuccess,
colors,
} from "../branding.js";
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
import { PlatformBackend } from "../backend/platform.js";
import {
colors,
printBanner,
printError,
printInfo,
printSuccess,
} from "../branding.js";
import {
CONFIG_FILE,
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
redactKey,
saveConfig,
} from "../config.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
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,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: sourceHeaders,
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
return new Promise((resolve, reject) => {
process.stdout.write(label);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const chars: string[] = [];
const chars: string[] = [];
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
process.stdin.on("data", onData);
});
process.stdin.on("data", onData);
});
}
function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
}
async function setupPlatform(config: Mem0Config): Promise<void> {
console.log();
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
console.log();
console.log();
console.log(
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
);
console.log();
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
}
async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
if (userId) config.defaults.userId = userId;
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
_systemUser,
);
if (userId) config.defaults.userId = userId;
}
async function validatePlatform(config: Mem0Config): Promise<void> {
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
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.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
}
export async function runInit(opts: { apiKey?: string; userId?: string } = {}): Promise<void> {
const config = createDefaultConfig();
export async function runInit(
opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
force?: boolean;
} = {},
): Promise<void> {
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// 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);
}
// 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);
}
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
await setupPlatform(config);
}
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
const result = await emailLogin(email, opts.code, baseUrl);
await validatePlatform(config);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
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";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── 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;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
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";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
await validatePlatform(config);
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
}
File diff suppressed because it is too large Load Diff
+125 -109
View File
@@ -3,137 +3,153 @@
*/
import fs from "node:fs";
import { createRequire } from "node:module";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJsonEnvelope } from "../output.js";
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 { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
const _require = createRequire(import.meta.url);
const VERSION: string = _require("../../package.json").version;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
}
const elapsed = (performance.now() - start) / 1000;
setCurrentCommand("status");
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = {
connected: false,
error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
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),
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
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`);
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${VERSION}`);
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
setCurrentCommand("import");
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let added = 0;
let failed = 0;
const start = performance.now();
let added = 0;
let failed = 0;
const start = performance.now();
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
}
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
}
}
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "import",
data: {
added,
failed,
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
}
+121 -99
View File
@@ -18,142 +18,164 @@ export const DEFAULT_BASE_URL = "https://api.mem0.ai";
export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
apiKey: string;
baseUrl: string;
userEmail: string;
}
export interface DefaultsConfig {
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
}
export interface Mem0Config {
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
}
export function createDefaultConfig(): Mem0Config {
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
},
};
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
},
};
}
export function ensureConfigDir(): string {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
}
export function loadConfig(): Mem0Config {
const config = createDefaultConfig();
const config = createDefaultConfig();
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
config.version = data.version ?? CONFIG_VERSION;
config.version = data.version ?? CONFIG_VERSION;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
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 ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
// Environment variable overrides
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
// Environment variable overrides
if (process.env.MEM0_API_KEY)
config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL)
config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID)
config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID)
config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
return config;
return config;
}
export function saveConfig(config: Mem0Config): void {
ensureConfigDir();
ensureConfigDir();
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
},
};
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
}
export function redactKey(key: string): string {
if (!key) return "(not set)";
if (key.length <= 8) return key.slice(0, 2) + "***";
return key.slice(0, 4) + "..." + key.slice(-4);
if (!key) return "(not set)";
if (key.length <= 8) return `${key.slice(0, 2)}***`;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}
/** Key map from dotted config path to the config object fields. */
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"],
"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"],
"defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"],
// 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"],
run_id: ["defaults", "runId"],
enable_graph: ["defaults", "enableGraph"],
};
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
}
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
export function setNestedValue(
config: Mem0Config,
dottedKey: string,
value: string,
): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = Number.parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
}
+2
View File
@@ -0,0 +1,2 @@
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string | undefined;
+284 -280
View File
@@ -4,40 +4,40 @@
*/
import chalk from "chalk";
import type { Command, Help, Option, Argument } from "commander";
import type { Argument, Command, Help, Option } from "commander";
// Colors imported from chalk directly to match Typer/Rich defaults
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
// ── Strip ANSI ───────────────────────────────────────────────────────────
// eslint-disable-next-line no-control-regex
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(str: string): number {
return str.replace(ANSI_RE, "").length;
return str.replace(ANSI_RE, "").length;
}
// ── Command display order (matches Python CLI) ──────────────────────────
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
},
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "event", "config"],
},
];
/** Flat order derived from COMMAND_GROUPS. */
@@ -46,88 +46,88 @@ const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
const OPTION_PANELS: Record<string, Record<string, string>> = {
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
};
const PANEL_ORDER: string[] = [
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
];
// ── Panel rendering ─────────────────────────────────────────────────────
@@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [
* ╰────────────────────────────────╯
* ```
*/
function renderPanel(
title: string,
rows: string[],
width: number,
): string {
if (rows.length === 0) return "";
function renderPanel(title: string, rows: string[], width: number): string {
if (rows.length === 0) return "";
// Inner width is total width minus the two border chars
const inner = width - 2;
// Inner width is total width minus the two border chars
const inner = width - 2;
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Bottom border: ╰─...─╯
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Bottom border: ╰─...─╯
const bottomLine =
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
});
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
});
return [topLine, ...contentLines, bottomLine].join("\n");
return [topLine, ...contentLines, bottomLine].join("\n");
}
// ── Format an option term (short + long) ────────────────────────────────
function formatOptionTerm(opt: Option): string {
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += " " + match[0];
}
}
return term;
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += ` ${match[0]}`;
}
}
return term;
}
// ── Get the long flag name for panel lookup ─────────────────────────────
function getLongFlag(opt: Option): string {
if (opt.long) return opt.long;
return opt.short || "";
if (opt.long) return opt.long;
return opt.short || "";
}
// ── Format a default value ──────────────────────────────────────────────
function formatDefault(opt: Option): string {
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
}
// ── The main help formatter ─────────────────────────────────────────────
export function richFormatHelp(cmd: Command, helper: Help): string {
const width = process.stdout.columns || 80;
const lines: string[] = [];
const width = process.stdout.columns || 80;
const lines: string[] = [];
const isRoot = !cmd.parent;
const isRoot = !cmd.parent;
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
}
lines.push("");
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
}
lines.push("");
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(
...visibleArgs.map((a: Argument) => a.name().length),
);
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap =
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped["Options"].push(opt);
}
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped.Options.push(opt);
}
}
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped["Options"].length > 0) {
const optRows = formatOptionRows(grouped["Options"]);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped.Options.length > 0) {
const optRows = formatOptionRows(grouped.Options);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
lines.push("");
return lines.join("\n");
lines.push("");
return lines.join("\n");
}
// ── Format option rows with aligned columns ─────────────────────────────
function formatOptionRows(opts: Option[]): string[] {
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
}
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
function sortCommands(cmds: Command[]): Command[] {
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
}
+687 -393
View File
File diff suppressed because it is too large Load Diff
+328 -177
View File
@@ -2,229 +2,380 @@
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
*/
import Table from "cli-table3";
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js";
const { brand, accent, success, error: errorColor, dim } = colors;
function formatDate(dtStr?: string): string | undefined {
if (!dtStr) return undefined;
try {
const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10);
} catch {
return dtStr?.slice(0, 10);
}
if (!dtStr) return undefined;
try {
const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10);
} catch {
return dtStr?.slice(0, 10);
}
}
export function formatMemoriesText(
memories: Record<string, unknown>[],
title = "memories",
memories: Record<string, unknown>[],
title = "memories",
): void {
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
for (let i = 0; i < memories.length; i++) {
const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined;
const cats = mem.categories;
if (Array.isArray(cats)) {
category = cats[0] as string | undefined;
}
for (let i = 0; i < memories.length; i++) {
const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined;
const cats = mem.categories;
if (Array.isArray(cats)) {
category = cats[0] as string | undefined;
}
console.log(` ${i + 1}. ${memoryText}`);
console.log(` ${i + 1}. ${memoryText}`);
const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`);
const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`);
if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`);
}
console.log();
}
if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`);
}
console.log();
}
}
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
const table = new Table({
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
colWidths: [12, 52, 16, 14],
wordWrap: true,
style: { head: [], border: [] },
});
export function formatMemoriesTable(
memories: Record<string, unknown>[],
opts: { showScore?: boolean } = {},
): void {
const head = opts.showScore
? [
accent("ID"),
accent("Score"),
accent("Memory"),
accent("Category"),
accent("Created"),
]
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
const table = new Table({
head,
colWidths,
wordWrap: true,
style: { head: [], border: [] },
});
for (const mem of memories) {
const memId = ((mem.id as string) ?? "").slice(0, 8);
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = memoryText.slice(0, 57) + "...";
}
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
table.push([dim(memId), memoryText, cat, created]);
}
for (const mem of memories) {
const memId = (mem.id as string) ?? "";
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = `${memoryText.slice(0, 57)}...`;
}
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? categories.length > 1
? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
if (opts.showScore) {
const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else {
table.push([dim(memId), memoryText, cat, created]);
}
}
console.log();
console.log(table.toString());
console.log();
console.log();
console.log(table.toString());
console.log();
}
export function formatJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
console.log(JSON.stringify(data, null, 2));
}
export function formatSingleMemory(
mem: Record<string, unknown>,
output = "text",
mem: Record<string, unknown>,
output = "text",
): void {
if (output === "json") {
formatJson(mem);
return;
}
if (output === "json") {
formatJson(mem);
return;
}
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories;
if (categories) {
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories;
if (categories) {
const catStr = Array.isArray(categories)
? categories.join(", ")
: String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Memory"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Memory"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function formatAddResult(
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
): void {
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
const results: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
const results: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
console.log();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
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(" "));
continue;
}
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 icon = accent(sym("⧗", "..."));
const parts = [
` ${icon} ${dim("Queued".padEnd(10))}`,
"Processing in background",
];
console.log(parts.join(" "));
if (eventId) {
console.log(` ${dim(` event_id: ${eventId}`)}`);
console.log(
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
);
}
continue;
}
const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
0,
8,
);
let icon: string;
let label: string;
if (event === "ADD") {
icon = success("+");
label = "Added";
} else if (event === "UPDATE") {
icon = accent("~");
label = "Updated";
} else if (event === "DELETE") {
icon = errorColor("-");
label = "Deleted";
} else if (event === "NOOP") {
icon = dim("·");
label = "No change";
} else {
icon = dim("?");
label = event;
}
let icon: string;
let label: string;
if (event === "ADD") {
icon = success("+");
label = "Added";
} else if (event === "UPDATE") {
icon = accent("~");
label = "Updated";
} else if (event === "DELETE") {
icon = errorColor("-");
label = "Deleted";
} else if (event === "NOOP") {
icon = dim("·");
label = "No change";
} else {
icon = dim("?");
label = event;
}
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" "));
}
console.log();
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" "));
}
console.log();
}
export function formatJsonEnvelope(opts: {
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
status?: string;
error?: string;
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
status?: string;
error?: string;
}): void {
const envelope: Record<string, unknown> = {
status: opts.status ?? "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope;
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
console.log(JSON.stringify(envelope, null, 2));
const envelope: Record<string, unknown> = {
status: opts.status ?? "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope;
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
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;
page?: number;
scopeIds?: Record<string, string | undefined>;
count: number;
durationSecs?: number;
page?: number;
scopeIds?: Record<string, string | undefined>;
}): void {
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined)
parts.push(`${opts.durationSecs.toFixed(2)}s`);
console.log(` ${dim(parts.join(" · "))}`);
console.log();
console.log(` ${dim(parts.join(" · "))}`);
console.log();
}
+23
View File
@@ -0,0 +1,23 @@
/**
* 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;
}
+106
View File
@@ -0,0 +1,106 @@
/**
* 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 */
}
}
+10
View File
@@ -0,0 +1,10 @@
import { createRequire } from "node:module";
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
// When running via tsx in dev/test mode, fall back to reading package.json.
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
export const CLI_VERSION: string =
typeof __CLI_VERSION__ !== "undefined"
? (__CLI_VERSION__ as string)
: (createRequire(import.meta.url)("../package.json") as { version: string })
.version;
+108
View File
@@ -0,0 +1,108 @@
/**
* 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(() => {});
+4 -4
View File
@@ -80,19 +80,19 @@ describe("printWarning", () => {
describe("printInfo", () => {
it("prints info message", () => {
printInfo("Important note");
expect(output).toContain("Important note");
expect(errOutput).toContain("Important note");
});
});
describe("printScope", () => {
it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" });
expect(output).toContain("alice");
expect(output).toContain("bot");
expect(errOutput).toContain("alice");
expect(errOutput).toContain("bot");
});
it("prints nothing when no IDs", () => {
printScope({});
expect(output).toBe("");
expect(errOutput).toBe("");
});
});
-7
View File
@@ -44,13 +44,6 @@ 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);
+222 -9
View File
@@ -5,6 +5,7 @@
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;
@@ -31,6 +32,7 @@ import { afterEach } from "vitest";
afterEach(() => {
console.log = originalLog;
console.error = originalError;
setAgentMode(false);
});
describe("cmdAdd", () => {
@@ -84,6 +86,59 @@ 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");
@@ -125,7 +180,7 @@ describe("cmdSearch", () => {
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
@@ -166,7 +221,7 @@ describe("cmdList", () => {
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
@@ -198,13 +253,6 @@ 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 () => {
@@ -219,3 +267,168 @@ 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");
});
});
+83
View File
@@ -10,6 +10,7 @@ import {
formatSingleMemory,
formatAddResult,
printResultSummary,
sanitizeAgentData,
} from "../src/output.js";
let output: string;
@@ -98,6 +99,18 @@ 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", () => {
@@ -113,3 +126,73 @@ 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();
});
});
+37
View File
@@ -71,5 +71,42 @@ 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" },
},
],
}),
};
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);
const pkg = _require('./package.json');
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
+11
View File
@@ -0,0 +1,11 @@
import { createRequire } from "node:module";
import { defineConfig } from "vitest/config";
const _require = createRequire(import.meta.url);
const pkg = _require("./package.json") as { version: string };
export default defineConfig({
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
+315 -5
View File
@@ -1,6 +1,12 @@
# mem0 CLI
# mem0 CLI (Python)
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents.
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+**
## Installation
@@ -18,22 +24,326 @@ 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
# Set up your configuration
# Interactive setup wizard
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
# 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>
```
## 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
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.1.0"
version = "0.2.2"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.1.0"
__version__ = "0.2.2"
+255 -30
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
import contextlib
import json as _json
import os
import stat as _stat_mod
import sys
from pathlib import Path
@@ -10,7 +13,7 @@ import typer
from rich.console import Console
from mem0_cli import __version__
from mem0_cli.branding import BRAND_COLOR, print_error
from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
console = Console()
err_console = Console(stderr=True)
@@ -43,7 +46,52 @@ entity_app = typer.Typer(
no_args_is_help=True,
rich_markup_mode="rich",
)
# entity_app registered after Memory commands to control panel ordering
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}")
# ── Helpers ───────────────────────────────────────────────────────────────
@@ -53,9 +101,16 @@ 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."""
"""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
from mem0_cli.backend import get_backend
from mem0_cli.config import load_config
from mem0_cli.backend.platform import AuthError
from mem0_cli.config import load_config, save_config
config = load_config()
@@ -72,7 +127,29 @@ def _get_backend_and_config(
)
raise typer.Exit(1)
return get_backend(config), config
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
def _get_backend(
@@ -114,9 +191,22 @@ 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 piped (not a TTY)."""
if not sys.stdin.isatty():
"""Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
if _stdin_is_piped():
return sys.stdin.read().strip() or None
return None
@@ -128,12 +218,26 @@ 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 ───────────────────────────────────────────────────────────
@@ -253,8 +357,12 @@ def search(
help="Specific fields to return (comma-separated).",
rich_help_panel="Search",
),
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
graph: bool = typer.Option(
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
),
no_graph: bool = typer.Option(
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -269,7 +377,7 @@ def search(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""Search memories by semantic query.
"""Query your memory store — semantic, keyword, or hybrid retrieval.
Examples:
mem0 search "preferences" --user-id alice
@@ -281,8 +389,8 @@ def search(
# STEP 7: stdin fallback for query
if query is None:
query = _read_stdin()
if query is None:
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
if not query or not query.strip():
print_error(err_console, "Search query cannot be empty.")
raise typer.Exit(1)
backend, config = _get_backend_and_config(api_key, base_url)
@@ -372,8 +480,12 @@ def list_cmd(
before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
),
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
graph: bool = typer.Option(
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
),
no_graph: bool = typer.Option(
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
),
output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -463,11 +575,19 @@ def update(
@app.command(rich_help_panel="Memory")
def delete(
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
memory_id: str | None = typer.Argument(
None, help="Memory ID to delete (omit when using --all or --entity)."
),
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
entity: bool = typer.Option(
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
),
project: bool = typer.Option(
False, "--project", help="With --all: delete ALL memories project-wide."
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
@@ -522,12 +642,14 @@ 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)
@@ -535,6 +657,7 @@ 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)
@@ -641,14 +764,12 @@ def entity_delete(
agent_id: str | None = typer.Option(
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
),
app_id: str | None = typer.Option(
None, "--app-id", help="App ID.", rich_help_panel="Scope"
),
run_id: str | None = typer.Option(
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
),
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
),
@@ -688,23 +809,98 @@ 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 ───────────────────────────────────────────────────
@app.command(rich_help_panel="Management")
def init(
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Default user ID (skip prompt)."
),
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
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.
Examples:
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
"""
from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id)
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
# (entity_app registered at module level, below sub-group definitions)
@@ -743,7 +939,6 @@ def status(
)
@app.command("import", rich_help_panel="Management")
def import_cmd(
file_path: str = typer.Argument(..., help="JSON file to import."),
@@ -810,7 +1005,7 @@ def _build_help_json() -> dict:
},
},
"search": {
"description": "Search memories by semantic query.",
"description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
"usage": "mem0 search <query> [OPTIONS]",
"arguments": {"query": {"description": "Search query.", "required": False}},
"options": {
@@ -914,6 +1109,24 @@ 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": {
@@ -965,6 +1178,7 @@ 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.",
},
@@ -994,7 +1208,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 Search memories by semantic query")
console.print(" search Query your memory store (semantic, keyword, hybrid)")
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")
@@ -1002,6 +1216,7 @@ 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()
@@ -1018,4 +1233,14 @@ 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()
+5
View File
@@ -104,6 +104,11 @@ 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:
+43 -23
View File
@@ -6,6 +6,7 @@ from typing import Any
import httpx
from mem0_cli import __version__
from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
@@ -21,11 +22,17 @@ 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.")
@@ -265,18 +272,35 @@ class PlatformBackend(Backend):
app_id: str | None = None,
run_id: str | None = None,
) -> dict:
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:
# 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:
raise ValueError("At least one entity ID is required for delete_entities.")
return self._request("DELETE", "/v1/entities/", params=params)
# 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/")
def status(
self,
@@ -284,19 +308,9 @@ class PlatformBackend(Backend):
user_id: str | None = None,
agent_id: str | None = None,
) -> dict[str, Any]:
"""Check connectivity by making a lightweight API call."""
"""Check connectivity using the ping endpoint."""
try:
# 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/")
self.ping()
return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)}
@@ -311,6 +325,12 @@ 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):
+48 -2
View File
@@ -43,6 +43,10 @@ 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}")
@@ -61,11 +65,28 @@ 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:
@@ -73,11 +94,19 @@ 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}")
@@ -89,7 +118,9 @@ 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):
@@ -97,6 +128,13 @@ 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):
@@ -105,6 +143,11 @@ 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
@@ -114,11 +157,14 @@ 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:
label = key.replace("_", " ").replace("id", "ID").strip()
parts.append(f"{label}={val}")
parts.append(f"{key}={val}")
if parts:
scope_str = ", ".join(parts)
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
+33 -9
View File
@@ -20,12 +20,17 @@ err_console = Console(stderr=True)
def cmd_config_show(*, output: str = "text") -> None:
"""Display current configuration (secrets redacted)."""
from mem0_cli.output import format_json_envelope
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"
config = load_config()
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_envelope(
console,
command="config show",
data={
@@ -84,25 +89,44 @@ 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:
# Redact secrets
if "api_key" in key or "key" in key.split(".")[-1:]:
console.print(redact_key(str(value)))
else:
console.print(str(value))
console.print(display_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
print_success(console, f"{key} = {display}")
if is_agent_mode():
format_agent_envelope(
console, command="config set", data={"key": key, "value": display}
)
else:
print_success(console, f"{key} = {display}")
else:
print_error(err_console, f"Unknown config key: {key}")
+62 -27
View File
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
print_success,
timed_status,
)
from mem0_cli.output import format_json
from mem0_cli.output import format_agent_envelope, format_json
console = Console()
err_console = Console(stderr=True)
@@ -26,9 +26,16 @@ 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(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
print_error(
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
)
raise typer.Exit(1)
_start = _time.perf_counter()
@@ -40,6 +47,16 @@ 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
@@ -75,39 +92,39 @@ 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.")
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:
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, f"Would delete entity {scope_str} 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} AND all its memories? This cannot be undone."
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
)
if not confirm:
print_info(console, "Cancelled.")
@@ -127,7 +144,25 @@ def cmd_entities_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
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":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
@@ -0,0 +1,176 @@
"""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()
+230 -14
View File
@@ -2,8 +2,11 @@
from __future__ import annotations
import os
import re
import sys
import httpx
import typer
from rich.console import Console
from rich.prompt import Prompt
@@ -16,7 +19,7 @@ from mem0_cli.branding import (
print_info,
print_success,
)
from mem0_cli.config import Mem0Config, save_config
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
console = Console()
err_console = Console(stderr=True)
@@ -84,7 +87,96 @@ def _prompt_secret(label: str) -> str:
return "".join(chars)
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _validate_email(email: str) -> None:
"""Exit with an error if *email* doesn't look like a valid address."""
if not _EMAIL_RE.match(email):
print_error(err_console, f"Invalid email address: {email!r}")
raise typer.Exit(1)
def _email_login(
email: str,
code: str | None,
base_url: str,
) -> dict:
"""Run the email verification code login flow.
Returns the parsed JSON response from the verify endpoint.
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
if not code:
# Step 1: Request verification code
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.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, "Verification code sent! Check your email.")
# Step 2: Get code from user
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint="Run: mem0 init --email <email> --code <code>",
)
raise typer.Exit(1)
console.print()
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
if not code:
print_error(err_console, "Code is required.")
raise typer.Exit(1)
# Step 3: Verify code
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.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Verification failed: {detail}")
raise typer.Exit(1)
return resp.json()
def run_init(
*,
api_key: str | None = None,
user_id: str | None = None,
email: str | None = None,
code: str | None = None,
force: bool = False,
) -> None:
"""Interactive setup wizard for mem0 CLI.
When both *api_key* and *user_id* are supplied, all prompts are skipped
@@ -93,6 +185,85 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
"""
config = Mem0Config()
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
if code and not email:
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:
print_error(err_console, "Cannot use both --api-key and --email.")
raise typer.Exit(1)
email = email.strip().lower()
_validate_email(email)
print_banner(console)
console.print()
print_info(console, f"Logging in as {email}...\n")
result = _email_login(email, code, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
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"
)
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# ── 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
@@ -102,20 +273,56 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
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")
# Use provided flags or prompt
# If no flags at all, ask user how they want to authenticate
if not api_key:
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
console.print()
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
if choice == "1":
console.print()
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
if not email_addr:
print_error(err_console, "Email is required.")
raise typer.Exit(1)
email_addr = email_addr.strip().lower()
_validate_email(email_addr)
print_info(console, f"Logging in as {email_addr}...\n")
result = _email_login(email_addr, None, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(
err_console, "Auth succeeded but no API key was returned. Contact support."
)
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"
)
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# API key flow
if api_key:
config.platform.api_key = api_key
else:
@@ -162,9 +369,10 @@ 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="mem0-cli",
default=_default_user,
)
if user_id:
config.defaults.user_id = user_id
@@ -184,11 +392,19 @@ 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="Check your API key and try again.",
hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
)
except Exception as e:
print_error(err_console, f"Connection test failed: {e}")
+229 -21
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
import json
import os
import stat as _stat_mod
import sys
import time as _time
from pathlib import Path
@@ -20,6 +22,7 @@ from mem0_cli.branding import (
)
from mem0_cli.output import (
format_add_result,
format_agent_envelope,
format_json,
format_memories_table,
format_memories_text,
@@ -31,6 +34,19 @@ 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,
@@ -50,6 +66,11 @@ 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
@@ -70,8 +91,8 @@ def cmd_add(
print_error(err_console, f"Invalid JSON in --messages: {e}")
raise typer.Exit(1) from None
# Read from stdin if no text and stdin is piped
elif not content and not sys.stdin.isatty():
# Read from stdin only if stdin is an actual pipe or file redirect
elif not content and _stdin_is_piped():
content = sys.stdin.read().strip()
if not content and not msgs:
@@ -95,6 +116,21 @@ def cmd_add(
except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
import re
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts:
try:
result = backend.add(
@@ -113,24 +149,66 @@ def cmd_add(
)
except Exception as e:
ts.error_msg = str(e)
print_error(err_console, str(e))
raise typer.Exit(1) from None
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 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"
)
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"
)
format_add_result(console, result, output)
@@ -152,6 +230,11 @@ 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:
@@ -164,6 +247,13 @@ def cmd_search(
if fields:
field_list = [f.strip() for f in fields.split(",")]
if top_k < 1:
print_error(err_console, "--top-k must be >= 1.")
raise typer.Exit(1)
if not (0.0 <= threshold <= 1.0):
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Searching memories...") as _ts:
try:
@@ -186,11 +276,35 @@ def cmd_search(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
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":
if results:
format_memories_table(console, results)
format_memories_table(console, results, show_score=True)
print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
)
@@ -212,6 +326,11 @@ 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)
@@ -219,7 +338,10 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
print_error(err_console, str(e))
raise typer.Exit(1) from None
format_single_memory(console, result, output)
if output == "agent":
format_agent_envelope(console, command="get", data=result)
else:
format_single_memory(console, result, output)
def cmd_list(
@@ -238,6 +360,18 @@ 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)
if page < 1:
print_error(err_console, "--page must be >= 1.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Listing memories...") as _ts:
try:
@@ -258,8 +392,28 @@ def cmd_list(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, results)
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(
console,
command="list",
data=results,
scope=scope or None,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
elif output == "table":
if results:
format_memories_table(console, results)
@@ -301,6 +455,11 @@ 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:
@@ -318,7 +477,14 @@ def cmd_update(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="update",
data=result,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
@@ -333,6 +499,11 @@ 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:
@@ -353,7 +524,14 @@ def cmd_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete",
data={"id": memory_id, "deleted": True},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
@@ -372,12 +550,17 @@ 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
if dry_run:
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "No changes made (dry run).")
return
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
if not force:
confirm = typer.confirm(
@@ -402,7 +585,14 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete-all",
data={"deleted": True, "scope": "project"},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
@@ -460,7 +650,25 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
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":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
+30 -9
View File
@@ -35,22 +35,26 @@ def cmd_status(
output: str = "text",
) -> None:
"""Check connectivity and auth."""
from mem0_cli.output import format_json_envelope
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"
_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 == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_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),
)
@@ -67,6 +71,14 @@ 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)
@@ -96,7 +108,12 @@ def cmd_import(
output: str = "text",
) -> None:
"""Import memories from a JSON file."""
from mem0_cli.output import format_json_envelope
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"
try:
data = json.loads(Path(file_path).read_text())
@@ -110,7 +127,9 @@ def cmd_import(
added = 0
failed = 0
_start = _time.perf_counter()
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
for item in track(
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
):
content = item.get("memory", item.get("text", item.get("content", "")))
if not content:
failed += 1
@@ -127,11 +146,13 @@ def cmd_import(
failed += 1
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
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(
console,
command="import",
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
data={"added": added, "failed": failed},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
return
+18 -1
View File
@@ -27,6 +27,7 @@ CONFIG_VERSION = 1
class PlatformConfig:
api_key: str = ""
base_url: str = DEFAULT_BASE_URL
user_email: str = ""
@dataclass
@@ -45,6 +46,18 @@ class Mem0Config:
platform: PlatformConfig = field(default_factory=PlatformConfig)
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",
"run_id": "defaults.run_id",
"enable_graph": "defaults.enable_graph",
}
def ensure_config_dir() -> Path:
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@@ -65,6 +78,7 @@ 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", "")
@@ -121,6 +135,7 @@ 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,
},
}
@@ -140,7 +155,8 @@ def redact_key(key: str) -> str:
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts:
@@ -153,6 +169,7 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
"""Set a config value by dotted path. Returns True on success."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts[:-1]:
+126 -9
View File
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print()
def format_memories_table(console: Console, memories: list[dict]) -> None:
def format_memories_table(
console: Console, memories: list[dict], *, show_score: bool = False
) -> None:
"""Render memories in a rich table."""
table = Table(
border_style=BRAND_COLOR,
@@ -57,20 +59,34 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("ID", style="dim", max_width=10)
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
if show_score:
table.add_column("Score", max_width=7, justify="right")
table.add_column("Memory", max_width=50, no_wrap=False)
table.add_column("Category", max_width=14)
table.add_column("Created", max_width=12)
for mem in memories:
mem_id = mem.get("id", "")[:8]
mem_id = mem.get("id", "")
memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60:
memory_text = memory_text[:57] + "..."
categories = mem.get("categories", [])
cat = categories[0] if isinstance(categories, list) and categories else "—"
if isinstance(categories, list) and categories:
cat = (
categories[0]
if len(categories) == 1
else f"{categories[0]} (+{len(categories) - 1})"
)
else:
cat = "—"
created = _format_date(mem.get("created_at")) or "—"
table.add_row(mem_id, memory_text, cat, created)
if show_score:
score = mem.get("score")
score_str = f"{score:.2f}" if score is not None else "—"
table.add_row(mem_id, score_str, memory_text, cat, created)
else:
table.add_row(mem_id, memory_text, cat, created)
console.print()
console.print(table)
@@ -139,16 +155,23 @@ 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", "")[:8]
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)
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
parts.append("[white]Processing in background[/]")
if event_id:
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
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}[/]")
continue
event = r.get("event", "ADD")
@@ -209,6 +232,100 @@ 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,
@@ -221,7 +338,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.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
if scope_parts:
parts.append(", ".join(scope_parts))
if duration_secs is not None:
+24
View File
@@ -0,0 +1,24 @@
"""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
+105
View File
@@ -0,0 +1,105 @@
"""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
@@ -0,0 +1,86 @@
"""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()
+37
View File
@@ -96,6 +96,43 @@ 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
+28 -8
View File
@@ -8,11 +8,19 @@ boundaries).
from __future__ import annotations
import os
import re
import subprocess
import sys
import pytest
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
return _ANSI_RE.sub("", text)
def _run(
args: list[str],
@@ -28,22 +36,35 @@ def _run(
reads config from ``<home_dir>/.mem0/config.json`` instead
of the user's real config. This is critical for tests that
depend on a clean (no API key) or custom config state.
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
fragment option names like --user-id into separately-styled ANSI segments,
making plain ``in`` checks fail. Stripping here is version-agnostic and
ensures all assertions see the same plain text regardless of terminal env.
"""
env = os.environ.copy()
# Strip all MEM0_ env vars so tests start clean
for key in list(env.keys()):
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
if home_dir:
env["HOME"] = home_dir
if env_override:
env.update(env_override)
return subprocess.run(
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
text=True,
env=env,
)
return subprocess.CompletedProcess(
args=result.args,
returncode=result.returncode,
stdout=_strip_ansi(result.stdout),
stderr=_strip_ansi(result.stderr),
)
@pytest.fixture
@@ -62,12 +83,6 @@ 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
@@ -161,7 +176,12 @@ class TestCLIIsolated:
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
assert (
"memory ID" in combined.lower()
or "--all" in combined
or "--entity" in combined
or "Error" in combined
)
def test_config_show_clean(self, clean_home):
"""config show with no config should still work."""
+495 -60
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import json
import typing
from io import StringIO
from unittest.mock import patch
@@ -16,6 +17,7 @@ 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,
@@ -28,7 +30,6 @@ from mem0_cli.commands.memory import (
from mem0_cli.commands.utils import (
cmd_import,
cmd_status,
cmd_version,
)
@@ -62,7 +63,7 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="text",
)
@@ -88,7 +89,7 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="text",
)
@@ -113,7 +114,7 @@ class TestAddCommand:
metadata='{"source": "test"}',
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="text",
)
@@ -139,7 +140,7 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="json",
)
@@ -165,7 +166,7 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="quiet",
)
@@ -176,30 +177,28 @@ 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.sys") as mock_sys,
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
pytest.raises((SystemExit, ClickExit)),
):
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",
)
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()
@@ -221,7 +220,7 @@ class TestAddCommand:
metadata="not-json",
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="text",
)
@@ -247,7 +246,7 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories=None,
output="text",
)
@@ -272,13 +271,73 @@ class TestAddCommand:
metadata=None,
immutable=False,
no_infer=False,
expires=None,
expires=None,
categories="health,prefs",
output="text",
)
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()
@@ -618,28 +677,6 @@ 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()
@@ -704,15 +741,6 @@ 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"
@@ -888,6 +916,28 @@ 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()
@@ -1024,3 +1074,388 @@ 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
+146
View File
@@ -11,6 +11,7 @@ from mem0_cli.output import (
format_memories_table,
format_memories_text,
format_single_memory,
sanitize_agent_data,
)
@@ -129,8 +130,153 @@ 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, and graph settings:
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
```python
# Update project with custom categories
@@ -101,6 +101,9 @@ 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="...",
@@ -108,7 +111,8 @@ client.project.update(
{"personal_info": "User personal information and preferences"},
{"work_context": "Professional context and work-related information"}
],
enable_graph=True
enable_graph=True,
multilingual=True
)
```
+44
View File
@@ -8,6 +8,34 @@ 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:**
@@ -817,6 +845,22 @@ 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,6 +24,7 @@ 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
@@ -52,6 +53,7 @@ const config = {
provider: "gemini",
config: {
model: "gemini-2.0-flash-001",
apiKey: process.env.GOOGLE_API_KEY || '',
temperature: 0.1
}
}
+42 -13
View File
@@ -133,6 +133,26 @@
"pages": [
"platform/contribute"
]
},
{
"group": "Release Notes",
"icon": "rocket",
"pages": [
"changelog"
]
}
]
},
{
"tab": "OpenClaw",
"groups": [
{
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw",
"integrations/hermes"
]
}
]
},
@@ -393,7 +413,6 @@
"integrations/autogen",
"integrations/agno",
"integrations/camel-ai",
"integrations/openclaw",
"integrations/openai-agents-sdk",
"integrations/google-ai-adk",
"integrations/mastra",
@@ -430,6 +449,28 @@
}
]
},
{
"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": [
@@ -517,18 +558,6 @@
]
}
]
},
{
"tab": "Release Notes",
"groups": [
{
"group": "Changelog",
"icon": "rocket",
"pages": [
"changelog"
]
}
]
}
]
}
+28
View File
@@ -381,4 +381,32 @@ 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>
+156
View File
@@ -0,0 +1,156 @@
---
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>
+212
View File
@@ -0,0 +1,212 @@
---
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>
+148
View File
@@ -0,0 +1,148 @@
---
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>
+106
View File
@@ -0,0 +1,106 @@
---
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>
+4
View File
@@ -3686,6 +3686,10 @@
"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."
}
}
}
+88 -16
View File
@@ -1,6 +1,6 @@
---
title: CLI
description: "Manage memories from your terminal. Available for Node.js and Python."
description: "Manage memories from your terminal — built for both humans and AI agents."
icon: "terminal"
iconType: "solid"
---
@@ -9,6 +9,10 @@ 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>
@@ -74,8 +78,23 @@ 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.
@@ -202,16 +221,32 @@ mem0 config get api_key # Get a specific value
mem0 config set user_id bob # Set a value
```
### `mem0 entities`
### `mem0 entity`
List or delete entities (users, agents, apps).
List or delete entities (users, agents, apps, runs).
```bash
mem0 entities list
mem0 entities list --type agent --output json
mem0 entities delete --user-id alice --force
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.
@@ -238,6 +273,7 @@ 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:
@@ -247,20 +283,54 @@ mem0 search "user preferences" --user-id alice --output json | jq '.data.results
## Use with AI agents
The CLI is designed to be used by AI agents and automation tools. Two features make this straightforward:
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:
- **`--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
- 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.
```bash
# 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
# 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
```
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.
<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.
## Environment variables
@@ -278,10 +348,12 @@ Environment variables take precedence over values in the config file, which take
## Global flags
These flags are available on all commands that interact with the API:
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 |
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mem0": {
"url": "https://mcp.mem0.ai/mcp/",
"bearer_token_env_var": "MEM0_API_KEY"
}
}
}
+35
View File
@@ -0,0 +1,35 @@
{
"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"
}
}
+69 -8
View File
@@ -1,6 +1,6 @@
# Mem0 Plugin for Claude Code, Claude Cowork & Cursor
# Mem0 Plugin for Claude Code, Claude Cowork, Cursor & 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**.
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**.
## Step 1: Set your API key
@@ -47,6 +47,65 @@ 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.
@@ -86,15 +145,17 @@ After installing, confirm the MCP server is connected:
## What's included
| 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 |
| 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 |
- **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.
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end. (Claude Code/Cursor only)
- **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
+62
View File
@@ -0,0 +1,62 @@
---
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.
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.4.4",
"version": "2.4.6",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -68,6 +68,11 @@
"!src/community/**"
]
},
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "mem0-ts"
},
"keywords": [
"mem0",
"api",
+1
View File
@@ -167,6 +167,7 @@ export interface PromptUpdatePayload {
exclusion_prompt?: string;
memory_depth?: string | null;
usecase_setting?: string | number;
multilingual?: boolean;
[key: string]: any;
}
+8 -2
View File
@@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types";
export class AzureOpenAIEmbedder implements Embedder {
private client: AzureOpenAI;
private model: string;
private embeddingDims?: number;
private embeddingDims: number | undefined;
constructor(config: EmbeddingConfig) {
if (!config.apiKey || !config.modelProperties?.endpoint) {
@@ -20,13 +20,16 @@ export class AzureOpenAIEmbedder implements Embedder {
...rest,
});
this.model = config.model || "text-embedding-3-small";
this.embeddingDims = config.embeddingDims || 1536;
this.embeddingDims = config.embeddingDims;
}
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;
}
@@ -35,6 +38,9 @@ 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);
}
+8 -4
View File
@@ -5,21 +5,23 @@ import { EmbeddingConfig } from "../types";
export class GoogleEmbedder implements Embedder {
private google: GoogleGenAI;
private model: string;
private embeddingDims?: number;
private embeddingDims: number | undefined;
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 || 1536;
this.embeddingDims = config.embeddingDims;
}
async embed(text: string): Promise<number[]> {
const response = await this.google.models.embedContent({
model: this.model,
contents: text,
config: { outputDimensionality: this.embeddingDims },
...(this.embeddingDims !== undefined && {
config: { outputDimensionality: this.embeddingDims },
}),
});
return response.embeddings![0].values!;
}
@@ -28,7 +30,9 @@ export class GoogleEmbedder implements Embedder {
const response = await this.google.models.embedContent({
model: this.model,
contents: texts,
config: { outputDimensionality: this.embeddingDims },
...(this.embeddingDims !== undefined && {
config: { outputDimensionality: this.embeddingDims },
}),
});
return response.embeddings!.map((item) => item.values!);
}
+8 -2
View File
@@ -5,7 +5,7 @@ import { EmbeddingConfig } from "../types";
export class OpenAIEmbedder implements Embedder {
private openai: OpenAI;
private model: string;
private embeddingDims?: number;
private embeddingDims: number | undefined;
constructor(config: EmbeddingConfig) {
this.openai = new OpenAI({
@@ -13,13 +13,16 @@ export class OpenAIEmbedder implements Embedder {
baseURL: config.baseURL || config.url,
});
this.model = config.model || "text-embedding-3-small";
this.embeddingDims = config.embeddingDims || 1536;
this.embeddingDims = config.embeddingDims;
}
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;
}
@@ -28,6 +31,9 @@ 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)
.single();
.maybeSingle();
if (error) throw error;
if (!data) return null;
@@ -0,0 +1,163 @@
/// <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");
});
});
});
@@ -0,0 +1,153 @@
/// <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");
});
});
});
@@ -0,0 +1,152 @@
/// <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");
});
});
});
+10
View File
@@ -676,6 +676,7 @@ 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.
@@ -689,6 +690,7 @@ 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.
@@ -718,6 +720,7 @@ 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 "
@@ -736,6 +739,7 @@ class MemoryClient:
"exclusion_prompt": exclusion_prompt,
"memory_depth": memory_depth,
"usecase_setting": usecase_setting,
"multilingual": multilingual,
}
)
response = self.client.patch(
@@ -756,6 +760,7 @@ class MemoryClient:
"exclusion_prompt": exclusion_prompt,
"memory_depth": memory_depth,
"usecase_setting": usecase_setting,
"multilingual": multilingual,
"sync_type": "sync",
},
)
@@ -1554,6 +1559,7 @@ 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.
@@ -1563,6 +1569,7 @@ 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.
@@ -1588,6 +1595,7 @@ 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"
@@ -1600,6 +1608,7 @@ class AsyncMemoryClient:
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"version": version,
"multilingual": multilingual,
}
)
response = await self.async_client.patch(
@@ -1616,6 +1625,7 @@ class AsyncMemoryClient:
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"version": version,
"multilingual": multilingual,
"sync_type": "async",
},
)
+12 -2
View File
@@ -399,6 +399,7 @@ 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.
@@ -408,6 +409,7 @@ 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.
@@ -424,11 +426,12 @@ 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"
"enable_graph, multilingual"
)
payload = self._prepare_params(
@@ -437,6 +440,7 @@ class Project(BaseProject):
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"multilingual": multilingual,
}
)
response = self._client.patch(
@@ -452,6 +456,7 @@ class Project(BaseProject):
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"multilingual": multilingual,
"sync_type": "sync",
},
)
@@ -716,6 +721,7 @@ 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.
@@ -725,6 +731,7 @@ 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.
@@ -741,11 +748,12 @@ 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"
"enable_graph, multilingual"
)
payload = self._prepare_params(
@@ -754,6 +762,7 @@ class AsyncProject(BaseProject):
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"multilingual": multilingual,
}
)
response = await self._client.patch(
@@ -769,6 +778,7 @@ class AsyncProject(BaseProject):
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"enable_graph": enable_graph,
"multilingual": multilingual,
"sync_type": "async",
},
)
+2 -3
View File
@@ -1,6 +1,6 @@
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class AzureMySQLConfig(BaseModel):
@@ -80,5 +80,4 @@ class AzureMySQLConfig(BaseModel):
return values
class Config:
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)
+2 -3
View File
@@ -1,6 +1,6 @@
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class CassandraConfig(BaseModel):
@@ -72,6 +72,5 @@ class CassandraConfig(BaseModel):
return values
class Config:
arbitrary_types_allowed = True
model_config = ConfigDict(arbitrary_types_allowed=True)
+3 -1
View File
@@ -1,7 +1,7 @@
from collections.abc import Callable
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class ElasticsearchConfig(BaseModel):
@@ -63,3 +63,5 @@ class ElasticsearchConfig(BaseModel):
f"Please input only the following fields: {', '.join(allowed_fields)}"
)
return values
model_config = ConfigDict(arbitrary_types_allowed=True)
+3 -1
View File
@@ -1,6 +1,6 @@
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class MongoDBConfig(BaseModel):
@@ -23,3 +23,5 @@ class MongoDBConfig(BaseModel):
f"Please provide only the following fields: {', '.join(allowed_fields)}."
)
return values
model_config = ConfigDict(arbitrary_types_allowed=False)
+2 -4
View File
@@ -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, Field
from pydantic import BaseModel, ConfigDict, Field
class NeptuneAnalyticsConfig(BaseModel):
@@ -22,6 +22,4 @@ 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 = {
"arbitrary_types_allowed": False,
}
model_config = ConfigDict(arbitrary_types_allowed=False)
+3 -1
View File
@@ -1,6 +1,6 @@
from typing import Any, Dict, Optional, Type, Union
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class OpenSearchConfig(BaseModel):
@@ -39,3 +39,5 @@ 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)
+3 -1
View File
@@ -1,6 +1,6 @@
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class PGVectorConfig(BaseModel):
@@ -50,3 +50,5 @@ 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)
+3 -1
View File
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, ConfigDict, Field, model_validator
class IndexMethod(str, Enum):
@@ -42,3 +42,5 @@ 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)
+26 -10
View File
@@ -1,15 +1,31 @@
from pydantic import BaseModel
from typing import Any, Dict
from pydantic import BaseModel, ConfigDict, Field, model_validator
class ValkeyConfig(BaseModel):
"""Configuration for Valkey vector store."""
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
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)
+2
View File
@@ -99,6 +99,8 @@ class DeepSeekLLM(LLMBase):
}
)
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice

Some files were not shown because too many files have changed in this diff Show More