Compare commits

..
Author SHA1 Message Date
Saket AryanandGitHub 5d40592e42 chore: version bump to beta1 (#4827) 2026-04-14 18:05:22 +05:30
a488e19044 feat(oss): port v3 pipeline with hybrid search, entity extraction, and additive scoring (#4805)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
Co-authored-by: chaithanyak42 <chaithanya.kumar42a@gmail.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-14 18:00:58 +05:30
57f944e18a fix: allow anonymousTelemetryId in openclaw.json config (#4826)
Co-authored-by: parteeksachdeva-123 <parteek.sachdeva@aerchain.io>
2026-04-14 17:31:28 +05:30
Gabriel SteinandGitHub fe3f7ae618 fix(plugin): remove invalid keys from Claude plugin config (#4821) 2026-04-14 01:13:03 +05:30
shafdevandGitHub 4a7e166f9a fix(tests): use top_k instead of limit in test_server_params (#4820) 2026-04-14 01:11:31 +05:30
KartikandGitHub 85768e78e7 fix(docs): remove chrome extension cookbooks (#4813) 2026-04-13 22:14:00 +05:30
YunsuandGitHub 7b395f3bf7 fix(openai): make store opt-in so it stops leaking to non-OpenAI backends (#4757) 2026-04-13 21:53:22 +05:30
HUANG XIAOandGitHub 4180409b09 fix(s3vectors): handle vector=None in update() to prevent boto3 validation error (#4594) 2026-04-13 21:12:49 +05:30
Joe WuandGitHub 649e719ce6 fix: LLM config manager falls back to userConf.url for baseURL (#4715) (#4761) 2026-04-13 20:46:18 +05:30
KartikandGitHub 1a53852d93 test: update valkey cluster search test to use top_k parameter (#4815) 2026-04-13 20:44:18 +05:30
Chinnu AbeyandGitHub ac9cdd4840 Fix incorrect use of SentenceTransformer for cross-encoder reranker models (#4806) 2026-04-13 20:14:22 +05:30
Swarnaprakash UdayakumarandGitHub cf530c4bec feat(valkey): add cluster mode enabled (CME) support (#4759) 2026-04-13 20:07:11 +05:30
Saket AryanandGitHub 92b958c1cc chore: bump Python SDK to v2.0.0b0 and Node SDK to v3.0.0-beta.0 (#4810) 2026-04-13 15:51:33 +05:30
Asish KumarandGitHub c239d8a483 fix(client): prevent feedback telemetry TypeError (#4795) 2026-04-12 03:06:05 +05:30
9d6b79a14e fix(sdk): removing the enable graph flag and switching from snake case to camel case for client ts sdk (#4776)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 02:40:54 +05:30
KartikandGitHub e44b46ef2e fix(sdk): removing deprecating param from our sdk and docs changes with it (#4740) 2026-04-12 00:34:58 +05:30
Saket AryanandGitHub 3882af7450 fix(cli): persistent anonymous telemetry ID + pass source=CLI in all API calls (#4789) 2026-04-11 21:00:05 +05:30
d39ebad09f fix(openclaw): persistent anonymous telemetry ID, flush fix, and email resolution (#4790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:57:14 +05:30
Gabriel SteinandGitHub 9d82e2329d refactor(telemetry): sample OSS hot-path events at 10% to reduce PostHog volume (#4771) 2026-04-11 16:25:38 +05:30
szinvasandGitHub 789cc9d607 docs - replace session_id with run_id (#4742) 2026-04-10 20:06:59 +05:30
e59e3d5f0c Add deepseek.ts to src/llms with corresponding unit tests. Updates fa… (#4613)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-10 20:06:22 +05:30
KartikandGitHub d926f3697c fix(docs): eliminate ~222 SEO redirect chains on docs.mem0.ai (#4768) 2026-04-10 19:33:45 +05:30
KartikandGitHub c996b0e7fa docs: removing the changelog.mdx file adn replacing it with new changelog system (#4750) 2026-04-10 19:07:43 +05:30
KartikandGitHub 78ca85a260 refactor: update OpenClaw plugin config, hook logic, and documentation (#4764) 2026-04-09 17:36:05 +05:30
KartikandGitHub 88f696a60a refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734) 2026-04-09 14:57:33 +05:30
081eca6d8f fix: guard temp_uuid_mapping lookups against LLM-hallucinated IDs (fixes #3931) (#4674)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-08 21:55:52 +05:30
KartikandGitHub 2434b9d550 docs: add ChatDev integration guide and update integrations list (#4751) 2026-04-08 20:16:04 +05:30
Rakhee SinghandGitHub 3ffea554bc fix(azure_openai): forward response_format to Azure OpenAI API (#4689) 2026-04-08 19:22:02 +05:30
Rakhee SinghandGitHub 1ad8a59b0c fix(deepseek): forward response_format to OpenAI-compatible API (#4688) 2026-04-08 19:21:11 +05:30
Saket AryanandGitHub a670333d67 feat: add AGENTS.md for AI coding agent instructions (#4726) 2026-04-06 21:32:43 +05:30
Saket AryanandGitHub 4c2db3e68b feat(skills): introduce Mem0 skill graph with dedicated CLI and Vercel AI SDK skills (#4725) 2026-04-06 20:41:29 +05:30
07f0d4f1e0 fix: use npx npm@latest for OIDC trusted publishing (#4724)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:22:00 +05:30
Saket AryanandGitHub 3565404eef fix: remove npm self-upgrade from CD workflows (#4723) 2026-04-06 17:11:28 +05:30
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
433 changed files with 36394 additions and 24427 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: jobs:
build-n-publish: build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
@@ -38,7 +39,7 @@ jobs:
# packages_dir: dist/ # packages_dir: dist/
- name: Publish distribution 📦 to PyPI - 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 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
packages_dir: dist/ packages_dir: dist/
+46
View File
@@ -0,0 +1,46 @@
name: Publish @mem0/cli 📦 to npm
on:
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/cli 📦 to npm
if: startsWith(github.event.release.tag_name, 'cli-node-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: cli/node
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+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/
+46
View File
@@ -0,0 +1,46 @@
name: Publish @mem0/openclaw-mem0 📦 to npm
on:
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/openclaw-mem0 📦 to npm
if: startsWith(github.event.release.tag_name, 'openclaw-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: openclaw
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+46
View File
@@ -0,0 +1,46 @@
name: Publish mem0ai 📦 to npm
on:
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish mem0ai 📦 to npm
if: startsWith(github.event.release.tag_name, 'ts-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: mem0-ts
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: mem0-ts/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+46
View File
@@ -0,0 +1,46 @@
name: Publish @mem0/vercel-ai-provider 📦 to npm
on:
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: vercel-ai-sdk
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+580
View File
@@ -0,0 +1,580 @@
# AGENTS.md
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
## Project Overview
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
- **Repository**: https://github.com/mem0ai/mem0
- **Documentation**: https://docs.mem0.ai
- **License**: Apache-2.0
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
### Key Directories
| Directory | Description |
|-----------|-------------|
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
| `docs/` | Documentation site (Mintlify) |
| `tests/` | Python SDK tests (pytest) |
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
| `pr-reviews/` | Pull request review materials |
### Core Package Dependencies
```
mem0 (Python SDK) mem0-ts (TypeScript SDK)
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
├── mem0/embeddings/ ├── src/llms/
├── mem0/vector_stores/ ├── src/embeddings/
├── mem0/graphs/ ├── src/vector_stores/
└── mem0/reranker/ └── src/graphs/
cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls)
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
openclaw/ ──▶ mem0ai (npm)
```
## Development Setup
### Requirements
- **Python**: 3.9+ (3.10+ for CLI)
- **Node.js**: v18+ (v20 or v22 recommended)
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
- **Hatch**: Python build/environment tool (`pip install hatch`)
- **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup
```bash
# Python SDK
hatch shell dev_py_3_11 # creates environment with all deps
pre-commit install # install git hooks
# TypeScript packages
cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI
cd vercel-ai-sdk && pnpm install # Vercel AI provider
cd openclaw && pnpm install # OpenClaw plugin
```
## Build, Lint, and Test Commands
### Python SDK (`mem0/`)
```bash
# Environment setup (uses Hatch)
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
# Linting and formatting
make lint # ruff check
make format # ruff format
make sort # isort mem0/
# Tests
make test # pytest tests/
make test-py-3.9 # test specific Python version (3.9–3.12)
# Build and publish
make build # hatch build
make publish # hatch publish
```
- **Python:** 3.9, 3.10, 3.11, 3.12
- **Linter/formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
### TypeScript SDK (`mem0-ts/`)
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup
pnpm run test # jest (all tests)
pnpm run test:unit # jest --coverage (unit tests only)
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
pnpm run test:ci # jest --coverage --ci (CI mode)
pnpm run test:watch # jest watch mode
```
- **Node:** 20, 22 (CI-tested)
- **Build:** tsup (CJS + ESM)
- **Test:** jest
- **Formatter:** prettier
### Python CLI (`cli/python/`)
```bash
cd cli/python
pip install -e ".[dev]" # dev install with ruff + pytest
ruff check . # lint
ruff format . # format
pytest # test
hatch build # build
```
- **Python:** 3.10+ (not 3.9)
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
- **Framework:** Typer + Rich + httpx
- **Entry point:** `mem0 = "mem0_cli.app:main"`
- **Source layout:** `src/mem0_cli/`
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
### Node CLI (`cli/node/`)
```bash
cd cli/node
pnpm install
pnpm run build # tsup
pnpm run lint # biome check src/
pnpm run lint:fix # biome check --write src/
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest run
pnpm run test:watch # vitest (watch mode)
pnpm run dev # tsx src/index.ts (development)
```
- **Node:** 18+ required
- **Build:** tsup (ESM)
- **Linter:** Biome (not ESLint, not Ruff)
- **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
```bash
cd vercel-ai-sdk
pnpm install
pnpm run build # tsup
pnpm run lint # eslint
pnpm run type-check # tsc --noEmit
pnpm run prettier-check # prettier --check
pnpm run test # jest
pnpm run test:edge # vitest (edge runtime)
pnpm run test:node # vitest (node runtime)
```
- **Build:** tsup (CJS + ESM)
- **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`openclaw/`)
```bash
cd openclaw
pnpm install
pnpm run build # tsup
pnpm run test # vitest run
```
- **Build:** tsup (ESM)
- **Test:** vitest (with Codecov in CI)
- **Plugin manifest:** `openclaw.plugin.json`
### Server (`server/`)
```bash
# Docker production build
cd server
make build # docker build -t mem0-api-server .
make run_local # docker run -p 8000:8000 with .env
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
cd server
docker-compose up # starts all 3 services
# mem0 API: localhost:8888
# PostgreSQL: localhost:8432
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
```
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
### OpenMemory (`openmemory/`)
```bash
# Full stack via Docker Compose
cd openmemory
docker-compose up
# Qdrant: localhost:6333
# API (MCP): localhost:8765
# UI: localhost:3000
# Individual development
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
cd openmemory/ui && npm run dev # Next.js frontend
# Tests
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
```
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
- **Vector store:** Qdrant
### Documentation (`docs/`)
```bash
make docs # or: cd docs && mintlify dev
```
- **Framework:** Mintlify
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation (`evaluation/`)
```bash
cd evaluation
make run-mem0-add # Run mem0 add experiments
make run-mem0-search # Run mem0 search experiments
make run-mem0-plus-add # With graph memory
make run-mem0-plus-search # With graph memory
make run-rag # RAG baseline
make run-full-context # Full context baseline
make run-langmem # LangMem comparison
make run-openai # OpenAI comparison
```
## Core APIs
### Python
| Function / Class | Purpose | Import |
|-----------------|---------|--------|
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
**Key `Memory` / `MemoryClient` methods:**
| Method | Purpose |
|--------|---------|
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
| `get(memory_id)` | Retrieve a single memory by ID |
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
| `update(memory_id, data)` | Update a memory |
| `delete(memory_id)` | Delete a memory |
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
| `history(memory_id)` | Get change history for a memory |
### TypeScript
| Export | Purpose | Import |
|--------|---------|--------|
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
## Import Patterns
### Python
| What | Import |
|------|--------|
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
| Configuration | `from mem0.configs.base import MemoryConfig` |
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
### TypeScript
| What | Import |
|------|--------|
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
## Coding Standards
### File Naming Conventions
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
### Python Conventions
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
- **Pydantic v2** for all data models and configuration.
- **Ruff** is the single linting and formatting tool — no black, no flake8.
- Root SDK: line length **120**
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
- **isort** with `profile = "black"` for import sorting.
- Ruff excludes `embedchain/` and `openmemory/` from root config.
### TypeScript Conventions
- **Build:** tsup across all packages.
- **Package manager:** pnpm everywhere (no npm, no yarn).
- **TypeScript strict mode** across all packages.
- **Linting varies by package:**
| Package | Linter | Formatter | Test Framework |
|---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest |
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `openclaw/` | — | — | vitest |
### Type Checking
Always run type checking after modifying TypeScript code:
```bash
cd <package> && pnpm run typecheck # or: tsc --noEmit
```
## Architecture
### Provider Pattern
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
| Category | Count | Examples |
|----------|-------|---------|
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
### Two Usage Modes
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
### Graph Memory
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
### MCP Integration
Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
- **Plugin:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
### Plugin & Skills System
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
### Adding a New Provider
To add a new LLM, embedding, vector store, or reranker provider:
1. Create `mem0/<category>/<provider_name>.py`
2. Inherit from the abstract base class in `mem0/<category>/base.py`
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
4. Register the provider in `mem0/<category>/__init__.py`
5. Add tests in `tests/<category>/<provider_name>/`
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
## CI/CD
### CI Workflows (automated testing)
| Workflow | File | Triggers | Tests |
|----------|------|----------|-------|
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
### CD Workflows (automated publishing)
| Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------|
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
### Utility Workflows
| Workflow | File | Purpose |
|----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
## Task Completion Guidelines
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
### Bug Fixes
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
2. **Implementation**: Fix the bug
3. **Manual verification**: Run the relevant test suite to confirm the fix
4. **Lint**: Run the appropriate linter for the package you modified
### New Features
1. **Implementation**: Build the feature following existing patterns
2. **Unit tests**: Comprehensive test coverage for new functionality
3. **Documentation**: Update relevant docs in `docs/` for public APIs
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
### New Provider (LLM / Embedding / Vector Store / Reranker)
1. **Implementation**: Follow the "Adding a New Provider" steps above
2. **Tests**: Add unit tests matching the pattern of existing providers
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
5. **Documentation**: Add an integration guide in `docs/integrations/`
### Refactoring / Internal Changes
- Unit tests for any changed behavior
- No documentation needed for internal-only changes
- Ensure all existing tests still pass
### When to Deviate
These are guidelines, not rigid rules. Adjust based on:
- **Scope**: Trivial fixes (typos, comments) may not need tests
- **Visibility**: Internal changes may not need documentation
- **Context**: Some changes span multiple categories — use judgment
When uncertain about expected artifacts, ask for clarification.
## Contributing Guidelines
### Workflow
1. Fork and clone the repository.
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
3. Make your changes — add tests, docs, and examples as appropriate.
4. Run linting and tests for every package you modified (see commands above).
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
7. Push and open a Pull Request against `main`.
### Pull Request Requirements
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
2. **Description** — Explain what the PR does and why it's needed.
3. **Type of Change** — Check the appropriate box:
- Bug fix / New feature / Breaking change / Refactor / Documentation update
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
5. **Test Coverage** — Check what applies:
- Added/updated unit tests
- Added/updated integration tests
- Tested manually (describe how)
- No tests needed (explain why)
6. **Checklist** — All must be checked before merge:
- [ ] Code follows the project's style guidelines
- [ ] Self-review performed
- [ ] Tests added that prove the fix/feature works
- [ ] New and existing tests pass locally
- [ ] Documentation updated if needed
### PR Description Template
```markdown
## Linked Issue
Closes #<!-- issue number -->
## Description
<!-- What does this PR do? Why is it needed? -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Refactor (no functional changes)
- [ ] Documentation update
## Breaking Changes
N/A
## Test Coverage
- [ ] I added/updated unit tests
- [ ] I added/updated integration tests
- [ ] I tested manually (describe below)
- [ ] No tests needed (explain why)
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added tests that prove my fix/feature works
- [ ] New and existing tests pass locally
- [ ] I have updated documentation if needed
```
### General Rules
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
- For `server/` and `openmemory/` work, use Docker Compose for local development.
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
### Contributing Guides
| Task | Guide |
|------|-------|
| Code contributions | `docs/contributing/development.mdx` |
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
## Do NOT
- Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials.
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
- Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
- Change public APIs without updating documentation in `docs/`.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+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. 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! 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
@@ -266,7 +266,7 @@ config = MemoryConfig(
graph_store=GraphStoreConfig(provider="neo4j", config={...}), # optional graph_store=GraphStoreConfig(provider="neo4j", config={...}), # optional
history_db_path="~/.mem0/history.db", history_db_path="~/.mem0/history.db",
version="v1.1", version="v1.1",
custom_fact_extraction_prompt="Custom prompt...", custom_instructions="Custom prompt...",
custom_update_memory_prompt="Custom prompt..." custom_update_memory_prompt="Custom prompt..."
) )
``` ```
@@ -684,7 +684,7 @@ Conversation: {messages}
""" """
config = MemoryConfig( config = MemoryConfig(
custom_fact_extraction_prompt=custom_extraction_prompt custom_instructions=custom_extraction_prompt
) )
memory = Memory(config) memory = Memory(config)
``` ```
+10 -1
View File
@@ -88,6 +88,13 @@ Install the sdk via pip:
pip install mem0ai pip install mem0ai
``` ```
For enhanced hybrid search with BM25 keyword matching and entity extraction, install with NLP support:
```bash
pip install mem0ai[nlp]
python -m spacy download en_core_web_sm
```
Install sdk via npm: Install sdk via npm:
```bash ```bash
npm install mem0ai npm install mem0ai
@@ -109,7 +116,9 @@ See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full comm
### Basic Usage ### Basic Usage
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview). Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
Mem0 uses `text-embedding-3-small` from OpenAI as the default embedding model. For best results with hybrid search (semantic + keyword + entity boosting), we recommend using at least [Qwen 600M](https://huggingface.co/Alibaba-NLP/gte-Qwen2-1.5B-instruct) or a comparable embedding model. See [Supported Embeddings](https://docs.mem0.ai/components/embedders/overview) for configuration details.
First step is to instantiate the memory: First step is to instantiate the memory:
+175 -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? ### Who is it for?
- Developers integrating mem0 into their workflows - 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 - DevOps/CI pipelines that need to manage memories programmatically
### Project Structure ### Project Structure
@@ -79,6 +79,7 @@ Apache-2.0
│ ├── init_cmd.py # run_init (interactive wizard) │ ├── init_cmd.py # run_init (interactive wizard)
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set │ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
│ ├── entities.py # cmd_entities_list, cmd_entities_delete │ ├── 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 │ └── utils.py # cmd_status, cmd_version, cmd_import
└── node/ └── node/
├── package.json # Node package config (tsup build) ├── package.json # Node package config (tsup build)
@@ -88,6 +89,7 @@ Apache-2.0
├── config.ts # Config loading/saving, env var overrides ├── config.ts # Config loading/saving, env var overrides
├── branding.ts # Colors, icons, banner, timedStatus, print helpers ├── branding.ts # Colors, icons, banner, timedStatus, print helpers
├── output.ts # Output formatting (text, json, table, quiet) ├── output.ts # Output formatting (text, json, table, quiet)
├── state.ts # Agent mode flag (setAgentMode, isAgentMode)
├── help.ts # Rich-style help formatter (panels, command ordering) ├── help.ts # Rich-style help formatter (panels, command ordering)
├── backend/ ├── backend/
│ ├── index.ts # Re-exports │ ├── index.ts # Re-exports
@@ -98,6 +100,7 @@ Apache-2.0
├── init.ts # runInit (interactive wizard) ├── init.ts # runInit (interactive wizard)
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet ├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete ├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
├── events.ts # cmdEventList, cmdEventStatus
└── utils.ts # cmdStatus, cmdVersion, cmdImport └── utils.ts # cmdStatus, cmdVersion, cmdImport
``` ```
@@ -155,9 +158,15 @@ Interactive setup wizard for mem0 CLI.
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). | | `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
| `--email` | string | No | - | Login via email verification code. | | `--email` | string | No | - | Login via email verification code. |
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). | | `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
| `--force` | bool | No | false | Overwrite existing config without confirmation. |
**Behavior:** **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):* *Email login flow (when `--email` is provided):*
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`. - 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 also provided, verifies immediately (fully non-interactive).
@@ -175,6 +184,7 @@ Interactive setup wizard for mem0 CLI.
```bash ```bash
mem0 init mem0 init
mem0 init --api-key m0-xxx --user-id alice 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
mem0 init --email alice@company.com --code 482901 mem0 init --email alice@company.com --code 482901
``` ```
@@ -700,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. Check connectivity and authentication.
@@ -721,20 +838,19 @@ Check connectivity and authentication.
| `--api-key` | string | - | Connection | Override API key. | | `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. | | `--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 output:**
```json ```json
{ {
"status": "success", "status": "success",
"command": "status", "command": "status",
"duration_ms": 112,
"data": { "data": {
"connected": true, "connected": true,
"backend": "platform", "backend": "platform",
"base_url": "https://api.mem0.ai", "base_url": "https://api.mem0.ai"
"latency_ms": 245 }
},
"duration_ms": 245
} }
``` ```
@@ -746,7 +862,7 @@ mem0 status -o json
--- ---
### 3.15 `help` ### 3.17 `help`
Show help. Use `--json` for machine-readable output (for LLM agents). Show help. Use `--json` for machine-readable output (for LLM agents).
@@ -806,6 +922,9 @@ The auth header name is `Authorization` and the scheme is `Token` (not Bearer).
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params | | Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
| List entities | `GET` | `/v1/entities/` | - | - | | List entities | `GET` | `/v1/entities/` | - | - |
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params | | 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`) ### How Filters Are Built (`_buildFilters` / `_build_filters`)
@@ -1108,6 +1227,8 @@ For `PENDING` events, displays "Processing in background" with the event ID.
### 7.1 Supported Modes Per Command ### 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 | | Command | text | json | table | quiet |
|----------------|------|------|-------|-------| |----------------|------|------|-------|-------|
| add | Y | Y | - | Y | | add | Y | Y | - | Y |
@@ -1122,12 +1243,16 @@ For `PENDING` events, displays "Processing in background" with the event ID.
| config set | (success msg) | - | - | - | | config set | (success msg) | - | - | - |
| entity list | - | Y | Y (default) | - | | entity list | - | Y | Y (default) | - |
| entity delete | Y | Y | - | Y | | entity delete | Y | Y | - | Y |
| event list | Y (table) | Y | - | - |
| event status | Y | Y | - | - |
| status | Y | Y | - | - | | status | Y | Y | - | - |
| help | Y | Y (--json) | - | - | | 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 ```json
{ {
@@ -1141,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: Fields:
- `status`: Always `"success"` (errors go to stderr before exit). - `status`: `"success"` or `"error"`.
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`). - `command`: The command name.
- `duration_ms`: Optional, elapsed time in milliseconds. - `duration_ms`: Optional, elapsed time in milliseconds.
- `scope`: Optional, active entity scope. - `scope`: Optional, active entity scope (omitted if empty).
- `count`: Optional, result count. - `count`: Optional, result count.
- `error`: Optional, error message string. - `error`: Only present when `status` is `"error"`.
- `data`: The primary payload. - `data`: The primary payload (sanitized in agent mode).
### 7.3 Text Output ### 7.3 Text Output
@@ -1219,6 +1369,16 @@ Destructive commands (`delete --all`, `delete --entity`, `entity delete`) requir
- CI/CD pipelines - CI/CD pipelines
- Scripting - 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 ### 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. 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.
+38 -6
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. 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 ## Installation
```bash ```bash
@@ -55,12 +57,45 @@ mem0 delete <memory-id>
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity | | `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
| `mem0 import` | Bulk import memories from a JSON file | | `mem0 import` | Bulk import memories from a JSON file |
| `mem0 config` | View or modify CLI configuration | | `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 status` | Verify API connection and display current project |
| `mem0 version` | Print the CLI version | | `mem0 version` | Print the CLI version |
Run `mem0 <command> --help` for detailed usage on any command. 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 ## Output formats
Control how results are displayed with `--output`: Control how results are displayed with `--output`:
@@ -68,13 +103,10 @@ Control how results are displayed with `--output`:
| Format | Description | | Format | Description |
|--------|-------------| |--------|-------------|
| `text` | Human-readable with colors and formatting (default) | | `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`) | | `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes | | `quiet` | Minimal — just IDs or status codes |
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
```bash
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
```
## Environment variables ## Environment variables
+2 -1
View File
@@ -515,7 +515,8 @@
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." }, { "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": "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": "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": "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." }
] ]
}, },
{ {
+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. 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 ## Prerequisites
- Node.js **18+** - Node.js **18+**
- pnpm (`npm install -g pnpm`) - pnpm (`npm install -g pnpm`) — for development only
## Installation ## Installation
@@ -13,22 +15,307 @@ The official command-line interface for [mem0](https://mem0.ai) — the memory l
npm install -g @mem0/cli npm install -g @mem0/cli
``` ```
Or from source: ## Quick start
```bash ```bash
cd node # Interactive setup wizard
pnpm install mem0 init
pnpm build
pnpm link --global
# Now use it like a normal CLI # Or login via email
mem0 --help 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 ```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 pnpm install
# Development mode (runs TypeScript directly, no build needed) # 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 # Or build first, then run the compiled JS
pnpm build pnpm build
node dist/index.js --help node dist/index.js --help
node dist/index.js add "test memory" --user-id alice
``` ```
## Quick Start ## Documentation
```bash Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
# Set up your configuration
mem0 init
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories
mem0 list --user-id alice
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MEM0_API_KEY` | API key (overrides config file) |
| `MEM0_BASE_URL` | API base URL |
| `MEM0_USER_ID` | Default user ID |
| `MEM0_AGENT_ID` | Default agent ID |
| `MEM0_APP_ID` | Default app ID |
| `MEM0_RUN_ID` | Default run ID |
| `MEM0_ENABLE_GRAPH` | Enable graph memory (true/false) |
## License ## License
+6 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@mem0/cli", "name": "@mem0/cli",
"version": "0.1.0", "version": "0.2.3",
"description": "The official CLI for mem0 — the memory layer for AI agents", "description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module", "type": "module",
"bin": { "bin": {
@@ -20,6 +20,11 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"author": "mem0.ai <founders@mem0.ai>", "author": "mem0.ai <founders@mem0.ai>",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "cli/node"
},
"keywords": ["mem0", "memory", "ai", "agents", "cli"], "keywords": ["mem0", "memory", "ai", "agents", "cli"],
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
+6
View File
@@ -89,11 +89,17 @@ export interface Backend {
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>; deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
ping(): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise< status(opts?: { userId?: string; agentId?: string }): Promise<
Record<string, unknown> Record<string, unknown>
>; >;
entities(entityType: 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 { export class AuthError extends Error {
+62 -37
View File
@@ -3,6 +3,8 @@
*/ */
import type { PlatformConfig } from "../config.js"; import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import { import {
APIError, APIError,
type AddOptions, type AddOptions,
@@ -24,6 +26,9 @@ export class PlatformBackend implements Backend {
this.headers = { this.headers = {
Authorization: `Token ${config.apiKey}`, Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
}; };
} }
@@ -38,9 +43,14 @@ export class PlatformBackend implements Backend {
url += `?${qs}`; url += `?${qs}`;
} }
const headers = {
...this.headers,
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
};
const fetchOpts: RequestInit = { const fetchOpts: RequestInit = {
method, method,
headers: this.headers, headers,
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}; };
if (opts?.json) { if (opts?.json) {
@@ -106,6 +116,7 @@ export class PlatformBackend implements Backend {
if (opts.expires) payload.expiration_date = opts.expires; if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories; if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
return (await this._request("POST", "/v1/memories/", { return (await this._request("POST", "/v1/memories/", {
json: payload, json: payload,
@@ -166,6 +177,7 @@ export class PlatformBackend implements Backend {
if (opts.keyword) payload.keyword_search = true; if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields; if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v2/memories/search/", { const result = (await this._request("POST", "/v2/memories/search/", {
json: payload, json: payload,
@@ -176,10 +188,9 @@ export class PlatformBackend implements Backend {
} }
async get(memoryId: string): Promise<Record<string, unknown>> { async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record< return (await this._request("GET", `/v1/memories/${memoryId}/`, {
string, params: { source: "CLI" },
unknown })) as Record<string, unknown>;
>;
} }
async listMemories( async listMemories(
@@ -217,6 +228,7 @@ export class PlatformBackend implements Backend {
}); });
if (apiFilters) payload.filters = apiFilters; if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true; if (opts.enableGraph) payload.enable_graph = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v2/memories/", { const result = (await this._request("POST", "/v2/memories/", {
json: payload, json: payload,
@@ -235,6 +247,7 @@ export class PlatformBackend implements Backend {
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
if (content) payload.text = content; if (content) payload.text = content;
if (metadata) payload.metadata = metadata; if (metadata) payload.metadata = metadata;
payload.source = "CLI";
return (await this._request("PUT", `/v1/memories/${memoryId}/`, { return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload, json: payload,
})) as Record<string, unknown>; })) as Record<string, unknown>;
@@ -245,7 +258,7 @@ export class PlatformBackend implements Backend {
opts: DeleteOptions = {}, opts: DeleteOptions = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
if (opts.all) { if (opts.all) {
const params: Record<string, string> = {}; const params: Record<string, string> = { source: "CLI" };
if (opts.userId) params.user_id = opts.userId; if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId; if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId; if (opts.appId) params.app_id = opts.appId;
@@ -255,50 +268,46 @@ export class PlatformBackend implements Backend {
})) as Record<string, unknown>; })) as Record<string, unknown>;
} }
if (memoryId) { if (memoryId) {
return (await this._request( return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
"DELETE", params: { source: "CLI" },
`/v1/memories/${memoryId}/`, })) as Record<string, unknown>;
)) as Record<string, unknown>;
} }
throw new Error("Either memoryId or --all is required"); throw new Error("Either memoryId or --all is required");
} }
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> { async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
const params: Record<string, string> = {}; // v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
if (opts.userId) params.user_id = opts.userId; const typeMap: [string, string | undefined][] = [
if (opts.agentId) params.agent_id = opts.agentId; ["user", opts.userId],
if (opts.appId) params.app_id = opts.appId; ["agent", opts.agentId],
if (opts.runId) params.run_id = opts.runId; ["app", opts.appId],
if (Object.keys(params).length === 0) { ["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."); throw new Error("At least one entity ID is required for deleteEntities.");
} }
return (await this._request("DELETE", "/v1/entities/", { // Delete each provided entity via the v2 path-based endpoint
params, let result: Record<string, unknown> = {};
})) as Record<string, unknown>; for (const [entityType, entityId] of entities) {
result = (await this._request(
"DELETE",
`/v2/entities/${entityType}/${entityId}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
return result;
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
} }
async status( async status(
opts: { userId?: string; agentId?: string } = {}, opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
try { try {
if (opts.userId || opts.agentId) { await this.ping();
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 }; return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) { } catch (e) {
return { return {
@@ -335,4 +344,20 @@ export class PlatformBackend implements Backend {
} }
return items; 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
>;
}
} }
+29 -6
View File
@@ -4,6 +4,7 @@
import chalk from "chalk"; import chalk from "chalk";
import ora, { type Ora } from "ora"; import ora, { type Ora } from "ora";
import { getCurrentCommand, isAgentMode } from "./state.js";
import { CLI_VERSION } from "./version.js"; import { CLI_VERSION } from "./version.js";
export const LOGO = ` export const LOGO = `
@@ -42,6 +43,7 @@ export function sym(fancy: string, plain: string): string {
} }
export function printBanner(): void { export function printBanner(): void {
if (isAgentMode()) return;
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2)) const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n"); const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`; const tagline = ` ${TAGLINE}`;
@@ -75,13 +77,29 @@ export function printBanner(): void {
} }
export function printSuccess(message: string): void { export function printSuccess(message: string): void {
if (isAgentMode()) return;
console.log(`${success(sym("✓", "[ok]"))} ${message}`); console.log(`${success(sym("✓", "[ok]"))} ${message}`);
} }
export function printError(message: string, hint?: string): void { export function printError(message: string, hint?: string): void {
if (isAgentMode()) {
const envelope = {
status: "error",
command: getCurrentCommand(),
error: message,
data: null,
};
console.log(JSON.stringify(envelope));
return;
}
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`); console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
if (hint) { const resolvedHint =
console.error(` ${dim(hint)}`); 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)}`);
} }
} }
@@ -90,15 +108,16 @@ export function printWarning(message: string): void {
} }
export function printInfo(message: string): void { export function printInfo(message: string): void {
if (isAgentMode()) return;
console.error(`${brand(sym("◆", "*"))} ${message}`); console.error(`${brand(sym("◆", "*"))} ${message}`);
} }
export function printScope(ids: Record<string, string | undefined>): void { export function printScope(ids: Record<string, string | undefined>): void {
if (isAgentMode()) return;
const parts: string[] = []; const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) { for (const [key, val] of Object.entries(ids)) {
if (val) { if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim(); parts.push(`${key}=${val}`);
parts.push(`${label}=${val}`);
} }
} }
if (parts.length > 0) { if (parts.length > 0) {
@@ -119,10 +138,14 @@ export async function timedStatus<T>(
message: string, message: string,
fn: (ctx: TimedStatusContext) => Promise<T>, fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> { ): Promise<T> {
if (isAgentMode()) {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
return fn(ctx);
}
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" }; const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({ const spinner = ora({
text: dim(message), text: dim(message),
color: "magenta", color: "yellow",
stream: process.stderr, stream: process.stderr,
}).start(); }).start();
const start = performance.now(); const start = performance.now();
@@ -139,7 +162,7 @@ export async function timedStatus<T>(
const elapsed = ((performance.now() - start) / 1000).toFixed(2); const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop(); spinner.stop();
if (ctx.errorMsg) { if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`); printError(`${ctx.errorMsg} (${elapsed}s)`);
} }
throw err; throw err;
} }
+25 -7
View File
@@ -11,15 +11,17 @@ import {
saveConfig, saveConfig,
setNestedValue, setNestedValue,
} from "../config.js"; } 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; const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void { export function cmdConfigShow(opts: { output?: string } = {}): void {
setCurrentCommand("config show");
const config = loadConfig(); const config = loadConfig();
if (opts.output === "json") { if (opts.output === "agent" || opts.output === "json") {
formatJsonEnvelope({ formatAgentEnvelope({
command: "config show", command: "config show",
data: { data: {
defaults: { defaults: {
@@ -66,6 +68,7 @@ export function cmdConfigShow(opts: { output?: string } = {}): void {
} }
export function cmdConfigGet(key: string): void { export function cmdConfigGet(key: string): void {
setCurrentCommand("config get");
const config = loadConfig(); const config = loadConfig();
const value = getNestedValue(config, key); const value = getNestedValue(config, key);
@@ -73,20 +76,35 @@ export function cmdConfigGet(key: string): void {
printError(`Unknown config key: ${key}`); printError(`Unknown config key: ${key}`);
} else { } else {
// Redact secrets // Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") { const displayValue =
console.log(redactKey(String(value))); key.includes("api_key") || key.split(".").pop() === "key"
? redactKey(String(value))
: String(value);
if (isAgentMode()) {
formatAgentEnvelope({
command: "config get",
data: { key, value: displayValue },
});
} else { } else {
console.log(String(value)); console.log(displayValue);
} }
} }
} }
export function cmdConfigSet(key: string, value: string): void { export function cmdConfigSet(key: string, value: string): void {
setCurrentCommand("config set");
const config = loadConfig(); const config = loadConfig();
if (setNestedValue(config, key, value)) { if (setNestedValue(config, key, value)) {
saveConfig(config); saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value; const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`); if (isAgentMode()) {
formatAgentEnvelope({
command: "config set",
data: { key, value: display },
});
} else {
printSuccess(`${key} = ${display}`);
}
} else { } else {
printError(`Unknown config key: ${key}`); printError(`Unknown config key: ${key}`);
} }
+31 -19
View File
@@ -12,7 +12,8 @@ import {
printSuccess, printSuccess,
timedStatus, timedStatus,
} from "../branding.js"; } from "../branding.js";
import { formatJson } from "../output.js"; import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors; const { brand, accent, dim } = colors;
@@ -23,6 +24,7 @@ export async function cmdEntitiesList(
entityType: string, entityType: string,
opts: { output: string }, opts: { output: string },
): Promise<void> { ): Promise<void> {
setCurrentCommand("entity list");
if (!VALID_TYPES.has(entityType)) { if (!VALID_TYPES.has(entityType)) {
printError( printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`, `Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
@@ -45,8 +47,13 @@ export async function cmdEntitiesList(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent" || opts.output === "json") {
formatJson(results); formatAgentEnvelope({
command: "entity list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return; return;
} }
@@ -86,6 +93,12 @@ export async function cmdEntitiesDelete(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("entity delete");
const { isAgentMode } = await import("../state.js");
if (isAgentMode() && !opts.force) {
printError("Destructive operation requires --force in agent mode.");
process.exit(1);
}
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) { if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError( printError(
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.", "Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
@@ -93,27 +106,20 @@ export async function cmdEntitiesDelete(
process.exit(1); process.exit(1);
} }
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
if (opts.dryRun) { if (opts.dryRun) {
const scopeParts: string[] = []; printInfo(`Would delete entity ${scope} and all its memories.`);
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."); printInfo("No changes made.");
return; return;
} }
if (!opts.force) { if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout, output: process.stdout,
@@ -148,7 +154,13 @@ export async function cmdEntitiesDelete(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent") {
formatAgentEnvelope({
command: "entity delete",
data: { deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`); 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();
}
+78 -15
View File
@@ -2,6 +2,7 @@
* mem0 init — interactive setup wizard. * mem0 init — interactive setup wizard.
*/ */
import fs from "node:fs";
import readline from "node:readline"; import readline from "node:readline";
import { PlatformBackend } from "../backend/platform.js"; import { PlatformBackend } from "../backend/platform.js";
import { import {
@@ -12,10 +13,12 @@ import {
printSuccess, printSuccess,
} from "../branding.js"; } from "../branding.js";
import { import {
CONFIG_FILE,
DEFAULT_BASE_URL, DEFAULT_BASE_URL,
type Mem0Config, type Mem0Config,
createDefaultConfig, createDefaultConfig,
loadConfig, loadConfig,
redactKey,
saveConfig, saveConfig,
} from "../config.js"; } from "../config.js";
@@ -38,10 +41,16 @@ async function emailLogin(
const url = baseUrl.replace(/\/+$/, ""); const url = baseUrl.replace(/\/+$/, "");
let codeValue = code; let codeValue = code;
const sourceHeaders = {
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
};
if (!codeValue) { if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, { const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: sourceHeaders,
body: JSON.stringify({ email }), body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}); });
@@ -82,7 +91,7 @@ async function emailLogin(
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, { const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: sourceHeaders,
body: JSON.stringify({ email, code: codeValue.trim() }), body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}); });
@@ -193,9 +202,10 @@ async function setupDefaults(config: Mem0Config): Promise<void> {
console.log(); console.log();
printInfo("Set default entity IDs (press Enter to skip).\n"); printInfo("Set default entity IDs (press Enter to skip).\n");
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
const userId = await promptLine( const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`, ` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli", _systemUser,
); );
if (userId) config.defaults.userId = userId; if (userId) config.defaults.userId = userId;
} }
@@ -211,10 +221,20 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
}); });
if (status.connected) { if (status.connected) {
printSuccess("Connected to mem0 Platform!"); 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 { } else {
printError( printError(
`Could not connect: ${status.error ?? "Unknown error"}`, `Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.", "Visit https://app.mem0.ai/dashboard/api-keys to get a new key, or run mem0 init again.",
); );
} }
} catch (e) { } catch (e) {
@@ -228,6 +248,7 @@ export async function runInit(
userId?: string; userId?: string;
email?: string; email?: string;
code?: string; code?: string;
force?: boolean;
} = {}, } = {},
): Promise<void> { ): Promise<void> {
const config = createDefaultConfig(); const config = createDefaultConfig();
@@ -247,6 +268,40 @@ export async function runInit(
process.exit(1); process.exit(1);
} }
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
fs.existsSync(CONFIG_FILE) &&
savedConfig.platform.apiKey
) {
console.log(
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
);
if (process.stdin.isTTY) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
" Overwrite existing config? This cannot be undone. [y/N] ",
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled. Use --force to skip this check.");
process.exit(0);
}
} else {
printError(
"Existing config would be overwritten.",
"Use --force to overwrite.",
);
process.exit(1);
}
}
// ── Email login flow ────────────────────────────────────────────────────── // ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) { if (opts.email) {
const email = opts.email.trim().toLowerCase(); const email = opts.email.trim().toLowerCase();
@@ -268,7 +323,9 @@ export async function runInit(
config.platform.apiKey = apiKeyVal; config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl; config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli"; config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
saveConfig(config); saveConfig(config);
console.log(); console.log();
@@ -283,6 +340,19 @@ export async function runInit(
// ── API key flow ────────────────────────────────────────────────────────── // ── 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 // Non-interactive: both flags provided
if (opts.apiKey && opts.userId) { if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey; config.platform.apiKey = opts.apiKey;
@@ -293,15 +363,6 @@ export async function runInit(
return; return;
} }
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
printBanner(); printBanner();
console.log(); console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n"); printInfo("Welcome! Let's set up your mem0 CLI.\n");
@@ -341,7 +402,9 @@ export async function runInit(
config.platform.apiKey = apiKeyVal; config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl; config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli"; config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
saveConfig(config); saveConfig(config);
console.log(); console.log();
+136 -27
View File
@@ -13,6 +13,7 @@ import {
} from "../branding.js"; } from "../branding.js";
import { import {
formatAddResult, formatAddResult,
formatAgentEnvelope,
formatJson, formatJson,
formatJsonEnvelope, formatJsonEnvelope,
formatMemoriesTable, formatMemoriesTable,
@@ -20,6 +21,18 @@ import {
formatSingleMemory, formatSingleMemory,
printResultSummary, printResultSummary,
} from "../output.js"; } from "../output.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
function _stdinIsPiped(): boolean {
if (isAgentMode()) return false;
try {
const stat = fs.fstatSync(0);
return stat.isFIFO() || stat.isFile();
} catch {
return false;
}
}
export async function cmdAdd( export async function cmdAdd(
backend: Backend, backend: Backend,
@@ -40,6 +53,7 @@ export async function cmdAdd(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("add");
let msgs: Record<string, unknown>[] | undefined; let msgs: Record<string, unknown>[] | undefined;
let content = text; let content = text;
@@ -64,8 +78,8 @@ export async function cmdAdd(
process.exit(1); process.exit(1);
} }
} }
// Read from stdin if piped // Read from stdin only if stdin is an actual pipe or file redirect
else if (!content && !process.stdin.isTTY) { else if (!content && _stdinIsPiped()) {
content = fs.readFileSync(0, "utf-8").trim(); content = fs.readFileSync(0, "utf-8").trim();
} }
@@ -136,8 +150,43 @@ export async function cmdAdd(
if (opts.output === "quiet") return; if (opts.output === "quiet") return;
// Deduplicate PENDING entries sharing the same event_id across all output modes
const rawResults: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
const seenEvents = new Set<string>();
const deduped: Record<string, unknown>[] = [];
for (const r of rawResults) {
if (r.status === "PENDING") {
const eid = (r.event_id as string) ?? "";
if (eid && seenEvents.has(eid)) continue;
if (eid) seenEvents.add(eid);
}
deduped.push(r);
}
// Write back so downstream formatters see deduplicated data
const dedupedResult: Record<string, unknown> = Array.isArray(result)
? (deduped as unknown as Record<string, unknown>)
: { ...result, results: deduped };
if (opts.output === "agent") {
const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "add",
data: deduped,
scope,
count: deduped.length,
});
return;
}
if (opts.output === "json") { if (opts.output === "json") {
formatAddResult(result, opts.output); formatAddResult(dedupedResult, opts.output);
return; return;
} }
@@ -148,14 +197,18 @@ export async function cmdAdd(
app_id: opts.appId, app_id: opts.appId,
run_id: opts.runId, run_id: opts.runId,
}); });
const results = Array.isArray(result) const count = deduped.length;
? result const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
: ((result.results as unknown[]) ?? [result]); if (allPending) {
const count = results.length; printSuccess(
printSuccess( `Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`, );
); } else {
formatAddResult(result, opts.output); printSuccess(
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
);
}
formatAddResult(dedupedResult, opts.output);
} }
export async function cmdSearch( export async function cmdSearch(
@@ -176,6 +229,7 @@ export async function cmdSearch(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("search");
if (!query) { if (!query) {
printError("No query provided. Pass a query argument or pipe via stdin."); printError("No query provided. Pass a query argument or pipe via stdin.");
process.exit(1); process.exit(1);
@@ -231,6 +285,23 @@ export async function cmdSearch(
if (opts.output === "quiet") return; if (opts.output === "quiet") return;
if (opts.output === "agent") {
const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "search",
data: results,
scope,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "json") { if (opts.output === "json") {
formatJson(results); formatJson(results);
} else if (opts.output === "table") { } else if (opts.output === "table") {
@@ -267,6 +338,7 @@ export async function cmdGet(
memoryId: string, memoryId: string,
opts: { output: string }, opts: { output: string },
): Promise<void> { ): Promise<void> {
setCurrentCommand("get");
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Fetching memory...", async () => { result = await timedStatus("Fetching memory...", async () => {
@@ -277,7 +349,11 @@ export async function cmdGet(
process.exit(1); process.exit(1);
} }
formatSingleMemory(result, opts.output); if (opts.output === "agent") {
formatAgentEnvelope({ command: "get", data: result });
} else {
formatSingleMemory(result, opts.output);
}
} }
export async function cmdList( export async function cmdList(
@@ -296,6 +372,7 @@ export async function cmdList(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("list");
if (opts.pageSize < 1) { if (opts.pageSize < 1) {
printError("--page-size must be >= 1."); printError("--page-size must be >= 1.");
process.exit(1); process.exit(1);
@@ -330,12 +407,19 @@ export async function cmdList(
if (opts.output === "quiet") return; if (opts.output === "quiet") return;
if (opts.output === "json") { if (opts.output === "agent" || opts.output === "json") {
formatJsonEnvelope({ const scope: Record<string, string | undefined> = {
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
};
formatAgentEnvelope({
command: "list", command: "list",
data: results, data: results,
scope,
count: results.length, count: results.length,
scope: { user_id: opts.userId, agent_id: opts.agentId }, durationMs: Math.round(elapsed * 1000),
}); });
} else if (opts.output === "table") { } else if (opts.output === "table") {
if (results.length > 0) { if (results.length > 0) {
@@ -374,6 +458,7 @@ export async function cmdUpdate(
text: string | undefined, text: string | undefined,
opts: { metadata?: string; output: string }, opts: { metadata?: string; output: string },
): Promise<void> { ): Promise<void> {
setCurrentCommand("update");
let meta: Record<string, unknown> | undefined; let meta: Record<string, unknown> | undefined;
if (opts.metadata) { if (opts.metadata) {
try { try {
@@ -396,7 +481,13 @@ export async function cmdUpdate(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent") {
formatAgentEnvelope({
command: "update",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
printSuccess( printSuccess(
@@ -410,6 +501,7 @@ export async function cmdDelete(
memoryId: string, memoryId: string,
opts: { output: string; dryRun?: boolean; force?: boolean }, opts: { output: string; dryRun?: boolean; force?: boolean },
): Promise<void> { ): Promise<void> {
setCurrentCommand("delete");
if (opts.dryRun) { if (opts.dryRun) {
let mem: Record<string, unknown>; let mem: Record<string, unknown>;
try { try {
@@ -436,7 +528,13 @@ export async function cmdDelete(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete",
data: { id: memoryId, deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
printSuccess( printSuccess(
@@ -458,16 +556,15 @@ export async function cmdDeleteAll(
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("delete-all");
const { isAgentMode } = await import("../state.js");
if (isAgentMode() && !opts.force) {
printError("Destructive operation requires --force in agent mode.");
process.exit(1);
}
if (opts.all) { if (opts.all) {
// Project-wide wipe using wildcard entity IDs // Project-wide wipe using wildcard entity IDs
if (opts.dryRun) { // Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
printInfo("Would delete ALL memories project-wide.");
printInfo(
"Run without --dry-run to see the actual count of deleted memories.",
);
printInfo("No changes made.");
return;
}
if (!opts.force) { if (!opts.force) {
const readline = await import("node:readline"); const readline = await import("node:readline");
@@ -509,7 +606,13 @@ export async function cmdDeleteAll(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete-all",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
if (result.message) { if (result.message) {
@@ -586,7 +689,13 @@ export async function cmdDeleteAll(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent") {
formatAgentEnvelope({
command: "delete-all",
data: result,
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result); formatJson(result);
} else if (opts.output !== "quiet") { } else if (opts.output !== "quiet") {
if (result.message) { if (result.message) {
+17 -7
View File
@@ -6,7 +6,8 @@ import fs from "node:fs";
import boxen from "boxen"; import boxen from "boxen";
import type { Backend } from "../backend/base.js"; import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js"; import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatJsonEnvelope } from "../output.js"; import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { setCurrentCommand } from "../state.js";
import { CLI_VERSION } from "../version.js"; import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors; const { brand, dim, success, error: errorColor } = colors;
@@ -15,6 +16,7 @@ export async function cmdStatus(
backend: Backend, backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {}, opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> { ): Promise<void> {
setCurrentCommand("status");
const start = performance.now(); const start = performance.now();
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
@@ -29,14 +31,13 @@ export async function cmdStatus(
} }
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") { if (opts.output === "agent" || opts.output === "json") {
formatJsonEnvelope({ formatAgentEnvelope({
command: "status", command: "status",
data: { data: {
connected: result.connected, connected: result.connected,
backend: result.backend ?? null, backend: result.backend ?? null,
base_url: result.base_url ?? null, base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
}, },
durationMs: Math.round(elapsed * 1000), durationMs: Math.round(elapsed * 1000),
}); });
@@ -56,6 +57,15 @@ export async function cmdStatus(
} }
if (result.error) { if (result.error) {
lines.push(` ${errorColor("Error:")} ${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`); lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
@@ -81,6 +91,7 @@ export async function cmdImport(
filePath: string, filePath: string,
opts: { userId?: string; agentId?: string; output?: string }, opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> { ): Promise<void> {
setCurrentCommand("import");
let data: Record<string, unknown>[]; let data: Record<string, unknown>[];
try { try {
const raw = fs.readFileSync(filePath, "utf-8"); const raw = fs.readFileSync(filePath, "utf-8");
@@ -125,13 +136,12 @@ export async function cmdImport(
const elapsed = (performance.now() - start) / 1000; const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line console.log(); // Clear progress line
if (opts.output === "json") { if (opts.output === "agent" || opts.output === "json") {
formatJsonEnvelope({ formatAgentEnvelope({
command: "import", command: "import",
data: { data: {
added, added,
failed, failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
}, },
durationMs: Math.round(elapsed * 1000), durationMs: Math.round(elapsed * 1000),
}); });
+20
View File
@@ -20,6 +20,7 @@ export const CONFIG_VERSION = 1;
export interface PlatformConfig { export interface PlatformConfig {
apiKey: string; apiKey: string;
baseUrl: string; baseUrl: string;
userEmail: string;
} }
export interface DefaultsConfig { export interface DefaultsConfig {
@@ -30,10 +31,15 @@ export interface DefaultsConfig {
enableGraph: boolean; enableGraph: boolean;
} }
export interface TelemetryConfig {
anonymousId: string;
}
export interface Mem0Config { export interface Mem0Config {
version: number; version: number;
defaults: DefaultsConfig; defaults: DefaultsConfig;
platform: PlatformConfig; platform: PlatformConfig;
telemetry: TelemetryConfig;
} }
export function createDefaultConfig(): Mem0Config { export function createDefaultConfig(): Mem0Config {
@@ -49,6 +55,10 @@ export function createDefaultConfig(): Mem0Config {
platform: { platform: {
apiKey: "", apiKey: "",
baseUrl: DEFAULT_BASE_URL, baseUrl: DEFAULT_BASE_URL,
userEmail: "",
},
telemetry: {
anonymousId: "",
}, },
}; };
} }
@@ -70,6 +80,7 @@ export function loadConfig(): Mem0Config {
const plat = data.platform ?? {}; const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? ""; config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL; config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
config.platform.userEmail = plat.user_email ?? "";
const defaults = data.defaults ?? {}; const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? ""; config.defaults.userId = defaults.user_id ?? "";
@@ -77,6 +88,9 @@ export function loadConfig(): Mem0Config {
config.defaults.appId = defaults.app_id ?? ""; config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? ""; config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false; config.defaults.enableGraph = defaults.enable_graph ?? false;
const telemetry = data.telemetry ?? {};
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
} }
// Environment variable overrides // Environment variable overrides
@@ -114,6 +128,10 @@ export function saveConfig(config: Mem0Config): void {
platform: { platform: {
api_key: config.platform.apiKey, api_key: config.platform.apiKey,
base_url: config.platform.baseUrl, base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
},
telemetry: {
anonymous_id: config.telemetry.anonymousId,
}, },
}; };
@@ -131,6 +149,7 @@ export function redactKey(key: string): string {
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = { const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"], "platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"], "platform.base_url": ["platform", "baseUrl"],
"platform.user_email": ["platform", "userEmail"],
"defaults.user_id": ["defaults", "userId"], "defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"], "defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"], "defaults.app_id": ["defaults", "appId"],
@@ -139,6 +158,7 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
// Short-form aliases // Short-form aliases
api_key: ["platform", "apiKey"], api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"], base_url: ["platform", "baseUrl"],
user_email: ["platform", "userEmail"],
user_id: ["defaults", "userId"], user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"], agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"], app_id: ["defaults", "appId"],
+1 -1
View File
@@ -36,7 +36,7 @@ const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
}, },
{ {
panel: "Management", panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"], commands: ["init", "status", "import", "help", "entity", "event", "config"],
}, },
]; ];
+213 -49
View File
@@ -8,21 +8,27 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { Command } from "commander"; import { Command } from "commander";
import { type Backend, getBackend } from "./backend/index.js"; import { AuthError, type Backend, getBackend } from "./backend/index.js";
import { colors, printError } from "./branding.js"; import { colors, printError, printWarning } from "./branding.js";
import type { Mem0Config } from "./config.js"; import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js"; import { loadConfig, saveConfig } from "./config.js";
import { richFormatHelp } from "./help.js"; import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js"; import { CLI_VERSION } from "./version.js";
const program = new Command(); const program = new Command();
// ── Validated user identity (set by getBackendAndConfig) ─────────────────
let _validatedUserEmail: string | undefined;
// ── Helpers ────────────────────────────────────────────────────────────── // ── Helpers ──────────────────────────────────────────────────────────────
function getBackendAndConfig( async function getBackendAndConfig(
apiKey?: string, apiKey?: string,
baseUrl?: string, baseUrl?: string,
): { backend: Backend; config: Mem0Config } { ): Promise<{ backend: Backend; config: Mem0Config }> {
const config = loadConfig(); const config = loadConfig();
if (apiKey) config.platform.apiKey = apiKey; if (apiKey) config.platform.apiKey = apiKey;
@@ -36,11 +42,58 @@ function getBackendAndConfig(
process.exit(1); process.exit(1);
} }
return { backend: getBackend(config), config }; const backend = getBackend(config);
// Validate the API key upfront with a fast timeout
try {
const pingData = (await Promise.race([
backend.ping(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), 5000),
),
])) as Record<string, unknown>;
const email = pingData?.user_email as string | undefined;
if (email) {
_validatedUserEmail = email;
if (config.platform.userEmail !== email) {
config.platform.userEmail = email;
try {
saveConfig(config);
} catch {
/* ignore */
}
}
}
} catch (e) {
if (e instanceof AuthError) {
printError(
"Invalid or expired API key.",
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
);
process.exit(1);
}
// Network error / timeout — warn but proceed
printWarning(
"Could not validate API key (network issue). Proceeding anyway.",
);
}
return { backend, config };
} }
function getBackendOnly(apiKey?: string, baseUrl?: string): Backend { async function getBackendOnly(
return getBackendAndConfig(apiKey, baseUrl).backend; apiKey?: string,
baseUrl?: string,
): Promise<Backend> {
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
}
function checkAgentMode(): boolean {
const rootOpts = program.opts();
const isAgent = !!(rootOpts.json || rootOpts.agent);
if (isAgent) setAgentMode(true);
return isAgent;
} }
/** /**
@@ -105,18 +158,45 @@ program
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`); console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
process.exit(0); process.exit(0);
}) })
.option("--json", "Output as JSON for agent/programmatic use.")
.option(
"--agent",
"Output as JSON for agent/programmatic use. (alias: --json)",
)
.usage("<command> [options]") .usage("<command> [options]")
.helpOption("--help", "Show this message and exit.") .helpOption("--help", "Show this message and exit.")
.addHelpCommand(false) .addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp }); .configureHelp({ formatHelp: richFormatHelp });
// ── Telemetry hook ───────────────────────────────────────────────────────
program.hook("preAction", (_thisCommand, actionCommand) => {
try {
const commandName = actionCommand.name();
const parentName = actionCommand.parent?.name();
const fullCommand =
parentName && parentName !== "mem0"
? `${parentName}.${commandName}`
: commandName;
const isAgent = !!(program.opts().json || program.opts().agent);
captureEvent(
`cli.${fullCommand}`,
{
command: fullCommand,
is_agent: isAgent,
},
_validatedUserEmail,
);
} catch {
/* silently swallow */
}
});
// ── Init ────────────────────────────────────────────────────────────────── // ── Init ──────────────────────────────────────────────────────────────────
program program
.command("init") .command("init")
.description( .description("Interactive setup wizard for mem0 CLI.")
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
)
.option("--api-key <key>", "API key (skip prompt).") .option("--api-key <key>", "API key (skip prompt).")
.option("-u, --user-id <id>", "Default user ID (skip prompt).") .option("-u, --user-id <id>", "Default user ID (skip prompt).")
.option("--email <email>", "Login via email verification code.") .option("--email <email>", "Login via email verification code.")
@@ -124,6 +204,7 @@ program
"--code <code>", "--code <code>",
"Verification code (use with --email for non-interactive login).", "Verification code (use with --email for non-interactive login).",
) )
.option("--force", "Overwrite existing config without confirmation.", false)
.addHelpText( .addHelpText(
"after", "after",
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
@@ -135,6 +216,7 @@ program
userId: opts.userId, userId: opts.userId,
email: opts.email, email: opts.email,
code: opts.code, code: opts.code,
force: opts.force,
}); });
}); });
@@ -165,17 +247,24 @@ program
) )
.action(async (text, opts) => { .action(async (text, opts) => {
const { cmdAdd } = await import("./commands/memory.js"); const { cmdAdd } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts); const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts); const enableGraph = resolveGraph(config, opts);
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph }); const output = isAgent ? "agent" : opts.output;
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph, output });
}); });
// ── Memory: search ──────────────────────────────────────────────────────── // ── Memory: search ────────────────────────────────────────────────────────
program program
.command("search [query]") .command("search [query]")
.description("Search memories by semantic query.") .description(
"Query your memory store — semantic, keyword, or hybrid retrieval.",
)
.option("-u, --user-id <id>", "Filter by user.") .option("-u, --user-id <id>", "Filter by user.")
.option("--agent-id <id>", "Filter by agent.") .option("--agent-id <id>", "Filter by agent.")
.option("--app-id <id>", "Filter by app.") .option("--app-id <id>", "Filter by app.")
@@ -215,9 +304,14 @@ program
process.exit(1); process.exit(1);
} }
const { cmdSearch } = await import("./commands/memory.js"); const { cmdSearch } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts); const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts); const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdSearch(backend, resolvedQuery, { await cmdSearch(backend, resolvedQuery, {
...ids, ...ids,
topK: opts.topK, topK: opts.topK,
@@ -227,7 +321,7 @@ program
filterJson: opts.filter, filterJson: opts.filter,
fields: opts.fields, fields: opts.fields,
enableGraph, enableGraph,
output: opts.output, output,
}); });
}); });
@@ -245,8 +339,10 @@ program
) )
.action(async (memoryId, opts) => { .action(async (memoryId, opts) => {
const { cmdGet } = await import("./commands/memory.js"); const { cmdGet } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
await cmdGet(backend, memoryId, { output: opts.output }); const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdGet(backend, memoryId, { output });
}); });
// ── Memory: list ────────────────────────────────────────────────────────── // ── Memory: list ──────────────────────────────────────────────────────────
@@ -279,9 +375,14 @@ program
) )
.action(async (opts) => { .action(async (opts) => {
const { cmdList } = await import("./commands/memory.js"); const { cmdList } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts); const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts); const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdList(backend, { await cmdList(backend, {
...ids, ...ids,
page: opts.page, page: opts.page,
@@ -290,7 +391,7 @@ program
after: opts.after, after: opts.after,
before: opts.before, before: opts.before,
enableGraph, enableGraph,
output: opts.output, output,
}); });
}); });
@@ -313,10 +414,12 @@ program
resolvedText = fs.readFileSync(0, "utf-8").trim(); resolvedText = fs.readFileSync(0, "utf-8").trim();
} }
const { cmdUpdate } = await import("./commands/memory.js"); const { cmdUpdate } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdUpdate(backend, memoryId, resolvedText, { await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata, metadata: opts.metadata,
output: opts.output, output,
}); });
}); });
@@ -352,6 +455,8 @@ program
].join("\n"), ].join("\n"),
) )
.action(async (memoryId, opts) => { .action(async (memoryId, opts) => {
const isAgent = checkAgentMode();
const output = isAgent ? "agent" : opts.output;
// ── Mutual-exclusion checks ── // ── Mutual-exclusion checks ──
if (memoryId && opts.all) { if (memoryId && opts.all) {
printError("Cannot combine <memoryId> with --all. Use one or the other."); printError("Cannot combine <memoryId> with --all. Use one or the other.");
@@ -380,9 +485,9 @@ program
// ── Dispatch: single memory ── // ── Dispatch: single memory ──
if (memoryId) { if (memoryId) {
const { cmdDelete } = await import("./commands/memory.js"); const { cmdDelete } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdDelete(backend, memoryId, { await cmdDelete(backend, memoryId, {
output: opts.output, output,
dryRun: opts.dryRun, dryRun: opts.dryRun,
force: opts.force, force: opts.force,
}); });
@@ -392,7 +497,7 @@ program
// ── Dispatch: --all ── // ── Dispatch: --all ──
if (opts.all) { if (opts.all) {
const { cmdDeleteAll } = await import("./commands/memory.js"); const { cmdDeleteAll } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig( const { backend, config } = await getBackendAndConfig(
opts.apiKey, opts.apiKey,
opts.baseUrl, opts.baseUrl,
); );
@@ -409,7 +514,7 @@ program
dryRun: opts.dryRun, dryRun: opts.dryRun,
all: opts.project, all: opts.project,
...ids, ...ids,
output: opts.output, output,
}); });
return; return;
} }
@@ -417,8 +522,8 @@ program
// ── Dispatch: --entity ── // ── Dispatch: --entity ──
if (opts.entity) { if (opts.entity) {
const { cmdEntitiesDelete } = await import("./commands/entities.js"); const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, opts); await cmdEntitiesDelete(backend, { ...opts, output });
return; return;
} }
}); });
@@ -440,7 +545,9 @@ configCmd
) )
.action(async (opts) => { .action(async (opts) => {
const { cmdConfigShow } = await import("./commands/config.js"); const { cmdConfigShow } = await import("./commands/config.js");
cmdConfigShow({ output: opts.output }); const isAgent = checkAgentMode();
const output = isAgent ? "agent" : opts.output;
cmdConfigShow({ output });
}); });
configCmd configCmd
@@ -452,6 +559,7 @@ configCmd
) )
.action(async (key) => { .action(async (key) => {
const { cmdConfigGet } = await import("./commands/config.js"); const { cmdConfigGet } = await import("./commands/config.js");
checkAgentMode();
cmdConfigGet(key); cmdConfigGet(key);
}); });
@@ -464,6 +572,7 @@ configCmd
) )
.action(async (key, value) => { .action(async (key, value) => {
const { cmdConfigSet } = await import("./commands/config.js"); const { cmdConfigSet } = await import("./commands/config.js");
checkAgentMode();
cmdConfigSet(key, value); cmdConfigSet(key, value);
}); });
@@ -487,8 +596,10 @@ entityCmd
) )
.action(async (entityType, opts) => { .action(async (entityType, opts) => {
const { cmdEntitiesList } = await import("./commands/entities.js"); const { cmdEntitiesList } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
await cmdEntitiesList(backend, entityType, { output: opts.output }); const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesList(backend, entityType, { output });
}); });
entityCmd entityCmd
@@ -509,8 +620,54 @@ entityCmd
) )
.action(async (opts) => { .action(async (opts) => {
const { cmdEntitiesDelete } = await import("./commands/entities.js"); const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
await cmdEntitiesDelete(backend, opts); const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesDelete(backend, { ...opts, output });
});
// ── Event subcommands ─────────────────────────────────────────────────────
const eventCmd = program
.command("event")
.description("Inspect background processing events.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
eventCmd
.command("list")
.description("List recent background processing events.")
.option("-o, --output <format>", "Output: table, json.", "table")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
)
.action(async (opts) => {
const { cmdEventList } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventList(backend, { output });
});
eventCmd
.command("status <eventId>")
.description("Check the status of a specific background event.")
.option("-o, --output <format>", "Output: text, json.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
)
.action(async (eventId, opts) => {
const { cmdEventStatus } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventStatus(backend, eventId, { output });
}); });
// ── Utility commands ────────────────────────────────────────────────────── // ── Utility commands ──────────────────────────────────────────────────────
@@ -524,11 +681,16 @@ program
.addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json") .addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json")
.action(async (opts) => { .action(async (opts) => {
const { cmdStatus } = await import("./commands/utils.js"); const { cmdStatus } = await import("./commands/utils.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const output = isAgent ? "agent" : opts.output;
await cmdStatus(backend, { await cmdStatus(backend, {
userId: config.defaults.userId || undefined, userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined, agentId: config.defaults.agentId || undefined,
output: opts.output, output,
}); });
}); });
@@ -546,12 +708,17 @@ program
) )
.action(async (filePath, opts) => { .action(async (filePath, opts) => {
const { cmdImport } = await import("./commands/utils.js"); const { cmdImport } = await import("./commands/utils.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl); const isAgent = checkAgentMode();
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts); const ids = resolveIds(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdImport(backend, filePath, { await cmdImport(backend, filePath, {
userId: ids.userId, userId: ids.userId,
agentId: ids.agentId, agentId: ids.agentId,
output: opts.output, output,
}); });
}); });
@@ -565,7 +732,9 @@ program
.option("--json", "Output machine-readable JSON for LLM agents.", false) .option("--json", "Output machine-readable JSON for LLM agents.", false)
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json") .addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
.action((opts) => { .action((opts) => {
if (opts.json) { // opts.json is set when `mem0 help --json` is used (subcommand flag).
// program.opts().json is set when the root --json global flag was used first.
if (opts.json || program.opts().json) {
// Load spec from parent directory // Load spec from parent directory
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.join(__dirname, "..", "..", "cli-spec.json"); const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
@@ -595,7 +764,9 @@ program
console.log( console.log(
" add Add a memory from text, messages, file, or stdin", " add Add a memory from text, messages, file, or stdin",
); );
console.log(" search Search memories by semantic query"); console.log(
" search Query your memory store (semantic, keyword, hybrid)",
);
console.log(" get Get a specific memory by ID"); console.log(" get Get a specific memory by ID");
console.log(" list List memories with optional filters"); console.log(" list List memories with optional filters");
console.log(" update Update a memory's text or metadata"); console.log(" update Update a memory's text or metadata");
@@ -605,6 +776,9 @@ program
console.log(" import Import memories from a JSON file"); console.log(" import Import memories from a JSON file");
console.log(" config Manage configuration (show, get, set)"); console.log(" config Manage configuration (show, get, set)");
console.log(" entity Manage entities (list, delete)"); console.log(" entity Manage entities (list, delete)");
console.log(
" event Inspect background events (list, status)",
);
console.log(" init Interactive setup wizard"); console.log(" init Interactive setup wizard");
console.log(" status Check connectivity and authentication"); console.log(" status Check connectivity and authentication");
console.log(); console.log();
@@ -616,16 +790,6 @@ program
} }
}); });
// ── Version ───────────────────────────────────────────────────────────────
program
.command("version")
.description("Show version.")
.action(async () => {
const { cmdVersion } = await import("./commands/utils.js");
cmdVersion();
});
// ── Entrypoint ──────────────────────────────────────────────────────────── // ── Entrypoint ────────────────────────────────────────────────────────────
program.parse(); program.parse();
+124 -3
View File
@@ -169,17 +169,26 @@ export function formatAddResult(
} }
console.log(); console.log();
const seenPendingEvents = new Set<string>();
for (const r of results) { for (const r of results) {
// Detect async PENDING response // Detect async PENDING response
if (r.status === "PENDING") { if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8); 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 icon = accent(sym("⧗", "..."));
const parts = [ const parts = [
` ${icon} ${dim("Queued".padEnd(10))}`, ` ${icon} ${dim("Queued".padEnd(10))}`,
"Processing in background", "Processing in background",
]; ];
if (eventId) parts.push(dim(`(event ${eventId})`));
console.log(parts.join(" ")); console.log(parts.join(" "));
if (eventId) {
console.log(` ${dim(` event_id: ${eventId}`)}`);
console.log(
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
);
}
continue; continue;
} }
@@ -238,6 +247,118 @@ export function formatJsonEnvelope(opts: {
console.log(JSON.stringify(envelope, null, 2)); 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: { export function printResultSummary(opts: {
count: number; count: number;
durationSecs?: number; durationSecs?: number;
@@ -249,7 +370,7 @@ export function printResultSummary(opts: {
if (opts.scopeIds) { if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds) const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v) .filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`); .map(([k, v]) => `${k}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", ")); if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
} }
if (opts.durationSecs !== undefined) if (opts.durationSecs !== undefined)
+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;
}
+153
View File
@@ -0,0 +1,153 @@
/**
* 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, randomUUID } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { CONFIG_FILE, loadConfig, saveConfig } 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 persistent per-machine anonymous ID, generating one if needed.
*
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
* repeat runs on the same machine share one PostHog identity instead of
* collapsing into a single shared fallback string.
*/
function getOrCreateAnonymousId(): string {
const config = loadConfig();
if (config.telemetry.anonymousId) {
return config.telemetry.anonymousId;
}
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
config.telemetry.anonymousId = newId;
try {
saveConfig(config);
} catch {
/* ignore persistence failure — still return the generated ID */
}
return newId;
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
* persistent per-machine anonymous ID.
*/
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 */
}
try {
return getOrCreateAnonymousId();
} catch {
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
}
}
/**
* 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();
// Detect anonymous → identified transition. If a stored anonymous_id
// exists and we just resolved to a real identity, fire a one-shot
// $identify event so PostHog stitches the pre-signup history onto
// the authenticated profile. Clear the stored id so we don't re-alias.
let anonIdToAlias: string | null = null;
if (
distinctId &&
!distinctId.startsWith("cli-anon-") &&
config.telemetry.anonymousId
) {
anonIdToAlias = config.telemetry.anonymousId;
config.telemetry.anonymousId = "";
try {
saveConfig(config);
} catch {
/* ignore — alias may double-fire next run, harmless */
}
}
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,
anonDistinctIdToAlias: anonIdToAlias,
};
const child = spawn(
process.execPath,
[SENDER_SCRIPT, JSON.stringify(context)],
{ detached: true, stdio: "ignore" },
);
child.unref();
} catch {
/* silently swallow */
}
}
+129
View File
@@ -0,0 +1,129 @@
/**
* 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 sendIdentifyEvent(ctx, payload, anonId) {
const identifyPayload = {
api_key: payload.api_key,
event: "$identify",
distinct_id: payload.distinct_id,
properties: {
$anon_distinct_id: anonId,
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
},
};
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
}
async function main() {
const ctx = JSON.parse(process.argv[2]);
const payload = ctx.payload;
if (ctx.needsEmail && ctx.mem0ApiKey) {
await resolveAndCacheEmail(ctx, payload);
}
// Fire $identify *after* email resolution so PostHog links the stored
// anonymous id directly to the final identity (email, not the api-key
// hash). The regular event is sent next so it lands under the merged
// profile.
if (ctx.anonDistinctIdToAlias) {
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
}
await sendPosthogEvent(ctx.posthogHost, payload);
}
main().catch(() => {});
-7
View File
@@ -44,13 +44,6 @@ describe("CLI Integration — help and version", () => {
expect(result.stdout).toContain("search"); 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", () => { it("help --json produces valid JSON", () => {
const result = run(["help", "--json"]); const result = run(["help", "--json"]);
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
+220 -7
View File
@@ -5,6 +5,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { createMockBackend } from "./setup.js"; import { createMockBackend } from "./setup.js";
import type { Backend } from "../src/backend/base.js"; import type { Backend } from "../src/backend/base.js";
import { setAgentMode } from "../src/state.js";
let mockBackend: Backend; let mockBackend: Backend;
@@ -31,6 +32,7 @@ import { afterEach } from "vitest";
afterEach(() => { afterEach(() => {
console.log = originalLog; console.log = originalLog;
console.error = originalError; console.error = originalError;
setAgentMode(false);
}); });
describe("cmdAdd", () => { 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", () => { describe("cmdSearch", () => {
it("searches and shows results in text mode", async () => { it("searches and shows results in text mode", async () => {
const { cmdSearch } = await import("../src/commands/memory.js"); const { cmdSearch } = await import("../src/commands/memory.js");
@@ -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", () => { describe("cmdEntitiesList", () => {
it("lists users in table mode", async () => { it("lists users in table mode", async () => {
@@ -219,3 +267,168 @@ describe("cmdEntitiesList", () => {
expect(output).toContain("alice"); 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, formatSingleMemory,
formatAddResult, formatAddResult,
printResultSummary, printResultSummary,
sanitizeAgentData,
} from "../src/output.js"; } from "../src/output.js";
let output: string; let output: string;
@@ -98,6 +99,18 @@ describe("formatAddResult", () => {
}); });
expect(output).toContain("Queued"); 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", () => { describe("printResultSummary", () => {
@@ -113,3 +126,73 @@ describe("printResultSummary", () => {
expect(output).not.toContain("results"); 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: "alice", count: 5 },
{ name: "bob", count: 3 }, { 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" },
},
],
}),
}; };
} }
+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 ## 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. > **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 ```bash
# Set up your configuration # Interactive setup wizard
mem0 init 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 # Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories # Search memories
mem0 search "What are Alice's preferences?" --user-id alice mem0 search "What are Alice's preferences?" --user-id alice
# List all memories # List all memories for a user
mem0 list --user-id alice 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 ## License
Apache-2.0 Apache-2.0
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mem0-cli" name = "mem0-cli"
version = "0.1.0" version = "0.2.3"
description = "The official CLI for mem0 — the memory layer for AI agents" description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md" readme = "README.md"
license = "Apache-2.0" license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer.""" """mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.1.0" __version__ = "0.2.3"
+215 -19
View File
@@ -2,7 +2,10 @@
from __future__ import annotations from __future__ import annotations
import contextlib
import json as _json import json as _json
import os
import stat as _stat_mod
import sys import sys
from pathlib import Path from pathlib import Path
@@ -10,7 +13,7 @@ import typer
from rich.console import Console from rich.console import Console
from mem0_cli import __version__ 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() console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)
@@ -43,7 +46,52 @@ entity_app = typer.Typer(
no_args_is_help=True, no_args_is_help=True,
rich_markup_mode="rich", 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 ─────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────
@@ -53,9 +101,16 @@ def _get_backend_and_config(
api_key: str | None = None, api_key: str | None = None,
base_url: 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.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() config = load_config()
@@ -72,7 +127,29 @@ def _get_backend_and_config(
) )
raise typer.Exit(1) 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( 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: def _read_stdin() -> str | None:
"""Read from stdin if it is piped (not a TTY).""" """Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
if not sys.stdin.isatty(): if _stdin_is_piped():
return sys.stdin.read().strip() or None return sys.stdin.read().strip() or None
return None return None
@@ -128,12 +218,26 @@ def _read_stdin() -> str | None:
def main_callback( def main_callback(
ctx: typer.Context, ctx: typer.Context,
version: bool = typer.Option(False, "--version", help="Show version and exit."), 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: ) -> None:
if json_agent:
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
if version: if version:
from mem0_cli.commands.utils import cmd_version from mem0_cli.commands.utils import cmd_version
_fire_telemetry("version")
cmd_version() cmd_version()
raise typer.Exit() raise typer.Exit()
if ctx.invoked_subcommand:
_fire_telemetry(ctx.invoked_subcommand)
# ── Memory: add ─────────────────────────────────────────────────────────── # ── Memory: add ───────────────────────────────────────────────────────────
@@ -273,7 +377,7 @@ def search(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection" None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
), ),
) -> None: ) -> None:
"""Search memories by semantic query. """Query your memory store — semantic, keyword, or hybrid retrieval.
Examples: Examples:
mem0 search "preferences" --user-id alice mem0 search "preferences" --user-id alice
@@ -538,12 +642,14 @@ def delete(
# ── Dispatch ───────────────────────────────────────────────────── # ── Dispatch ─────────────────────────────────────────────────────
if memory_id is not None: if memory_id is not None:
_fire_telemetry("delete", {"delete_mode": "single"})
from mem0_cli.commands.memory import cmd_delete from mem0_cli.commands.memory import cmd_delete
backend = _get_backend(api_key, base_url) backend = _get_backend(api_key, base_url)
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output) cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
elif all_: elif all_:
_fire_telemetry("delete", {"delete_mode": "all"})
from mem0_cli.commands.memory import cmd_delete_all from mem0_cli.commands.memory import cmd_delete_all
backend, config = _get_backend_and_config(api_key, base_url) backend, config = _get_backend_and_config(api_key, base_url)
@@ -551,6 +657,7 @@ def delete(
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output) cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
else: # --entity else: # --entity
_fire_telemetry("delete", {"delete_mode": "entity"})
from mem0_cli.commands.entities import cmd_entities_delete from mem0_cli.commands.entities import cmd_entities_delete
backend = _get_backend(api_key, base_url) backend = _get_backend(api_key, base_url)
@@ -702,6 +809,70 @@ def entity_delete(
app.add_typer(entity_app, name="entity", rich_help_panel="Management") 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 ─────────────────────────────────────────────────── # ── Management commands ───────────────────────────────────────────────────
@@ -715,6 +886,9 @@ def init(
code: str | None = typer.Option( code: str | None = typer.Option(
None, "--code", help="Verification code (use with --email for non-interactive login)." 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: ) -> None:
"""Interactive setup wizard for mem0 CLI. """Interactive setup wizard for mem0 CLI.
@@ -726,7 +900,7 @@ def init(
""" """
from mem0_cli.commands.init_cmd import run_init from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id, email=email, code=code) 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) # (entity_app registered at module level, below sub-group definitions)
@@ -831,7 +1005,7 @@ def _build_help_json() -> dict:
}, },
}, },
"search": { "search": {
"description": "Search memories by semantic query.", "description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
"usage": "mem0 search <query> [OPTIONS]", "usage": "mem0 search <query> [OPTIONS]",
"arguments": {"query": {"description": "Search query.", "required": False}}, "arguments": {"query": {"description": "Search query.", "required": False}},
"options": { "options": {
@@ -935,6 +1109,24 @@ def _build_help_json() -> dict:
"value": {"description": "Value to set.", "required": True}, "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": { "entity": {
"description": "Manage entities.", "description": "Manage entities.",
"subcommands": { "subcommands": {
@@ -986,6 +1178,7 @@ def _build_help_json() -> dict:
"global_options": { "global_options": {
"--api-key": "Override API key (env: MEM0_API_KEY).", "--api-key": "Override API key (env: MEM0_API_KEY).",
"--base-url": "Override API base URL.", "--base-url": "Override API base URL.",
"--json / --agent": "Output as JSON for agent/programmatic use.",
"--help": "Show help for a command.", "--help": "Show help for a command.",
"--version": "Show version and exit.", "--version": "Show version and exit.",
}, },
@@ -1015,7 +1208,7 @@ def help(
console.print("Usage: mem0 <command> [OPTIONS]\n") console.print("Usage: mem0 <command> [OPTIONS]\n")
console.print("[bold]Commands:[/]") console.print("[bold]Commands:[/]")
console.print(" add Add a memory from text, messages, file, or stdin") 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(" get Get a specific memory by ID")
console.print(" list List memories with optional filters") console.print(" list List memories with optional filters")
console.print(" update Update a memory's text or metadata") console.print(" update Update a memory's text or metadata")
@@ -1023,6 +1216,7 @@ def help(
console.print(" import Import memories from a JSON file") console.print(" import Import memories from a JSON file")
console.print(" config Manage configuration (show, get, set)") console.print(" config Manage configuration (show, get, set)")
console.print(" entity Manage entities (list, delete)") console.print(" entity Manage entities (list, delete)")
console.print(" event Inspect background events (list, status)")
console.print(" init Interactive setup wizard") console.print(" init Interactive setup wizard")
console.print(" status Check connectivity and authentication") console.print(" status Check connectivity and authentication")
console.print() console.print()
@@ -1031,14 +1225,6 @@ def help(
console.print() console.print()
@app.command(rich_help_panel="Utility")
def version() -> None:
"""Show version and exit."""
from mem0_cli.commands.utils import cmd_version
cmd_version()
# Register config subgroup here so it appears after help in Management panel # Register config subgroup here so it appears after help in Management panel
app.add_typer(config_app, name="config", rich_help_panel="Management") app.add_typer(config_app, name="config", rich_help_panel="Management")
@@ -1047,4 +1233,14 @@ app.add_typer(config_app, name="config", rich_help_panel="Management")
def main() -> None: 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() app()
+6
View File
@@ -104,6 +104,12 @@ class Backend(ABC):
@abstractmethod @abstractmethod
def entities(self, entity_type: str) -> list[dict]: ... 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: def get_backend(config: Mem0Config) -> Backend:
"""Return the Platform backend.""" """Return the Platform backend."""
+53 -26
View File
@@ -6,6 +6,7 @@ from typing import Any
import httpx import httpx
from mem0_cli import __version__
from mem0_cli.backend.base import Backend from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig from mem0_cli.config import PlatformConfig
@@ -21,11 +22,17 @@ class PlatformBackend(Backend):
headers={ headers={
"Authorization": f"Token {config.api_key}", "Authorization": f"Token {config.api_key}",
"Content-Type": "application/json", "Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Client-Version": __version__,
}, },
timeout=30.0, timeout=30.0,
) )
def _request(self, method: str, path: str, **kwargs: Any) -> Any: 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) resp = self._client.request(method, path, **kwargs)
if resp.status_code == 401: if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.") raise AuthError("Authentication failed. Your API key may be invalid or expired.")
@@ -86,6 +93,7 @@ class PlatformBackend(Backend):
payload["categories"] = categories payload["categories"] = categories
if enable_graph: if enable_graph:
payload["enable_graph"] = True payload["enable_graph"] = True
payload["source"] = "CLI"
return self._request("POST", "/v1/memories/", json=payload) return self._request("POST", "/v1/memories/", json=payload)
@@ -165,6 +173,7 @@ class PlatformBackend(Backend):
payload["fields"] = fields payload["fields"] = fields
if enable_graph: if enable_graph:
payload["enable_graph"] = True payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/search/", json=payload) result = self._request("POST", "/v2/memories/search/", json=payload)
return ( return (
@@ -174,7 +183,7 @@ class PlatformBackend(Backend):
) )
def get(self, memory_id: str) -> dict: def get(self, memory_id: str) -> dict:
return self._request("GET", f"/v1/memories/{memory_id}/") return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
def list_memories( def list_memories(
self, self,
@@ -213,6 +222,7 @@ class PlatformBackend(Backend):
payload["filters"] = api_filters payload["filters"] = api_filters
if enable_graph: if enable_graph:
payload["enable_graph"] = True payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/", json=payload, params=params) result = self._request("POST", "/v2/memories/", json=payload, params=params)
return ( return (
@@ -229,6 +239,7 @@ class PlatformBackend(Backend):
payload["text"] = content payload["text"] = content
if metadata: if metadata:
payload["metadata"] = metadata payload["metadata"] = metadata
payload["source"] = "CLI"
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload) return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
def delete( def delete(
@@ -242,7 +253,7 @@ class PlatformBackend(Backend):
run_id: str | None = None, run_id: str | None = None,
) -> dict: ) -> dict:
if all: if all:
params: dict[str, str] = {} params: dict[str, str] = {"source": "CLI"}
if user_id: if user_id:
params["user_id"] = user_id params["user_id"] = user_id
if agent_id: if agent_id:
@@ -253,7 +264,7 @@ class PlatformBackend(Backend):
params["run_id"] = run_id params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params) return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id: elif memory_id:
return self._request("DELETE", f"/v1/memories/{memory_id}/") return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
else: else:
raise ValueError("Either memory_id or --all is required") raise ValueError("Either memory_id or --all is required")
@@ -265,18 +276,37 @@ class PlatformBackend(Backend):
app_id: str | None = None, app_id: str | None = None,
run_id: str | None = None, run_id: str | None = None,
) -> dict: ) -> dict:
params: dict[str, str] = {} # v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
if user_id: type_map = {
params["user_id"] = user_id "user": user_id,
if agent_id: "agent": agent_id,
params["agent_id"] = agent_id "app": app_id,
if app_id: "run": run_id,
params["app_id"] = app_id }
if run_id: entities = {t: v for t, v in type_map.items() if v}
params["run_id"] = run_id if not entities:
if not params:
raise ValueError("At least one entity ID is required for delete_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}/", params={"source": "CLI"}
)
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( def status(
self, self,
@@ -284,19 +314,9 @@ class PlatformBackend(Backend):
user_id: str | None = None, user_id: str | None = None,
agent_id: str | None = None, agent_id: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Check connectivity by making a lightweight API call.""" """Check connectivity using the ping endpoint."""
try: try:
# If entity IDs are available, validate with a minimal memories list self.ping()
if user_id or agent_id:
payload: dict[str, Any] = {}
params = {"page": "1", "page_size": "1"}
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
if api_filters:
payload["filters"] = api_filters
self._request("POST", "/v2/memories/", json=payload, params=params)
else:
# No entity IDs — use entities endpoint to validate API key
self._request("GET", "/v1/entities/")
return {"connected": True, "backend": "platform", "base_url": self.base_url} return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e: except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)} return {"connected": False, "backend": "platform", "error": str(e)}
@@ -311,6 +331,13 @@ class PlatformBackend(Backend):
items = [e for e in items if e.get("type", "").lower() == target_type] items = [e for e in items if e.get("type", "").lower() == target_type]
return items 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): class AuthError(Exception):
pass pass
+48 -2
View File
@@ -43,6 +43,10 @@ def _sym(fancy: str, plain: str) -> str:
def print_banner(console: Console) -> None: def print_banner(console: Console) -> None:
"""Print the mem0 welcome banner.""" """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}") logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_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: def print_success(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("✓", "[ok]") sym = _sym("✓", "[ok]")
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}") console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
def print_error(console: Console, message: str, hint: str | None = None) -> None: 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]") sym = _sym("✗", "[error]")
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}") console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
if hint: 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: def print_warning(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("⚠", "[warn]") sym = _sym("⚠", "[warn]")
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}") console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
def print_info(console: Console, message: str) -> None: def print_info(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("◆", "*") sym = _sym("◆", "*")
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}") 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 The spinner and timing output are sent to stderr (via ``_err``) so they
never contaminate machine-readable stdout. The *console* parameter is never contaminate machine-readable stdout. The *console* parameter is
kept for backward compatibility but is not used for spinner output. 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: class _Ctx:
def __init__(self): def __init__(self):
@@ -97,6 +128,13 @@ def timed_status(console: Console, message: str):
self.error_msg = "" self.error_msg = ""
ctx = _Ctx() ctx = _Ctx()
if is_agent_mode():
try:
yield ctx
except Exception:
raise
return
start = time.perf_counter() start = time.perf_counter()
try: try:
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err): with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
@@ -105,6 +143,11 @@ def timed_status(console: Console, message: str):
elapsed = time.perf_counter() - start elapsed = time.perf_counter() - start
if ctx.error_msg: if ctx.error_msg:
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)") 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 raise
else: else:
elapsed = time.perf_counter() - start 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: def print_scope(console: Console, **ids: str | None) -> None:
"""Show active entity scope if any IDs are set.""" """Show active entity scope if any IDs are set."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
parts = [] parts = []
for key, val in ids.items(): for key, val in ids.items():
if val: if val:
label = key.replace("_", " ").replace("id", "ID").strip() parts.append(f"{key}={val}")
parts.append(f"{label}={val}")
if parts: if parts:
scope_str = ", ".join(parts) scope_str = ", ".join(parts)
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]") 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: def cmd_config_show(*, output: str = "text") -> None:
"""Display current configuration (secrets redacted).""" """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() config = load_config()
if output == "json": if output in ("json", "agent"):
format_json_envelope( format_agent_envelope(
console, console,
command="config show", command="config show",
data={ data={
@@ -84,25 +89,44 @@ def cmd_config_show(*, output: str = "text") -> None:
def cmd_config_get(key: str) -> None: def cmd_config_get(key: str) -> None:
"""Get a config value.""" """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() config = load_config()
value = get_nested_value(config, key) value = get_nested_value(config, key)
if value is None: if value is None:
print_error(err_console, f"Unknown config key: {key}") 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: else:
# Redact secrets console.print(display_value)
if "api_key" in key or "key" in key.split(".")[-1:]:
console.print(redact_key(str(value)))
else:
console.print(str(value))
def cmd_config_set(key: str, value: str) -> None: def cmd_config_set(key: str, value: str) -> None:
"""Set a config value.""" """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() config = load_config()
if set_nested_value(config, key, value): if set_nested_value(config, key, value):
save_config(config) save_config(config)
display = redact_key(value) if "key" in key else value 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: else:
print_error(err_console, f"Unknown config key: {key}") print_error(err_console, f"Unknown config key: {key}")
+56 -25
View File
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
print_success, print_success,
timed_status, timed_status,
) )
from mem0_cli.output import format_json from mem0_cli.output import format_agent_envelope, format_json
console = Console() console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)
@@ -26,6 +26,11 @@ err_console = Console(stderr=True)
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None: def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
"""List entities of a given type.""" """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"} valid_types = {"users", "agents", "apps", "runs"}
if entity_type not in valid_types: if entity_type not in valid_types:
print_error( print_error(
@@ -42,6 +47,16 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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": if output == "json":
format_json(console, results) format_json(console, results)
return return
@@ -77,41 +92,39 @@ def cmd_entities_delete(
output: str, output: str,
) -> None: ) -> None:
"""Delete an entity and all its memories (cascade delete).""" """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]): if not any([user_id, agent_id, app_id, run_id]):
print_error( print_error(
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id." err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
) )
raise typer.Exit(1) 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: if dry_run:
scope_parts = [] print_info(console, f"Would delete entity {scope_str} and all its memories.")
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
print_info(console, f"Would delete entity {scope} and all its memories.")
print_info(console, "No changes made (dry run).") print_info(console, "No changes made (dry run).")
return return
if not force: 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( 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: if not confirm:
print_info(console, "Cancelled.") print_info(console, "Cancelled.")
@@ -131,7 +144,25 @@ def cmd_entities_delete(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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) format_json(console, result)
elif output != "quiet": elif output != "quiet":
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)") 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()
+61 -14
View File
@@ -19,7 +19,7 @@ from mem0_cli.branding import (
print_info, print_info,
print_success, print_success,
) )
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
console = Console() console = Console()
err_console = Console(stderr=True) err_console = Console(stderr=True)
@@ -108,6 +108,10 @@ def _email_login(
The caller expects at minimum an ``api_key`` field. The caller expects at minimum an ``api_key`` field.
""" """
url = base_url.rstrip("/") url = base_url.rstrip("/")
_source_headers = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
}
with httpx.Client(timeout=30.0) as client: with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code # If code is already provided, skip sending — user already has a code
@@ -116,6 +120,7 @@ def _email_login(
resp = client.post( resp = client.post(
f"{url}/api/v1/auth/email_code/", f"{url}/api/v1/auth/email_code/",
json={"email": email}, json={"email": email},
headers=_source_headers,
) )
if resp.status_code == 429: if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.") print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -148,6 +153,7 @@ def _email_login(
resp = client.post( resp = client.post(
f"{url}/api/v1/auth/email_code/verify/", f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()}, json={"email": email, "code": code.strip()},
headers=_source_headers,
) )
if resp.status_code == 429: if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.") print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -169,6 +175,7 @@ def run_init(
user_id: str | None = None, user_id: str | None = None,
email: str | None = None, email: str | None = None,
code: str | None = None, code: str | None = None,
force: bool = False,
) -> None: ) -> None:
"""Interactive setup wizard for mem0 CLI. """Interactive setup wizard for mem0 CLI.
@@ -184,6 +191,29 @@ def run_init(
print_error(err_console, "--code requires --email.") print_error(err_console, "--code requires --email.")
raise typer.Exit(1) 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 ────────────────────────────────────────────── # ── Email login flow ──────────────────────────────────────────────
if email: if email:
if api_key: if api_key:
@@ -205,7 +235,10 @@ def run_init(
raise typer.Exit(1) raise typer.Exit(1)
config.platform.api_key = api_key_val config.platform.api_key = api_key_val
config.platform.base_url = base_url config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli" 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) save_config(config)
@@ -220,6 +253,17 @@ def run_init(
# ── API key flow (existing) ─────────────────────────────────────── # ── 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 # Fully non-interactive when both flags provided
if api_key and user_id: if api_key and user_id:
config.platform.api_key = api_key config.platform.api_key = api_key
@@ -229,15 +273,6 @@ def run_init(
print_success(console, "Configuration saved to ~/.mem0/config.json") print_success(console, "Configuration saved to ~/.mem0/config.json")
return 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) print_banner(console)
console.print() console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n") print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
@@ -271,7 +306,10 @@ def run_init(
raise typer.Exit(1) raise typer.Exit(1)
config.platform.api_key = api_key_val config.platform.api_key = api_key_val
config.platform.base_url = base_url config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli" 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) save_config(config)
@@ -331,9 +369,10 @@ def _setup_defaults(config: Mem0Config) -> None:
console.print() console.print()
print_info(console, "Set default entity IDs (press Enter to skip).\n") 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( user_id = Prompt.ask(
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]", f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
default="mem0-cli", default=_default_user,
) )
if user_id: if user_id:
config.defaults.user_id = user_id config.defaults.user_id = user_id
@@ -353,11 +392,19 @@ def _validate_platform(config: Mem0Config) -> None:
) )
if status.get("connected"): if status.get("connected"):
print_success(console, "Connected to mem0 Platform!") 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: else:
print_error( print_error(
err_console, err_console,
f"Could not connect: {status.get('error', 'Unknown error')}", 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: except Exception as e:
print_error(err_console, f"Connection test failed: {e}") print_error(err_console, f"Connection test failed: {e}")
+188 -23
View File
@@ -3,6 +3,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import stat as _stat_mod
import sys import sys
import time as _time import time as _time
from pathlib import Path from pathlib import Path
@@ -20,6 +22,7 @@ from mem0_cli.branding import (
) )
from mem0_cli.output import ( from mem0_cli.output import (
format_add_result, format_add_result,
format_agent_envelope,
format_json, format_json,
format_memories_table, format_memories_table,
format_memories_text, format_memories_text,
@@ -31,6 +34,19 @@ console = Console()
err_console = Console(stderr=True) 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( def cmd_add(
backend: Backend, backend: Backend,
text: str | None, text: str | None,
@@ -50,6 +66,11 @@ def cmd_add(
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Add a memory.""" """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 msgs = None
content = text content = text
@@ -70,8 +91,8 @@ def cmd_add(
print_error(err_console, f"Invalid JSON in --messages: {e}") print_error(err_console, f"Invalid JSON in --messages: {e}")
raise typer.Exit(1) from None raise typer.Exit(1) from None
# Read from stdin if no text and stdin is piped # Read from stdin only if stdin is an actual pipe or file redirect
elif not content and not sys.stdin.isatty(): elif not content and _stdin_is_piped():
content = sys.stdin.read().strip() content = sys.stdin.read().strip()
if not content and not msgs: if not content and not msgs:
@@ -133,18 +154,61 @@ def cmd_add(
if output == "quiet": if output == "quiet":
return 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": if output == "json":
format_add_result(console, result, output) format_add_result(console, result, output)
return return
console.print() console.print()
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id) print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
# Count results count = len(deduped)
results = result if isinstance(result, list) else result.get("results", [result]) all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
count = len(results) if results else 0 if all_pending:
print_success( print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted" 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) format_add_result(console, result, output)
@@ -166,6 +230,11 @@ def cmd_search(
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Search memories.""" """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 filters = None
if filter_json: if filter_json:
try: try:
@@ -210,6 +279,27 @@ def cmd_search(
if output == "quiet": if output == "quiet":
return 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": if output == "json":
format_json(console, results) format_json(console, results)
elif output == "table": elif output == "table":
@@ -236,6 +326,11 @@ def cmd_search(
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None: def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
"""Get a specific memory by ID.""" """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: with timed_status(err_console, "Fetching memory...") as _ts:
try: try:
result = backend.get(memory_id) result = backend.get(memory_id)
@@ -243,7 +338,10 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
print_error(err_console, str(e)) print_error(err_console, str(e))
raise typer.Exit(1) from None 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( def cmd_list(
@@ -262,6 +360,11 @@ def cmd_list(
output: str = "table", output: str = "table",
) -> None: ) -> None:
"""List memories.""" """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: if page_size < 1:
print_error(err_console, "--page-size must be >= 1.") print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1) raise typer.Exit(1)
@@ -292,15 +395,24 @@ def cmd_list(
if output == "quiet": if output == "quiet":
return return
if output == "json": if output in ("json", "agent"):
from mem0_cli.output import format_json_envelope scope = {
k: v
format_json_envelope( 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, console,
command="list", command="list",
data=results, data=results,
scope=scope or None,
count=len(results), count=len(results),
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}, duration_ms=int(_elapsed * 1000),
) )
elif output == "table": elif output == "table":
if results: if results:
@@ -343,6 +455,11 @@ def cmd_update(
output: str, output: str,
) -> None: ) -> None:
"""Update a memory.""" """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 meta = None
if metadata: if metadata:
try: try:
@@ -360,7 +477,14 @@ def cmd_update(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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) format_json(console, result)
elif output != "quiet": elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)") print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
@@ -375,6 +499,11 @@ def cmd_delete(
output: str, output: str,
) -> None: ) -> None:
"""Delete a single memory by ID.""" """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: if dry_run:
# Fetch and display what would be deleted # Fetch and display what would be deleted
try: try:
@@ -395,7 +524,14 @@ def cmd_delete(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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) format_json(console, result)
elif output != "quiet": elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)") print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
@@ -414,13 +550,17 @@ def cmd_delete_all(
output: str, output: str,
) -> None: ) -> None:
"""Delete all memories matching a scope.""" """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_: if all_:
# Project-wide wipe using wildcard entity IDs # Project-wide wipe using wildcard entity IDs
if dry_run: # Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "Run without --dry-run to see the actual count.")
print_info(console, "No changes made (dry run).")
return
if not force: if not force:
confirm = typer.confirm( confirm = typer.confirm(
@@ -445,7 +585,14 @@ def cmd_delete_all(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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) format_json(console, result)
elif output != "quiet": elif output != "quiet":
if isinstance(result, dict) and "message" in result: if isinstance(result, dict) and "message" in result:
@@ -503,7 +650,25 @@ def cmd_delete_all(
raise typer.Exit(1) from None raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start _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) format_json(console, result)
elif output != "quiet": elif output != "quiet":
if isinstance(result, dict) and "message" in result: if isinstance(result, dict) and "message" in result:
+27 -8
View File
@@ -35,22 +35,26 @@ def cmd_status(
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Check connectivity and auth.""" """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() _start = _time.perf_counter()
with timed_status(err_console, "Checking connection...") as _ts: with timed_status(err_console, "Checking connection...") as _ts:
result = backend.status(user_id=user_id, agent_id=agent_id) result = backend.status(user_id=user_id, agent_id=agent_id)
_elapsed = _time.perf_counter() - _start _elapsed = _time.perf_counter() - _start
if output == "json": if output in ("json", "agent"):
format_json_envelope( format_agent_envelope(
console, console,
command="status", command="status",
data={ data={
"connected": result.get("connected", False), "connected": result.get("connected", False),
"backend": result.get("backend", "?"), "backend": result.get("backend", "?"),
"base_url": result.get("base_url", ""), "base_url": result.get("base_url", ""),
"latency_ms": int(_elapsed * 1000),
}, },
duration_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']}") lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
if result.get("error"): if result.get("error"):
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['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") lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
content = "\n".join(lines) content = "\n".join(lines)
@@ -96,7 +108,12 @@ def cmd_import(
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Import memories from a JSON file.""" """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: try:
data = json.loads(Path(file_path).read_text()) data = json.loads(Path(file_path).read_text())
@@ -129,11 +146,13 @@ def cmd_import(
failed += 1 failed += 1
_elapsed = _time.perf_counter() - _start _elapsed = _time.perf_counter() - _start
if output == "json": if output in ("json", "agent"):
format_json_envelope( scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
format_agent_envelope(
console, console,
command="import", 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), duration_ms=int(_elapsed * 1000),
) )
return return
+16
View File
@@ -27,6 +27,7 @@ CONFIG_VERSION = 1
class PlatformConfig: class PlatformConfig:
api_key: str = "" api_key: str = ""
base_url: str = DEFAULT_BASE_URL base_url: str = DEFAULT_BASE_URL
user_email: str = ""
@dataclass @dataclass
@@ -38,16 +39,23 @@ class DefaultsConfig:
enable_graph: bool = False enable_graph: bool = False
@dataclass
class TelemetryConfig:
anonymous_id: str = ""
@dataclass @dataclass
class Mem0Config: class Mem0Config:
version: int = CONFIG_VERSION version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig) defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig) platform: PlatformConfig = field(default_factory=PlatformConfig)
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
SHORT_KEY_ALIASES: dict[str, str] = { SHORT_KEY_ALIASES: dict[str, str] = {
"api_key": "platform.api_key", "api_key": "platform.api_key",
"base_url": "platform.base_url", "base_url": "platform.base_url",
"user_email": "platform.user_email",
"user_id": "defaults.user_id", "user_id": "defaults.user_id",
"agent_id": "defaults.agent_id", "agent_id": "defaults.agent_id",
"app_id": "defaults.app_id", "app_id": "defaults.app_id",
@@ -76,6 +84,7 @@ def load_config() -> Mem0Config:
plat = data.get("platform", {}) plat = data.get("platform", {})
config.platform.api_key = plat.get("api_key", "") config.platform.api_key = plat.get("api_key", "")
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL) config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
config.platform.user_email = plat.get("user_email", "")
defaults = data.get("defaults", {}) defaults = data.get("defaults", {})
config.defaults.user_id = defaults.get("user_id", "") config.defaults.user_id = defaults.get("user_id", "")
@@ -84,6 +93,9 @@ def load_config() -> Mem0Config:
config.defaults.run_id = defaults.get("run_id", "") config.defaults.run_id = defaults.get("run_id", "")
config.defaults.enable_graph = defaults.get("enable_graph", False) config.defaults.enable_graph = defaults.get("enable_graph", False)
telemetry = data.get("telemetry", {})
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
# Environment variable overrides # Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY") env_key = os.environ.get("MEM0_API_KEY")
if env_key: if env_key:
@@ -132,6 +144,10 @@ def save_config(config: Mem0Config) -> None:
"platform": { "platform": {
"api_key": config.platform.api_key, "api_key": config.platform.api_key,
"base_url": config.platform.base_url, "base_url": config.platform.base_url,
"user_email": config.platform.user_email,
},
"telemetry": {
"anonymous_id": config.telemetry.anonymous_id,
}, },
} }
+105 -4
View File
@@ -155,16 +155,23 @@ def format_add_result(console: Console, result: dict | list, output: str = "text
return return
console.print() console.print()
seen_pending_events: set[str] = set()
for r in results: for r in results:
# Detect async PENDING response from Platform API # Detect async PENDING response from Platform API
if r.get("status") == "PENDING": 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('⧗', '...')}[/]" icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"] parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
parts.append("[white]Processing in background[/]") parts.append("[white]Processing in background[/]")
if event_id:
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
console.print(" ".join(parts)) 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 continue
event = r.get("event", "ADD") event = r.get("event", "ADD")
@@ -225,6 +232,100 @@ def format_json_envelope(
console.print_json(json.dumps(envelope, default=str)) 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( def print_result_summary(
console: Console, console: Console,
count: int, count: int,
@@ -237,7 +338,7 @@ def print_result_summary(
parts = [f"{count} result{'s' if count != 1 else ''}"] parts = [f"{count} result{'s' if count != 1 else ''}"]
if page is not None: if page is not None:
parts.append(f"page {page}") 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: if scope_parts:
parts.append(", ".join(scope_parts)) parts.append(", ".join(scope_parts))
if duration_secs is not None: 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
+146
View File
@@ -0,0 +1,146 @@
"""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 contextlib
import hashlib
import json
import os
import platform
import subprocess
import sys
import uuid
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_or_create_anonymous_id() -> str:
"""Return a persistent per-machine anonymous ID, generating one if needed.
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
repeat runs on the same machine share one PostHog identity instead of
collapsing into a single shared fallback string.
"""
from mem0_cli.config import load_config, save_config
config = load_config()
if config.telemetry.anonymous_id:
return config.telemetry.anonymous_id
new_id = f"cli-anon-{uuid.uuid4().hex}"
config.telemetry.anonymous_id = new_id
with contextlib.suppress(Exception):
save_config(config)
return new_id
def _get_distinct_id() -> str:
"""Return a stable anonymous identifier for the current user.
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
persistent per-machine anonymous ID.
"""
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
try:
return _get_or_create_anonymous_id()
except Exception:
return f"cli-anon-{uuid.uuid4().hex}"
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, save_config
from mem0_cli.state import is_agent_mode
config = load_config()
distinct_id = pre_resolved_email or _get_distinct_id()
# Detect anonymous → identified transition. If a stored anonymous_id
# exists and we just resolved to a real identity, fire a one-shot
# $identify event so PostHog stitches the pre-signup history onto
# the authenticated profile. Clear the stored id so we don't re-alias.
anon_id_to_alias: str | None = None
if (
distinct_id
and not distinct_id.startswith("cli-anon-")
and config.telemetry.anonymous_id
):
anon_id_to_alias = config.telemetry.anonymous_id
config.telemetry.anonymous_id = ""
with contextlib.suppress(Exception):
save_config(config)
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),
"anon_distinct_id_to_alias": anon_id_to_alias,
}
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
+108
View File
@@ -0,0 +1,108 @@
"""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)
# Fire $identify *after* email resolution so PostHog links the stored
# anonymous id directly to the final identity (email, not the api-key
# hash). The regular event is sent next so it lands under the merged
# profile.
anon_id = ctx.get("anon_distinct_id_to_alias")
if anon_id:
_send_identify_event(ctx, payload, anon_id)
_send_posthog_event(ctx["posthog_host"], payload)
def _send_identify_event(ctx: dict, payload: dict, anon_id: str) -> None:
"""Send a PostHog $identify event aliasing anon_id → payload['distinct_id']."""
identify_payload = {
"api_key": payload["api_key"],
"event": "$identify",
"distinct_id": payload["distinct_id"],
"properties": {
"$anon_distinct_id": anon_id,
"$lib": payload.get("properties", {}).get("$lib", "posthog-python"),
},
}
_send_posthog_event(ctx["posthog_host"], identify_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": "alice", "count": 5},
{"name": "bob", "count": 3}, {"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 return backend
-5
View File
@@ -83,11 +83,6 @@ class TestCLIIntegration:
assert "add" in result.stdout assert "add" in result.stdout
assert "search" 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): def test_add_help(self):
result = _run(["add", "--help"]) result = _run(["add", "--help"])
assert result.returncode == 0 assert result.returncode == 0
+487 -52
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import typing
from io import StringIO from io import StringIO
from unittest.mock import patch from unittest.mock import patch
@@ -16,6 +17,7 @@ from mem0_cli.commands.config_cmd import (
cmd_config_show, cmd_config_show,
) )
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list 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 ( from mem0_cli.commands.memory import (
cmd_add, cmd_add,
cmd_delete, cmd_delete,
@@ -28,7 +30,6 @@ from mem0_cli.commands.memory import (
from mem0_cli.commands.utils import ( from mem0_cli.commands.utils import (
cmd_import, cmd_import,
cmd_status, cmd_status,
cmd_version,
) )
@@ -176,30 +177,28 @@ class TestAddCommand:
def test_add_no_content_exits(self, mock_backend): def test_add_no_content_exits(self, mock_backend):
console, _buf = _make_console() console, _buf = _make_console()
err_console, _err_buf = _make_err_console() err_console, _err_buf = _make_err_console()
# Patch stdin.isatty to return True so it doesn't try to read stdin
with ( with (
patch("mem0_cli.commands.memory.console", console), patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_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 cmd_add(
with pytest.raises((SystemExit, ClickExit)): mock_backend,
cmd_add( None,
mock_backend, user_id="alice",
None, agent_id=None,
user_id="alice", app_id=None,
agent_id=None, run_id=None,
app_id=None, messages=None,
run_id=None, file=None,
messages=None, metadata=None,
file=None, immutable=False,
metadata=None, no_infer=False,
immutable=False, expires=None,
no_infer=False, categories=None,
expires=None, output="text",
categories=None, )
output="text",
)
def test_add_invalid_metadata_json(self, mock_backend): def test_add_invalid_metadata_json(self, mock_backend):
console, _buf = _make_console() console, _buf = _make_console()
@@ -279,6 +278,66 @@ class TestAddCommand:
mock_backend.add.assert_called_once() 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: class TestSearchCommand:
def test_search_text(self, mock_backend): def test_search_text(self, mock_backend):
console, buf = _make_console() console, buf = _make_console()
@@ -618,28 +677,6 @@ class TestDeleteAllCommand:
run_id="*", 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): def test_delete_all_project_wide_async_response(self, mock_backend):
mock_backend.delete.return_value = {"message": "Memories deletion started..."} mock_backend.delete.return_value = {"message": "Memories deletion started..."}
console, buf = _make_console() console, buf = _make_console()
@@ -704,15 +741,6 @@ class TestStatusCommand:
assert '"status"' in output 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: class TestImportCommand:
def test_import_json(self, mock_backend, tmp_path): def test_import_json(self, mock_backend, tmp_path):
file_path = tmp_path / "import.json" file_path = tmp_path / "import.json"
@@ -888,6 +916,28 @@ class TestEntitiesDeleteCommand:
output = buf.getvalue() output = buf.getvalue()
assert "deleted" in output.lower() 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): def test_delete_entity_no_id_exits(self, mock_backend):
console, _buf = _make_console() console, _buf = _make_console()
err_console, _err_buf = _make_err_console() err_console, _err_buf = _make_err_console()
@@ -1024,3 +1074,388 @@ class TestEnableGraph:
) )
call_kwargs = mock_backend.list_memories.call_args call_kwargs = mock_backend.list_memories.call_args
assert call_kwargs.kwargs.get("enable_graph") is True 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_table,
format_memories_text, format_memories_text,
format_single_memory, format_single_memory,
sanitize_agent_data,
) )
@@ -129,8 +130,153 @@ class TestAddResult:
output = buf.getvalue() output = buf.getvalue()
assert output.strip() == "" 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): def test_format_add_result_empty(self):
console, buf = _make_console() console, buf = _make_console()
format_add_result(console, {"results": []}, "text") format_add_result(console, {"results": []}, "text")
output = buf.getvalue() output = buf.getvalue()
assert "No memories extracted" in output 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
+2 -2
View File
@@ -10,7 +10,7 @@ description: "REST APIs for memory management, search, and entity operations"
Mem0 provides a comprehensive REST API for integrating advanced memory capabilities into your applications. Create, search, update, and manage memories across users, agents, and custom entities with simple HTTP requests. Mem0 provides a comprehensive REST API for integrating advanced memory capabilities into your applications. Create, search, update, and manage memories across users, agents, and custom entities with simple HTTP requests.
<Info> <Info>
**Quick start:** Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys) and make your first memory operation in minutes. **Quick start:** Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a> and make your first memory operation in minutes.
</Info> </Info>
--- ---
@@ -87,7 +87,7 @@ All API requests require authentication using Token-based authentication. Includ
Authorization: Token <your-api-key> Authorization: Token <your-api-key>
``` ```
Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys). Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
<Warning> <Warning>
**Keep your API key secure.** Never expose it in client-side code or public repositories. Use environment variables and server-side requests only. **Keep your API key secure.** Never expose it in client-side code or public repositories. Use environment variables and server-side requests only.
@@ -47,8 +47,6 @@ Provide at least one message or direct memory string. Most callers supply `messa
| `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. | | `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. |
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). | | `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. | | `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
| `async_mode` | boolean (default `true`) | Optional | Controls asynchronous processing. Most clients leave this enabled. |
| `output_format` | string (default `v1.1`) | Optional | Response format. `v1.1` wraps results in a `results` array. |
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership. > \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
@@ -83,20 +81,3 @@ Successful requests return an array of events queued for processing. Each event
``` ```
</CodeGroup> </CodeGroup>
## Graph relationships
Add Memories can enrich the knowledge graph on write. Set `enable_graph: true` to create entity nodes and relationships for the stored memory. Use this when you want downstream `get_all` or search calls to traverse connected entities.
<CodeGroup>
```json Graph-aware request
{
"user_id": "alice",
"messages": [
{ "role": "user", "content": "I met with Dr. Lee at General Hospital." }
],
"enable_graph": true
}
```
</CodeGroup>
The response follows the same format, and related entities become available in [Graph Memory](/platform/features/graph-memory) queries.
+1 -2
View File
@@ -62,8 +62,7 @@ To retrieve graph memory relationships between entities, pass `output_format="v1
memories = client.get_all( memories = client.get_all(
filters={ filters={
"user_id": "alex" "user_id": "alex"
}, }
output_format="v1.1"
) )
``` ```
+8 -11
View File
@@ -32,7 +32,7 @@ Example with the mem0 Python package:
```python ```python
from mem0 import MemoryClient from mem0 import MemoryClient
client = MemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID') client = MemoryClient(api_key="your-api-key")
``` ```
</Tab> </Tab>
@@ -41,10 +41,7 @@ client = MemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID')
```javascript ```javascript
import { MemoryClient } from "mem0ai"; import { MemoryClient } from "mem0ai";
const client = new MemoryClient({ const client = new MemoryClient({ apiKey: "your-api-key" });
organizationId: "YOUR_ORG_ID",
projectId: "YOUR_PROJECT_ID"
});
``` ```
</Tab> </Tab>
@@ -82,7 +79,7 @@ new_project = client.project.create(
### Update Project Settings ### 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 ```python
# Update project with custom categories # Update project with custom categories
@@ -98,8 +95,8 @@ client.project.update(
custom_instructions="..." custom_instructions="..."
) )
# Enable graph memory for the project # Use the input language for memory storage and retrieval
client.project.update(enable_graph=True) client.project.update(multilingual=True)
# Update multiple settings at once # Update multiple settings at once
client.project.update( client.project.update(
@@ -108,7 +105,7 @@ client.project.update(
{"personal_info": "User personal information and preferences"}, {"personal_info": "User personal information and preferences"},
{"work_context": "Professional context and work-related information"} {"work_context": "Professional context and work-related information"}
], ],
enable_graph=True multilingual=True
) )
``` ```
@@ -168,11 +165,11 @@ All project methods are available in async mode:
from mem0 import AsyncMemoryClient from mem0 import AsyncMemoryClient
async def manage_project(): async def manage_project():
client = AsyncMemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID') client = AsyncMemoryClient(api_key="your-api-key")
# All methods support async/await # All methods support async/await
project_info = await client.project.get() project_info = await client.project.get()
await client.project.update(enable_graph=True) await client.project.update(multilingual=True)
members = await client.project.get_members() members = await client.project.get_members()
# To call the async function properly # To call the async function properly
+82
View File
@@ -0,0 +1,82 @@
---
title: "Highlights"
description: "Major product launches, headline features, and milestones for Mem0."
mode: "wide"
---
<Update label="2026-04-06" description="Mem0 Skill Graph">
**Mem0 Skill Graph — In-Context Documentation for AI Agents**
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow — no doc searching required. Three interconnected skills launched:
- **mem0 Core Skill** — Complete Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more
- **mem0-cli Skill** — Terminal command reference, configuration walkthroughs, and CI/CD recipes
- **mem0-vercel-ai-sdk Skill** — Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup
</Update>
<Update label="2026-04-06" description="Mem0 CLI v0.2.2">
**Official Mem0 CLI — Now on PyPI and npm**
A full-featured command-line interface for Mem0, available in both Python and Node.js:
- **Install:** `pip install mem0-cli` or `npm install -g @mem0/cli`
- **Full command suite** — `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`
- **Interactive setup** — `mem0 init` with email verification or direct API key entry
- **Works everywhere** — Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable** — `--json` flag for CI/CD pipelines and automation
- **Dual SDK** — Same commands, same experience across Python and Node.js
</Update>
<Update label="2026-04-04" description="OpenClaw v1.0.4">
**OpenClaw Plugin — Production-Ready**
The OpenClaw Mem0 plugin went from initial release to production-ready in one week (v1.0.0 → v1.0.4):
- **Skills-based memory architecture** — New extraction pipeline with skill-loader, batched extraction, and domain-aware memory triage
- **Dream gate** — Automatic memory consolidation during idle periods for higher-quality long-term recall
- **Interactive CLI** — `openclaw mem0 init`, `status`, `config`, `import`, and `event` commands
- **Unified tool naming** — `memory_add` and `memory_delete` replace 4 legacy tools, matching the platform API
- **Security hardened** — Path traversal protection, pinned dependencies, 329 tests across 10 files
</Update>
<Update label="2026-04-02" description="Mem0 Plugin for AI Editors">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
Launched a unified Mem0 plugin across three major AI development environments — Claude Code and Cursor first (March 25), then Codex (April 2):
- **9 MCP memory tools** — add, search, get, update, delete, bulk delete, entity management via `mcp.mem0.ai`
- **Lifecycle hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server** — Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport** — New MCP transport protocol for real-time streaming
- **Codex-specific skill** — Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
</Update>
<Update label="2026-03-21" description="New Providers">
**Apache AGE, Turbopuffer, MiniMax, and pgvector for Node.js**
Major expansion of the provider ecosystem:
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
- **Turbopuffer** — New vector database provider for Python SDK
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
- **Reasoning models** — `reasoning_effort` parameter for OpenAI o1/o3-style models
</Update>
<Update label="2026-03-14" description="Mem0 Platform Skill">
**Mem0 Platform Skill on skills.sh**
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
</Update>
+199
View File
@@ -0,0 +1,199 @@
---
title: "OpenClaw"
description: "Release notes for the OpenClaw plugin and agent harness."
mode: "wide"
---
<Update label="2026-04-11" description="v1.0.6">
**Bug Fixes:**
- **Telemetry:** Replaced shared `"anonymous-openclaw"` fallback with a persistent per-machine random hash (`openclaw-anon-<uuid>`), so anonymous plugin users are counted individually in PostHog ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch anonymous history onto the authenticated profile ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Fixed event loss on short-lived CLI invocations — added `beforeExit` handler to flush queued events before the process exits ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added lazy `/v1/ping/` email resolution so users who configure API key outside `mem0 init` show as their email in PostHog, not an md5 hash ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Unified CLI event prefix from `openclaw.<cmd>` to `openclaw.cli.<cmd>` on the needsSetup branch to match the authenticated branch ([#4790](https://github.com/mem0ai/mem0/pull/4790))
**Improvements:**
- **API:** Added `source: "OPENCLAW"` to all provider calls (`add`, `search`, `getAll`) across tools, CLI commands, recall, and the OSS backend adapter ([#4790](https://github.com/mem0ai/mem0/pull/4790))
</Update>
<Update label="2026-04-07" description="v1.0.5">
**Bug Fixes:**
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
- **OSS pgvector crash** ([#4727](https://github.com/mem0ai/mem0/issues/4727)): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
**Removed:**
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
</Update>
<Update label="2026-04-04" description="v1.0.4">
**New Features:**
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status`, `openclaw mem0 config show`, `openclaw mem0 config set`
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
- **`event list` / `event status` CLI commands**: Monitor background processing events
- **`fs-safe.ts` module**: Isolated filesystem wrappers in a separate entry point
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
- **Test suite**: 329 tests across 10 test files
**Changes:**
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts`
- **Code splitting**: tsup builds with `splitting: true` and two entry points
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`)
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race`
- **Auto-capture fire-and-forget**: `provider.add()` runs in background via `.then()/.catch()`
- **Auto-capture minimum content gate**: Skips extraction when total user content is fewer than 50 chars
**Removed:**
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- `memory_history` tool and `history` CLI command — deprecated
</Update>
<Update label="2026-04-03" description="v1.0.3">
**Bug Fixes:**
- **Security**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal
- **Noise filter**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction`
**Changes:**
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
**Tests:**
- 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
</Update>
<Update label="2026-04-02" description="v1.0.2">
**Bug Fixes:**
- **Security**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — plugin-side env resolution was redundant and triggered static analysis warnings ([#4676](https://github.com/mem0ai/mem0/pull/4676))
</Update>
<Update label="2026-04-02" description="v1.0.1">
**New Features:**
- **CD workflow**: Added continuous deployment workflow with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file ([#4667](https://github.com/mem0ai/mem0/pull/4667))
**Bug Fixes:**
- **Dream gate**: Fixed cheap-first ordering, session isolation, and verified completion ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup**: Plugin now starts gracefully when no API key is configured ([#4669](https://github.com/mem0ai/mem0/pull/4669))
</Update>
<Update label="2026-04-01" description="v1.0.0">
**New Features:**
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Memory consolidation and dream-cycle processing during idle periods
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
**Changes:**
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
</Update>
<Update label="2026-03-26" description="v0.4.1">
**New Features:**
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions
**Bug Fixes:**
- **Credential detection**: Improved detection of credentials, API keys, and secrets in extraction instructions (#4552)
- **Standalone timestamps**: Prevented extraction of standalone timestamps as memories (#4550)
</Update>
<Update label="2026-03-16" description="v0.4.0">
**New Features:**
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers
- **Subagent hallucination prevention**: Detects ephemeral subagent sessions and routes recall to parent namespace
- **Dynamic recall thresholding**: Memories scoring less than 50% of top result are dropped
- **SQLite resilience**: Init error recovery with automatic retry for OSS mode
- **`disableHistory` config option**: New `oss.disableHistory` flag
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
**Changes:**
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Relaxed extraction instructions: related facts kept together to preserve context
**Bug Fixes:**
- **Concurrent session race condition**: Lifecycle hooks now use `ctx.sessionKey` directly instead of a shared mutable variable
</Update>
<Update label="2026-03-12" description="v0.3.1">
**New Features:**
- **Message filtering pipeline**: Multi-stage noise removal before extraction
- **Broad recall for new sessions**: Short or new-session prompts trigger secondary broad search
- **Client-side threshold filtering**: Safety net that drops low-relevance results
- **Temporal anchoring**: Extraction instructions now include current date
- 55 unit tests covering filtering and isolation helpers
**Changes:**
- Extraction window expanded from last 10 to last 20 messages
- Rewritten custom extraction instructions for conciseness and deduplication
- Refactored monolithic `index.ts` (1772 lines) into 6 focused modules
</Update>
<Update label="2026-03-10" description="v0.3.0">
**Bug Fixes:**
- Updated `mem0ai` dependency with sqlite3 to better-sqlite3 migration (#4270)
</Update>
<Update label="2026-03-09" description="v0.2.0">
**New Features:**
- Per-agent memory isolation for multi-agent setups via `agentId`
- "Understanding userId" section in docs
**Changes:**
- Updated config examples to use concrete `userId` values instead of placeholders
**Bug Fixes:**
- Migrated platform search to Mem0 v2 API
</Update>
<Update label="2026-02-19" description="v0.1.2">
**New Features:**
- Source field for openclaw memory entries
**Bug Fixes:**
- Auto-recall injection and auto-capture message drop
</Update>
<Update label="2026-02-02" description="v0.1.0">
**New Features:**
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
</Update>
+290
View File
@@ -0,0 +1,290 @@
---
title: "Platform"
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
mode: "wide"
---
<Update label="2025-07-23" description="">
**Bug Fixes:**
- **Memory:** Fixed ADD functionality
</Update>
<Update label="2025-07-19" description="">
**New Features:**
- **UI:** Added Settings UI and latency display
- **Performance:** Neo4j query optimization
**Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
</Update>
<Update label="2025-07-18" description="">
**Improvements:**
- **UI:** Updated Event UI
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error
</Update>
<Update label="2025-07-17" description="">
**New Features:**
- **UI:** New Settings Page
- **Memory:** Duplicate memories entities support
**Improvements:**
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
</Update>
<Update label="2025-07-16" description="">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
**Bug Fixes:**
- **API:** Fixed pagination for get all API
</Update>
<Update label="2025-07-12" description="">
**Bug Fixes:**
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
**Improvements:**
- **Rate Limiting:** New rate limit for V2 Search
**Bug Fixes:**
- **Slack:** Fixed Slack rate limit error with backend improvements
</Update>
<Update label="2025-07-10" description="">
**Improvements:**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
</Update>
<Update label="2025-07-09" description="">
**Improvements:**
- **Graph:** Graph Optimizations V2 and memory improvements
</Update>
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
</Update>
+128 -286
View File
@@ -1,13 +1,40 @@
--- ---
title: "Product Updates" title: "SDK & Tools"
description: "Latest releases, bug fixes, and improvements for the Mem0 Python and TypeScript SDKs." description: "Release notes for the Mem0 Python SDK, TypeScript SDK, Vercel AI SDK, CLI, and editor plugins."
mode: "wide" mode: "wide"
--- ---
<Tabs> <Tabs>
<Tab title="Python"> <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"> <Update label="2026-03-28" description="v1.0.9">
**New Features & Updates:** **New Features & Updates:**
@@ -816,6 +843,21 @@ mode: "wide"
</Tab> </Tab>
<Tab title="TypeScript"> <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"> <Update label="2026-03-28" description="v2.4.4">
@@ -1116,352 +1158,152 @@ mode: "wide"
</Tab> </Tab>
<Tab title="Platform"> <Tab title="CLI">
<Update label="2025-07-23" description=""> <Update label="2026-04-11" description="Python v0.2.3 / Node v0.2.3">
**Bug Fixes:** **Bug Fixes:**
- **Memory:** Fixed ADD functionality - **Telemetry:** Replaced shared `"anonymous-cli"` fallback with a persistent per-machine random hash (`cli-anon-<uuid>`), so anonymous CLI users are counted individually in PostHog instead of collapsing into one identity ([#4789](https://github.com/mem0ai/mem0/pull/4789))
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch pre-signup anonymous history onto the authenticated user profile ([#4789](https://github.com/mem0ai/mem0/pull/4789))
**Improvements:**
- **API:** All API calls now include `source=CLI` in request bodies (POST/PUT) and query params (GET/DELETE) for server-side attribution ([#4789](https://github.com/mem0ai/mem0/pull/4789))
</Update> </Update>
<Update label="2025-07-19" description=""> <Update label="2026-04-06" description="Python v0.2.2 / Node v0.2.2">
**New Features:** **New Features:**
- **UI:** Added Settings UI and latency display - **Telemetry:** Added PostHog telemetry and source tracking to both Python and Node CLIs ([#4699](https://github.com/mem0ai/mem0/pull/4699))
- **Performance:** Neo4j query optimization - **Validation:** API key validated upfront via `/v1/ping/` on startup — fail-fast with a helpful error instead of cryptic 401s ([#4701](https://github.com/mem0ai/mem0/pull/4701))
**Bug Fixes:** **Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions - **CD:** Fixed OIDC trusted publishing with `npx npm@latest` ([#4724](https://github.com/mem0ai/mem0/pull/4724))
- **CD:** Removed npm self-upgrade from CD workflows ([#4723](https://github.com/mem0ai/mem0/pull/4723))
</Update> </Update>
<Update label="2025-07-18" description=""> <Update label="2026-04-03" description="Python v0.2.1 / Node v0.2.1">
**Improvements:** **New Features:**
- **UI:** Updated Event UI - **Docs:** Comprehensive README with installation, usage examples, and purple branding ([#4680](https://github.com/mem0ai/mem0/pull/4680))
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**Bug Fixes:** **Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error - **npm:** Added `repository` field to Node packages for npm provenance ([#4671](https://github.com/mem0ai/mem0/pull/4671))
- **CD:** Added CD workflows for Node SDK packages with OIDC trusted publishing ([#4670](https://github.com/mem0ai/mem0/pull/4670))
</Update> </Update>
<Update label="2025-07-17" description=""> <Update label="2026-04-02" description="Python v0.2.0 / Node v0.1.1">
**New Features:** **New Features:**
- **UI:** New Settings Page - **`event` commands:** `mem0 event list` shows recent background processing events in a table; `mem0 event status <id>` shows full detail including nested memory results ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Memory:** Duplicate memories entities support - **`--json` / `--agent` flag:** Root-level flag switches all command output to a structured JSON envelope for programmatic/agent consumption. Envelope format: `{"status", "command", "duration_ms", "scope", "count", "data"}` ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Agent output sanitization:** Raw API responses projected to only relevant fields per command (e.g., `add` → `{id, memory, event}`, `search` → `{id, memory, score, created_at, categories}`) ([#4649](https://github.com/mem0ai/mem0/pull/4649))
**Improvements:** - **Email login:** Added email verification code login to `mem0 init` ([#4623](https://github.com/mem0ai/mem0/pull/4623))
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries - **Brand update:** Updated color palette from purple to golden ([#4664](https://github.com/mem0ai/mem0/pull/4664))
- **CI/CD:** Added CI pipelines and CD workflows for both CLIs ([#4640](https://github.com/mem0ai/mem0/pull/4640), [#4653](https://github.com/mem0ai/mem0/pull/4653))
</Update>
<Update label="2025-07-16" description="">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
**Bug Fixes:** **Bug Fixes:**
- **API:** Fixed pagination for get all API - **Node:** Fixed critical `MODULE_NOT_FOUND` crash on `status`, `import`, and all commands when installed globally — replaced runtime `createRequire` with build-time version injection ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Node:** API errors now show full response detail instead of bare "Bad Request" ([#4636](https://github.com/mem0ai/mem0/pull/4636))
</Update> - **Python:** Fixed double error printing on all commands ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **`status` command:** Replaced heavyweight `/v1/entities/` check with dedicated `GET /v1/ping/` endpoint ([#4649](https://github.com/mem0ai/mem0/pull/4649))
<Update label="2025-07-12" description=""> - **`add` command:** Deduplicated PENDING results from API; changed misleading count message ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`init` command:** Partial flags now work in non-TTY; warns before overwriting existing config; added `--force` flag ([#4649](https://github.com/mem0ai/mem0/pull/4649))
**Bug Fixes:** - **`delete` command:** Fixed entity delete via v2 API for all entity types ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
**Improvements:** **Improvements:**
- **Rate Limiting:** New rate limit for V2 Search - Tables now show full UUIDs (was truncated to 8 chars, making `mem0 get <id>` fail) ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- Search table includes Score column ([#4636](https://github.com/mem0ai/mem0/pull/4636))
**Bug Fixes:** - `config get api_key` short-form aliases added ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Slack:** Fixed Slack rate limit error with backend improvements - Client-side validation for `--expires`, `--page-size`, `--page`, `--top-k`, `--threshold`, and empty content ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- `printInfo` / `printScope` moved to stderr to avoid contaminating JSON piping ([#4636](https://github.com/mem0ai/mem0/pull/4636))
</Update> </Update>
<Update label="2025-07-10" description=""> <Update label="2026-03-26" description="Python v0.1.0 / Node v0.1.0">
**Improvements:** **Initial Release — Official Mem0 CLI**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
</Update> A full-featured command-line interface for Mem0, available in both Python and Node.js:
<Update label="2025-07-09" description=""> - **Install:** `pip install mem0-cli` (Python) or `npm install -g @mem0/cli` (Node.js)
- **Full command suite:** `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`
**Improvements:** - **Interactive setup:** `mem0 init` with API key entry and user ID configuration
- **Graph:** Graph Optimizations V2 and memory improvements - **Works everywhere:** Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable:** `-o json` flag for CI/CD pipelines and automation
</Update> - **Dual SDK:** Same commands, same experience across Python and Node.js
- **Shared spec:** Both implementations driven by a single `cli-spec.json` ensuring identical behavior ([#4575](https://github.com/mem0ai/mem0/pull/4575))
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
</Update> </Update>
</Tab> </Tab>
<Tab title="Vercel AI SDK"> <Tab title="Plugins">
<Update label="2025-12-26" description="v2.0.5"> <Update label="2026-04-02" description="mem0-plugin v1.0.0">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
The unified Mem0 plugin for AI development environments:
- **9 MCP memory tools:** `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities` — all via `mcp.mem0.ai`
- **Lifecycle hooks:** Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server:** Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport:** New MCP transport protocol for real-time streaming
- **Codex-specific skill:** Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
- **Supported editors:** Claude Code, Claude Cowork, Cursor, Codex
</Update>
<Update label="2025-12-26" description="Vercel AI SDK v2.0.5">
**Bug Fix:** **Bug Fix:**
- **Vercel AI SDK:** Removed unnecessary dependencies to make the package lighter. - Removed unnecessary dependencies to make the package lighter.
</Update> </Update>
<Update label="2025-09-25" description="v2.0.4"> <Update label="2025-09-25" description="Vercel AI SDK v2.0.3 – v2.0.4">
**New Features:**
- Added file support for multimodal capabilities with memory context (v2.0.3)
**Bug Fix:** **Bug Fix:**
- **Vercel AI SDK:** Fixed version parameter in the AI SDK to use V2 for addition. - Fixed version parameter to use V2 for addition (v2.0.4)
</Update> </Update>
<Update label="2025-09-25" description="v2.0.3"> <Update label="2025-09-03" description="Vercel AI SDK v2.0.2">
**New Features:**
- **Vercel AI SDK:** Added file support for multimodal capabilities with memory context
</Update>
<Update label="2025-09-03" description="v2.0.2">
**Bug Fix:** **Bug Fix:**
- **Vercel AI SDK:** Fixed streaming response in the AI SDK. - Fixed streaming response in the AI SDK.
</Update> </Update>
<Update label="2025-08-05" description="v2.0.1"> <Update label="2025-08-05" description="Vercel AI SDK v2.0.0 – v2.0.1">
**New Features:** **New Features:**
- **Vercel AI SDK:** Added a new param `host` to the config. - Migration to AI SDK V5 (v2.0.0)
- Added `host` param to the config (v2.0.1)
</Update> </Update>
<Update label="2025-08-05" description="v2.0.0"> <Update label="2025-06-15" description="Vercel AI SDK v1.0.6">
**New Features:** **New Features:**
- **Vercel AI SDK:** Migration to AI SDK V5. - Added `filter_memories` param.
</Update> </Update>
<Update label="2025-06-15" description="v1.0.6"> <Update label="2025-05-23" description="Vercel AI SDK v1.0.5">
**New Features:** **New Features:**
- **Vercel AI SDK:** Added param `filter_memories`. - Added support for Google provider.
</Update> </Update>
<Update label="2025-05-23" description="v1.0.5"> <Update label="2025-05-10" description="Vercel AI SDK v1.0.3 – v1.0.4">
**New Features:** **New Features:**
- **Vercel AI SDK:** Added support for Google provider. - Added support for `output_format` param (v1.0.4)
</Update>
<Update label="2025-05-10" description="v1.0.4">
**New Features:**
- **Vercel AI SDK:** Added support for new param `output_format`.
</Update>
<Update label="2025-05-08" description="v1.0.3">
**Improvements:** **Improvements:**
- **Vercel AI SDK:** Added support for graceful failure in cases services are down. - Added graceful failure handling when services are down (v1.0.3)
</Update> </Update>
<Update label="2025-05-01" description="v1.0.1"> <Update label="2025-05-01" description="Vercel AI SDK v1.0.1">
**New Features:** **New Features:**
- **Vercel AI SDK:** Added support for graph memories - Added support for graph memories.
</Update> </Update>
</Tab> </Tab>
</Tabs> </Tabs>
@@ -24,6 +24,7 @@ config = {
"provider": "gemini", "provider": "gemini",
"config": { "config": {
"model": "gemini-2.0-flash-001", "model": "gemini-2.0-flash-001",
"api_key": "your-gemini-api-key",
"temperature": 0.2, "temperature": 0.2,
"max_tokens": 2000, "max_tokens": 2000,
"top_p": 1.0 "top_p": 1.0
@@ -52,6 +53,7 @@ const config = {
provider: "gemini", provider: "gemini",
config: { config: {
model: "gemini-2.0-flash-001", model: "gemini-2.0-flash-001",
apiKey: process.env.GOOGLE_API_KEY || '',
temperature: 0.1 temperature: 0.1
} }
} }
+1 -1
View File
@@ -15,7 +15,7 @@ Mem0 supports LangChain as a provider for vector store integration. LangChain pr
```python Python ```python Python
import os import os
from mem0 import Memory from mem0 import Memory
from langchain_community.vectorstores import Chroma from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings from langchain_openai import OpenAIEmbeddings
# Initialize a LangChain vector store # Initialize a LangChain vector store
+21
View File
@@ -50,4 +50,25 @@ Here are the parameters available for configuring Valkey:
| `hnsw_m` | Number of bi-directional links for HNSW | `16` | | `hnsw_m` | Number of bi-directional links for HNSW | `16` |
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` | | `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` | | `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
| `cluster_mode` | Enable cluster mode for Valkey cluster (CME) deployments | `false` |
| `distance_metric` | Distance metric for vector similarity | `cosine` | | `distance_metric` | Distance metric for vector similarity | `cosine` |
## Cluster Mode
To use Valkey with cluster mode enabled (CME), set `cluster_mode` to `true`:
```python
config = {
"vector_store": {
"provider": "valkey",
"config": {
"collection_name": "memories",
"valkey_url": "valkey://cluster-endpoint:6379",
"embedding_model_dims": 1536,
"cluster_mode": True
}
}
}
```
When cluster mode is enabled, the connector uses `ValkeyCluster` instead of the standalone client, which handles `MOVED`/`ASK` redirections automatically. Search queries are coordinated across all shards by the valkey-search module's built-in coordinator. See the [valkey-search documentation](https://github.com/valkey-io/valkey-search) for details on cluster mode behavior.
@@ -45,7 +45,7 @@ Before you begin, follow these steps to set up the demo application:
OPENAI_API_KEY=your_openai_api_key OPENAI_API_KEY=your_openai_api_key
MEM0_API_KEY=your_mem0_api_key MEM0_API_KEY=your_mem0_api_key
``` ```
You can obtain your `MEM0_API_KEY` by signing up at [Mem0 API Dashboard](https://app.mem0.ai/dashboard/api-keys). You can obtain your `MEM0_API_KEY` by signing up at <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Dashboard</a>.
5. Start the development server: 5. Start the development server:
```bash ```bash
@@ -129,13 +129,13 @@ async def search_memories(
user_id=USER_ID, user_id=USER_ID,
limit=5, limit=5,
threshold=0.7, # Higher threshold for more relevant results threshold=0.7, # Higher threshold for more relevant results
) )
# Format and return the results # Format and return the results
if not results.get('results', []): if not results.get('results', []):
return "I don't have any relevant memories about this topic." return "I don't have any relevant memories about this topic."
memories = [f"• {result['memory']}" for result in results.get('results', [])] memories = [f"• {result['memory']}" for result in results.get('results', [])]
return "Here's what I remember that might be relevant:\n" + "\n".join(memories) return "Here's what I remember that might be relevant:\n" + "\n".join(memories)
``` ```
@@ -345,13 +345,13 @@ async def search_memories(
user_id=USER_ID, user_id=USER_ID,
limit=5, limit=5,
threshold=0.7, # Higher threshold for more relevant results threshold=0.7, # Higher threshold for more relevant results
) )
# Format and return the results # Format and return the results
if not results.get('results', []): if not results.get('results', []):
return "I don't have any relevant memories about this topic." return "I don't have any relevant memories about this topic."
memories = [f"• {result['memory']}" for result in results.get('results', [])] memories = [f"• {result['memory']}" for result in results.get('results', [])]
return "Here's what I remember that might be relevant:\n" + "\n".join(memories) return "Here's what I remember that might be relevant:\n" + "\n".join(memories)
@@ -38,7 +38,7 @@ client = MemoryClient(api_key="your-api-key")
``` ```
<Note> <Note>
Replace `your-api-key` with your actual Mem0 API key from the [dashboard](https://app.mem0.ai). Without proper API authentication, memory operations will fail. Replace `your-api-key` with your actual Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a>. Without proper API authentication, memory operations will fail.
</Note> </Note>
--- ---
@@ -17,7 +17,7 @@ from mem0 import MemoryClient
client = MemoryClient(api_key="m0-...") client = MemoryClient(api_key="m0-...")
``` ```
Grab an API key from the <Link href="https://app.mem0.ai/">Mem0 dashboard</Link> to get started. Grab an API key from the <a href="https://app.mem0.ai/" rel="nofollow">Mem0 dashboard</a> to get started.
## Store and Retrieve Scoped Memories ## Store and Retrieve Scoped Memories
@@ -20,7 +20,7 @@ client = MemoryClient(api_key="your-api-key")
``` ```
<Note> <Note>
Your API key needs export permissions to download memory data. Check your project settings on the [dashboard](https://app.mem0.ai) if export operations fail with authentication errors. Your API key needs export permissions to download memory data. Check your project settings on the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a> if export operations fail with authentication errors.
</Note> </Note>
Let's add some sample memories to work with: Let's add some sample memories to work with:
@@ -1,70 +0,0 @@
---
title: Browser Extension Memory
description: "Add Mem0's universal memory layer to Chrome chat surfaces."
---
Enhance your AI interactions with Mem0, a Chrome extension that introduces a universal memory layer across platforms like ChatGPT, Claude, and Perplexity. Mem0 ensures seamless context sharing, making your AI experiences more personalized and efficient.
<Note>
We now support Grok! The Mem0 Chrome Extension has been updated to work with Grok, bringing the same powerful memory capabilities to your Grok conversations.
</Note>
## Features
- **Universal Memory Layer**: Share context seamlessly across ChatGPT, Claude, Perplexity, and Grok.
- **Smart Context Detection**: Automatically captures relevant information from your conversations.
- **Intelligent Memory Retrieval**: Surfaces pertinent memories at the right time.
- **One-Click Sync**: Easily synchronize with existing ChatGPT memories.
- **Memory Dashboard**: Manage all your memories in one centralized location.
## Installation
You can install the Mem0 Chrome Extension using one of the following methods:
### Method 1: Chrome Web Store Installation
1. **Download the Extension**: Open Google Chrome and navigate to the [Mem0 Chrome Extension page](https://chromewebstore.google.com/detail/mem0/onihkkbipkfeijkadecaafbgagkhglop?hl=en).
2. **Add to Chrome**: Click on the "Add to Chrome" button.
3. **Confirm Installation**: In the pop-up dialog, click "Add extension" to confirm. The Mem0 icon should now appear in your Chrome toolbar.
### Method 2: Manual Installation
1. **Download the Extension**: Clone or download the extension files from the [Mem0 Chrome Extension GitHub repository](https://github.com/mem0ai/mem0-chrome-extension).
2. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
3. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
4. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
5. **Confirm Installation**: The Mem0 Chrome Extension should now appear in your Chrome toolbar.
## Usage
1. **Locate the Mem0 Icon**: After installation, find the Mem0 icon in your Chrome toolbar.
2. **Sign In**: Click the icon and sign in with your Google account.
3. **Interact with AI Assistants**:
- **ChatGPT and Perplexity**: Continue your conversations as usual; Mem0 operates seamlessly in the background.
- **Claude**: Click the Mem0 button or use the shortcut `Ctrl + M` to activate memory functions.
## Configuration
- **API Key**: Obtain your API key from the Mem0 Dashboard to connect the extension to the Mem0 API.
- **User ID**: This is your unique identifier in the Mem0 system. If not provided, it defaults to `chrome-extension-user`.
## Demo Video
<iframe width="700" height="400" src="https://www.youtube.com/embed/dqenCMMlfwQ?si=zhGVrkq6IS_0Jwyj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
## Privacy and Data Security
Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.
---
<CardGroup cols={2}>
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
Learn the foundations of memory-powered assistants that work across platforms.
</Card>
<Card title="Multimodal Support" icon="image" href="/platform/features/multimodal-support">
Extend your browser interactions with vision and audio memory.
</Card>
</CardGroup>
@@ -55,7 +55,7 @@ GEMINI_API_KEY=your-gemini-api-key-here
``` ```
<Note> <Note>
Ensure you have your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai) and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys). Ensure you have your Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Dashboard</a> and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys).
</Note> </Note>
## Gemini Memory Agent ## Gemini Memory Agent
@@ -41,7 +41,7 @@ Set up your environment variables:
- `MEM0_API_KEY`: Your Mem0 Platform API key - `MEM0_API_KEY`: Your Mem0 Platform API key
- `OPENAI_API_KEY`: Your OpenAI API key - `OPENAI_API_KEY`: Your OpenAI API key
You can obtain your Mem0 Platform API key from the [Mem0 Platform](https://app.mem0.ai). You can obtain your Mem0 Platform API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.
## Complete Implementation ## Complete Implementation
@@ -357,7 +357,7 @@ Based on our previous session, I remember we covered Vision Language Models and
## Help & Resources ## Help & Resources
- [LlamaIndex Agent Workflows](https://docs.llamaindex.ai/en/stable/use_cases/agents/) - [LlamaIndex Agent Workflows](https://docs.llamaindex.ai/en/stable/use_cases/agents/)
- [Mem0 Platform](https://app.mem0.ai/) - <a href="https://app.mem0.ai/" rel="nofollow">Mem0 Platform</a>
--- ---
@@ -25,7 +25,7 @@ os.environ["OPENAI_API_KEY"] = "<your-openai-api-key>"
llm = OpenAI(model="gpt-4.1-nano-2025-04-14") llm = OpenAI(model="gpt-4.1-nano-2025-04-14")
``` ```
Initialize the Mem0 client. You can find your API key [here](https://app.mem0.ai/dashboard/api-keys). Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview). Initialize the Mem0 client. You can find your API key <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">here</a>. Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview).
```python ```python
os.environ["MEM0_API_KEY"] = "<your-mem0-api-key>" os.environ["MEM0_API_KEY"] = "<your-mem0-api-key>"
@@ -754,7 +754,7 @@ Exact output varies as Mem0 automatically extracts and deduplicates entities. Th
- [MiroFish GitHub](https://github.com/666ghj/MiroFish) — Source code and setup guide - [MiroFish GitHub](https://github.com/666ghj/MiroFish) — Source code and setup guide
- [MiroFish Documentation](https://deepwiki.com/666ghj/MiroFish) — Full framework docs - [MiroFish Documentation](https://deepwiki.com/666ghj/MiroFish) — Full framework docs
- [Mem0 Graph Memory](/open-source/features/graph-memory) — Graph Memory documentation - [Mem0 Graph Memory](/open-source/features/graph-memory) — Graph Memory documentation
- [Mem0 Documentation](https://docs.mem0.ai/) — Full API reference - [Mem0 Documentation](https://docs.mem0.ai/introduction) — Full API reference
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Graph Memory" icon="network-wired" href="/open-source/features/graph-memory"> <Card title="Graph Memory" icon="network-wired" href="/open-source/features/graph-memory">
@@ -222,8 +222,8 @@ context = Mem0Context(user_id="user123")
## Resources ## Resources
- [Mem0 Documentation](https://docs.mem0.ai) - [Mem0 Documentation](https://docs.mem0.ai/introduction)
- [Mem0 Dashboard](https://app.mem0.ai/dashboard) - <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
- [API Reference](https://docs.mem0.ai/api-reference) - [API Reference](https://docs.mem0.ai/api-reference)
--- ---
+1 -1
View File
@@ -137,7 +137,7 @@ With Mem0 and AWS services like Bedrock, OpenSearch, and Neptune Analytics, you
<Card title="Neptune Analytics with Mem0" icon="database" href="/cookbooks/integrations/neptune-analytics"> <Card title="Neptune Analytics with Mem0" icon="database" href="/cookbooks/integrations/neptune-analytics">
Explore graph-based memory storage with AWS Neptune Analytics. Explore graph-based memory storage with AWS Neptune Analytics.
</Card> </Card>
<Card title="Graph Memory Features" icon="sitemap" href="/platform/features/graph-memory"> <Card title="Graph Memory Features" icon="sitemap" href="/open-source/features/graph-memory">
Learn how to leverage knowledge graphs for entity relationships. Learn how to leverage knowledge graphs for entity relationships.
</Card> </Card>
</CardGroup> </CardGroup>
@@ -23,7 +23,7 @@ MEM0_API_KEY=your_mem0_api_key
OPENAI_API_KEY=your_openai_api_key OPENAI_API_KEY=your_openai_api_key
``` ```
Get your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys). Get your Mem0 API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
### Configuration ### Configuration
@@ -308,8 +308,8 @@ run().catch(console.error);
## Resources ## Resources
- [Mem0 Documentation](https://docs.mem0.ai) - [Mem0 Documentation](https://docs.mem0.ai/introduction)
- [Mem0 Dashboard](https://app.mem0.ai/dashboard) - <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
- [API Reference](https://docs.mem0.ai/api-reference) - [API Reference](https://docs.mem0.ai/api-reference)
- [OpenAI Documentation](https://platform.openai.com/docs) - [OpenAI Documentation](https://platform.openai.com/docs)
+1 -4
View File
@@ -201,10 +201,7 @@ Here are some examples of how Mem0 can be integrated into various applications:
> >
Persistent personality for Eliza agents. Persistent personality for Eliza agents.
</Card> </Card>
<Card title="Browser Extension Memory" icon="globe" href="/cookbooks/frameworks/chrome-extension"> </CardGroup>
Universal memory layer for Chrome.
</Card>
</CardGroup>
--- ---
+1 -3
View File
@@ -21,7 +21,7 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
- **Messages** – The ordered list of user/assistant turns you send to `add`. - **Messages** – The ordered list of user/assistant turns you send to `add`.
- **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages. - **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
- **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later. - **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
- **User / Session identifiers** – `user_id`, `session_id`, or `run_id` that scope the memory for future searches. - **User / Session identifiers** – `user_id`, `agent_id`, or `run_id` that scope the memory for future searches.
## How does it work? ## How does it work?
@@ -85,7 +85,6 @@ const messages = [
await client.add(messages, { await client.add(messages, {
user_id: "alice", user_id: "alice",
version: "v2",
}); });
``` ```
</CodeGroup> </CodeGroup>
@@ -173,7 +172,6 @@ For full list of supported fields, required formats, and advanced options, see t
| Capability | Mem0 Platform | Mem0 OSS | | Capability | Mem0 Platform | Mem0 OSS |
| --- | --- | --- | | --- | --- | --- |
| Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage | | Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage |
| Graph writes | Toggle per request (`enable_graph=True`) | Requires configuring a graph provider |
| Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs | | Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs |
| Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI | | Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI |
@@ -120,7 +120,7 @@ import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: "your-api-key" }); const client = new MemoryClient({ apiKey: "your-api-key" });
client.deleteAll({ user_id: "alice" }) client.deleteAll({ userId: "alice" })
.then(result => console.log(result)) .then(result => console.log(result))
.catch(error => console.error(error)); .catch(error => console.error(error));
``` ```
@@ -162,12 +162,12 @@ import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: "your-api-key" }); const client = new MemoryClient({ apiKey: "your-api-key" });
// Delete all memories across every user in the project // Delete all memories across every user in the project
client.deleteAll({ user_id: "*" }) client.deleteAll({ userId: "*" })
.then(result => console.log(result)) .then(result => console.log(result))
.catch(error => console.error(error)); .catch(error => console.error(error));
// Full project wipe — all four filters must be explicitly set to "*" // Full project wipe — all four filters must be explicitly set to "*"
client.deleteAll({ user_id: "*", agent_id: "*", app_id: "*", run_id: "*" }) client.deleteAll({ userId: "*", agentId: "*", appId: "*", runId: "*" })
.then(result => console.log(result)) .then(result => console.log(result))
.catch(error => console.error(error)); .catch(error => console.error(error));
``` ```
+4 -4
View File
@@ -52,7 +52,7 @@ Mem0 maps these classic categories onto its layered storage so you can decide wh
Mem0 stores each layer separately and merges them when you query: Mem0 stores each layer separately and merges them when you query:
1. **Capture** – Messages enter the conversation layer while the turn is active. 1. **Capture** – Messages enter the conversation layer while the turn is active.
2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `session_id`, and metadata. 2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `run_id`, and metadata.
3. **Retrieve** – The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history. 3. **Retrieve** – The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
```python ```python
@@ -66,19 +66,19 @@ memory = Memory(api_key=os.environ["MEM0_API_KEY"])
memory.add( memory.add(
["I'm Alex and I prefer boutique hotels."], ["I'm Alex and I prefer boutique hotels."],
user_id="alex", user_id="alex",
session_id="trip-planning-2025", run_id="trip-planning-2025",
) )
# Later in the session, pull long-term + session context # Later in the session, pull long-term + session context
results = memory.search( results = memory.search(
"Any hotel preferences?", "Any hotel preferences?",
user_id="alex", user_id="alex",
session_id="trip-planning-2025", run_id="trip-planning-2025",
) )
``` ```
<Tip> <Tip>
Use `session_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization. Use `run_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
</Tip> </Tip>
## When should you use each layer? ## When should you use each layer?
+80 -16
View File
@@ -12,7 +12,7 @@
"logo": { "logo": {
"light": "/logo/light.svg", "light": "/logo/light.svg",
"dark": "/logo/dark.svg", "dark": "/logo/dark.svg",
"href": "https://app.mem0.ai/" "href": "https://mem0.ai"
}, },
"navigation": { "navigation": {
"anchors": [ "anchors": [
@@ -70,7 +70,6 @@
"platform/features/v2-memory-filters", "platform/features/v2-memory-filters",
"platform/features/entity-scoped-memory", "platform/features/entity-scoped-memory",
"platform/features/async-client", "platform/features/async-client",
"platform/features/async-mode-default-change",
"platform/features/multimodal-support", "platform/features/multimodal-support",
"platform/features/custom-categories" "platform/features/custom-categories"
] ]
@@ -79,7 +78,6 @@
"group": "Advanced Features", "group": "Advanced Features",
"icon": "bolt", "icon": "bolt",
"pages": [ "pages": [
"platform/features/graph-memory",
"platform/features/graph-threshold", "platform/features/graph-threshold",
"platform/features/advanced-retrieval", "platform/features/advanced-retrieval",
"platform/advanced-memory-operations", "platform/advanced-memory-operations",
@@ -94,8 +92,7 @@
"pages": [ "pages": [
"platform/features/direct-import", "platform/features/direct-import",
"platform/features/memory-export", "platform/features/memory-export",
"platform/features/timestamp", "platform/features/timestamp"
"platform/features/expiration-date"
] ]
}, },
{ {
@@ -122,8 +119,6 @@
"icon": "arrow-right", "icon": "arrow-right",
"pages": [ "pages": [
"migration/oss-to-platform", "migration/oss-to-platform",
"migration/v0-to-v1",
"migration/breaking-changes",
"migration/api-changes" "migration/api-changes"
] ]
}, },
@@ -136,6 +131,19 @@
} }
] ]
}, },
{
"tab": "OpenClaw",
"groups": [
{
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw",
"integrations/hermes"
]
}
]
},
{ {
"tab": "Open Source", "tab": "Open Source",
"groups": [ "groups": [
@@ -159,7 +167,7 @@
"open-source/features/reranker-search", "open-source/features/reranker-search",
"open-source/features/async-memory", "open-source/features/async-memory",
"open-source/features/multimodal-support", "open-source/features/multimodal-support",
"open-source/features/custom-fact-extraction-prompt", "open-source/features/custom-instructions",
"open-source/features/custom-update-memory-prompt", "open-source/features/custom-update-memory-prompt",
"open-source/features/rest-api", "open-source/features/rest-api",
"open-source/features/openai_compatibility" "open-source/features/openai_compatibility"
@@ -365,7 +373,6 @@
"cookbooks/frameworks/llamaindex-multiagent", "cookbooks/frameworks/llamaindex-multiagent",
"cookbooks/frameworks/multimodal-retrieval", "cookbooks/frameworks/multimodal-retrieval",
"cookbooks/frameworks/eliza-os-character", "cookbooks/frameworks/eliza-os-character",
"cookbooks/frameworks/chrome-extension",
"cookbooks/frameworks/gemini-3-with-mem0-mcp", "cookbooks/frameworks/gemini-3-with-mem0-mcp",
"cookbooks/frameworks/mirofish-swarm-memory" "cookbooks/frameworks/mirofish-swarm-memory"
] ]
@@ -393,11 +400,11 @@
"integrations/autogen", "integrations/autogen",
"integrations/agno", "integrations/agno",
"integrations/camel-ai", "integrations/camel-ai",
"integrations/openclaw",
"integrations/openai-agents-sdk", "integrations/openai-agents-sdk",
"integrations/google-ai-adk", "integrations/google-ai-adk",
"integrations/mastra", "integrations/mastra",
"integrations/vercel-ai-sdk" "integrations/vercel-ai-sdk",
"integrations/chatdev"
] ]
}, },
{ {
@@ -430,6 +437,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", "tab": "API Reference",
"groups": [ "groups": [
@@ -522,10 +551,13 @@
"tab": "Release Notes", "tab": "Release Notes",
"groups": [ "groups": [
{ {
"group": "Changelog", "group": "Release Notes",
"icon": "rocket", "icon": "rocket",
"pages": [ "pages": [
"changelog" "changelog/highlights",
"changelog/sdk",
"changelog/platform",
"changelog/openclaw"
] ]
} }
] ]
@@ -579,6 +611,34 @@
] ]
}, },
"redirects": [ "redirects": [
{
"source": "/migration/breaking-changes",
"destination": "/"
},
{
"source": "/migration/v0-to-v1",
"destination": "/"
},
{
"source": "/platform/features/expiration-date",
"destination": "/"
},
{
"source": "/platform/features/async-mode-default-change",
"destination": "/"
},
{
"source": "/open-source/features/custom-fact-extraction-prompt",
"destination": "/open-source/features/custom-instructions"
},
{
"source": "/platform/features/graph-memory",
"destination": "/open-source/features/graph-memory"
},
{
"source": "/changelog",
"destination": "/changelog/highlights"
},
{ {
"source": "/api-reference/memory/v2-search-memories", "source": "/api-reference/memory/v2-search-memories",
"destination": "/api-reference/memory/search-memories" "destination": "/api-reference/memory/search-memories"
@@ -737,7 +797,11 @@
}, },
{ {
"source": "/examples/chrome-extension", "source": "/examples/chrome-extension",
"destination": "/cookbooks/frameworks/chrome-extension" "destination": "/cookbooks/overview"
},
{
"source": "/cookbooks/frameworks/chrome-extension",
"destination": "/cookbooks/overview"
}, },
{ {
"source": "/examples", "source": "/examples",
@@ -777,7 +841,7 @@
}, },
{ {
"source": "/v0x/examples/chrome-extension", "source": "/v0x/examples/chrome-extension",
"destination": "/cookbooks/frameworks/chrome-extension" "destination": "/cookbooks/overview"
}, },
{ {
"source": "/v0x/examples/youtube-assistant", "source": "/v0x/examples/youtube-assistant",
@@ -929,7 +993,7 @@
}, },
{ {
"source": "/features/graph-memory", "source": "/features/graph-memory",
"destination": "/platform/features/graph-memory" "destination": "/open-source/features/graph-memory"
}, },
{ {
"source": "/features/:slug", "source": "/features/:slug",
+35
View File
@@ -381,4 +381,39 @@ Here are the available integrations for Mem0:
> >
Build AI agents with persistent memory using Mastra's framework and tools. Build AI agents with persistent memory using Mastra's framework and tools.
</Card> </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>
<Card
title="ChatDev"
icon="comments"
href="/integrations/chatdev"
>
Add persistent cloud-managed memory to ChatDev multi-agent workflows with zero-code YAML configuration.
</Card>
</CardGroup> </CardGroup>
+1 -1
View File
@@ -24,7 +24,7 @@ pip install mem0ai agentops python-dotenv
2. Valid API keys: 2. Valid API keys:
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys) - [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
- OpenAI API Key (for LLM operations) - OpenAI API Key (for LLM operations)
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) (optional, for cloud operations) - <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a> (optional, for cloud operations)
## Basic Integration Example ## Basic Integration Example
+1 -1
View File
@@ -23,7 +23,7 @@ pip install agno mem0ai python-dotenv
``` ```
2. Valid API keys: 2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) - <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
- OpenAI API Key (for the agent model) - OpenAI API Key (for the agent model)
## Quick Integration (Using `Mem0Tools`) ## Quick Integration (Using `Mem0Tools`)
+1 -1
View File
@@ -19,7 +19,7 @@ pip install autogen mem0ai openai python-dotenv
First, we'll import the necessary libraries and set up our configurations. First, we'll import the necessary libraries and set up our configurations.
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note> <Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```python ```python
import os import os
+243
View File
@@ -0,0 +1,243 @@
---
title: ChatDev
description: "Add persistent, cloud-managed memory to ChatDev multi-agent workflows with Mem0 — no code required, just YAML configuration."
---
Build multi-agent workflows in [ChatDev](https://github.com/OpenBMB/ChatDev) with persistent memory powered by Mem0. ChatDev is a zero-code multi-agent platform where agents, tools, and workflows are defined entirely in YAML. Mem0 integrates as a built-in memory store (`type: mem0`), giving your agents cloud-managed semantic search and cross-session persistence — all without writing any code.
## Overview
In this guide, you'll:
1. Set up ChatDev with the Mem0 memory store
2. Configure agents with persistent memory using YAML
3. Enable automatic memory retrieval and storage across conversations
4. Leverage cross-session persistence for personalized multi-agent interactions
## Prerequisites
- **Python 3.12+**
- **[uv](https://docs.astral.sh/uv/)** — Python package manager
- **Node.js 18+** and **npm** — only needed if using the web console
- A **Mem0 API key** from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>
- An **OpenAI API key** (or another LLM provider supported by ChatDev)
## Setup and Configuration
Install ChatDev and its dependencies (includes `mem0ai`):
```bash
git clone https://github.com/OpenBMB/ChatDev.git
cd ChatDev
uv sync
```
If you plan to use the web console, also install the frontend:
```bash
cd frontend && npm install && cd ..
```
Set up your environment variables in a `.env` file:
<Note>Get your Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
```bash
MEM0_API_KEY=your-mem0-api-key
API_KEY=your-openai-api-key
BASE_URL=https://api.openai.com/v1
```
## Configure Mem0 Memory Store
In your ChatDev workflow YAML, add a Mem0 memory store in the `memory` section:
```yaml
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: my-user-123 # optional: scope memories to a user
agent_id: my-agent # optional: scope memories to an agent
```
Mem0 handles all storage, embeddings, and search server-side — no local vector databases or embedding models are needed.
## Attach Memory to an Agent
Reference the memory store in your agent node's `memories` list:
```yaml
nodes:
- id: writer
type: agent
config:
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions.
memories:
- name: mem0_store
top_k: 5
similarity_threshold: 0.5 # minimum relevance score (0.0–1.0); set to -1.0 to disable
retrieve_stage:
- gen
read: true
write: true
```
- **`read: true`** — Agent retrieves relevant memories before generating a response
- **`write: true`** — Agent stores new memories from user input after each interaction
- **`top_k`** — Number of memories to retrieve per query
- **`similarity_threshold`** — Minimum relevance score for retrieved memories. Set to `-1.0` to return all results regardless of score
- **`retrieve_stage`** — When to retrieve memories. Options: `pre_gen_thinking` (before generation), `gen` (during generation), `post_gen_thinking` (after generation), `finished` (after completion)
## Full Example Workflow
Here's a complete workflow YAML that creates a memory-backed conversational agent:
```yaml
version: 0.4.0
graph:
description: Memory-backed conversation using Mem0
nodes:
- id: writer
type: agent
config:
base_url: ${BASE_URL}
api_key: ${API_KEY}
provider: openai
name: gpt-5.4
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions. If memory sections are provided
(wrapped by ===== Related Memories =====), incorporate
relevant context from those memories into your response.
params:
temperature: 0.7
max_tokens: 2000
memories:
- name: mem0_store
top_k: 5
retrieve_stage:
- gen
read: true
write: true
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: project-user-123
agent_id: writer-agent
start:
- writer
end: []
```
Run the workflow:
```bash
# Option 1: CLI (recommended for quick testing)
uv run python run.py --path yaml_instance/demo_mem0_memory.yaml --name my_project
# Option 2: Web console
make dev
# Backend starts at http://localhost:6400, frontend at http://localhost:5173
```
To use the web console, open `http://localhost:5173`, create a new workflow, and paste your YAML configuration into the editor. The web console provides a visual chat interface for interacting with your memory-backed agents.
## How It Works
When an agent with Mem0 memory receives input, the following cycle runs automatically:
**1. Retrieve** — Before generating a response, ChatDev queries Mem0 with the user's input using semantic search. Relevant memories are injected into the agent's context in this format:
```
===== Related Memories =====
--- mem0_store ---
1. User's favorite language is Rust
2. User lives in San Francisco
===== End of Memory =====
```
This is why the role prompt in the example references `===== Related Memories =====` — the agent needs to know how to use this injected context.
**2. Generate** — The agent produces a response using the retrieved memories as additional context.
**3. Store** — After generation, the user's input is sent to Mem0 via `client.add()`. Mem0's extraction model automatically identifies and stores facts, preferences, and key information. Only user input is stored — agent output is excluded to keep memories clean.
Memories persist in Mem0's cloud across all sessions. The next time the same `user_id` or `agent_id` is used, previous memories are automatically retrieved.
## Dual-Scope Memory (User + Agent)
When both `user_id` and `agent_id` are configured, Mem0 uses an OR filter to search across both scopes in a single query:
```yaml
memory:
- name: shared_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: alice # stores user preferences ("Alice prefers dark mode")
agent_id: support-bot # stores agent-learned context ("Resolved Alice's billing issue")
```
This means retrieval returns memories from **both** the user's scope and the agent's scope. Writes include both IDs, so each memory is accessible from either dimension. Use this when you want an agent to remember both what the user told it *and* what the agent learned across sessions.
## Configuration Reference
### Memory Store Config
| Field | Required | Description |
|-------|----------|-------------|
| `api_key` | Yes | Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> |
| `user_id` | No | Scope memories to a specific user |
| `agent_id` | No | Scope memories to a specific agent |
### Memory Attachment Config
| Field | Default | Description |
|-------|---------|-------------|
| `top_k` | `3` | Number of memories to retrieve |
| `similarity_threshold` | `-1.0` (disabled) | Minimum relevance score. Set a value between `0.0` and `1.0` to filter low-relevance results. Default (`-1.0`) returns all matches without filtering |
| `retrieve_stage` | `["gen"]` | When to retrieve: `pre_gen_thinking`, `gen`, `post_gen_thinking`, or `finished` |
| `read` | `true` | Whether the agent retrieves memories |
| `write` | `true` | Whether the agent stores new memories |
## Tips and Common Pitfalls
<Info>
**Indexing delay** — Freshly stored memories may take a few seconds to become searchable. If a memory isn't retrieved immediately after being stored, wait a moment and try again.
</Info>
- **No memories returned on first run** — This is expected. Memories are stored *after* the agent responds, so the first interaction has no prior context. Memories appear starting from the second interaction onward.
- **`mem0ai` not installed** — If you see `ImportError: mem0ai is required for Mem0Memory`, run `uv add mem0ai` or `pip install mem0ai` to add the dependency.
- **Invalid API key** — A wrong or expired `MEM0_API_KEY` will log errors like `Mem0 search failed` or `Mem0 add failed` but won't crash the agent. Check your key at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.
- **Pipeline headers in memories** — ChatDev automatically strips internal pipeline headers (e.g., `=== INPUT FROM TASK (user) ===`) before sending text to Mem0, so your memories stay clean.
- **Clearing test memories** — To delete memories created during testing, use the Mem0 dashboard at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> or the Python SDK: `MemoryClient().delete_all(user_id="your-test-user")`.
## Key Features
1. **Zero-Code Integration** — Configure Mem0 entirely through YAML, no Python code required
2. **Cloud-Managed Storage** — Mem0 handles embeddings, persistence, and search server-side
3. **Semantic Search** — Retrieve contextually relevant memories, not just keyword matches
4. **Cross-Session Persistence** — Memories survive across runs, sessions, and restarts
5. **Multi-Agent Memory Sharing** — Multiple agents can share memories through common `user_id` or `agent_id` scopes
6. **Intelligent Input Processing** — Only user input is stored; agent output is excluded to prevent noisy memories
## Conclusion
By adding Mem0 as a memory store in ChatDev, your multi-agent workflows gain persistent, intelligent memory with zero code changes. Agents automatically remember past interactions and use that context to provide personalized, coherent responses across sessions.
<CardGroup cols={2}>
<Card title="CrewAI Integration" icon="users" href="/integrations/crewai">
Build multi-agent systems with CrewAI and Mem0
</Card>
<Card title="AutoGen Integration" icon="robot" href="/integrations/autogen">
Build conversational agents with AutoGen and Mem0
</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:
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (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:
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (starts with `m0-`)
2. OpenAI Codex access
3. Your API key exported in your shell:
```bash
export MEM0_API_KEY="m0-your-api-key"
```
## Installation
### Option A — Repo Marketplace (Recommended for Teams)
Add a `.agents/plugins/marketplace.json` to your repository root:
```json
{
"name": "mem0-plugins",
"interface": {
"displayName": "Mem0 Plugins"
},
"plugins": [
{
"name": "mem0",
"source": {
"source": "local",
"path": "./plugins/mem0"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
```
Then in Codex, browse the repo's plugin directory and install Mem0.
### Option B — Personal Marketplace
Add to `~/.agents/plugins/marketplace.json`:
```json
{
"name": "mem0-plugins",
"interface": {
"displayName": "Mem0 Plugins"
},
"plugins": [
{
"name": "mem0",
"source": {
"source": "local",
"path": "/path/to/mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
```
### Option C — Manual MCP Configuration
Add to your Codex MCP config:
```json
{
"mcpServers": {
"mem0": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${MEM0_API_KEY}"
}
}
}
}
```
<Info icon="check">
Start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
</Info>
## What's Included
| Component | Plugin Install | MCP Only |
|-----------|:--------------:|:--------:|
| MCP Server (9 memory tools) | Yes | Yes |
| Memory Protocol Skill | Yes | No |
| Mem0 SDK Skill | Yes | No |
## Available MCP Tools
Once installed, the following tools are available in every Codex session:
| Tool | Description |
|------|-------------|
| `add_memory` | Save text or conversation history for a user/agent |
| `search_memories` | Semantic search across memories with filters |
| `get_memories` | List memories with filters and pagination |
| `get_memory` | Retrieve a specific memory by ID |
| `update_memory` | Overwrite a memory's text by ID |
| `delete_memory` | Delete a single memory by ID |
| `delete_all_memories` | Bulk delete all memories in scope |
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
## Memory Protocol Skill
Codex uses a skill-based approach instead of lifecycle hooks. When installed via the plugin marketplace, the memory protocol skill instructs the agent to:
### On Every New Task
1. Call `search_memories` with a query related to the current task to load relevant context
2. Review returned memories to understand what was learned in prior sessions
3. Optionally call `get_memories` to browse all stored memories
### After Completing Significant Work
Store key learnings using `add_memory` with structured metadata:
| What to store | Metadata type |
|--------------|---------------|
| Architectural decisions | `{"type": "decision"}` |
| Strategies that worked | `{"type": "task_learning"}` |
| Failed approaches | `{"type": "anti_pattern"}` |
| User preferences observed | `{"type": "user_preference"}` |
| Environment discoveries | `{"type": "environmental"}` |
| Conventions established | `{"type": "convention"}` |
### Before Losing Context
Store a comprehensive session summary including goals, accomplishments, decisions, files modified, and current state with metadata `{"type": "session_state"}`.
## Plugin Manifest
The Codex plugin manifest (`.codex-plugin/plugin.json`) follows the Codex plugin specification:
```json
{
"name": "mem0",
"version": "0.1.0",
"description": "Mem0 memory layer for AI applications.",
"skills": "./skills/",
"mcpServers": "./.codex-mcp.json",
"interface": {
"displayName": "Mem0",
"shortDescription": "Persistent memory layer for AI coding workflows",
"category": "Productivity",
"capabilities": ["Read", "Write"]
}
}
```
## Example Workflow
```text
# Task 1: Setting up a new service
You: Create a REST API for the notifications service using Express and TypeScript.
# Codex searches memories, finds user preferences from prior tasks.
# After completing the task, Mem0 stores:
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
# - Convention: "All API routes follow /api/v1/{resource} pattern"
# - Preference: "User prefers explicit error types over generic catch-all"
# Task 2 (days later): Extending the service
You: Add WebSocket support for real-time notification delivery.
# Codex searches memories, retrieves the architecture decisions and conventions.
# Follows the same patterns established in the first task.
```
## Troubleshooting
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
- **No tools appearing** — Restart your Codex session after plugin installation
- **Plugin not found** — Ensure `.agents/plugins/marketplace.json` is at the repository root and `source.path` points to the correct plugin directory
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
Detailed MCP configuration for all clients
</Card>
<Card title="Claude Code Integration" icon={<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/></svg>} href="/integrations/claude-code">
Add Mem0 memory to Claude Code workflows
</Card>
</CardGroup>
+1 -1
View File
@@ -22,7 +22,7 @@ pip install crewai crewai-tools mem0ai
Import required modules and set up configurations: Import required modules and set up configurations:
<Note>Remember to get your API keys from [Mem0 Platform](https://app.mem0.ai), [OpenAI](https://platform.openai.com) and [Serper Dev](https://serper.dev) for search capabilities.</Note> <Note>Remember to get your API keys from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>, [OpenAI](https://platform.openai.com) and [Serper Dev](https://serper.dev) for search capabilities.</Note>
```python ```python
import os import os
+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:
- <a href="https://app.mem0.ai" rel="nofollow">Sign up at app.mem0.ai</a>
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Get your API key</a> (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>
+3 -3
View File
@@ -38,7 +38,7 @@ npx flowise start
### 2. Obtain Your Mem0 API Key ### 2. Obtain Your Mem0 API Key
1. Navigate to the [Mem0 API Key dashboard](https://app.mem0.ai/dashboard/api-keys). 1. Navigate to the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key dashboard</a>.
2. Generate or copy your existing Mem0 API Key. 2. Generate or copy your existing Mem0 API Key.
![Mem0 API Key](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/api-key.png) ![Mem0 API Key](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/api-key.png)
@@ -70,7 +70,7 @@ Test your memory configuration:
1. Save your Flowise configuration 1. Save your Flowise configuration
2. Run a test chat and store some information 2. Run a test chat and store some information
3. Verify the stored memories in the [Mem0 Dashboard](https://app.mem0.ai/dashboard/requests) 3. Verify the stored memories in the <a href="https://app.mem0.ai/dashboard/requests" rel="nofollow">Mem0 Dashboard</a>
![Flowise Test Chat](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/flowise-chat-1.png) ![Flowise Test Chat](https://raw.githubusercontent.com/FlowiseAI/FlowiseDocs/main/en/.gitbook/assets/mem0/flowise-chat-1.png)
@@ -103,7 +103,7 @@ Available settings include:
### Platform Configuration ### Platform Configuration
Additional settings available in [Mem0 Project Settings](https://app.mem0.ai/dashboard/project-settings): Additional settings available in <a href="https://app.mem0.ai/dashboard/project-settings" rel="nofollow">Mem0 Project Settings</a>:
1. **Custom Instructions**: Define memory extraction rules 1. **Custom Instructions**: Define memory extraction rules
2. **Expiration Date**: Set automatic memory cleanup periods 2. **Expiration Date**: Set automatic memory cleanup periods
+2 -2
View File
@@ -22,7 +22,7 @@ pip install google-adk mem0ai python-dotenv
``` ```
2. Valid API keys: 2. Valid API keys:
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) - <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
- Google AI Studio API Key - Google AI Studio API Key
## Basic Integration Example ## Basic Integration Example
@@ -268,7 +268,7 @@ memories = mem0.search(
{"categories": {"contains": "travel"}} {"categories": {"contains": "travel"}}
] ]
}, },
limit=5 top_k=5
) )
# Configure agent with custom model settings # Configure agent with custom model settings
+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 <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.</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>

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