Compare commits

..
Author SHA1 Message Date
kartik-mem0 e0fb194c76 feat: add one-click Docker setup for OpenClaw + Mem0 2026-04-01 10:55:50 +05:30
Noah StappandGitHub 215d8b5a71 fix: only list authorized collections when listing MongoDB collections (#3888) 2026-03-31 23:09:36 +05:30
Genaro SanchezandGitHub 82525dbf0f docs: update Twitter references to X (formerly Twitter) (#4432) 2026-03-31 22:44:04 +05:30
32c1ccba5b ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:30:31 +05:30
Saket AryanandGitHub 9bebcf45f7 fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636) 2026-03-31 20:28:48 +05:30
Rakhee SinghandGitHub 93bd4e248c fix(deepseek): forward response_format to OpenAI-compatible API (#4635) 2026-03-31 18:06:53 +05:30
af19495f66 feat(cli): add email verification code login to mem0 init (#4623)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-03-31 16:57:02 +05:30
dbac83218f fix(vllm): forward response_format to OpenAI-compatible API (#4608)
Co-authored-by: rasingh5 <rasingh@demandbase.com>
2026-03-31 13:25:45 +05:30
PrathameshandGitHub 3618aeff22 Link fix (#4631) 2026-03-30 21:36:00 -07:00
PrathameshandGitHub 25e25aaa2f redesign docs introduction page with compact 3x2 grid layout (#4598) 2026-03-30 16:14:02 -07:00
HUANG XIAOandGitHub 5d30af9560 feat(bedrock): add MiniMax provider support for AWS Bedrock (#4609) 2026-03-30 20:40:24 +05:30
47969aaa6b fix(ts): extract JSON from chatty LLM responses in fact retrieval (#4533)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-03-30 20:33:11 +05:30
7213e1b1a6 fix: reset graph database in Memory.reset() (#4185)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-03-30 18:45:38 +05:30
Br1anandGitHub 97291aa119 fix: make AsyncMemory.from_config a regular classmethod (#4183) 2026-03-30 16:19:37 +05:30
KartikandGitHub b8a5ca1b70 chore: bump mem0ai and mem0-ts versions to 1.0.9/2.4.4 and update changelog (#4585) 2026-03-28 22:42:21 +05:30
431cba20e9 fix(ts): work around Qdrant Cloud "Illegal host" error (#4565)
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-03-28 21:50:36 +05:30
KartikandGitHub 4482228383 chore: fix failing CI lint errors (#4584) 2026-03-28 21:45:47 +05:30
HimanshuandGitHub ae49edec44 fix(memory): centralize entity cleanup and skip malformed LLM relation dicts (#4515) 2026-03-28 21:23:05 +05:30
KartikandGitHub 4b7f51d194 docs: add installation and configuration guide for email automation with Mem0 open source (#4567) 2026-03-28 21:12:31 +05:30
KartikandGitHub 41abb571a2 feat: adding oss version of companion cookbook (#4564) 2026-03-28 21:11:59 +05:30
Anchi LiandGitHub e280665578 fix: remove README.md from wheel shared-data (#4052) 2026-03-28 20:57:51 +05:30
Br1anandGitHub 27a6e7863e fix: rebuild FAISS index on vector deletion (#4178) 2026-03-28 20:56:39 +05:30
KartikandGitHub ae8e03c6b7 docs: add content-writing cookbook to operations (#4566) 2026-03-28 20:52:54 +05:30
Dan SiwiecandGitHub 376be3b6d4 [docs] fix python quickstart code snippet #3770 (#3771) 2026-03-28 20:40:10 +05:30
KartikandGitHub cd2dd7cc54 refactor: update default Gemini and Vertex AI embedder model to gemini-embedding-001 (#4571) 2026-03-28 20:39:27 +05:30
Agam PandeyandGitHub ece654811a fix(docs): add Token prefix to Events API code examples (#3927) 2026-03-28 20:38:42 +05:30
Saket AryanandGitHub 13d42a99e9 docs: improve CLI dev workflow and prioritize Node.js installation (#4579) 2026-03-27 17:50:03 -07:00
Saket AryanandGitHub 3225e30859 feat: add official mem0 CLI (Python & TypeScript) (#4575) 2026-03-28 05:03:01 +05:30
KartikandGitHub 88fd0e77d0 docs: add navigation links for platform features in docs (#4572) 2026-03-28 01:43:44 +05:30
KartikandGitHub c2698f2b71 fix: preserve original actor_id during memory update (#4570) 2026-03-28 00:02:58 +05:30
e87240d4d9 fix(vector_stores): handle vector=None in Milvus and Qdrant update methods (#4568)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:50:54 +05:30
Gopal BagaswarandGitHub 68cf4e118d feat: add reasoning_effort parameter support for reasoning models (#4461) 2026-03-27 17:54:44 +05:30
12624555b4 fix(memory): set updated_at on creation and preserve pre-existing created_at (#4499)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:59:44 +05:30
e8004b93db chore: updating the sdk version and changelog (#4561)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 22:21:31 +05:30
Gabriel SteinandGitHub 4e414e4015 fix(plugin): make Cursor plugin fully functional (#4547) 2026-03-26 09:46:42 -07:00
mintlify[bot]andGitHub 16455789d4 Fix short SEO description in integration guide template (#4551) 2026-03-26 09:39:54 -07:00
3ac4e047de fix: merge multiple filter operators for same key (#4559)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:28:37 +05:30
a2ffca3266 fix: prevent SQL injection in Databricks vector store (#4558)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:22:34 +05:30
Felippe MercurioandGitHub 3a9fcdbec2 fix(oss): make pgvector pg import compatible with ESM (#4544) 2026-03-26 20:07:39 +05:30
515f87b6bd docs: fix OSS REST API endpoint discrepancies (#4555)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:05:23 +05:30
5edc0cc99f fix: update parameter added to the update function which is exposed t… (#3799)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:02:33 +05:30
7fff26f374 docs: fix LLM reranker config examples and field names (#4539)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:58:05 +05:30
VictorECDSAandGitHub 1aecfadf45 fix: add timestamps for DELETE operations in history (#4492) 2026-03-26 18:52:10 +05:30
7e06aeeada fix(openclaw): improve credential detection in extraction instructions (#4552)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:28:18 +05:30
2a59c9fd99 fix: handle chatty LLM responses in JSON parsing (#4525)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:39:17 +05:30
669ed184e4 fix(openclaw): prevent extraction of standalone timestamps as memories (#4550)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:19:50 +05:30
Gabriel SteinandGitHub 3c2683c1b5 feat: add Mem0 plugin for Claude Code and Cursor (#4518) 2026-03-25 14:45:59 -07:00
HimanshuandGitHub f06e2d744d Fix/OpenAI embedding dimensions 4153 (#4481) 2026-03-25 19:59:28 +05:30
lamost423andGitHub f9e30304d7 fix: sanitize hyphens in Neo4j Cypher relationship names (#4154) 2026-03-25 19:58:27 +05:30
7e3b727528 Fix: prevent double embedding in mem0.add (fixes #3723) (#3996)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:14:48 +05:30
13c7f84eec MCP: add Streamable HTTP transport endpoint (#4122)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 16:36:37 +05:30
924ac00c52 feat: expose infer param in MCP add_memories tool (#4517)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 15:53:50 +05:30
2a36960f4c fix: prevent in-place mutation of metadata in _create_memory (#4529)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:56:30 +05:30
HimanshuandGitHub 2e0f91e70d fix(bedrock): omit topP for Anthropic Converse; use AWSBedrockConfig in LlmFactory (#4469) 2026-03-25 11:22:52 +05:30
Saket AryanandGitHub d1b4b304c7 chore: replace local MCP and Smithery with cloud MCP server (#4532) 2026-03-25 04:58:00 +05:30
Saket AryanandGitHub 2868bfe749 docs: remove OpenMemory references from docs, README, and issue templates (#4520) 2026-03-25 04:24:13 +05:30
KartikandGitHub 5431badfd4 fix: preserve custom metadata when updating memory (#4495) 2026-03-24 10:58:21 +05:30
195 changed files with 24775 additions and 1376 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"name": "mem0-plugins",
"owner": {
"name": "Mem0",
"email": "support@mem0.ai"
},
"metadata": {
"description": "Official Mem0 plugins for Claude"
},
"plugins": [
{
"name": "mem0",
"source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.1.0"
}
]
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "mem0-plugins",
"owner": {
"name": "Mem0",
"email": "support@mem0.ai"
},
"metadata": {
"description": "Official Mem0 plugins for Cursor"
},
"plugins": [
{
"name": "mem0",
"source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.1.0"
}
]
}
-1
View File
@@ -14,7 +14,6 @@ body:
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenMemory (MCP Server)
- OpenClaw
- REST API
- Other
+1 -1
View File
@@ -1,4 +1,4 @@
blank_issues_enabled: false
blank_issues_enabled: true
contact_links:
- name: Discord Community
url: https://discord.gg/6PzXDgEjG5
@@ -14,7 +14,6 @@ body:
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenMemory (MCP Server)
- OpenClaw
- REST API
- Benchmarks / Evals
-2
View File
@@ -12,8 +12,6 @@ component:
matcher: "Graph Memory"
- label: "ollama"
matcher: "Ollama"
- label: "openmemory"
matcher: "OpenMemory"
- label: "openclaw"
matcher: "OpenClaw"
- label: "rest-api"
+100
View File
@@ -0,0 +1,100 @@
name: CLI Node CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: cli/node
run: pnpm run lint
- name: Type check
working-directory: cli/node
run: pnpm run typecheck
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Run tests
working-directory: cli/node
run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Build
working-directory: cli/node
run: pnpm run build
- name: Verify dist output
run: |
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
+79
View File
@@ -0,0 +1,79 @@
name: CLI Python CI
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
pull_request:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: cli/python
run: ruff check .
- name: Check formatting
working-directory: cli/python
run: ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: cli/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: cli/python
run: hatch build --clean
- name: Verify dist output
run: |
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+16 -4
View File
@@ -15,8 +15,6 @@
<a href="https://mem0.dev/DiG">Join Discord</a>
·
<a href="https://mem0.dev/demo">Demo</a>
·
<a href="https://mem0.dev/openmemory">OpenMemory</a>
</p>
<p align="center">
@@ -95,6 +93,20 @@ Install sdk via npm:
npm install mem0ai
```
### CLI
Manage memories from your terminal:
```bash
npm install -g @mem0/cli # or: pip install mem0-cli
mem0 init
mem0 add "Prefers dark mode and vim keybindings" --user-id alice
mem0 search "What does Alice prefer?" --user-id alice
```
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
### 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).
@@ -150,7 +162,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
## 📚 Documentation & Support
- Full docs: https://docs.mem0.ai
- Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
- Contact: founders@mem0.ai
## Citation
@@ -168,4 +180,4 @@ We now have a paper you can cite:
## ⚖️ License
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
# mem0 CLI
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.
## Installation
```bash
npm install -g @mem0/cli
```
```bash
pip install mem0-cli
```
Both packages install a `mem0` binary with identical behavior.
## Quick start
```bash
# Interactive setup wizard
mem0 init
# Or login via email (get a new API key)
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories for a user
mem0 list --user-id alice
# Update a memory
mem0 update <memory-id> "I switched to light mode"
# Delete a memory
mem0 delete <memory-id>
```
## Commands
| Command | Description |
|---------|-------------|
| `mem0 init` | Setup wizard — login via email or configure API key manually |
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
| `mem0 search` | Search memories using natural language |
| `mem0 list` | List memories with optional filters and pagination |
| `mem0 get` | Retrieve a specific memory by ID |
| `mem0 update` | Update the text or metadata of a memory |
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
| `mem0 import` | Bulk import memories from a JSON file |
| `mem0 config` | View or modify CLI configuration |
| `mem0 entities` | List or delete entities (users, agents, apps) |
| `mem0 status` | Verify API connection and display current project |
| `mem0 version` | Print the CLI version |
Run `mem0 <command> --help` for detailed usage on any command.
## 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` or agent consumption |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
```bash
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
```
## 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`) |
## Implementations
| Language | Directory | Package | Docs |
|----------|-----------|---------|------|
| TypeScript | [`node/`](./node/) | `@mem0/cli` | [README](./node/README.md) |
| Python | [`python/`](./python/) | `mem0-cli` | [README](./python/README.md) |
## Documentation
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
## License
Apache-2.0
+548
View File
@@ -0,0 +1,548 @@
{
"specVersion": 1,
"cli": {
"name": "mem0",
"version": "0.1.0",
"description": "The Memory Layer for AI Agents",
"helpText": "\u25c6 mem0 CLI \u2014 The Memory Layer for AI Agents"
},
"branding": {
"logoMini": "\u25c6 mem0",
"tagline": "The Memory Layer for AI Agents",
"colors": {
"brand": "#8b5cf6",
"accent": "#a78bfa",
"success": "#22c55e",
"error": "#ef4444",
"warning": "#f59e0b",
"dim": "#6b7280"
},
"icons": {
"success": "\u2713",
"error": "\u2717",
"warning": "\u26a0",
"info": "\u25c6",
"pending": "\u29d7",
"add": "+",
"update": "~",
"delete": "-",
"noop": "\u00b7",
"connected": "\u25cf",
"disconnected": "\u25cf"
},
"logo": "███\u2557 ███\u2557███████\u2557███\u2557 ███\u2557 ██████\u2557 ██████\u2557██\u2557 ██\u2557\n████\u2557 ████\u2551██\u2554\u2550\u2550\u2550\u2550\u255d████\u2557 ████\u2551██\u2554\u2550████\u2557 ██\u2554\u2550\u2550\u2550\u2550\u255d██\u2551 ██\u2551\n██\u2554████\u2554██\u2551█████\u2557 ██\u2554████\u2554██\u2551██\u2551██\u2554██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551\u255a██\u2554\u255d██\u2551██\u2554\u2550\u2550\u255d ██\u2551\u255a██\u2554\u255d██\u2551████\u2554\u255d██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551 \u255a\u2550\u255d ██\u2551███████\u2557██\u2551 \u255a\u2550\u255d ██\u2551\u255a██████\u2554\u255d \u255a██████\u2557███████\u2557██\u2551\n\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d"
},
"config": {
"configDir": "~/.mem0",
"configFile": "config.json",
"version": 1,
"defaultBaseUrl": "https://api.mem0.ai",
"sections": {
"platform": {
"fields": {
"api_key": {
"type": "string",
"default": "",
"envVar": "MEM0_API_KEY",
"redact": true
},
"base_url": {
"type": "string",
"default": "https://api.mem0.ai",
"envVar": "MEM0_BASE_URL"
}
}
},
"defaults": {
"fields": {
"user_id": {
"type": "string",
"default": "",
"envVar": "MEM0_USER_ID"
},
"agent_id": {
"type": "string",
"default": "",
"envVar": "MEM0_AGENT_ID"
},
"app_id": {
"type": "string",
"default": "",
"envVar": "MEM0_APP_ID"
},
"run_id": {
"type": "string",
"default": "",
"envVar": "MEM0_RUN_ID"
},
"enable_graph": {
"type": "boolean",
"default": false,
"envVar": "MEM0_ENABLE_GRAPH"
}
}
}
}
},
"api": {
"endpoints": {
"add": { "method": "POST", "path": "/v1/memories/" },
"search": { "method": "POST", "path": "/v2/memories/search/" },
"get": { "method": "GET", "path": "/v1/memories/{memory_id}/" },
"list": { "method": "POST", "path": "/v2/memories/" },
"update": { "method": "PUT", "path": "/v1/memories/{memory_id}/" },
"delete": { "method": "DELETE", "path": "/v1/memories/{memory_id}/" },
"deleteAll": { "method": "DELETE", "path": "/v1/memories/" },
"entities": { "method": "GET", "path": "/v1/entities/" },
"deleteEntities": { "method": "DELETE", "path": "/v1/entities/" }
},
"authHeader": "Authorization",
"authScheme": "Token",
"timeout": 30,
"entityTypeMap": {
"users": "user",
"agents": "agent",
"apps": "app",
"runs": "run"
}
},
"errors": {
"AuthError": {
"httpStatus": 401,
"message": "Authentication failed. Your API key may be invalid or expired."
},
"NotFoundError": {
"httpStatus": 404,
"messageTemplate": "Resource not found: {path}"
},
"APIError": {
"httpStatus": 400,
"messageTemplate": "Bad request to {path}: {detail}"
},
"noApiKey": {
"message": "No API key configured.",
"hint": "Run 'mem0 init' or set MEM0_API_KEY environment variable."
}
},
"optionGroups": {
"scope": {
"label": "Scope",
"options": ["user_id", "agent_id", "app_id", "run_id"]
},
"search": {
"label": "Search",
"options": ["top_k", "threshold", "rerank", "keyword", "filter_json", "fields", "graph", "no_graph"]
},
"pagination": {
"label": "Pagination",
"options": ["page", "page_size"]
},
"filters": {
"label": "Filters",
"options": ["category", "after", "before", "graph", "no_graph"]
},
"output": {
"label": "Output",
"options": ["output"]
},
"connection": {
"label": "Connection",
"options": ["api_key", "base_url"]
}
},
"globalOptions": [
{
"name": "api_key",
"flags": ["--api-key"],
"type": "string",
"required": false,
"envVar": "MEM0_API_KEY",
"help": "Override API key.",
"panel": "Connection"
},
{
"name": "base_url",
"flags": ["--base-url"],
"type": "string",
"required": false,
"help": "Override API base URL.",
"panel": "Connection"
},
{
"name": "version",
"flags": ["--version"],
"type": "boolean",
"required": false,
"help": "Show version and exit."
}
],
"commands": [
{
"name": "add",
"description": "Add a memory from text, messages, file, or stdin.",
"usage": "mem0 add <text> [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": true,
"resolveGraph": true,
"confirmDangerous": false,
"outputFormats": ["text", "json", "quiet"],
"defaultOutput": "text",
"arguments": [
{
"name": "text",
"type": "string",
"required": false,
"help": "Text content to add as a memory."
}
],
"options": [
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
{ "name": "messages", "flags": ["--messages"], "type": "string", "help": "Conversation messages as JSON." },
{ "name": "file", "flags": ["--file", "-f"], "type": "path", "help": "Read messages from JSON file." },
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Custom metadata as JSON." },
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
],
"apiEndpoint": "add"
},
{
"name": "search",
"description": "Search memories by semantic query.",
"usage": "mem0 search <query> [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": true,
"resolveGraph": true,
"confirmDangerous": false,
"outputFormats": ["text", "json", "table"],
"defaultOutput": "text",
"arguments": [
{
"name": "query",
"type": "string",
"required": true,
"help": "Search query."
}
],
"options": [
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
{ "name": "top_k", "flags": ["--top-k", "-k", "--limit"], "type": "integer", "default": 10, "help": "Number of results.", "panel": "Search" },
{ "name": "threshold", "flags": ["--threshold"], "type": "float", "default": 0.3, "help": "Minimum similarity score.", "panel": "Search" },
{ "name": "rerank", "flags": ["--rerank"], "type": "boolean", "default": false, "help": "Enable reranking (Platform only).", "panel": "Search" },
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
],
"apiEndpoint": "search"
},
{
"name": "get",
"description": "Get a specific memory by ID.",
"usage": "mem0 get <memory_id> [OPTIONS]",
"needsBackend": true,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"outputFormats": ["text", "json"],
"defaultOutput": "text",
"arguments": [
{
"name": "memory_id",
"type": "string",
"required": true,
"help": "Memory ID to retrieve."
}
],
"options": [
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json.", "panel": "Output" }
],
"apiEndpoint": "get"
},
{
"name": "list",
"description": "List memories with optional filters.",
"usage": "mem0 list [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": true,
"resolveGraph": true,
"confirmDangerous": false,
"outputFormats": ["text", "json", "table"],
"defaultOutput": "table",
"arguments": [],
"options": [
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
{ "name": "page", "flags": ["--page"], "type": "integer", "default": 1, "help": "Page number.", "panel": "Pagination" },
{ "name": "page_size", "flags": ["--page-size"], "type": "integer", "default": 100, "help": "Results per page.", "panel": "Pagination" },
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
],
"apiEndpoint": "list"
},
{
"name": "update",
"description": "Update a memory's text or metadata.",
"usage": "mem0 update <memory_id> [text] [OPTIONS]",
"needsBackend": true,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"outputFormats": ["text", "json", "quiet"],
"defaultOutput": "text",
"arguments": [
{
"name": "memory_id",
"type": "string",
"required": true,
"help": "Memory ID to update."
},
{
"name": "text",
"type": "string",
"required": false,
"help": "New memory text."
}
],
"options": [
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
],
"apiEndpoint": "update"
},
{
"name": "delete",
"description": "Delete a memory, all memories matching a scope, or an entity.",
"usage": "mem0 delete [memory_id] [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": true,
"resolveGraph": false,
"confirmDangerous": true,
"outputFormats": ["text", "json", "quiet"],
"defaultOutput": "text",
"arguments": [
{
"name": "memory_id",
"type": "string",
"required": false,
"help": "Memory ID to delete (omit when using --all or --entity)."
}
],
"options": [
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
],
"apiEndpoint": "delete",
"notes": "Mutually exclusive modes: (1) mem0 delete <id> -- single memory, (2) mem0 delete --all [scope] -- bulk delete, (3) mem0 delete --entity [scope] -- entity cascade delete. Cannot combine <memoryId> with --all or --entity, and cannot combine --all with --entity."
},
{
"name": "import",
"description": "Import memories from a JSON file.",
"usage": "mem0 import <file_path> [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": true,
"resolveGraph": false,
"confirmDangerous": false,
"outputFormats": ["text"],
"defaultOutput": "text",
"arguments": [
{
"name": "file_path",
"type": "string",
"required": true,
"help": "JSON file to import."
}
],
"options": [
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Override user ID.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Override agent ID.", "panel": "Scope" }
],
"apiEndpoint": "add"
},
{
"name": "config",
"description": "Manage mem0 configuration.",
"isGroup": true,
"subcommands": [
{
"name": "show",
"description": "Display current configuration (secrets redacted).",
"usage": "mem0 config show",
"needsBackend": false,
"needsConfig": false,
"arguments": [],
"options": []
},
{
"name": "get",
"description": "Get a configuration value.",
"usage": "mem0 config get <key>",
"needsBackend": false,
"needsConfig": false,
"arguments": [
{
"name": "key",
"type": "string",
"required": true,
"help": "Config key (e.g. platform.api_key)."
}
],
"options": []
},
{
"name": "set",
"description": "Set a configuration value.",
"usage": "mem0 config set <key> <value>",
"needsBackend": false,
"needsConfig": false,
"arguments": [
{
"name": "key",
"type": "string",
"required": true,
"help": "Config key (e.g. platform.api_key)."
},
{
"name": "value",
"type": "string",
"required": true,
"help": "Value to set."
}
],
"options": []
}
]
},
{
"name": "entity",
"description": "Manage entities.",
"isGroup": true,
"subcommands": [
{
"name": "list",
"description": "List all entities of a given type.",
"usage": "mem0 entity list <entity_type>",
"needsBackend": true,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"outputFormats": ["table", "json"],
"defaultOutput": "table",
"arguments": [
{
"name": "entity_type",
"type": "string",
"required": true,
"help": "Entity type: users, agents, apps, runs.",
"choices": ["users", "agents", "apps", "runs"]
}
],
"options": [
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: table, json.", "panel": "Output" }
],
"apiEndpoint": "entities"
},
{
"name": "delete",
"description": "Delete an entity and ALL its memories (cascade).",
"usage": "mem0 entity delete [OPTIONS]",
"needsBackend": true,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": true,
"outputFormats": ["text", "json", "quiet"],
"defaultOutput": "text",
"arguments": [],
"options": [
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "User ID.", "panel": "Scope" },
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Agent ID.", "panel": "Scope" },
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "App ID.", "panel": "Scope" },
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Run ID.", "panel": "Scope" },
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
],
"apiEndpoint": "deleteEntities"
}
]
},
{
"name": "init",
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
"usage": "mem0 init [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": [
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." }
]
},
{
"name": "status",
"description": "Check connectivity and authentication.",
"usage": "mem0 status [OPTIONS]",
"needsBackend": true,
"needsConfig": true,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": []
},
{
"name": "help",
"description": "Show help. Use --json for machine-readable output (for LLM agents).",
"usage": "mem0 help [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": [
{ "name": "json", "flags": ["--json"], "type": "boolean", "default": false, "help": "Output machine-readable JSON for LLM agents." }
]
}
]
}
+75
View File
@@ -0,0 +1,75 @@
# mem0 CLI (Node.js)
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
## Prerequisites
- Node.js **18+**
- pnpm (`npm install -g pnpm`)
## Installation
```bash
npm install -g @mem0/cli
```
Or from source:
```bash
cd node
pnpm install
pnpm build
pnpm link --global
# Now use it like a normal CLI
mem0 --help
```
## Running during development
```bash
cd node
pnpm install
# Development mode (runs TypeScript directly, no build needed)
pnpm dev --help
pnpm dev add "test memory" --user-id alice
pnpm dev search "test" --user-id alice
# Or build first, then run the compiled JS
pnpm build
node dist/index.js --help
node dist/index.js add "test memory" --user-id alice
```
## Quick Start
```bash
# Set up your configuration
mem0 init
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories
mem0 list --user-id alice
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MEM0_API_KEY` | API key (overrides config file) |
| `MEM0_BASE_URL` | API base URL |
| `MEM0_USER_ID` | Default user ID |
| `MEM0_AGENT_ID` | Default agent ID |
| `MEM0_APP_ID` | Default app ID |
| `MEM0_RUN_ID` | Default run ID |
| `MEM0_ENABLE_GRAPH` | Enable graph memory (true/false) |
## License
Apache-2.0
+91
View File
@@ -0,0 +1,91 @@
# Development
## Prerequisites
- Node.js **18+**
- pnpm (`npm install -g pnpm`)
## Setup
From the `node/` directory:
```bash
pnpm install
```
## Running the CLI
There are two ways to run the CLI during development:
### Option 1: Development mode (no build needed)
Uses `tsx` to run TypeScript directly. Pass CLI arguments after `pnpm dev`:
```bash
pnpm dev --help
pnpm dev version
pnpm dev add "test memory" --user-id alice
pnpm dev search "test" --user-id alice
pnpm dev config show
```
> **Note:** Do NOT use `pnpm dev -- --help`. With pnpm, arguments pass through directly — adding `--` inserts a literal `--` that breaks the CLI parser.
### Option 2: Build and run compiled JS
```bash
# Build first
pnpm build
# Run the compiled CLI
node dist/index.js --help
node dist/index.js version
node dist/index.js add "test memory" --user-id alice
```
### Option 3: Link globally (makes `mem0` available system-wide)
```bash
pnpm build
pnpm link --global
# Now use it like a normal CLI
mem0 --help
mem0 version
```
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
## Build
```bash
pnpm build
```
The compiled output is in `dist/`.
## Run tests
```bash
# Run all tests
pnpm test
# Watch mode
pnpm test:watch
```
## Lint
```bash
# Check
pnpm lint
# Auto-fix
pnpm lint:fix
```
## Type checking
```bash
pnpm typecheck
```
+42
View File
@@ -0,0 +1,42 @@
{
"name": "@mem0/cli",
"version": "0.1.0",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
"mem0": "./dist/index.js"
},
"scripts": {
"build": "tsup",
"dev": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
"lint": "biome check src/",
"lint:fix": "biome check --write src/",
"typecheck": "tsc --noEmit"
},
"engines": {
"node": ">=18.0.0"
},
"license": "Apache-2.0",
"author": "mem0.ai <founders@mem0.ai>",
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
"publishConfig": {
"access": "public"
},
"dependencies": {
"commander": "^12.0.0",
"chalk": "^5.3.0",
"cli-table3": "^0.6.4",
"ora": "^8.0.0",
"boxen": "^7.1.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"tsup": "^8.0.0",
"tsx": "^4.7.0",
"vitest": "^1.5.0",
"@biomejs/biome": "^1.7.0",
"@types/node": "^20.0.0"
}
}
+2066
View File
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
/**
* Abstract backend interface and factory.
*/
import type { Mem0Config } from "../config.js";
import { PlatformBackend } from "./platform.js";
export interface AddOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
metadata?: Record<string, unknown>;
immutable?: boolean;
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
}
export interface SearchOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK?: number;
threshold?: number;
rerank?: boolean;
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
}
export interface ListOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
}
export interface DeleteOptions {
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface EntityIds {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface Backend {
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
search(
query: string,
opts?: SearchOptions,
): Promise<Record<string, unknown>[]>;
get(memoryId: string): Promise<Record<string, unknown>>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>;
delete(
memoryId?: string,
opts?: DeleteOptions,
): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<
Record<string, unknown>
>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
}
export class AuthError extends Error {
constructor(
message = "Authentication failed. Your API key may be invalid or expired.",
) {
super(message);
this.name = "AuthError";
}
}
export class NotFoundError extends Error {
constructor(path: string) {
super(`Resource not found: ${path}`);
this.name = "NotFoundError";
}
}
export class APIError extends Error {
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
}
export function getBackend(config: Mem0Config): Backend {
return new PlatformBackend(config.platform);
}
+14
View File
@@ -0,0 +1,14 @@
/**
* Backend factory re-export.
*/
export { getBackend } from "./base.js";
export type {
Backend,
AddOptions,
SearchOptions,
ListOptions,
DeleteOptions,
EntityIds,
} from "./base.js";
export { AuthError, NotFoundError, APIError } from "./base.js";
+338
View File
@@ -0,0 +1,338 @@
/**
* Platform (SaaS) backend — communicates with api.mem0.ai.
*/
import type { PlatformConfig } from "../config.js";
import {
APIError,
type AddOptions,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
} from "./base.js";
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
const fetchOpts: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const resp = await fetch(url, fetchOpts);
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail =
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
let detail: string = resp.statusText;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.detail ?? body.message ?? resp.statusText) as string;
} catch {
/* ignore */
}
throw new Error(`HTTP ${resp.status}: ${detail}`);
}
if (resp.status === 204) {
return {};
}
return resp.json();
}
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", {
json: payload,
})) as Record<string, unknown>;
}
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (
opts.extraFilters &&
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters;
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
async search(
query: string,
opts: SearchOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
async listMemories(
opts: ListOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
gte: opts.after,
};
}
if (opts.before) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
params,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
return (await this._request("DELETE", "/v1/memories/", {
params,
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request(
"DELETE",
`/v1/memories/${memoryId}/`,
)) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
if (Object.keys(params).length === 0) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
return (await this._request("DELETE", "/v1/entities/", {
params,
})) as Record<string, unknown>;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
if (opts.userId || opts.agentId) {
const payload: Record<string, unknown> = {};
const statusParams: Record<string, string> = {
page: "1",
page_size: "1",
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
});
if (apiFilters) payload.filters = apiFilters;
await this._request("POST", "/v2/memories/", {
json: payload,
params: statusParams,
});
} else {
await this._request("GET", "/v1/entities/");
}
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
}
+149
View File
@@ -0,0 +1,149 @@
/**
* Branding and ASCII art for mem0 CLI.
*/
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { CLI_VERSION } from "./version.js";
export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
`;
export const LOGO_MINI = "◆ mem0";
export const TAGLINE = "The Memory Layer for AI Agents";
export const BRAND_COLOR = "#8b5cf6";
export const ACCENT_COLOR = "#a78bfa";
export const SUCCESS_COLOR = "#22c55e";
export const ERROR_COLOR = "#ef4444";
export const WARNING_COLOR = "#f59e0b";
export const DIM_COLOR = "#6b7280";
const brand = chalk.hex(BRAND_COLOR);
const accent = chalk.hex(ACCENT_COLOR);
const success = chalk.hex(SUCCESS_COLOR);
const error = chalk.hex(ERROR_COLOR);
const warning = chalk.hex(WARNING_COLOR);
const dim = chalk.hex(DIM_COLOR);
/**
* Choose a symbol based on TTY/NO_COLOR. Fancy for interactive terminals,
* plain-text for piped/non-TTY or NO_COLOR environments.
*/
export function sym(fancy: string, plain: string): string {
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
}
export function printBanner(): void {
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
);
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] =
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
}
export function printSuccess(message: string): void {
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
}
export function printError(message: string, hint?: string): void {
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
}
}
export function printWarning(message: string): void {
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
}
export function printInfo(message: string): void {
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
parts.push(`${label}=${val}`);
}
}
if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
}
export interface TimedStatusContext {
successMsg: string;
errorMsg: string;
}
/**
* Run an async function with a spinner, timing the operation.
* Equivalent to Python's timed_status context manager.
*/
export async function timedStatus<T>(
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({
text: dim(message),
color: "magenta",
stream: process.stderr,
}).start();
const start = performance.now();
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
}
/** Format helpers using brand colors for external use. */
export const colors = { brand, accent, success, error, warning, dim };
+93
View File
@@ -0,0 +1,93 @@
/**
* Config management commands: show, set, get.
*/
import Table from "cli-table3";
import { colors, printError, printSuccess } from "../branding.js";
import {
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push([
"defaults.agent_id",
config.defaults.agentId || dim("(not set)"),
]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString());
console.log();
}
export function cmdConfigGet(key: string): void {
const config = loadConfig();
const value = getNestedValue(config, key);
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
}
export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
}
+156
View File
@@ -0,0 +1,156 @@
/**
* Entity management commands.
*/
import readline from "node:readline";
import Table from "cli-table3";
import type { Backend } from "../backend/base.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatJson } from "../output.js";
const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList(
backend: Backend,
entityType: string,
opts: { output: string },
): Promise<void> {
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
}
export async function cmdEntitiesDelete(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError(
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
);
process.exit(1);
}
if (opts.dryRun) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(
`Would delete entity ${scopeParts.join(", ")} and all its memories.`,
);
printInfo("No changes made.");
return;
}
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
}
+383
View File
@@ -0,0 +1,383 @@
/**
* mem0 init — interactive setup wizard.
*/
import readline from "node:readline";
import { PlatformBackend } from "../backend/platform.js";
import {
colors,
printBanner,
printError,
printInfo,
printSuccess,
} from "../branding.js";
import {
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
saveConfig,
} from "../config.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
async function emailLogin(
email: string,
code: string | undefined,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const chars: string[] = [];
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
process.stdin.on("data", onData);
});
}
function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
}
async function setupPlatform(config: Mem0Config): Promise<void> {
console.log();
console.log(
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
);
console.log();
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
}
async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
"mem0-cli",
);
if (userId) config.defaults.userId = userId;
}
async function validatePlatform(config: Mem0Config): Promise<void> {
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
}
export async function runInit(
opts: {
apiKey?: string;
userId?: string;
email?: string;
code?: string;
} = {},
): Promise<void> {
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── API key flow ──────────────────────────────────────────────────────────
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.defaults.userId = opts.userId || "mem0-cli";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
await validatePlatform(config);
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
}
+600
View File
@@ -0,0 +1,600 @@
/**
* Memory CRUD commands: add, search, get, list, update, delete.
*/
import fs from "node:fs";
import type { Backend } from "../backend/base.js";
import {
printError,
printInfo,
printScope,
printSuccess,
timedStatus,
} from "../branding.js";
import {
formatAddResult,
formatJson,
formatJsonEnvelope,
formatMemoriesTable,
formatMemoriesText,
formatSingleMemory,
printResultSummary,
} from "../output.js";
export async function cmdAdd(
backend: Backend,
text: string | undefined,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
messages?: string;
file?: string;
metadata?: string;
immutable: boolean;
noInfer: boolean;
expires?: string;
categories?: string;
enableGraph: boolean;
output: string;
},
): Promise<void> {
let msgs: Record<string, unknown>[] | undefined;
let content = text;
// Read from file
if (opts.file) {
try {
const raw = fs.readFileSync(opts.file, "utf-8");
msgs = JSON.parse(raw);
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
}
// Parse messages JSON
else if (opts.messages) {
try {
msgs = JSON.parse(opts.messages);
} catch (e) {
printError(
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
);
process.exit(1);
}
}
// Read from stdin if piped
else if (!content && !process.stdin.isTTY) {
content = fs.readFileSync(0, "utf-8").trim();
}
if (content !== undefined && content.trim() === "") {
printError("Content cannot be empty.");
process.exit(1);
}
if (!content && !msgs) {
printError(
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
);
process.exit(1);
}
// Validate --expires
if (opts.expires) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
printError(
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
);
process.exit(1);
}
if (new Date(opts.expires) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
}
}
let meta: Record<string, unknown> | undefined;
if (opts.metadata) {
try {
meta = JSON.parse(opts.metadata);
} catch {
printError("Invalid JSON in --metadata.");
process.exit(1);
}
}
let cats: string[] | undefined;
if (opts.categories) {
try {
cats = JSON.parse(opts.categories);
} catch {
cats = opts.categories.split(",").map((c) => c.trim());
}
}
let result: Record<string, unknown>;
try {
result = await timedStatus("Adding memory...", async () => {
return backend.add(content ?? undefined, msgs, {
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
metadata: meta,
immutable: opts.immutable,
infer: !opts.noInfer,
expires: opts.expires,
categories: cats,
enableGraph: opts.enableGraph,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatAddResult(result, opts.output);
return;
}
console.log();
printScope({
user_id: opts.userId,
agent_id: opts.agentId,
app_id: opts.appId,
run_id: opts.runId,
});
const results = Array.isArray(result)
? result
: ((result.results as unknown[]) ?? [result]);
const count = results.length;
printSuccess(
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
);
formatAddResult(result, opts.output);
}
export async function cmdSearch(
backend: Backend,
query: string | undefined,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK: number;
threshold: number;
rerank: boolean;
keyword: boolean;
filterJson?: string;
fields?: string;
enableGraph: boolean;
output: string;
},
): Promise<void> {
if (!query) {
printError("No query provided. Pass a query argument or pipe via stdin.");
process.exit(1);
}
let filters: Record<string, unknown> | undefined;
if (opts.filterJson) {
try {
filters = JSON.parse(opts.filterJson);
} catch {
printError("Invalid JSON in --filter.");
process.exit(1);
}
}
const fieldList = opts.fields
? opts.fields.split(",").map((f) => f.trim())
: undefined;
if (opts.topK < 1) {
printError("--top-k must be >= 1.");
process.exit(1);
}
if (opts.threshold < 0 || opts.threshold > 1) {
printError("--threshold must be between 0.0 and 1.0.");
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus("Searching memories...", async () => {
// biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above
return backend.search(query!, {
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
topK: opts.topK,
threshold: opts.threshold,
rerank: opts.rerank,
keyword: opts.keyword,
filters,
fields: fieldList,
enableGraph: opts.enableGraph,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatJson(results);
} else if (opts.output === "table") {
if (results.length > 0) {
formatMemoriesTable(results, { showScore: true });
printResultSummary({
count: results.length,
durationSecs: elapsed,
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
});
} else {
console.log();
printInfo("No memories found matching your query.");
console.log();
}
} else {
if (results.length > 0) {
formatMemoriesText(results);
printResultSummary({
count: results.length,
durationSecs: elapsed,
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
});
} else {
console.log();
printInfo("No memories found matching your query.");
console.log();
}
}
}
export async function cmdGet(
backend: Backend,
memoryId: string,
opts: { output: string },
): Promise<void> {
let result: Record<string, unknown>;
try {
result = await timedStatus("Fetching memory...", async () => {
return backend.get(memoryId);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
formatSingleMemory(result, opts.output);
}
export async function cmdList(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page: number;
pageSize: number;
category?: string;
after?: string;
before?: string;
enableGraph: boolean;
output: string;
},
): Promise<void> {
if (opts.pageSize < 1) {
printError("--page-size must be >= 1.");
process.exit(1);
}
if (opts.page < 1) {
printError("--page must be >= 1.");
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus("Listing memories...", async () => {
return backend.listMemories({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
page: opts.page,
pageSize: opts.pageSize,
category: opts.category,
after: opts.after,
before: opts.before,
enableGraph: opts.enableGraph,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "quiet") return;
if (opts.output === "json") {
formatJsonEnvelope({
command: "list",
data: results,
count: results.length,
scope: { user_id: opts.userId, agent_id: opts.agentId },
});
} else if (opts.output === "table") {
if (results.length > 0) {
formatMemoriesTable(results);
printResultSummary({
count: results.length,
durationSecs: elapsed,
page: opts.page,
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
});
} else {
console.log();
printInfo("No memories found.");
console.log();
}
} else {
if (results.length > 0) {
formatMemoriesText(results, "memories");
printResultSummary({
count: results.length,
durationSecs: elapsed,
page: opts.page,
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
});
} else {
console.log();
printInfo("No memories found.");
console.log();
}
}
}
export async function cmdUpdate(
backend: Backend,
memoryId: string,
text: string | undefined,
opts: { metadata?: string; output: string },
): Promise<void> {
let meta: Record<string, unknown> | undefined;
if (opts.metadata) {
try {
meta = JSON.parse(opts.metadata);
} catch {
printError("Invalid JSON in --metadata.");
process.exit(1);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Updating memory...", async () => {
return backend.update(memoryId, text, meta);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
);
}
}
export async function cmdDelete(
backend: Backend,
memoryId: string,
opts: { output: string; dryRun?: boolean; force?: boolean },
): Promise<void> {
if (opts.dryRun) {
let mem: Record<string, unknown>;
try {
mem = await backend.get(memoryId);
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const text = (mem.memory ?? mem.text ?? "") as string;
printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`);
printInfo("No changes made.");
return;
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting...", async () => {
return backend.delete(memoryId);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
);
}
}
export async function cmdDeleteAll(
backend: Backend,
opts: {
force: boolean;
dryRun?: boolean;
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
output: string;
},
): Promise<void> {
if (opts.all) {
// Project-wide wipe using wildcard entity IDs
if (opts.dryRun) {
printInfo("Would delete ALL memories project-wide.");
printInfo(
"Run without --dry-run to see the actual count of deleted memories.",
);
printInfo("No changes made.");
return;
}
if (!opts.force) {
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
"\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ",
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus(
"Deleting all memories project-wide...",
async () => {
return backend.delete(undefined, {
all: true,
userId: "*",
agentId: "*",
appId: "*",
runId: "*",
});
},
);
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
if (result.message) {
printInfo(
"Deletion started. Memories will be removed in the background.",
);
} else {
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
}
}
return;
}
if (opts.dryRun) {
let memories: Record<string, unknown>[];
try {
memories = await backend.listMemories({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
printInfo(`Would delete ${memories.length} memories.`);
printInfo("No changes made.");
return;
}
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope =
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
const readline = await import("node:readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting all memories...", async () => {
return backend.delete(undefined, {
all: true,
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
if (result.message) {
printInfo(
"Deletion started. Memories will be removed in the background.",
);
} else {
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
}
}
}
+145
View File
@@ -0,0 +1,145 @@
/**
* Utility commands: status, version, import.
*/
import fs from "node:fs";
import boxen from "boxen";
import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatJsonEnvelope } from "../output.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = {
connected: false,
error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJsonEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let added = 0;
let failed = 0;
const start = performance.now();
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
}
}
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: {
added,
failed,
duration_s: Number.parseFloat(elapsed.toFixed(2)),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
}
+175
View File
@@ -0,0 +1,175 @@
/**
* Configuration management for mem0 CLI.
*
* Config precedence (highest to lowest):
* 1. CLI flags (--api-key, --base-url, etc.)
* 2. Environment variables (MEM0_API_KEY, etc.)
* 3. Config file (~/.mem0/config.json)
* 4. Defaults
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export const CONFIG_DIR = path.join(os.homedir(), ".mem0");
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
}
export interface DefaultsConfig {
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
}
export interface Mem0Config {
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
}
export function createDefaultConfig(): Mem0Config {
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
},
};
}
export function ensureConfigDir(): string {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
}
export function loadConfig(): Mem0Config {
const config = createDefaultConfig();
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
config.version = data.version ?? CONFIG_VERSION;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
// Environment variable overrides
if (process.env.MEM0_API_KEY)
config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL)
config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID)
config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID)
config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
return config;
}
export function saveConfig(config: Mem0Config): void {
ensureConfigDir();
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
}
export function redactKey(key: string): string {
if (!key) return "(not set)";
if (key.length <= 8) return `${key.slice(0, 2)}***`;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}
/** Key map from dotted config path to the config object fields. */
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"],
// Short-form aliases
api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"],
user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"],
run_id: ["defaults", "runId"],
enable_graph: ["defaults", "enableGraph"],
};
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
}
export function setNestedValue(
config: Mem0Config,
dottedKey: string,
value: string,
): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = Number.parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
}
+2
View File
@@ -0,0 +1,2 @@
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string | undefined;
+378
View File
@@ -0,0 +1,378 @@
/**
* Rich-style help formatter for Commander.js that matches the Python CLI's
* Typer + Rich output (rounded box panels, brand purple, grouped options).
*/
import chalk from "chalk";
import type { Argument, Command, Help, Option } from "commander";
// Colors imported from chalk directly to match Typer/Rich defaults
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
// ── Strip ANSI ───────────────────────────────────────────────────────────
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(str: string): number {
return str.replace(ANSI_RE, "").length;
}
// ── Command display order (matches Python CLI) ──────────────────────────
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
},
];
/** Flat order derived from COMMAND_GROUPS. */
const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
const OPTION_PANELS: Record<string, Record<string, string>> = {
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
};
const PANEL_ORDER: string[] = [
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
];
// ── Panel rendering ─────────────────────────────────────────────────────
/**
* Render a Rich-style ROUNDED box panel.
*
* ```
* ╭─ Title ────────────────────────╮
* │ row content padded │
* ╰────────────────────────────────╯
* ```
*/
function renderPanel(title: string, rows: string[], width: number): string {
if (rows.length === 0) return "";
// Inner width is total width minus the two border chars
const inner = width - 2;
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Bottom border: ╰─...─╯
const bottomLine =
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
});
return [topLine, ...contentLines, bottomLine].join("\n");
}
// ── Format an option term (short + long) ────────────────────────────────
function formatOptionTerm(opt: Option): string {
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += ` ${match[0]}`;
}
}
return term;
}
// ── Get the long flag name for panel lookup ─────────────────────────────
function getLongFlag(opt: Option): string {
if (opt.long) return opt.long;
return opt.short || "";
}
// ── Format a default value ──────────────────────────────────────────────
function formatDefault(opt: Option): string {
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
}
// ── The main help formatter ─────────────────────────────────────────────
export function richFormatHelp(cmd: Command, helper: Help): string {
const width = process.stdout.columns || 80;
const lines: string[] = [];
const isRoot = !cmd.parent;
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
}
lines.push("");
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(
...visibleArgs.map((a: Argument) => a.name().length),
);
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap =
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped.Options.push(opt);
}
}
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped.Options.length > 0) {
const optRows = formatOptionRows(grouped.Options);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
lines.push("");
return lines.join("\n");
}
// ── Format option rows with aligned columns ─────────────────────────────
function formatOptionRows(opts: Option[]): string[] {
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
}
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
function sortCommands(cmds: Command[]): Command[] {
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
}
+631
View File
@@ -0,0 +1,631 @@
#!/usr/bin/env node
/**
* Main CLI application — the entrypoint for `mem0`.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import { type Backend, getBackend } from "./backend/index.js";
import { colors, printError } from "./branding.js";
import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { CLI_VERSION } from "./version.js";
const program = new Command();
// ── Helpers ──────────────────────────────────────────────────────────────
function getBackendAndConfig(
apiKey?: string,
baseUrl?: string,
): { backend: Backend; config: Mem0Config } {
const config = loadConfig();
if (apiKey) config.platform.apiKey = apiKey;
if (baseUrl) config.platform.baseUrl = baseUrl;
if (!config.platform.apiKey) {
printError(
"No API key configured.",
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
);
process.exit(1);
}
return { backend: getBackend(config), config };
}
function getBackendOnly(apiKey?: string, baseUrl?: string): Backend {
return getBackendAndConfig(apiKey, baseUrl).backend;
}
/**
* Resolve entity IDs: CLI flag > config default > undefined.
*
* If any explicit ID is provided, only use explicit IDs (don't mix
* in defaults for other entity types which would over-filter).
* If no explicit IDs, fall back to all configured defaults.
*/
function resolveIds(
config: Mem0Config,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
},
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
const hasExplicit = !!(
opts.userId ||
opts.agentId ||
opts.appId ||
opts.runId
);
if (hasExplicit) {
return {
userId: opts.userId || undefined,
agentId: opts.agentId || undefined,
appId: opts.appId || undefined,
runId: opts.runId || undefined,
};
}
return {
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
appId: config.defaults.appId || undefined,
runId: config.defaults.runId || undefined,
};
}
/**
* Resolve graph tri-state: --no-graph > --graph > config default.
*/
function resolveGraph(
config: Mem0Config,
opts: { graph?: boolean; noGraph?: boolean },
): boolean {
if (opts.noGraph) return false;
if (opts.graph) return true;
return config.defaults.enableGraph;
}
// ── Main program ──────────────────────────────────────────────────────────
program
.name("mem0")
.description(
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
)
.option("--version", "Show version and exit.")
.on("option:version", () => {
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
process.exit(0);
})
.usage("<command> [options]")
.helpOption("--help", "Show this message and exit.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
// ── Init ──────────────────────────────────────────────────────────────────
program
.command("init")
.description(
"Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
)
.option("--api-key <key>", "API key (skip prompt).")
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
.option("--email <email>", "Login via email verification code.")
.option(
"--code <code>",
"Verification code (use with --email for non-interactive login).",
)
.addHelpText(
"after",
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
)
.action(async (opts) => {
const { runInit } = await import("./commands/init.js");
await runInit({
apiKey: opts.apiKey,
userId: opts.userId,
email: opts.email,
code: opts.code,
});
});
// ── Memory: add ───────────────────────────────────────────────────────────
program
.command("add [text]")
.description("Add a memory from text, messages, file, or stdin.")
.option("-u, --user-id <id>", "Scope to user.")
.option("--agent-id <id>", "Scope to agent.")
.option("--app-id <id>", "Scope to app.")
.option("--run-id <id>", "Scope to run.")
.option("--messages <json>", "Conversation messages as JSON.")
.option("-f, --file <path>", "Read messages from JSON file.")
.option("-m, --metadata <json>", "Custom metadata as JSON.")
.option("--immutable", "Prevent future updates.", false)
.option("--no-infer", "Skip inference, store raw.")
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
.option("--categories <value>", "Categories (JSON array or comma-separated).")
.option("--graph", "Enable graph memory extraction.", false)
.option("--no-graph", "Disable graph memory extraction.")
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json',
)
.action(async (text, opts) => {
const { cmdAdd } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph });
});
// ── Memory: search ────────────────────────────────────────────────────────
program
.command("search [query]")
.description("Search memories by semantic query.")
.option("-u, --user-id <id>", "Filter by user.")
.option("--agent-id <id>", "Filter by agent.")
.option("--app-id <id>", "Filter by app.")
.option("--run-id <id>", "Filter by run.")
.option(
"-k, --top-k <n>",
"Number of results.",
(v) => Number.parseInt(v),
10,
)
.option(
"--threshold <n>",
"Minimum similarity score.",
(v) => Number.parseFloat(v),
0.3,
)
.option("--rerank", "Enable reranking (Platform only).", false)
.option("--keyword", "Use keyword search.", false)
.option("--filter <json>", "Advanced filter expression (JSON).")
.option("--fields <list>", "Specific fields to return (comma-separated).")
.option("--graph", "Enable graph in search.", false)
.option("--no-graph", "Disable graph in search.")
.option("-o, --output <format>", "Output: text, json, table.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
)
.action(async (query, opts) => {
let resolvedQuery = query;
if (!resolvedQuery && !process.stdin.isTTY) {
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
}
if (!resolvedQuery) {
printError("No query provided. Pass a query argument or pipe via stdin.");
process.exit(1);
}
const { cmdSearch } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
await cmdSearch(backend, resolvedQuery, {
...ids,
topK: opts.topK,
threshold: opts.threshold,
rerank: opts.rerank,
keyword: opts.keyword,
filterJson: opts.filter,
fields: opts.fields,
enableGraph,
output: opts.output,
});
});
// ── Memory: get ───────────────────────────────────────────────────────────
program
.command("get <memoryId>")
.description("Get a specific memory by ID.")
.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 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json",
)
.action(async (memoryId, opts) => {
const { cmdGet } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdGet(backend, memoryId, { output: opts.output });
});
// ── Memory: list ──────────────────────────────────────────────────────────
program
.command("list")
.description("List memories with optional filters.")
.option("-u, --user-id <id>", "Filter by user.")
.option("--agent-id <id>", "Filter by agent.")
.option("--app-id <id>", "Filter by app.")
.option("--run-id <id>", "Filter by run.")
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
.option(
"--page-size <n>",
"Results per page.",
(v) => Number.parseInt(v),
100,
)
.option("--category <name>", "Filter by category.")
.option("--after <date>", "Created after (YYYY-MM-DD).")
.option("--before <date>", "Created before (YYYY-MM-DD).")
.option("--graph", "Enable graph in listing.", false)
.option("--no-graph", "Disable graph in listing.")
.option("-o, --output <format>", "Output: text, json, table.", "table")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
)
.action(async (opts) => {
const { cmdList } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
await cmdList(backend, {
...ids,
page: opts.page,
pageSize: opts.pageSize,
category: opts.category,
after: opts.after,
before: opts.before,
enableGraph,
output: opts.output,
});
});
// ── Memory: update ────────────────────────────────────────────────────────
program
.command("update <memoryId> [text]")
.description("Update a memory's text or metadata.")
.option("-m, --metadata <json>", "Update metadata (JSON).")
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
`\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`,
)
.action(async (memoryId, text, opts) => {
let resolvedText = text;
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
resolvedText = fs.readFileSync(0, "utf-8").trim();
}
const { cmdUpdate } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata,
output: opts.output,
});
});
// ── Memory: delete (consolidated) ─────────────────────────────────────────
program
.command("delete [memoryId]")
.description("Delete a memory, all memories matching a scope, or an entity.")
.option("--all", "Delete all memories matching scope filters.", false)
.option(
"--entity",
"Delete the entity itself and all its memories (cascade).",
false,
)
.option("--project", "With --all: delete ALL memories project-wide.", false)
.option("--dry-run", "Show what would be deleted without deleting.", false)
.option("--force", "Skip confirmation.", false)
.option("-u, --user-id <id>", "Scope to user.")
.option("--agent-id <id>", "Scope to agent.")
.option("--app-id <id>", "Scope to app.")
.option("--run-id <id>", "Scope to run.")
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
[
"\nExamples:",
" $ mem0 delete abc-123-def-456 # single memory",
" $ mem0 delete --all -u alice --force # all memories for user",
" $ mem0 delete --all --project --force # project-wide wipe",
" $ mem0 delete --entity -u alice --force # entity + all its memories",
].join("\n"),
)
.action(async (memoryId, opts) => {
// ── Mutual-exclusion checks ──
if (memoryId && opts.all) {
printError("Cannot combine <memoryId> with --all. Use one or the other.");
process.exit(1);
}
if (memoryId && opts.entity) {
printError(
"Cannot combine <memoryId> with --entity. Use one or the other.",
);
process.exit(1);
}
if (opts.all && opts.entity) {
printError("Cannot combine --all with --entity. Use one or the other.");
process.exit(1);
}
if (!memoryId && !opts.all && !opts.entity) {
printError(
"Specify a memory ID, --all, or --entity.\n" +
" mem0 delete <id> Delete a single memory\n" +
" mem0 delete --all [scope] Delete all memories matching scope\n" +
" mem0 delete --entity [scope] Delete an entity and all its memories",
);
process.exit(1);
}
// ── Dispatch: single memory ──
if (memoryId) {
const { cmdDelete } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdDelete(backend, memoryId, {
output: opts.output,
dryRun: opts.dryRun,
force: opts.force,
});
return;
}
// ── Dispatch: --all ──
if (opts.all) {
const { cmdDeleteAll } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = opts.project
? {
userId: undefined,
agentId: undefined,
appId: undefined,
runId: undefined,
}
: resolveIds(config, opts);
await cmdDeleteAll(backend, {
force: opts.force,
dryRun: opts.dryRun,
all: opts.project,
...ids,
output: opts.output,
});
return;
}
// ── Dispatch: --entity ──
if (opts.entity) {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, opts);
return;
}
});
// ── Config subcommands ────────────────────────────────────────────────────
const configCmd = program
.command("config")
.description("Manage mem0 configuration.")
.addHelpCommand(false);
configCmd
.command("show")
.description("Display current configuration (secrets redacted).")
.option("-o, --output <format>", "Output: text, json.", "text")
.addHelpText(
"after",
"\nExamples:\n $ mem0 config show\n $ mem0 config show -o json",
)
.action(async (opts) => {
const { cmdConfigShow } = await import("./commands/config.js");
cmdConfigShow({ output: opts.output });
});
configCmd
.command("get <key>")
.description("Get a configuration value.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id",
)
.action(async (key) => {
const { cmdConfigGet } = await import("./commands/config.js");
cmdConfigGet(key);
});
configCmd
.command("set <key> <value>")
.description("Set a configuration value.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai",
)
.action(async (key, value) => {
const { cmdConfigSet } = await import("./commands/config.js");
cmdConfigSet(key, value);
});
// ── Entity subcommand group ───────────────────────────────────────────────
const entityCmd = program
.command("entity")
.description("Manage entities.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
entityCmd
.command("list <entityType>")
.description("List all entities of a given type.")
.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 entity list users\n $ mem0 entity list agents -o json",
)
.action(async (entityType, opts) => {
const { cmdEntitiesList } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesList(backend, entityType, { output: opts.output });
});
entityCmd
.command("delete")
.description("Delete an entity and ALL its memories (cascade).")
.option("--dry-run", "Show what would be deleted without deleting.", false)
.option("-u, --user-id <id>", "Scope to user.")
.option("--agent-id <id>", "Scope to agent.")
.option("--app-id <id>", "Scope to app.")
.option("--run-id <id>", "Scope to run.")
.option("--force", "Skip confirmation.", false)
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
.addHelpText(
"after",
"\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run",
)
.action(async (opts) => {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, opts);
});
// ── Utility commands ──────────────────────────────────────────────────────
program
.command("status")
.description("Check connectivity and authentication.")
.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 status\n $ mem0 status -o json")
.action(async (opts) => {
const { cmdStatus } = await import("./commands/utils.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
await cmdStatus(backend, {
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
output: opts.output,
});
});
program
.command("import <filePath>")
.description("Import memories from a JSON file.")
.option("-u, --user-id <id>", "Override user ID.")
.option("--agent-id <id>", "Override agent ID.")
.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 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json",
)
.action(async (filePath, opts) => {
const { cmdImport } = await import("./commands/utils.js");
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const ids = resolveIds(config, opts);
await cmdImport(backend, filePath, {
userId: ids.userId,
agentId: ids.agentId,
output: opts.output,
});
});
// ── Help (machine-readable) ──────────────────────────────────────────────
program
.command("help")
.description(
"Show help. Use --json for machine-readable output (for LLM agents).",
)
.option("--json", "Output machine-readable JSON for LLM agents.", false)
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
.action((opts) => {
if (opts.json) {
// Load spec from parent directory
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
if (fs.existsSync(specPath)) {
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
console.log(JSON.stringify(spec, null, 2));
} else {
console.log(
JSON.stringify(
{
name: "mem0",
version: CLI_VERSION,
description: "The Memory Layer for AI Agents",
},
null,
2,
),
);
}
} else {
const { brand: b } = colors;
console.log(
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
);
console.log("Usage: mem0 <command> [OPTIONS]\n");
console.log("Commands:");
console.log(
" add Add a memory from text, messages, file, or stdin",
);
console.log(" search Search memories by semantic query");
console.log(" get Get a specific memory by ID");
console.log(" list List memories with optional filters");
console.log(" update Update a memory's text or metadata");
console.log(
" delete Delete a memory, all memories, or an entity",
);
console.log(" import Import memories from a JSON file");
console.log(" config Manage configuration (show, get, set)");
console.log(" entity Manage entities (list, delete)");
console.log(" init Interactive setup wizard");
console.log(" status Check connectivity and authentication");
console.log();
console.log(" mem0 <command> --help Get help for a command");
console.log(
" mem0 help --json Machine-readable help (for LLM agents)",
);
console.log();
}
});
// ── Version ───────────────────────────────────────────────────────────────
program
.command("version")
.description("Show version.")
.action(async () => {
const { cmdVersion } = await import("./commands/utils.js");
cmdVersion();
});
// ── Entrypoint ────────────────────────────────────────────────────────────
program.parse();
+260
View File
@@ -0,0 +1,260 @@
/**
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
*/
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js";
const { brand, accent, success, error: errorColor, dim } = colors;
function formatDate(dtStr?: string): string | undefined {
if (!dtStr) return undefined;
try {
const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10);
} catch {
return dtStr?.slice(0, 10);
}
}
export function formatMemoriesText(
memories: Record<string, unknown>[],
title = "memories",
): void {
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
for (let i = 0; i < memories.length; i++) {
const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined;
const cats = mem.categories;
if (Array.isArray(cats)) {
category = cats[0] as string | undefined;
}
console.log(` ${i + 1}. ${memoryText}`);
const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`);
if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`);
}
console.log();
}
}
export function formatMemoriesTable(
memories: Record<string, unknown>[],
opts: { showScore?: boolean } = {},
): void {
const head = opts.showScore
? [
accent("ID"),
accent("Score"),
accent("Memory"),
accent("Category"),
accent("Created"),
]
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
const table = new Table({
head,
colWidths,
wordWrap: true,
style: { head: [], border: [] },
});
for (const mem of memories) {
const memId = (mem.id as string) ?? "";
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = `${memoryText.slice(0, 57)}...`;
}
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? categories.length > 1
? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
if (opts.showScore) {
const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else {
table.push([dim(memId), memoryText, cat, created]);
}
}
console.log();
console.log(table.toString());
console.log();
}
export function formatJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}
export function formatSingleMemory(
mem: Record<string, unknown>,
output = "text",
): void {
if (output === "json") {
formatJson(mem);
return;
}
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories;
if (categories) {
const catStr = Array.isArray(categories)
? categories.join(", ")
: String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Memory"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function formatAddResult(
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
): void {
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
const results: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
console.log();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
const icon = accent(sym("⧗", "..."));
const parts = [
` ${icon} ${dim("Queued".padEnd(10))}`,
"Processing in background",
];
if (eventId) parts.push(dim(`(event ${eventId})`));
console.log(parts.join(" "));
continue;
}
const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
0,
8,
);
let icon: string;
let label: string;
if (event === "ADD") {
icon = success("+");
label = "Added";
} else if (event === "UPDATE") {
icon = accent("~");
label = "Updated";
} else if (event === "DELETE") {
icon = errorColor("-");
label = "Deleted";
} else if (event === "NOOP") {
icon = dim("·");
label = "No change";
} else {
icon = dim("?");
label = event;
}
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" "));
}
console.log();
}
export function formatJsonEnvelope(opts: {
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
status?: string;
error?: string;
}): void {
const envelope: Record<string, unknown> = {
status: opts.status ?? "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope;
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
console.log(JSON.stringify(envelope, null, 2));
}
export function printResultSummary(opts: {
count: number;
durationSecs?: number;
page?: number;
scopeIds?: Record<string, string | undefined>;
}): void {
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined)
parts.push(`${opts.durationSecs.toFixed(2)}s`);
console.log(` ${dim(parts.join(" · "))}`);
console.log();
}
+10
View File
@@ -0,0 +1,10 @@
import { createRequire } from "node:module";
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
// When running via tsx in dev/test mode, fall back to reading package.json.
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
export const CLI_VERSION: string =
typeof __CLI_VERSION__ !== "undefined"
? (__CLI_VERSION__ as string)
: (createRequire(import.meta.url)("../package.json") as { version: string })
.version;
+98
View File
@@ -0,0 +1,98 @@
/**
* Tests for branding utilities.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
BRAND_COLOR,
SUCCESS_COLOR,
ERROR_COLOR,
TAGLINE,
LOGO_MINI,
printSuccess,
printError,
printWarning,
printInfo,
printScope,
} from "../src/branding.js";
let output: string;
let errOutput: string;
const originalLog = console.log;
const originalError = console.error;
beforeEach(() => {
output = "";
errOutput = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
console.error = (...args: unknown[]) => {
errOutput += args.map(String).join(" ") + "\n";
};
});
afterEach(() => {
console.log = originalLog;
console.error = originalError;
});
describe("branding constants", () => {
it("has correct brand color", () => {
expect(BRAND_COLOR).toBe("#8b5cf6");
});
it("has correct tagline", () => {
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
});
it("has correct logo mini", () => {
expect(LOGO_MINI).toBe("◆ mem0");
});
});
describe("printSuccess", () => {
it("prints success message", () => {
printSuccess("Operation completed");
expect(output).toContain("Operation completed");
});
});
describe("printError", () => {
it("prints error message to stderr", () => {
printError("Something failed");
expect(errOutput).toContain("Something failed");
});
it("prints hint when provided to stderr", () => {
printError("Failed", "Try again");
expect(errOutput).toContain("Try again");
});
});
describe("printWarning", () => {
it("prints warning message to stderr", () => {
printWarning("Be careful");
expect(errOutput).toContain("Be careful");
});
});
describe("printInfo", () => {
it("prints info message", () => {
printInfo("Important note");
expect(errOutput).toContain("Important note");
});
});
describe("printScope", () => {
it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" });
expect(errOutput).toContain("alice");
expect(errOutput).toContain("bot");
});
it("prints nothing when no IDs", () => {
printScope({});
expect(errOutput).toBe("");
});
});
+156
View File
@@ -0,0 +1,156 @@
/**
* Integration tests — invoke CLI as subprocess to test end-to-end.
*/
import { describe, it, expect } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function run(
args: string[],
opts: { home?: string; env?: Record<string, string> } = {},
): { stdout: string; stderr: string; exitCode: number } {
const env = { ...process.env };
// Strip MEM0_ env vars
for (const key of Object.keys(env)) {
if (key.startsWith("MEM0_")) delete env[key];
}
if (opts.home) env.HOME = opts.home;
if (opts.env) Object.assign(env, opts.env);
try {
const stdout = execSync(
`npx tsx src/index.ts ${args.join(" ")}`,
{ cwd: path.join(__dirname, ".."), env, encoding: "utf-8", timeout: 15000 },
);
return { stdout, stderr: "", exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: e.status ?? 1,
};
}
}
describe("CLI Integration — help and version", () => {
it("shows help with --help", () => {
const result = run(["--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("mem0");
expect(result.stdout).toContain("add");
expect(result.stdout).toContain("search");
});
it("shows version with --version", () => {
const result = run(["--version"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("0.1.0");
});
it("help --json produces valid JSON", () => {
const result = run(["help", "--json"]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
// spec may have cli.name or top-level name
const name = parsed.name ?? parsed.cli?.name;
expect(name).toBe("mem0");
});
it("shows add help", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("user-id");
expect(result.stdout).toContain("messages");
});
it("shows search help", () => {
const result = run(["search", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("top-k");
});
it("shows list help", () => {
const result = run(["list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("page-size");
});
it("shows delete help with --all, --entity, --project", () => {
const result = run(["delete", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--all");
expect(result.stdout).toContain("--entity");
expect(result.stdout).toContain("--project");
expect(result.stdout).toContain("--force");
expect(result.stdout.toLowerCase()).toContain("memory");
});
it("delete with no args errors", () => {
const result = run(["delete"]);
expect(result.exitCode).not.toBe(0);
const combined = result.stdout + result.stderr;
expect(combined).toContain("--all");
});
it("shows entity list help", () => {
const result = run(["entity", "list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout.toLowerCase()).toContain("entitytype");
});
it("shows entity delete help", () => {
const result = run(["entity", "delete", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--user-id");
expect(result.stdout).toContain("--force");
});
it("shows import help", () => {
const result = run(["import", "--help"]);
expect(result.exitCode).toBe(0);
});
it("add help has --graph flag", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
});
it("search help has --graph flag", () => {
const result = run(["search", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
});
it("list help has --graph flag", () => {
const result = run(["list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
});
});
describe("CLI Integration — isolated (clean home)", () => {
function cleanHome(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
}
it("add without API key errors", () => {
const home = cleanHome();
const result = run(["add", "test", "--user-id", "alice"], { home });
expect(result.exitCode).not.toBe(0);
const combined = result.stdout + result.stderr;
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
fs.rmSync(home, { recursive: true, force: true });
});
it("config show works with clean home", () => {
const home = cleanHome();
const result = run(["config", "show"], { home });
expect(result.exitCode).toBe(0);
fs.rmSync(home, { recursive: true, force: true });
});
});
+221
View File
@@ -0,0 +1,221 @@
/**
* Tests for CLI commands using mock backend.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createMockBackend } from "./setup.js";
import type { Backend } from "../src/backend/base.js";
let mockBackend: Backend;
// Capture console.log and console.error output
let output: string;
let errOutput: string;
const originalLog = console.log;
const originalError = console.error;
beforeEach(() => {
mockBackend = createMockBackend();
output = "";
errOutput = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
console.error = (...args: unknown[]) => {
errOutput += args.map(String).join(" ") + "\n";
};
});
// Restore after each test
import { afterEach } from "vitest";
afterEach(() => {
console.log = originalLog;
console.error = originalError;
});
describe("cmdAdd", () => {
it("adds text memory", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "I prefer dark mode", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
});
it("adds from messages JSON", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, undefined, {
userId: "alice",
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
immutable: false,
noInfer: false,
enableGraph: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
});
it("outputs json format", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "json",
});
expect(output).toContain("results");
});
it("quiet mode produces no memory content", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "quiet",
});
expect(output).not.toContain("dark mode");
});
});
describe("cmdSearch", () => {
it("searches and shows results in text mode", async () => {
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: "text",
});
expect(output).toContain("Found 2");
});
it("outputs json format", async () => {
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: "json",
});
expect(output).toContain("memory");
});
it("shows no results message", async () => {
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "nonexistent", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
enableGraph: false,
output: "text",
});
expect(errOutput).toContain("No memories found");
});
});
describe("cmdGet", () => {
it("gets memory in text mode", async () => {
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
expect(output).toContain("dark mode");
});
it("gets memory in json mode", async () => {
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
expect(output).toContain("memory");
});
});
describe("cmdList", () => {
it("lists in table mode", async () => {
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
enableGraph: false,
output: "table",
});
expect(output).toContain("dark mode");
});
it("shows empty message", async () => {
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
enableGraph: false,
output: "text",
});
expect(errOutput).toContain("No memories found");
});
});
describe("cmdUpdate", () => {
it("updates memory", async () => {
const { cmdUpdate } = await import("../src/commands/memory.js");
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
expect(output.toLowerCase()).toContain("updated");
});
});
describe("cmdDelete", () => {
it("deletes memory", async () => {
const { cmdDelete } = await import("../src/commands/memory.js");
await cmdDelete(mockBackend, "abc-123", { output: "text" });
expect(output.toLowerCase()).toContain("deleted");
});
});
describe("cmdDeleteAll", () => {
it("deletes all with force", async () => {
const { cmdDeleteAll } = await import("../src/commands/memory.js");
await cmdDeleteAll(mockBackend, {
force: true,
userId: "alice",
output: "text",
});
expect(output.toLowerCase()).toContain("deleted");
});
});
describe("cmdVersion", () => {
it("shows version", async () => {
const { cmdVersion } = await import("../src/commands/utils.js");
cmdVersion();
expect(output).toContain("0.1.0");
});
});
describe("cmdEntitiesList", () => {
it("lists users in table mode", async () => {
const { cmdEntitiesList } = await import("../src/commands/entities.js");
await cmdEntitiesList(mockBackend, "users", { output: "table" });
expect(output).toContain("alice");
});
it("lists in json mode", async () => {
const { cmdEntitiesList } = await import("../src/commands/entities.js");
await cmdEntitiesList(mockBackend, "users", { output: "json" });
expect(output).toContain("alice");
});
});
+113
View File
@@ -0,0 +1,113 @@
/**
* Tests for configuration management.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
createDefaultConfig,
loadConfig,
saveConfig,
redactKey,
getNestedValue,
setNestedValue,
CONFIG_DIR,
CONFIG_FILE,
} from "../src/config.js";
// Use a temp directory for config during tests
let origConfigDir: string;
let origConfigFile: string;
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
// Monkey-patch the module-level constants
// We'll use env vars and direct file manipulation instead
// Clear MEM0_ env vars
for (const key of Object.keys(process.env)) {
if (key.startsWith("MEM0_")) {
delete process.env[key];
}
}
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe("redactKey", () => {
it("returns '(not set)' for empty key", () => {
expect(redactKey("")).toBe("(not set)");
});
it("redacts short key", () => {
expect(redactKey("abc")).toBe("ab***");
});
it("redacts normal key", () => {
const result = redactKey("m0-abcdefgh12345678");
expect(result).toBe("m0-a...5678");
expect(result).not.toContain("abcdefgh");
});
it("redacts exactly 8-char key as short", () => {
expect(redactKey("12345678")).toBe("12***");
});
});
describe("createDefaultConfig", () => {
it("has correct defaults", () => {
const config = createDefaultConfig();
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
expect(config.platform.apiKey).toBe("");
expect(config.defaults.userId).toBe("");
expect(config.defaults.enableGraph).toBe(false);
});
});
describe("getNestedValue", () => {
it("gets platform.api_key", () => {
const config = createDefaultConfig();
config.platform.apiKey = "test-key";
expect(getNestedValue(config, "platform.api_key")).toBe("test-key");
});
it("returns undefined for nonexistent key", () => {
const config = createDefaultConfig();
expect(getNestedValue(config, "nonexistent.key")).toBeUndefined();
});
it("gets defaults.user_id", () => {
const config = createDefaultConfig();
config.defaults.userId = "alice";
expect(getNestedValue(config, "defaults.user_id")).toBe("alice");
});
});
describe("setNestedValue", () => {
it("sets platform.api_key", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "platform.api_key", "new-key")).toBe(true);
expect(config.platform.apiKey).toBe("new-key");
});
it("returns false for nonexistent key", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "nonexistent.key", "val")).toBe(false);
});
it("sets defaults.user_id", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "defaults.user_id", "bob")).toBe(true);
expect(config.defaults.userId).toBe("bob");
});
it("coerces boolean for enable_graph", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "defaults.enable_graph", "true")).toBe(true);
expect(config.defaults.enableGraph).toBe(true);
});
});
+115
View File
@@ -0,0 +1,115 @@
/**
* Tests for output formatting.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
formatMemoriesText,
formatMemoriesTable,
formatJson,
formatSingleMemory,
formatAddResult,
printResultSummary,
} from "../src/output.js";
let output: string;
const originalLog = console.log;
beforeEach(() => {
output = "";
console.log = (...args: unknown[]) => {
output += args.map(String).join(" ") + "\n";
};
});
afterEach(() => {
console.log = originalLog;
});
const sampleMemories = [
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
score: 0.92,
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
score: 0.78,
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
];
describe("formatMemoriesText", () => {
it("shows count and memory content", () => {
formatMemoriesText(sampleMemories);
expect(output).toContain("Found 2");
expect(output).toContain("dark mode");
expect(output).toContain("vim keybindings");
});
it("shows scores and IDs", () => {
formatMemoriesText(sampleMemories);
expect(output).toContain("0.92");
expect(output).toContain("abc-123-");
});
});
describe("formatMemoriesTable", () => {
it("renders a table with memory content", () => {
formatMemoriesTable(sampleMemories);
expect(output).toContain("dark mode");
});
});
describe("formatJson", () => {
it("outputs valid JSON", () => {
formatJson({ key: "value" });
expect(JSON.parse(output)).toEqual({ key: "value" });
});
});
describe("formatSingleMemory", () => {
it("shows memory text in text mode", () => {
formatSingleMemory(sampleMemories[0], "text");
expect(output).toContain("dark mode");
});
it("outputs JSON in json mode", () => {
formatSingleMemory(sampleMemories[0], "json");
expect(output).toContain("memory");
});
});
describe("formatAddResult", () => {
it("shows ADD event", () => {
formatAddResult({
results: [{ id: "abc-123", memory: "Test", event: "ADD" }],
});
expect(output).toContain("Added");
});
it("shows PENDING event", () => {
formatAddResult({
results: [{ status: "PENDING", event_id: "evt-12345678" }],
});
expect(output).toContain("Queued");
});
});
describe("printResultSummary", () => {
it("shows count and duration", () => {
printResultSummary({ count: 5, durationSecs: 1.23 });
expect(output).toContain("5 results");
expect(output).toContain("1.23s");
});
it("handles singular", () => {
printResultSummary({ count: 1 });
expect(output).toContain("1 result");
expect(output).not.toContain("results");
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* Shared test helpers and mock factories for mem0 CLI tests.
*/
import { vi } from "vitest";
import type { Backend } from "../src/backend/base.js";
/** Create a mock backend with all methods stubbed with sensible defaults. */
export function createMockBackend(): Backend {
return {
add: vi.fn().mockResolvedValue({
results: [
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
event: "ADD",
},
],
}),
search: vi.fn().mockResolvedValue([
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
score: 0.92,
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
score: 0.78,
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
]),
get: vi.fn().mockResolvedValue({
id: "abc-123-def-456",
memory: "User prefers dark mode",
created_at: "2026-02-15T10:30:00Z",
updated_at: "2026-02-20T08:00:00Z",
metadata: { source: "onboarding" },
categories: ["preferences"],
}),
listMemories: vi.fn().mockResolvedValue([
{
id: "abc-123-def-456",
memory: "User prefers dark mode",
created_at: "2026-02-15T10:30:00Z",
categories: ["preferences"],
},
{
id: "ghi-789-jkl-012",
memory: "User uses vim keybindings",
created_at: "2026-03-01T14:00:00Z",
categories: ["tools"],
},
]),
update: vi.fn().mockResolvedValue({ id: "abc-123-def-456", memory: "Updated memory" }),
delete: vi.fn().mockResolvedValue({ status: "deleted" }),
status: vi.fn().mockResolvedValue({
connected: true,
backend: "platform",
base_url: "https://api.mem0.ai",
}),
deleteEntities: vi.fn().mockResolvedValue({ message: "Entity deleted" }),
entities: vi.fn().mockResolvedValue([
{ name: "alice", count: 5 },
{ name: "bob", count: 3 },
]),
};
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);
const pkg = _require('./package.json');
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
+11
View File
@@ -0,0 +1,11 @@
import { createRequire } from "node:module";
import { defineConfig } from "vitest/config";
const _require = createRequire(import.meta.url);
const pkg = _require("./package.json") as { version: string };
export default defineConfig({
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
+43
View File
@@ -0,0 +1,43 @@
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
.PHONY: install dev lint format test build clean publish publish-test shell
$(VENV)/bin/activate:
python3 -m venv $(VENV)
$(PIP) install -U pip
install: $(VENV)/bin/activate
$(PIP) install -e .
dev: $(VENV)/bin/activate
$(PIP) install -e ".[dev]"
lint: dev
$(VENV)/bin/ruff check .
$(VENV)/bin/ruff format --check .
format: dev
$(VENV)/bin/ruff check --fix .
$(VENV)/bin/ruff format .
test: dev
$(VENV)/bin/pytest
build: clean $(VENV)/bin/activate
$(PIP) install hatch
$(VENV)/bin/hatch build
clean:
rm -rf dist/
publish: build
$(VENV)/bin/hatch publish
publish-test: build
$(VENV)/bin/hatch publish --repo test
shell: $(VENV)/bin/activate
@echo "Spawning a new shell with the virtual environment activated..."
@VIRTUAL_ENV=$(CURDIR)/$(VENV) PATH=$(CURDIR)/$(VENV)/bin:$$PATH exec $(SHELL)
+39
View File
@@ -0,0 +1,39 @@
# mem0 CLI
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents.
## Installation
### Using pipx (recommended)
```bash
pipx install mem0-cli
```
### Using pip
```bash
pip install mem0-cli
```
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
## Quick Start
```bash
# Set up your configuration
mem0 init
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories
mem0 list --user-id alice
```
## License
Apache-2.0
+107
View File
@@ -0,0 +1,107 @@
# Development
## Prerequisites
- Python **3.10+**
- `make` (optional — you can use plain Python commands instead)
All commands below should be run from the `python/` directory:
```bash
cd python
```
## Setup
### Using Make (recommended)
All `make` targets automatically create a virtual environment (`.venv/`) and install the required dependencies — no manual setup needed.
```bash
# Install the CLI in editable mode
make install
# Install with dev tools (tests + linting)
make dev
```
### Using Python directly
```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
# Install in editable mode
pip install -e .
# With dev tools
pip install -e ".[dev]"
```
## Make targets
| Target | Description |
| ------------------- | ------------------------------------------------ |
| `make install` | Create venv and install the CLI (editable mode) |
| `make dev` | Create venv and install CLI + dev dependencies |
| `make test` | Run all tests (installs dev deps if needed) |
| `make lint` | Run linter and format check |
| `make format` | Auto-fix lint issues and format code |
| `make build` | Build distribution packages |
| `make clean` | Remove `dist/` |
| `make publish` | Build and publish to PyPI |
| `make publish-test` | Build and publish to Test PyPI |
| `make shell` | Open a new shell with the venv activated |
## Run tests
```bash
# Using Make
make test
# Using Python directly
pytest
# Run a specific test file
pytest tests/test_cli_integration.py
# Run a single test
pytest -k test_help
```
## Run the CLI
```bash
# Using Make — drop into an activated shell
make shell
mem0 --help
# Using Python directly (with venv activated)
source .venv/bin/activate
mem0 --help
mem0 version
# Or run without activating
.venv/bin/mem0 --help
```
## Lint
```bash
# Using Make
make lint # check only
make format # auto-fix
# Using Python directly (with venv activated)
ruff check .
ruff format .
```
## Optional extras
### OSS integration
```bash
pip install -e ".[oss]"
```
+77
View File
@@ -0,0 +1,77 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.1.0"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "mem0.ai", email = "founders@mem0.ai" },
]
keywords = ["mem0", "memory", "ai", "agents", "cli"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
]
dependencies = [
"typer>=0.9.0",
"rich>=13.0.0",
"httpx>=0.24.0",
]
[project.optional-dependencies]
oss = ["mem0ai>=0.1.0"]
dev = [
"pytest>=7.0",
"pytest-asyncio>=0.21",
"ruff>=0.1.0",
]
[project.scripts]
mem0 = "mem0_cli.app:main"
[tool.hatch.build.targets.wheel]
packages = ["src/mem0_cli"]
[tool.hatch.build.targets.sdist]
include = ["src/mem0_cli"]
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # pyflakes
"I", # isort (import sorting)
"W", # pycodestyle warnings
"UP", # pyupgrade (modern Python syntax)
"B", # flake8-bugbear (common bugs)
"SIM", # flake8-simplify
"RUF", # ruff-specific rules
]
ignore = [
"E501", # line too long — handled by formatter
"B008", # function call in default arg — required by Typer's Option/Argument pattern
"SIM108", # ternary operator — sometimes less readable
]
[tool.ruff.lint.isort]
known-first-party = ["mem0_cli"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
+3
View File
@@ -0,0 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.1.0"
+5
View File
@@ -0,0 +1,5 @@
"""Allow running with `python -m mem0_cli`."""
from mem0_cli.app import main
main()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
"""Backend abstraction layer for mem0 CLI."""
from mem0_cli.backend.base import Backend, get_backend
__all__ = ["Backend", "get_backend"]
+112
View File
@@ -0,0 +1,112 @@
"""Abstract backend interface and factory."""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any
from mem0_cli.config import Mem0Config
class Backend(ABC):
"""Abstract interface for mem0 backends."""
@abstractmethod
def add(
self,
content: str | None = None,
messages: list[dict] | None = None,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
metadata: dict | None = None,
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
enable_graph: bool = False,
) -> dict: ...
@abstractmethod
def search(
self,
query: str,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
top_k: int = 10,
threshold: float = 0.3,
rerank: bool = False,
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
enable_graph: bool = False,
) -> list[dict]: ...
@abstractmethod
def get(self, memory_id: str) -> dict: ...
@abstractmethod
def list_memories(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
page: int = 1,
page_size: int = 100,
category: str | None = None,
after: str | None = None,
before: str | None = None,
enable_graph: bool = False,
) -> list[dict]: ...
@abstractmethod
def update(
self, memory_id: str, content: str | None = None, metadata: dict | None = None
) -> dict: ...
@abstractmethod
def delete(
self,
memory_id: str | None = None,
*,
all: bool = False,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
) -> dict: ...
@abstractmethod
def delete_entities(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
) -> dict: ...
@abstractmethod
def status(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
) -> dict[str, Any]: ...
@abstractmethod
def entities(self, entity_type: str) -> list[dict]: ...
def get_backend(config: Mem0Config) -> Backend:
"""Return the Platform backend."""
from mem0_cli.backend.platform import PlatformBackend
return PlatformBackend(config.platform)
+324
View File
@@ -0,0 +1,324 @@
"""Platform (SaaS) backend — communicates with api.mem0.ai."""
from __future__ import annotations
from typing import Any
import httpx
from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
class PlatformBackend(Backend):
"""Backend that talks to the mem0 Platform API."""
def __init__(self, config: PlatformConfig) -> None:
self.config = config
self.base_url = config.base_url.rstrip("/")
self._client = httpx.Client(
base_url=self.base_url,
headers={
"Authorization": f"Token {config.api_key}",
"Content-Type": "application/json",
},
timeout=30.0,
)
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
resp = self._client.request(method, path, **kwargs)
if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
if resp.status_code == 404:
raise NotFoundError(f"Resource not found: {path}")
if resp.status_code == 400:
# Extract API error detail when available
try:
detail = resp.json().get("detail", resp.text)
except Exception:
detail = resp.text
raise APIError(f"Bad request to {path}: {detail}")
resp.raise_for_status()
if resp.status_code == 204:
return {}
return resp.json()
def add(
self,
content: str | None = None,
messages: list[dict] | None = None,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
metadata: dict | None = None,
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
enable_graph: bool = False,
) -> dict:
payload: dict[str, Any] = {}
if messages:
payload["messages"] = messages
elif content:
payload["messages"] = [{"role": "user", "content": content}]
if user_id:
payload["user_id"] = user_id
if agent_id:
payload["agent_id"] = agent_id
if app_id:
payload["app_id"] = app_id
if run_id:
payload["run_id"] = run_id
if metadata:
payload["metadata"] = metadata
if immutable:
payload["immutable"] = True
if not infer:
payload["infer"] = False
if expires:
payload["expiration_date"] = expires
if categories:
payload["categories"] = categories
if enable_graph:
payload["enable_graph"] = True
return self._request("POST", "/v1/memories/", json=payload)
def _build_filters(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
extra_filters: dict | None = None,
) -> dict | None:
"""Build a filters dict for v2 API endpoints.
Entity IDs are ANDed (all provided IDs must match).
Extra filters (date ranges, categories) are also ANDed.
"""
# If caller passed a pre-built filter structure (e.g. --filter from CLI), use it directly
if extra_filters and ("AND" in extra_filters or "OR" in extra_filters):
return extra_filters
# Build AND conditions for entity IDs
and_conditions: list[dict[str, Any]] = []
if user_id:
and_conditions.append({"user_id": user_id})
if agent_id:
and_conditions.append({"agent_id": agent_id})
if app_id:
and_conditions.append({"app_id": app_id})
if run_id:
and_conditions.append({"run_id": run_id})
# Append any extra filters (dates, categories)
if extra_filters:
for k, v in extra_filters.items():
and_conditions.append({k: v})
if len(and_conditions) == 1:
return and_conditions[0]
elif and_conditions:
return {"AND": and_conditions}
else:
return None
def search(
self,
query: str,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
top_k: int = 10,
threshold: float = 0.3,
rerank: bool = False,
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
enable_graph: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
api_filters = self._build_filters(
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
extra_filters=filters,
)
if api_filters:
payload["filters"] = api_filters
if rerank:
payload["rerank"] = True
if keyword:
payload["keyword_search"] = True
if fields:
payload["fields"] = fields
if enable_graph:
payload["enable_graph"] = True
result = self._request("POST", "/v2/memories/search/", json=payload)
return (
result
if isinstance(result, list)
else result.get("results", result.get("memories", []))
)
def get(self, memory_id: str) -> dict:
return self._request("GET", f"/v1/memories/{memory_id}/")
def list_memories(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
page: int = 1,
page_size: int = 100,
category: str | None = None,
after: str | None = None,
before: str | None = None,
enable_graph: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {}
params = {"page": str(page), "page_size": str(page_size)}
# Build filters for v2 API — entity IDs and date filters go inside "filters"
extra: dict[str, Any] = {}
if category:
extra["categories"] = {"contains": category}
if after:
extra["created_at"] = {**(extra.get("created_at", {})), "gte": after}
if before:
extra["created_at"] = {**(extra.get("created_at", {})), "lte": before}
api_filters = self._build_filters(
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
extra_filters=extra if extra else None,
)
if api_filters:
payload["filters"] = api_filters
if enable_graph:
payload["enable_graph"] = True
result = self._request("POST", "/v2/memories/", json=payload, params=params)
return (
result
if isinstance(result, list)
else result.get("results", result.get("memories", []))
)
def update(
self, memory_id: str, content: str | None = None, metadata: dict | None = None
) -> dict:
payload: dict[str, Any] = {}
if content:
payload["text"] = content
if metadata:
payload["metadata"] = metadata
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
def delete(
self,
memory_id: str | None = None,
*,
all: bool = False,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
) -> dict:
if all:
params: dict[str, str] = {}
if user_id:
params["user_id"] = user_id
if agent_id:
params["agent_id"] = agent_id
if app_id:
params["app_id"] = app_id
if run_id:
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
return self._request("DELETE", f"/v1/memories/{memory_id}/")
else:
raise ValueError("Either memory_id or --all is required")
def delete_entities(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
) -> dict:
params: dict[str, str] = {}
if user_id:
params["user_id"] = user_id
if agent_id:
params["agent_id"] = agent_id
if app_id:
params["app_id"] = app_id
if run_id:
params["run_id"] = run_id
if not params:
raise ValueError("At least one entity ID is required for delete_entities.")
return self._request("DELETE", "/v1/entities/", params=params)
def status(
self,
*,
user_id: str | None = None,
agent_id: str | None = None,
) -> dict[str, Any]:
"""Check connectivity by making a lightweight API call."""
try:
# If entity IDs are available, validate with a minimal memories list
if user_id or agent_id:
payload: dict[str, Any] = {}
params = {"page": "1", "page_size": "1"}
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
if api_filters:
payload["filters"] = api_filters
self._request("POST", "/v2/memories/", json=payload, params=params)
else:
# No entity IDs — use entities endpoint to validate API key
self._request("GET", "/v1/entities/")
return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)}
def entities(self, entity_type: str) -> list[dict]:
result = self._request("GET", "/v1/entities/")
items = result if isinstance(result, list) else result.get("results", [])
# Filter by entity type client-side (API returns all types)
type_map = {"users": "user", "agents": "agent", "apps": "app", "runs": "run"}
target_type = type_map.get(entity_type)
if target_type:
items = [e for e in items if e.get("type", "").lower() == target_type]
return items
class AuthError(Exception):
pass
class NotFoundError(Exception):
pass
class APIError(Exception):
pass
+130
View File
@@ -0,0 +1,130 @@
"""Branding and ASCII art for mem0 CLI."""
import os
import sys
import time
from contextlib import contextmanager
from rich.console import Console
from rich.panel import Panel
from rich.status import Status
from rich.text import Text
# stderr console for spinners, errors, and timing messages
_err = Console(stderr=True)
LOGO = r"""
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
"""
LOGO_MINI = "◆ mem0"
TAGLINE = "The Memory Layer for AI Agents"
BRAND_COLOR = "#8b5cf6" # Purple
ACCENT_COLOR = "#a78bfa"
SUCCESS_COLOR = "#22c55e"
ERROR_COLOR = "#ef4444"
WARNING_COLOR = "#f59e0b"
DIM_COLOR = "#6b7280"
def _sym(fancy: str, plain: str) -> str:
"""Return *fancy* when stdout is a TTY with colour, else *plain*."""
if not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None:
return plain
return fancy
def print_banner(console: Console) -> None:
"""Print the mem0 welcome banner."""
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
content = Text()
content.append_text(logo_text)
content.append_text(tagline)
panel = Panel(
content,
border_style=BRAND_COLOR,
padding=(0, 2),
subtitle=f"[{DIM_COLOR}]Python SDK · v{_get_version()}[/]",
subtitle_align="right",
)
console.print(panel)
def print_success(console: Console, message: str) -> None:
sym = _sym("✓", "[ok]")
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
def print_error(console: Console, message: str, hint: str | None = None) -> None:
sym = _sym("✗", "[error]")
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
if hint:
console.print(f" [{DIM_COLOR}]{hint}[/]")
def print_warning(console: Console, message: str) -> None:
sym = _sym("⚠", "[warn]")
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
def print_info(console: Console, message: str) -> None:
sym = _sym("◆", "*")
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
@contextmanager
def timed_status(console: Console, message: str):
"""Spinner with automatic timing. Yields a context object for setting the final message.
The spinner and timing output are sent to stderr (via ``_err``) so they
never contaminate machine-readable stdout. The *console* parameter is
kept for backward compatibility but is not used for spinner output.
"""
class _Ctx:
def __init__(self):
self.success_msg = ""
self.error_msg = ""
ctx = _Ctx()
start = time.perf_counter()
try:
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
yield ctx
except Exception:
elapsed = time.perf_counter() - start
if ctx.error_msg:
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
raise
else:
elapsed = time.perf_counter() - start
if ctx.success_msg:
print_success(_err, f"{ctx.success_msg} ({elapsed:.2f}s)")
def print_scope(console: Console, **ids: str | None) -> None:
"""Show active entity scope if any IDs are set."""
parts = []
for key, val in ids.items():
if val:
label = key.replace("_", " ").replace("id", "ID").strip()
parts.append(f"{label}={val}")
if parts:
scope_str = ", ".join(parts)
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
def _get_version() -> str:
from mem0_cli import __version__
return __version__
@@ -0,0 +1 @@
"""CLI command modules."""
@@ -0,0 +1,108 @@
"""Config management commands: show, set, get."""
from __future__ import annotations
from rich.console import Console
from rich.table import Table
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, print_error, print_success
from mem0_cli.config import (
get_nested_value,
load_config,
redact_key,
save_config,
set_nested_value,
)
console = Console()
err_console = Console(stderr=True)
def cmd_config_show(*, output: str = "text") -> None:
"""Display current configuration (secrets redacted)."""
from mem0_cli.output import format_json_envelope
config = load_config()
if output == "json":
format_json_envelope(
console,
command="config show",
data={
"defaults": {
"user_id": config.defaults.user_id or None,
"agent_id": config.defaults.agent_id or None,
"app_id": config.defaults.app_id or None,
"run_id": config.defaults.run_id or None,
"enable_graph": config.defaults.enable_graph,
},
"platform": {
"api_key": redact_key(config.platform.api_key),
"base_url": config.platform.base_url,
},
},
)
return
console.print()
console.print(f" [{BRAND_COLOR}]◆ mem0 Configuration[/]\n")
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 2))
table.add_column("Key", style="bold")
table.add_column("Value")
# Defaults
table.add_row(
"defaults.user_id",
config.defaults.user_id or f"[{DIM_COLOR}](not set)[/]",
)
table.add_row(
"defaults.agent_id",
config.defaults.agent_id or f"[{DIM_COLOR}](not set)[/]",
)
table.add_row(
"defaults.app_id",
config.defaults.app_id or f"[{DIM_COLOR}](not set)[/]",
)
table.add_row(
"defaults.run_id",
config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
)
table.add_row(
"defaults.enable_graph",
str(config.defaults.enable_graph).lower(),
)
table.add_row("", "")
# Platform
table.add_row("[bold]platform.api_key[/]", redact_key(config.platform.api_key))
table.add_row("platform.base_url", config.platform.base_url)
console.print(table)
console.print()
def cmd_config_get(key: str) -> None:
"""Get a config value."""
config = load_config()
value = get_nested_value(config, key)
if value is None:
print_error(err_console, f"Unknown config key: {key}")
else:
# Redact secrets
if "api_key" in key or "key" in key.split(".")[-1:]:
console.print(redact_key(str(value)))
else:
console.print(str(value))
def cmd_config_set(key: str, value: str) -> None:
"""Set a config value."""
config = load_config()
if set_nested_value(config, key, value):
save_config(config)
display = redact_key(value) if "key" in key else value
print_success(console, f"{key} = {display}")
else:
print_error(err_console, f"Unknown config key: {key}")
@@ -0,0 +1,137 @@
"""Entity management commands."""
from __future__ import annotations
import time as _time
import typer
from rich.console import Console
from rich.table import Table
from mem0_cli.backend.base import Backend
from mem0_cli.branding import (
ACCENT_COLOR,
BRAND_COLOR,
DIM_COLOR,
print_error,
print_info,
print_success,
timed_status,
)
from mem0_cli.output import format_json
console = Console()
err_console = Console(stderr=True)
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
"""List entities of a given type."""
valid_types = {"users", "agents", "apps", "runs"}
if entity_type not in valid_types:
print_error(
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
)
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, f"Fetching {entity_type}...") as _ts:
try:
results = backend.entities(entity_type)
except Exception as e:
print_error(err_console, str(e), hint="This feature may require the mem0 Platform.")
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, results)
return
if not results:
print_info(console, f"No {entity_type} found.")
return
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 1))
table.add_column("Name / ID", style="bold")
table.add_column("Created", max_width=12)
for entity in results:
name = entity.get("name", entity.get("id", "—"))
created = str(entity.get("created_at", "—"))[:10]
table.add_row(str(name), created)
console.print()
console.print(table)
console.print(f" [{DIM_COLOR}]{len(results)} {entity_type} ({_elapsed:.2f}s)[/]")
console.print()
def cmd_entities_delete(
backend: Backend,
*,
user_id: str | None,
agent_id: str | None,
app_id: str | None,
run_id: str | None,
force: bool,
dry_run: bool = False,
output: str,
) -> None:
"""Delete an entity and all its memories (cascade delete)."""
if not any([user_id, agent_id, app_id, run_id]):
print_error(
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
)
raise typer.Exit(1)
if dry_run:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
print_info(console, f"Would delete entity {scope} and all its memories.")
print_info(console, "No changes made (dry run).")
return
if not force:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
confirm = typer.confirm(
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
)
if not confirm:
print_info(console, "Cancelled.")
raise typer.Exit(0)
_start = _time.perf_counter()
with timed_status(err_console, "Deleting entity...") as _ts:
try:
result = backend.delete_entities(
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
@@ -0,0 +1,363 @@
"""mem0 init — interactive setup wizard."""
from __future__ import annotations
import os
import re
import sys
import httpx
import typer
from rich.console import Console
from rich.prompt import Prompt
from mem0_cli.branding import (
BRAND_COLOR,
DIM_COLOR,
print_banner,
print_error,
print_info,
print_success,
)
from mem0_cli.config import DEFAULT_BASE_URL, Mem0Config, save_config
console = Console()
err_console = Console(stderr=True)
def _prompt_secret(label: str) -> str:
"""Prompt for a secret value, echoing '*' for each character typed."""
sys.stdout.write(label)
sys.stdout.flush()
chars: list[str] = []
if sys.platform == "win32":
import msvcrt
while True:
ch = msvcrt.getwch()
if ch in ("\r", "\n"):
sys.stdout.write("\n")
sys.stdout.flush()
break
if ch == "\x03":
raise KeyboardInterrupt
if ch in ("\x08", "\x7f"): # backspace
if chars:
chars.pop()
sys.stdout.write("\b \b")
sys.stdout.flush()
else:
chars.append(ch)
sys.stdout.write("*")
sys.stdout.flush()
else:
import termios
import tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(fd)
while True:
ch = sys.stdin.read(1)
if ch in ("\r", "\n"):
sys.stdout.write("\r\n")
sys.stdout.flush()
break
if ch == "\x03":
raise KeyboardInterrupt
if ch in ("\x7f", "\x08"): # backspace/delete
if chars:
chars.pop()
sys.stdout.write("\b \b")
sys.stdout.flush()
elif ch == "\x15": # Ctrl+U — clear line
sys.stdout.write("\b \b" * len(chars))
sys.stdout.flush()
chars = []
elif ch >= " ": # ignore other control characters
chars.append(ch)
sys.stdout.write("*")
sys.stdout.flush()
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return "".join(chars)
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _validate_email(email: str) -> None:
"""Exit with an error if *email* doesn't look like a valid address."""
if not _EMAIL_RE.match(email):
print_error(err_console, f"Invalid email address: {email!r}")
raise typer.Exit(1)
def _email_login(
email: str,
code: str | None,
base_url: str,
) -> dict:
"""Run the email verification code login flow.
Returns the parsed JSON response from the verify endpoint.
The caller expects at minimum an ``api_key`` field.
"""
url = base_url.rstrip("/")
with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code
if not code:
# Step 1: Request verification code
resp = client.post(
f"{url}/api/v1/auth/email_code/",
json={"email": email},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, "Verification code sent! Check your email.")
# Step 2: Get code from user
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint="Run: mem0 init --email <email> --code <code>",
)
raise typer.Exit(1)
console.print()
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
if not code:
print_error(err_console, "Code is required.")
raise typer.Exit(1)
# Step 3: Verify code
resp = client.post(
f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()},
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Verification failed: {detail}")
raise typer.Exit(1)
return resp.json()
def run_init(
*,
api_key: str | None = None,
user_id: str | None = None,
email: str | None = None,
code: str | None = None,
) -> None:
"""Interactive setup wizard for mem0 CLI.
When both *api_key* and *user_id* are supplied, all prompts are skipped
(non-interactive mode). When running in a non-TTY without the required
flags, an error message is printed.
"""
config = Mem0Config()
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
if code and not email:
print_error(err_console, "--code requires --email.")
raise typer.Exit(1)
# ── Email login flow ──────────────────────────────────────────────
if email:
if api_key:
print_error(err_console, "Cannot use both --api-key and --email.")
raise typer.Exit(1)
email = email.strip().lower()
_validate_email(email)
print_banner(console)
console.print()
print_info(console, f"Logging in as {email}...\n")
result = _email_login(email, code, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# ── API key flow (existing) ───────────────────────────────────────
# Fully non-interactive when both flags provided
if api_key and user_id:
config.platform.api_key = api_key
config.defaults.user_id = user_id
_validate_platform(config)
save_config(config)
print_success(console, "Configuration saved to ~/.mem0/config.json")
return
# Non-TTY without full flags -> error
if not sys.stdin.isatty() and (not api_key or not user_id):
print_error(
err_console,
"Non-interactive terminal detected and required flags missing.",
hint="Run: mem0 init --api-key <key> --user-id <id>",
)
raise typer.Exit(1)
print_banner(console)
console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
# If no flags at all, ask user how they want to authenticate
if not api_key:
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
console.print()
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
if choice == "1":
console.print()
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
if not email_addr:
print_error(err_console, "Email is required.")
raise typer.Exit(1)
email_addr = email_addr.strip().lower()
_validate_email(email_addr)
print_info(console, f"Logging in as {email_addr}...\n")
result = _email_login(email_addr, None, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(
err_console, "Auth succeeded but no API key was returned. Contact support."
)
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.defaults.user_id = user_id or "mem0-cli"
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# API key flow
if api_key:
config.platform.api_key = api_key
else:
_setup_platform(config)
if user_id:
config.defaults.user_id = user_id
else:
_setup_defaults(config)
_validate_platform(config)
save_config(config)
console.print()
print_success(console, "Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
if config.defaults.user_id:
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
else:
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode" --user-id alice[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences" --user-id alice[/]')
console.print()
def _setup_platform(config: Mem0Config) -> None:
"""Platform setup flow."""
console.print()
console.print(f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys[/]")
console.print()
console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
api_key = _prompt_secret("")
if not api_key:
print_error(err_console, "API key is required.")
raise typer.Exit(1)
config.platform.api_key = api_key
def _setup_defaults(config: Mem0Config) -> None:
"""Collect default entity IDs."""
console.print()
print_info(console, "Set default entity IDs (press Enter to skip).\n")
user_id = Prompt.ask(
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
default="mem0-cli",
)
if user_id:
config.defaults.user_id = user_id
def _validate_platform(config: Mem0Config) -> None:
"""Validate platform connection after all inputs are collected."""
console.print()
print_info(console, "Validating connection...")
try:
from mem0_cli.backend.platform import PlatformBackend
backend = PlatformBackend(config.platform)
status = backend.status(
user_id=config.defaults.user_id or None,
agent_id=config.defaults.agent_id or None,
)
if status.get("connected"):
print_success(console, "Connected to mem0 Platform!")
else:
print_error(
err_console,
f"Could not connect: {status.get('error', 'Unknown error')}",
hint="Check your API key and try again.",
)
except Exception as e:
print_error(err_console, f"Connection test failed: {e}")
+512
View File
@@ -0,0 +1,512 @@
"""Memory CRUD commands: add, search, get, list, update, delete."""
from __future__ import annotations
import json
import sys
import time as _time
from pathlib import Path
import typer
from rich.console import Console
from mem0_cli.backend.base import Backend
from mem0_cli.branding import (
print_error,
print_info,
print_scope,
print_success,
timed_status,
)
from mem0_cli.output import (
format_add_result,
format_json,
format_memories_table,
format_memories_text,
format_single_memory,
print_result_summary,
)
console = Console()
err_console = Console(stderr=True)
def cmd_add(
backend: Backend,
text: str | None,
*,
user_id: str | None,
agent_id: str | None,
app_id: str | None,
run_id: str | None,
messages: str | None,
file: Path | None,
metadata: str | None,
immutable: bool,
no_infer: bool,
expires: str | None,
categories: str | None,
enable_graph: bool = False,
output: str = "text",
) -> None:
"""Add a memory."""
msgs = None
content = text
# Read from file
if file:
try:
raw = Path(file).read_text()
msgs = json.loads(raw)
except (FileNotFoundError, json.JSONDecodeError) as e:
print_error(err_console, f"Failed to read file: {e}")
raise typer.Exit(1) from None
# Parse messages JSON
elif messages:
try:
msgs = json.loads(messages)
except json.JSONDecodeError as e:
print_error(err_console, f"Invalid JSON in --messages: {e}")
raise typer.Exit(1) from None
# Read from stdin if no text and stdin is piped
elif not content and not sys.stdin.isatty():
content = sys.stdin.read().strip()
if not content and not msgs:
print_error(
err_console, "No content provided. Pass text, --messages, --file, or pipe via stdin."
)
raise typer.Exit(1)
meta = None
if metadata:
try:
meta = json.loads(metadata)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
cats = None
if categories:
try:
cats = json.loads(categories)
except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
import re
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts:
try:
result = backend.add(
content=content,
messages=msgs,
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
metadata=meta,
immutable=immutable,
infer=not no_infer,
expires=expires,
categories=cats,
enable_graph=enable_graph,
)
except Exception as e:
ts.error_msg = str(e)
raise typer.Exit(1) from None
if output == "quiet":
return
if output == "json":
format_add_result(console, result, output)
return
console.print()
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
# Count results
results = result if isinstance(result, list) else result.get("results", [result])
count = len(results) if results else 0
print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
)
format_add_result(console, result, output)
def cmd_search(
backend: Backend,
query: str,
*,
user_id: str | None,
agent_id: str | None,
app_id: str | None,
run_id: str | None,
top_k: int,
threshold: float,
rerank: bool,
keyword: bool,
filter_json: str | None,
fields: str | None,
enable_graph: bool = False,
output: str = "text",
) -> None:
"""Search memories."""
filters = None
if filter_json:
try:
filters = json.loads(filter_json)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --filter.")
raise typer.Exit(1) from None
field_list = None
if fields:
field_list = [f.strip() for f in fields.split(",")]
if top_k < 1:
print_error(err_console, "--top-k must be >= 1.")
raise typer.Exit(1)
if not (0.0 <= threshold <= 1.0):
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Searching memories...") as _ts:
try:
results = backend.search(
query,
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
top_k=top_k,
threshold=threshold,
rerank=rerank,
keyword=keyword,
filters=filters,
fields=field_list,
enable_graph=enable_graph,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json":
format_json(console, results)
elif output == "table":
if results:
format_memories_table(console, results, show_score=True)
print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
)
else:
console.print()
print_info(console, "No memories found matching your query.")
console.print()
else:
if results:
format_memories_text(console, results)
print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
)
else:
console.print()
print_info(console, "No memories found matching your query.")
console.print()
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
"""Get a specific memory by ID."""
with timed_status(err_console, "Fetching memory...") as _ts:
try:
result = backend.get(memory_id)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
format_single_memory(console, result, output)
def cmd_list(
backend: Backend,
*,
user_id: str | None,
agent_id: str | None,
app_id: str | None,
run_id: str | None,
page: int,
page_size: int,
category: str | None,
after: str | None,
before: str | None,
enable_graph: bool = False,
output: str = "table",
) -> None:
"""List memories."""
if page_size < 1:
print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1)
if page < 1:
print_error(err_console, "--page must be >= 1.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Listing memories...") as _ts:
try:
results = backend.list_memories(
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
page=page,
page_size=page_size,
category=category,
after=after,
before=before,
enable_graph=enable_graph,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "json":
from mem0_cli.output import format_json_envelope
format_json_envelope(
console,
command="list",
data=results,
count=len(results),
scope={k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v},
)
elif output == "table":
if results:
format_memories_table(console, results)
print_result_summary(
console,
len(results),
duration_secs=_elapsed,
page=page,
user_id=user_id,
agent_id=agent_id,
)
else:
console.print()
print_info(console, "No memories found.")
console.print()
else:
if results:
format_memories_text(console, results, title="memories")
print_result_summary(
console,
len(results),
duration_secs=_elapsed,
page=page,
user_id=user_id,
agent_id=agent_id,
)
else:
console.print()
print_info(console, "No memories found.")
console.print()
def cmd_update(
backend: Backend,
memory_id: str,
text: str | None,
*,
metadata: str | None,
output: str,
) -> None:
"""Update a memory."""
meta = None
if metadata:
try:
meta = json.loads(metadata)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
_start = _time.perf_counter()
with timed_status(err_console, "Updating memory...") as _ts:
try:
result = backend.update(memory_id, content=text, metadata=meta)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
def cmd_delete(
backend: Backend,
memory_id: str,
*,
dry_run: bool = False,
force: bool = False,
output: str,
) -> None:
"""Delete a single memory by ID."""
if dry_run:
# Fetch and display what would be deleted
try:
mem = backend.get(memory_id)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
format_single_memory(console, mem, output)
print_info(console, "No changes made (dry run).")
return
_start = _time.perf_counter()
with timed_status(err_console, "Deleting...") as _ts:
try:
result = backend.delete(memory_id=memory_id)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
def cmd_delete_all(
backend: Backend,
*,
force: bool,
dry_run: bool = False,
all_: bool = False,
user_id: str | None,
agent_id: str | None,
app_id: str | None,
run_id: str | None,
output: str,
) -> None:
"""Delete all memories matching a scope."""
if all_:
# Project-wide wipe using wildcard entity IDs
if dry_run:
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "Run without --dry-run to see the actual count.")
print_info(console, "No changes made (dry run).")
return
if not force:
confirm = typer.confirm(
"\n ⚠ Delete ALL memories across the ENTIRE project? This cannot be undone."
)
if not confirm:
print_info(console, "Cancelled.")
raise typer.Exit(0)
_start = _time.perf_counter()
with timed_status(err_console, "Deleting all memories project-wide...") as _ts:
try:
result = backend.delete(
all=True,
user_id="*",
agent_id="*",
app_id="*",
run_id="*",
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
print_info(console, "Deletion started. Memories will be removed in the background.")
else:
print_success(console, f"All project memories deleted ({_elapsed:.2f}s)")
return
if dry_run:
# List matching memories and show count
try:
results = backend.list_memories(
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
count = len(results)
print_info(console, f"Would delete {count} memor{'y' if count == 1 else 'ies'}.")
print_info(console, "No changes made (dry run).")
return
if not force:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts) if scope_parts else "ALL entities"
confirm = typer.confirm(f"\n ⚠ Delete ALL memories for {scope}? This cannot be undone.")
if not confirm:
print_info(console, "Cancelled.")
raise typer.Exit(0)
_start = _time.perf_counter()
with timed_status(err_console, "Deleting all memories...") as _ts:
try:
result = backend.delete(
all=True,
user_id=user_id,
agent_id=agent_id,
app_id=app_id,
run_id=run_id,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
print_info(console, "Deletion started. Memories will be removed in the background.")
else:
print_success(console, f"All matching memories deleted ({_elapsed:.2f}s)")
+143
View File
@@ -0,0 +1,143 @@
"""Utility commands: status, version, import."""
from __future__ import annotations
import json
import time as _time
from pathlib import Path
import typer
from rich.console import Console
from rich.panel import Panel
from rich.progress import track
from mem0_cli import __version__
from mem0_cli.backend.base import Backend
from mem0_cli.branding import (
BRAND_COLOR,
DIM_COLOR,
ERROR_COLOR,
SUCCESS_COLOR,
print_error,
print_success,
timed_status,
)
console = Console()
err_console = Console(stderr=True)
def cmd_status(
backend: Backend,
*,
user_id: str | None = None,
agent_id: str | None = None,
output: str = "text",
) -> None:
"""Check connectivity and auth."""
from mem0_cli.output import format_json_envelope
_start = _time.perf_counter()
with timed_status(err_console, "Checking connection...") as _ts:
result = backend.status(user_id=user_id, agent_id=agent_id)
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
console,
command="status",
data={
"connected": result.get("connected", False),
"backend": result.get("backend", "?"),
"base_url": result.get("base_url", ""),
"latency_ms": int(_elapsed * 1000),
},
duration_ms=int(_elapsed * 1000),
)
return
lines = []
if result.get("connected"):
lines.append(f" [{SUCCESS_COLOR}]●[/] Connected")
else:
lines.append(f" [{ERROR_COLOR}]●[/] Disconnected")
lines.append(f" [{DIM_COLOR}]Backend:[/] {result.get('backend', '?')}")
if result.get("base_url"):
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
if result.get("error"):
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
content = "\n".join(lines)
panel = Panel(
content,
title=f"[{BRAND_COLOR}]Connection Status[/]",
title_align="left",
border_style=BRAND_COLOR,
padding=(1, 1),
)
console.print()
console.print(panel)
console.print()
def cmd_version() -> None:
"""Show version."""
console.print(f" [{BRAND_COLOR}]◆ Mem0[/] CLI v{__version__}")
def cmd_import(
backend: Backend,
file_path: str,
*,
user_id: str | None,
agent_id: str | None,
output: str = "text",
) -> None:
"""Import memories from a JSON file."""
from mem0_cli.output import format_json_envelope
try:
data = json.loads(Path(file_path).read_text())
except (FileNotFoundError, json.JSONDecodeError) as e:
print_error(err_console, f"Failed to read file: {e}")
raise typer.Exit(1) from None
if not isinstance(data, list):
data = [data]
added = 0
failed = 0
_start = _time.perf_counter()
for item in track(
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
):
content = item.get("memory", item.get("text", item.get("content", "")))
if not content:
failed += 1
continue
try:
backend.add(
content=content,
user_id=user_id or item.get("user_id"),
agent_id=agent_id or item.get("agent_id"),
metadata=item.get("metadata"),
)
added += 1
except Exception:
failed += 1
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
console,
command="import",
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
duration_ms=int(_elapsed * 1000),
)
return
print_success(err_console, f"Imported {added} memories ({_elapsed:.2f}s)")
if failed:
print_error(err_console, f"{failed} memories failed to import.")
+189
View File
@@ -0,0 +1,189 @@
"""Configuration management for mem0 CLI.
Config precedence (highest to lowest):
1. CLI flags (--api-key, --base-url, etc.)
2. Environment variables (MEM0_API_KEY, etc.)
3. Config file (~/.mem0/config.json)
4. Defaults
"""
from __future__ import annotations
import json
import os
import stat
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
CONFIG_DIR = Path.home() / ".mem0"
CONFIG_FILE = CONFIG_DIR / "config.json"
DEFAULT_BASE_URL = "https://api.mem0.ai"
CONFIG_VERSION = 1
@dataclass
class PlatformConfig:
api_key: str = ""
base_url: str = DEFAULT_BASE_URL
@dataclass
class DefaultsConfig:
user_id: str = ""
agent_id: str = ""
app_id: str = ""
run_id: str = ""
enable_graph: bool = False
@dataclass
class Mem0Config:
version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig)
SHORT_KEY_ALIASES: dict[str, str] = {
"api_key": "platform.api_key",
"base_url": "platform.base_url",
"user_id": "defaults.user_id",
"agent_id": "defaults.agent_id",
"app_id": "defaults.app_id",
"run_id": "defaults.run_id",
"enable_graph": "defaults.enable_graph",
}
def ensure_config_dir() -> Path:
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700
return CONFIG_DIR
def load_config() -> Mem0Config:
"""Load config from file, applying env var overrides."""
config = Mem0Config()
if CONFIG_FILE.exists():
with open(CONFIG_FILE) as f:
data = json.load(f)
config.version = data.get("version", CONFIG_VERSION)
plat = data.get("platform", {})
config.platform.api_key = plat.get("api_key", "")
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
defaults = data.get("defaults", {})
config.defaults.user_id = defaults.get("user_id", "")
config.defaults.agent_id = defaults.get("agent_id", "")
config.defaults.app_id = defaults.get("app_id", "")
config.defaults.run_id = defaults.get("run_id", "")
config.defaults.enable_graph = defaults.get("enable_graph", False)
# Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY")
if env_key:
config.platform.api_key = env_key
env_base = os.environ.get("MEM0_BASE_URL")
if env_base:
config.platform.base_url = env_base
env_user_id = os.environ.get("MEM0_USER_ID")
if env_user_id:
config.defaults.user_id = env_user_id
env_agent_id = os.environ.get("MEM0_AGENT_ID")
if env_agent_id:
config.defaults.agent_id = env_agent_id
env_app_id = os.environ.get("MEM0_APP_ID")
if env_app_id:
config.defaults.app_id = env_app_id
env_run_id = os.environ.get("MEM0_RUN_ID")
if env_run_id:
config.defaults.run_id = env_run_id
env_graph = os.environ.get("MEM0_ENABLE_GRAPH")
if env_graph:
config.defaults.enable_graph = env_graph.lower() in ("true", "1", "yes")
return config
def save_config(config: Mem0Config) -> None:
"""Write config to disk with secure permissions."""
ensure_config_dir()
data: dict[str, Any] = {
"version": config.version,
"defaults": {
"user_id": config.defaults.user_id,
"agent_id": config.defaults.agent_id,
"app_id": config.defaults.app_id,
"run_id": config.defaults.run_id,
"enable_graph": config.defaults.enable_graph,
},
"platform": {
"api_key": config.platform.api_key,
"base_url": config.platform.base_url,
},
}
with open(CONFIG_FILE, "w") as f:
json.dump(data, f, indent=2)
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
def redact_key(key: str) -> str:
"""Redact an API key for display: m0-xxx...xxx"""
if not key:
return "(not set)"
if len(key) <= 8:
return key[:2] + "***"
return key[:4] + "..." + key[-4:]
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts:
if hasattr(obj, part):
obj = getattr(obj, part)
else:
return None
return obj
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
"""Set a config value by dotted path. Returns True on success."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts[:-1]:
if hasattr(obj, part):
obj = getattr(obj, part)
else:
return False
final_key = parts[-1]
if not hasattr(obj, final_key):
return False
current = getattr(obj, final_key)
# Type coercion
if isinstance(current, bool):
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
elif isinstance(current, int):
value = int(value) # type: ignore[assignment]
setattr(obj, final_key, value)
return True
+258
View File
@@ -0,0 +1,258 @@
"""Output formatting for mem0 CLI — text, JSON, table, quiet modes."""
from __future__ import annotations
import json
from datetime import datetime
from typing import Any
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, SUCCESS_COLOR, _sym
def format_memories_text(console: Console, memories: list[dict], title: str = "memories") -> None:
"""Render memories in human-friendly text mode."""
count = len(memories)
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
for i, mem in enumerate(memories, 1):
memory_text = mem.get("memory", mem.get("text", ""))
mem_id = mem.get("id", "")[:8]
score = mem.get("score")
created = _format_date(mem.get("created_at"))
category = mem.get("categories", [None])
if isinstance(category, list):
category = category[0] if category else None
line = Text()
line.append(f" {i}. ", style="bold")
line.append(memory_text, style="white")
console.print(line)
details = []
if score is not None:
details.append(f"Score: {score:.2f}")
if mem_id:
details.append(f"ID: {mem_id}")
if created:
details.append(f"Created: {created}")
if category:
details.append(f"Category: {category}")
if details:
detail_str = " · ".join(details)
console.print(f" [{DIM_COLOR}]{detail_str}[/]")
console.print()
def format_memories_table(
console: Console, memories: list[dict], *, show_score: bool = False
) -> None:
"""Render memories in a rich table."""
table = Table(
border_style=BRAND_COLOR,
header_style=f"bold {ACCENT_COLOR}",
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
if show_score:
table.add_column("Score", max_width=7, justify="right")
table.add_column("Memory", max_width=50, no_wrap=False)
table.add_column("Category", max_width=14)
table.add_column("Created", max_width=12)
for mem in memories:
mem_id = mem.get("id", "")
memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60:
memory_text = memory_text[:57] + "..."
categories = mem.get("categories", [])
if isinstance(categories, list) and categories:
cat = (
categories[0]
if len(categories) == 1
else f"{categories[0]} (+{len(categories) - 1})"
)
else:
cat = "—"
created = _format_date(mem.get("created_at")) or "—"
if show_score:
score = mem.get("score")
score_str = f"{score:.2f}" if score is not None else "—"
table.add_row(mem_id, score_str, memory_text, cat, created)
else:
table.add_row(mem_id, memory_text, cat, created)
console.print()
console.print(table)
console.print()
def format_json(console: Console, data: Any) -> None:
"""Output data as pretty-printed JSON."""
console.print_json(json.dumps(data, default=str))
def format_single_memory(console: Console, mem: dict, output: str = "text") -> None:
"""Format a single memory for display."""
if output == "json":
format_json(console, mem)
return
memory_text = mem.get("memory", mem.get("text", ""))
mem_id = mem.get("id", "")
lines = []
lines.append(f" [white bold]{memory_text}[/]")
lines.append("")
if mem_id:
lines.append(f" [{DIM_COLOR}]ID:[/] {mem_id}")
created = _format_date(mem.get("created_at"))
if created:
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
updated = _format_date(mem.get("updated_at"))
if updated:
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
meta = mem.get("metadata")
if meta:
lines.append(f" [{DIM_COLOR}]Metadata:[/] {json.dumps(meta)}")
categories = mem.get("categories")
if categories:
cat_str = ", ".join(categories) if isinstance(categories, list) else categories
lines.append(f" [{DIM_COLOR}]Categories:[/] {cat_str}")
content = "\n".join(lines)
panel = Panel(
content,
title=f"[{BRAND_COLOR}]Memory[/]",
title_align="left",
border_style=BRAND_COLOR,
padding=(1, 1),
)
console.print()
console.print(panel)
console.print()
def format_add_result(console: Console, result: dict | list, output: str = "text") -> None:
"""Format the result of an add operation."""
if output == "json":
format_json(console, result)
return
if output == "quiet":
return
# result from API is typically {"results": [...]}
results = result if isinstance(result, list) else result.get("results", [result])
if not results:
console.print(f" [{DIM_COLOR}]No memories extracted.[/]")
return
console.print()
for r in results:
# Detect async PENDING response from Platform API
if r.get("status") == "PENDING":
event_id = r.get("event_id", "")[:8]
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
parts.append("[white]Processing in background[/]")
if event_id:
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
console.print(" ".join(parts))
continue
event = r.get("event", "ADD")
memory = r.get("memory") or r.get("text") or r.get("content") or r.get("data") or ""
mem_id = (r.get("id") or r.get("memory_id") or "")[:8]
if event == "ADD":
icon = f"[{SUCCESS_COLOR}]+[/]"
label = "Added"
elif event == "UPDATE":
icon = f"[{ACCENT_COLOR}]~[/]"
label = "Updated"
elif event == "DELETE":
icon = "[red]-[/]"
label = "Deleted"
elif event == "NOOP":
icon = f"[{DIM_COLOR}]·[/]"
label = "No change"
else:
icon = f"[{DIM_COLOR}]?[/]"
label = event
# Build the display line
parts = [f" {icon} [{DIM_COLOR}]{label:<10}[/]"]
if memory:
parts.append(f"[white]{memory}[/]")
if mem_id:
parts.append(f"[{DIM_COLOR}]({mem_id})[/]")
console.print(" ".join(parts))
console.print()
def format_json_envelope(
console: Console,
*,
command: str,
data: Any,
duration_ms: int | None = None,
scope: dict | None = None,
count: int | None = None,
status: str = "success",
error: str | None = None,
) -> None:
"""Output structured JSON envelope for AI agent consumption."""
envelope: dict[str, Any] = {
"status": status,
"command": command,
}
if duration_ms is not None:
envelope["duration_ms"] = duration_ms
if scope is not None:
envelope["scope"] = scope
if count is not None:
envelope["count"] = count
if error:
envelope["error"] = error
envelope["data"] = data
console.print_json(json.dumps(envelope, default=str))
def print_result_summary(
console: Console,
count: int,
*,
duration_secs: float | None = None,
page: int | None = None,
**scope_ids: str | None,
) -> None:
"""Print a summary footer after result lists."""
parts = [f"{count} result{'s' if count != 1 else ''}"]
if page is not None:
parts.append(f"page {page}")
scope_parts = [f"{k.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
if scope_parts:
parts.append(", ".join(scope_parts))
if duration_secs is not None:
parts.append(f"{duration_secs:.2f}s")
summary = " · ".join(parts)
console.print(f" [{DIM_COLOR}]{summary}[/]")
console.print()
def _format_date(dt_str: str | None) -> str | None:
if not dt_str:
return None
try:
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
return dt.strftime("%Y-%m-%d")
except (ValueError, AttributeError):
return str(dt_str)[:10] if dt_str else None
View File
+109
View File
@@ -0,0 +1,109 @@
"""Shared fixtures for mem0 CLI tests."""
from __future__ import annotations
import os
from unittest.mock import MagicMock
import pytest
from mem0_cli.backend.base import Backend
from mem0_cli.config import Mem0Config
@pytest.fixture(autouse=True)
def isolate_config(tmp_path, monkeypatch):
"""Redirect config to a temp directory so tests don't touch real config."""
fake_config_dir = tmp_path / ".mem0"
fake_config_file = fake_config_dir / "config.json"
monkeypatch.setattr("mem0_cli.config.CONFIG_DIR", fake_config_dir)
monkeypatch.setattr("mem0_cli.config.CONFIG_FILE", fake_config_file)
# Also patch the commands that import config
monkeypatch.setattr("mem0_cli.commands.config_cmd.CONFIG_DIR", fake_config_dir, raising=False)
# Clear any MEM0 env vars
for key in list(os.environ.keys()):
if key.startswith("MEM0_"):
monkeypatch.delenv(key, raising=False)
return fake_config_dir
@pytest.fixture
def mock_backend():
"""Return a mock backend with all methods stubbed."""
backend = MagicMock(spec=Backend)
# Default return values
backend.add.return_value = {
"results": [
{
"id": "abc-123-def-456",
"memory": "User prefers dark mode",
"event": "ADD",
}
]
}
backend.search.return_value = [
{
"id": "abc-123-def-456",
"memory": "User prefers dark mode",
"score": 0.92,
"created_at": "2026-02-15T10:30:00Z",
"categories": ["preferences"],
},
{
"id": "ghi-789-jkl-012",
"memory": "User uses vim keybindings",
"score": 0.78,
"created_at": "2026-03-01T14:00:00Z",
"categories": ["tools"],
},
]
backend.get.return_value = {
"id": "abc-123-def-456",
"memory": "User prefers dark mode",
"created_at": "2026-02-15T10:30:00Z",
"updated_at": "2026-02-20T08:00:00Z",
"metadata": {"source": "onboarding"},
"categories": ["preferences"],
}
backend.list_memories.return_value = [
{
"id": "abc-123-def-456",
"memory": "User prefers dark mode",
"created_at": "2026-02-15T10:30:00Z",
"categories": ["preferences"],
},
{
"id": "ghi-789-jkl-012",
"memory": "User uses vim keybindings",
"created_at": "2026-03-01T14:00:00Z",
"categories": ["tools"],
},
]
backend.update.return_value = {"id": "abc-123-def-456", "memory": "Updated memory"}
backend.delete.return_value = {"status": "deleted"}
backend.status.return_value = {
"connected": True,
"backend": "platform",
"base_url": "https://api.mem0.ai",
}
backend.delete_entities.return_value = {"message": "Entity deleted"}
backend.entities.return_value = [
{"name": "alice", "count": 5},
{"name": "bob", "count": 3},
]
return backend
@pytest.fixture
def sample_config():
"""Return a sample config object."""
config = Mem0Config()
config.platform.api_key = "m0-test-key-12345678"
config.platform.base_url = "https://api.mem0.ai"
return config
+54
View File
@@ -0,0 +1,54 @@
"""Tests for branding and output helpers."""
from __future__ import annotations
from io import StringIO
from rich.console import Console
from mem0_cli.branding import print_banner, print_error, print_info, print_success, print_warning
def _make_console() -> tuple[Console, StringIO]:
buf = StringIO()
return Console(file=buf, force_terminal=False, no_color=True, width=80), buf
class TestBranding:
def test_print_banner(self):
console, buf = _make_console()
print_banner(console)
output = buf.getvalue()
# Banner contains the mem0 ASCII art and tagline
assert "Memory Layer" in output or "mem" in output.lower()
def test_print_success(self):
console, buf = _make_console()
print_success(console, "It worked!")
output = buf.getvalue()
assert "It worked!" in output
def test_print_error(self):
console, buf = _make_console()
print_error(console, "Something failed", hint="Try this fix")
output = buf.getvalue()
assert "Something failed" in output
assert "Try this fix" in output
def test_print_error_no_hint(self):
console, buf = _make_console()
print_error(console, "Failed")
output = buf.getvalue()
assert "Failed" in output
def test_print_warning(self):
console, buf = _make_console()
print_warning(console, "Watch out")
output = buf.getvalue()
assert "Watch out" in output
def test_print_info(self):
console, buf = _make_console()
print_info(console, "FYI")
output = buf.getvalue()
assert "FYI" in output
+263
View File
@@ -0,0 +1,263 @@
"""Integration tests — invoke CLI as subprocess to test end-to-end.
These tests launch the CLI as a real subprocess, so they must manage
environment isolation themselves (monkeypatch doesn't cross process
boundaries).
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import pytest
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
def _strip_ansi(text: str) -> str:
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
return _ANSI_RE.sub("", text)
def _run(
args: list[str],
env_override: dict | None = None,
home_dir: str | None = None,
) -> subprocess.CompletedProcess:
"""Run mem0 CLI command and capture output.
Args:
args: CLI arguments.
env_override: Extra env vars to set.
home_dir: If provided, set HOME to this path so the subprocess
reads config from ``<home_dir>/.mem0/config.json`` instead
of the user's real config. This is critical for tests that
depend on a clean (no API key) or custom config state.
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
fragment option names like --user-id into separately-styled ANSI segments,
making plain ``in`` checks fail. Stripping here is version-agnostic and
ensures all assertions see the same plain text regardless of terminal env.
"""
env = os.environ.copy()
# Strip all MEM0_ env vars so tests start clean
for key in list(env.keys()):
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
if home_dir:
env["HOME"] = home_dir
if env_override:
env.update(env_override)
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
text=True,
env=env,
)
return subprocess.CompletedProcess(
args=result.args,
returncode=result.returncode,
stdout=_strip_ansi(result.stdout),
stderr=_strip_ansi(result.stderr),
)
@pytest.fixture
def clean_home(tmp_path):
"""Return a temp directory to use as HOME, ensuring no ~/.mem0 exists."""
return str(tmp_path)
class TestCLIIntegration:
"""Tests that only inspect help text / version — no config needed."""
def test_help(self):
result = _run(["--help"])
assert result.returncode == 0
assert "mem0" in result.stdout
assert "add" in result.stdout
assert "search" in result.stdout
def test_version_flag(self):
result = _run(["--version"])
assert result.returncode == 0
assert "0.1.0" in result.stdout
def test_add_help(self):
result = _run(["add", "--help"])
assert result.returncode == 0
assert "user-id" in result.stdout
assert "messages" in result.stdout
def test_add_help_has_scope_panel(self):
"""Verify rich_help_panel grouping shows in help output."""
result = _run(["add", "--help"])
assert result.returncode == 0
assert "Scope" in result.stdout
def test_search_help(self):
result = _run(["search", "--help"])
assert result.returncode == 0
assert "top-k" in result.stdout
def test_list_help(self):
result = _run(["list", "--help"])
assert result.returncode == 0
assert "page-size" in result.stdout
def test_delete_help(self):
result = _run(["delete", "--help"])
assert result.returncode == 0
assert "--all" in result.stdout
assert "--entity" in result.stdout
assert "--project" in result.stdout
assert "--force" in result.stdout
assert "--dry-run" in result.stdout
def test_entity_list_help(self):
result = _run(["entity", "list", "--help"])
assert result.returncode == 0
assert "entity-type" in result.stdout.lower() or "entity_type" in result.stdout.lower()
def test_entity_delete_help(self):
result = _run(["entity", "delete", "--help"])
assert result.returncode == 0
assert "--user-id" in result.stdout
assert "--force" in result.stdout
def test_import_help(self):
result = _run(["import", "--help"])
assert result.returncode == 0
def test_no_args_shows_help(self):
"""no_args_is_help=True makes Typer print help and exit with code 2."""
result = _run([])
# Typer returns exit code 2 for "no command given" — this is standard
# Click/Typer behaviour and not an error.
assert result.returncode in (0, 2)
assert "Usage" in result.stdout
class TestCLIIsolated:
"""Tests that need a clean HOME to avoid reading the user's real config."""
def test_add_no_key_errors(self, clean_home):
"""Without an API key, `mem0 add` must fail with a helpful message."""
result = _run(
["add", "test", "--user-id", "alice"],
home_dir=clean_home,
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "API key" in combined or "api" in combined.lower() or "Error" in combined
def test_search_no_key_errors(self, clean_home):
"""Without an API key, `mem0 search` must fail."""
result = _run(
["search", "preferences", "--user-id", "alice"],
home_dir=clean_home,
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "API key" in combined or "Error" in combined
def test_list_no_key_errors(self, clean_home):
"""Without an API key, `mem0 list` must fail."""
result = _run(["list"], home_dir=clean_home)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "API key" in combined or "Error" in combined
def test_delete_no_id_no_all_errors(self, clean_home):
"""Delete without memory_id, --all, or --entity must fail."""
result = _run(
["delete", "--api-key", "m0-fake-key"],
home_dir=clean_home,
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert (
"memory ID" in combined.lower()
or "--all" in combined
or "--entity" in combined
or "Error" in combined
)
def test_config_show_clean(self, clean_home):
"""config show with no config should still work."""
result = _run(["config", "show"], home_dir=clean_home)
assert result.returncode == 0
assert "backend" in result.stdout.lower() or "platform" in result.stdout.lower()
def test_config_set_and_get_roundtrip(self, clean_home):
"""config set then config get should return the set value."""
_run(
["config", "set", "defaults.user_id", "integration-test-user"],
home_dir=clean_home,
)
result = _run(
["config", "get", "defaults.user_id"],
home_dir=clean_home,
)
assert result.returncode == 0
assert "integration-test-user" in result.stdout
def test_import_nonexistent_file(self, clean_home):
"""Importing a nonexistent file should fail gracefully."""
result = _run(
["import", "/nonexistent/file.json", "--api-key", "m0-fake"],
home_dir=clean_home,
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "Failed" in combined or "Error" in combined or "error" in combined
def test_add_no_content_errors(self, clean_home):
"""add with no text/messages/file should fail."""
result = _run(
["add", "--user-id", "alice", "--api-key", "m0-fake"],
home_dir=clean_home,
)
assert result.returncode != 0
combined = result.stderr + result.stdout
assert "No content" in combined or "Error" in combined
class TestCLINewFeatures:
"""Tests for MCP parity features: --graph, --limit, entities delete."""
def test_add_help_has_graph(self):
result = _run(["add", "--help"])
assert result.returncode == 0
assert "--graph" in result.stdout
def test_search_help_has_graph_and_limit(self):
result = _run(["search", "--help"])
assert result.returncode == 0
assert "--graph" in result.stdout
assert "--limit" in result.stdout
def test_list_help_has_graph(self):
result = _run(["list", "--help"])
assert result.returncode == 0
assert "--graph" in result.stdout
def test_delete_entity_via_delete_flag(self):
"""delete --entity should appear in help output."""
result = _run(["delete", "--help"])
assert result.returncode == 0
assert "--entity" in result.stdout
def test_entity_delete_has_scope_options(self):
"""entity delete should expose scope options."""
result = _run(["entity", "delete", "--help"])
assert result.returncode == 0
assert "--user-id" in result.stdout
assert "--force" in result.stdout
assert "--app-id" in result.stdout
assert "--run-id" in result.stdout
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
"""Tests for configuration management."""
from __future__ import annotations
import os
from mem0_cli.config import (
Mem0Config,
get_nested_value,
load_config,
redact_key,
save_config,
set_nested_value,
)
class TestRedactKey:
def test_empty_key(self):
assert redact_key("") == "(not set)"
def test_short_key(self):
assert redact_key("abc") == "ab***"
def test_normal_key(self):
result = redact_key("m0-abcdefgh12345678")
assert result == "m0-a...5678"
assert "abcdefgh" not in result
def test_exact_8_chars(self):
# 8 chars is <= 8, so it gets the short redaction
assert redact_key("12345678") == "12***"
class TestConfig:
def test_default_config(self):
config = Mem0Config()
assert config.platform.base_url == "https://api.mem0.ai"
assert config.platform.api_key == ""
def test_save_and_load(self, isolate_config):
config = Mem0Config()
config.platform.api_key = "m0-test-key"
save_config(config)
loaded = load_config()
assert loaded.platform.api_key == "m0-test-key"
def test_env_var_override(self, isolate_config, monkeypatch):
config = Mem0Config()
config.platform.api_key = "file-key"
save_config(config)
monkeypatch.setenv("MEM0_API_KEY", "env-key")
loaded = load_config()
assert loaded.platform.api_key == "env-key"
def test_load_nonexistent_config(self, isolate_config):
config = load_config()
assert config.platform.api_key == ""
def test_config_file_permissions(self, isolate_config):
config = Mem0Config()
config.platform.api_key = "secret"
save_config(config)
from mem0_cli.config import CONFIG_FILE
mode = os.stat(CONFIG_FILE).st_mode & 0o777
assert mode == 0o600
def test_defaults_save_and_load(self, isolate_config):
config = Mem0Config()
config.defaults.user_id = "alice"
config.defaults.agent_id = "support-bot"
config.defaults.app_id = "my-app"
config.defaults.run_id = "run-001"
save_config(config)
loaded = load_config()
assert loaded.defaults.user_id == "alice"
assert loaded.defaults.agent_id == "support-bot"
assert loaded.defaults.app_id == "my-app"
assert loaded.defaults.run_id == "run-001"
def test_defaults_env_var_override(self, isolate_config, monkeypatch):
config = Mem0Config()
config.defaults.user_id = "file-user"
save_config(config)
monkeypatch.setenv("MEM0_USER_ID", "env-user")
monkeypatch.setenv("MEM0_AGENT_ID", "env-agent")
loaded = load_config()
assert loaded.defaults.user_id == "env-user"
assert loaded.defaults.agent_id == "env-agent"
def test_backward_compat_no_defaults_key(self, isolate_config):
"""Old config files without 'defaults' key should load fine."""
import json
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
ensure_config_dir()
# Write a config without the "defaults" key
data = {
"version": 1,
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
}
with open(CONFIG_FILE, "w") as f:
json.dump(data, f)
loaded = load_config()
assert loaded.platform.api_key == "m0-test"
assert loaded.defaults.user_id == ""
assert loaded.defaults.agent_id == ""
def test_default_config_has_empty_defaults(self):
config = Mem0Config()
assert config.defaults.user_id == ""
assert config.defaults.agent_id == ""
assert config.defaults.app_id == ""
assert config.defaults.run_id == ""
assert config.defaults.enable_graph is False
def test_enable_graph_save_and_load(self, isolate_config):
config = Mem0Config()
config.defaults.enable_graph = True
save_config(config)
loaded = load_config()
assert loaded.defaults.enable_graph is True
def test_enable_graph_env_var_true(self, isolate_config, monkeypatch):
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "true")
loaded = load_config()
assert loaded.defaults.enable_graph is True
def test_enable_graph_env_var_false(self, isolate_config, monkeypatch):
config = Mem0Config()
config.defaults.enable_graph = True
save_config(config)
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "false")
loaded = load_config()
assert loaded.defaults.enable_graph is False
def test_backward_compat_no_enable_graph_key(self, isolate_config):
"""Old config files without 'enable_graph' key should default to False."""
import json
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
ensure_config_dir()
data = {
"version": 1,
"defaults": {"user_id": "alice"},
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
}
with open(CONFIG_FILE, "w") as f:
json.dump(data, f)
loaded = load_config()
assert loaded.defaults.enable_graph is False
assert loaded.defaults.user_id == "alice"
class TestNestedAccess:
def test_get_nested_value(self):
config = Mem0Config()
config.platform.api_key = "test-key"
assert get_nested_value(config, "platform.api_key") == "test-key"
def test_get_nonexistent_key(self):
config = Mem0Config()
assert get_nested_value(config, "nonexistent.key") is None
def test_set_nested_value(self):
config = Mem0Config()
assert set_nested_value(config, "platform.api_key", "new-key")
assert config.platform.api_key == "new-key"
def test_set_nonexistent_key(self):
config = Mem0Config()
assert set_nested_value(config, "nonexistent.key", "val") is False
def test_get_defaults_user_id(self):
config = Mem0Config()
config.defaults.user_id = "alice"
assert get_nested_value(config, "defaults.user_id") == "alice"
def test_set_defaults_user_id(self):
config = Mem0Config()
assert set_nested_value(config, "defaults.user_id", "bob")
assert config.defaults.user_id == "bob"
def test_set_defaults_enable_graph(self):
config = Mem0Config()
assert set_nested_value(config, "defaults.enable_graph", "true")
assert config.defaults.enable_graph is True
class TestResolveIds:
def test_cli_flag_overrides_default(self):
from mem0_cli.app import _resolve_ids
config = Mem0Config()
config.defaults.user_id = "default-user"
ids = _resolve_ids(
config,
user_id="cli-user",
agent_id=None,
)
assert ids["user_id"] == "cli-user"
def test_default_used_when_flag_is_none(self):
from mem0_cli.app import _resolve_ids
config = Mem0Config()
config.defaults.user_id = "default-user"
config.defaults.agent_id = "default-agent"
ids = _resolve_ids(config, user_id=None, agent_id=None)
assert ids["user_id"] == "default-user"
assert ids["agent_id"] == "default-agent"
def test_none_when_neither_set(self):
from mem0_cli.app import _resolve_ids
config = Mem0Config()
ids = _resolve_ids(config, user_id=None, agent_id=None)
assert ids["user_id"] is None
assert ids["agent_id"] is None
assert ids["app_id"] is None
assert ids["run_id"] is None
def test_empty_string_treated_as_unset(self):
from mem0_cli.app import _resolve_ids
config = Mem0Config()
config.defaults.user_id = ""
ids = _resolve_ids(config, user_id=None)
assert ids["user_id"] is None
+136
View File
@@ -0,0 +1,136 @@
"""Tests for output formatting."""
from __future__ import annotations
from io import StringIO
from rich.console import Console
from mem0_cli.output import (
format_add_result,
format_memories_table,
format_memories_text,
format_single_memory,
)
def _make_console() -> tuple[Console, StringIO]:
buf = StringIO()
return Console(file=buf, force_terminal=False, no_color=True, width=120, highlight=False), buf
SAMPLE_MEMORIES = [
{
"id": "abc-123-def-456",
"memory": "User prefers dark mode",
"score": 0.92,
"created_at": "2026-02-15T10:30:00Z",
"categories": ["preferences"],
},
{
"id": "ghi-789-jkl-012",
"memory": "User uses vim keybindings",
"score": 0.78,
"created_at": "2026-03-01T14:00:00Z",
"categories": ["tools"],
},
]
class TestTextFormat:
def test_format_memories_text(self):
console, buf = _make_console()
format_memories_text(console, SAMPLE_MEMORIES)
output = buf.getvalue()
assert "Found 2 memories" in output
assert "dark mode" in output
assert "vim keybindings" in output
assert "0.92" in output
def test_format_memories_text_empty(self):
console, buf = _make_console()
format_memories_text(console, [])
output = buf.getvalue()
assert "Found 0" in output
class TestTableFormat:
def test_format_memories_table(self):
console, buf = _make_console()
format_memories_table(console, SAMPLE_MEMORIES)
output = buf.getvalue()
assert "dark mode" in output
assert "abc-123-" in output
def test_format_memories_table_empty(self):
console, buf = _make_console()
format_memories_table(console, [])
output = buf.getvalue()
# Should still render (empty table)
assert "ID" in output
class TestSingleMemory:
def test_format_single_memory_text(self):
console, buf = _make_console()
mem = SAMPLE_MEMORIES[0]
format_single_memory(console, mem, "text")
output = buf.getvalue()
assert "dark mode" in output
assert "abc-123-def-456" in output
def test_format_single_memory_json(self):
console, buf = _make_console()
mem = SAMPLE_MEMORIES[0]
format_single_memory(console, mem, "json")
output = buf.getvalue()
assert '"memory"' in output
class TestAddResult:
def test_format_add_result_text(self):
console, buf = _make_console()
result = {
"results": [
{"id": "abc-123-def-456", "memory": "User prefers dark mode", "event": "ADD"},
]
}
format_add_result(console, result, "text")
output = buf.getvalue()
assert "dark mode" in output
assert "Added" in output
def test_format_add_result_update_event(self):
console, buf = _make_console()
result = {
"results": [
{"id": "abc-123", "memory": "Updated pref", "event": "UPDATE"},
]
}
format_add_result(console, result, "text")
output = buf.getvalue()
assert "Updated" in output
def test_format_add_result_noop(self):
console, buf = _make_console()
result = {
"results": [
{"id": "abc-123", "memory": "Same thing", "event": "NOOP"},
]
}
format_add_result(console, result, "text")
output = buf.getvalue()
assert "No change" in output
def test_format_add_result_quiet(self):
console, buf = _make_console()
result = {"results": [{"id": "abc-123", "memory": "Quiet", "event": "ADD"}]}
format_add_result(console, result, "quiet")
output = buf.getvalue()
assert output.strip() == ""
def test_format_add_result_empty(self):
console, buf = _make_console()
format_add_result(console, {"results": []}, "text")
output = buf.getvalue()
assert "No memories extracted" in output
+1 -2
View File
@@ -10,5 +10,4 @@ List recent events for your organization and project.
- **Dashboards**: Summarize adds/searches over time by paging through events.
- **Alerting**: Poll for `FAILED` events and trigger follow-up workflows.
- **Audit**: Store the returned payload/metadata for compliance logs.
- **Audit**: Store the returned payload/metadata for compliance logs.
@@ -52,6 +52,10 @@ Provide at least one message or direct memory string. Most callers supply `messa
> \* 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.
<Tip>
Need more details? See [all request parameters](#body-messages) below for complete field descriptions, types, and constraints.
</Tip>
## Response
Successful requests return an array of events queued for processing. Each event includes the generated memory text and an identifier you can persist for auditing.
+84 -9
View File
@@ -8,20 +8,74 @@ mode: "wide"
<Tabs>
<Tab title="Python">
<Update label="2026-03-28" description="v1.0.9">
**New Features & Updates:**
- **LLMs:** Added `reasoning_effort` parameter support for reasoning models ([#4461](https://github.com/mem0ai/mem0/pull/4461))
**Bug Fixes:**
- **Core:** Preserved original `actor_id` during memory update ([#4570](https://github.com/mem0ai/mem0/pull/4570))
- **Core:** Set `updated_at` on creation and preserve pre-existing `created_at` ([#4499](https://github.com/mem0ai/mem0/pull/4499))
- **Core:** Centralized entity cleanup and skip malformed LLM relation dicts ([#4515](https://github.com/mem0ai/mem0/pull/4515))
- **Core:** Removed `README.md` from wheel shared-data ([#4052](https://github.com/mem0ai/mem0/pull/4052))
- **Vector Stores:** Handled `vector=None` in Milvus and Qdrant update methods ([#4568](https://github.com/mem0ai/mem0/pull/4568))
- **Vector Stores:** Rebuilt FAISS index on vector deletion ([#4178](https://github.com/mem0ai/mem0/pull/4178))
**Improvements:**
- **Embeddings:** Updated default Gemini and Vertex AI embedder model to `gemini-embedding-001` ([#4571](https://github.com/mem0ai/mem0/pull/4571))
</Update>
<Update label="2026-03-26" description="v1.0.8">
**New Features & Updates:**
- **Vector Stores:** Integrated Turbopuffer as a vector database provider ([#4428](https://github.com/mem0ai/mem0/pull/4428))
- **LLMs:** Added MiniMax LLM provider ([#4431](https://github.com/mem0ai/mem0/pull/4431))
**Bug Fixes:**
- **Core:** Fixed merging of multiple filter operators for the same key ([#4559](https://github.com/mem0ai/mem0/pull/4559))
- **Core:** Prevented in-place mutation of metadata in `_create_memory` ([#4529](https://github.com/mem0ai/mem0/pull/4529))
- **Core:** Preserved custom metadata when updating memory ([#4495](https://github.com/mem0ai/mem0/pull/4495))
- **Core:** Handled chatty LLM responses in JSON parsing ([#4525](https://github.com/mem0ai/mem0/pull/4525))
- **Core:** Prevented double embedding in `mem0.add` ([#3996](https://github.com/mem0ai/mem0/pull/3996))
- **Core:** Raised `ValueError` when deleting nonexistent memory ([#4455](https://github.com/mem0ai/mem0/pull/4455))
- **Core:** Cleaned up graph store data on `Memory.delete()` ([#4505](https://github.com/mem0ai/mem0/pull/4505))
- **Vector Stores:** Prevented SQL injection in Databricks vector store ([#4558](https://github.com/mem0ai/mem0/pull/4558))
- **Vector Stores:** Upgraded MongoDB vector store from deprecated `knnVector` to GA `vectorSearch` ([#3995](https://github.com/mem0ai/mem0/pull/3995))
- **Vector Stores:** Prevented embedding corruption in Valkey and Redis when vector is `None` ([#4362](https://github.com/mem0ai/mem0/pull/4362))
- **Vector Stores:** Accepted default `/tmp/chroma` path in `ChromaDbConfig` validator ([#4179](https://github.com/mem0ai/mem0/pull/4179))
- **Vector Stores:** Wrapped vector and payload in lists for `Langchain.update` ([#4446](https://github.com/mem0ai/mem0/pull/4446))
- **Graph:** Soft-delete graph relationships instead of hard `DELETE` ([#4188](https://github.com/mem0ai/mem0/pull/4188))
- **Graph:** Sanitized hyphens in Neo4j Cypher relationship names ([#4154](https://github.com/mem0ai/mem0/pull/4154))
- **Graph:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
- **Qdrant:** Fixed `do not remove local path on init` ([#4475](https://github.com/mem0ai/mem0/pull/4475))
- **Qdrant:** Implemented enhanced metadata filtering operators ([#4127](https://github.com/mem0ai/mem0/pull/4127))
- **Embeddings:** Fixed OpenAI embedding dimensions ([#4481](https://github.com/mem0ai/mem0/pull/4481))
- **LLMs:** Omitted `topP` for Anthropic Converse in Bedrock; used `AWSBedrockConfig` in `LlmFactory` ([#4469](https://github.com/mem0ai/mem0/pull/4469))
- **LLMs:** Avoided sending both `temperature` and `top_p` to Anthropic API ([#4471](https://github.com/mem0ai/mem0/pull/4471))
- **LLMs:** Handled `None` content and empty candidates in `GeminiLLM` parsing ([#4462](https://github.com/mem0ai/mem0/pull/4462))
- **LLMs:** Added missing `_parse_response` to `AzureOpenAIStructuredLLM` ([#4434](https://github.com/mem0ai/mem0/pull/4434))
- **History:** Added timestamps for `DELETE` operations in history ([#4492](https://github.com/mem0ai/mem0/pull/4492))
**Improvements:**
- **Vector Stores:** Added vector validation to OpenSearchDB to ensure non-null, non-empty, and correct-dimension vectors ([#4472](https://github.com/mem0ai/mem0/pull/4472))
</Update>
<Update label="2026-03-19" description="v1.0.7">
**Bug Fixes:**
- **Core:** Fixed control characters in LLM JSON responses causing parse failures (#4420)
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` (#4404)
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch (#4418)
- **Core:** Normalized malformed LLM fact output before embedding (#4224)
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility (#4058)
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response (#4176)
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers (#4405)
- **Vector Stores:** Cast `vector_distance` to float in Redis search (#4377)
- **Core:** Fixed control characters in LLM JSON responses causing parse failures ([#4420](https://github.com/mem0ai/mem0/pull/4420))
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` ([#4404](https://github.com/mem0ai/mem0/pull/4404))
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch ([#4418](https://github.com/mem0ai/mem0/pull/4418))
- **Core:** Normalized malformed LLM fact output before embedding ([#4224](https://github.com/mem0ai/mem0/pull/4224))
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility ([#4058](https://github.com/mem0ai/mem0/pull/4058))
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response ([#4176](https://github.com/mem0ai/mem0/pull/4176))
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers ([#4405](https://github.com/mem0ai/mem0/pull/4405))
- **Vector Stores:** Cast `vector_distance` to float in Redis search ([#4377](https://github.com/mem0ai/mem0/pull/4377))
**Improvements:**
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403)
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling ([#4403](https://github.com/mem0ai/mem0/pull/4403))
</Update>
@@ -763,6 +817,27 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2026-03-28" description="v2.4.4">
**Bug Fixes:**
- **OSS:** Fixed Qdrant Cloud "Illegal host" error by defaulting to port 6333 when URL has no explicit port ([#4565](https://github.com/mem0ai/mem0/pull/4565))
</Update>
<Update label="2026-03-26" description="v2.4.3">
**New Features & Updates:**
- **OSS:** Added pgvector support to NodeJS OSS `VectorStoreFactory` ([#3997](https://github.com/mem0ai/mem0/pull/3997))
**Bug Fixes:**
- **OSS:** Made pgvector `pg` import compatible with ESM ([#4544](https://github.com/mem0ai/mem0/pull/4544))
- **OSS:** Registered pgvector in `VectorStoreFactory` ([#4502](https://github.com/mem0ai/mem0/pull/4502))
- **OSS:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
- **OSS:** Fixed `toCamelCase` in Redis `get` method for the payload ([#3172](https://github.com/mem0ai/mem0/pull/3172))
- **Client:** Fixed Zod Schema incompatibility with OpenAI Structured Outputs API ([#3462](https://github.com/mem0ai/mem0/pull/3462))
</Update>
<Update label="2026-03-19" description="v2.4.2">
**Bug Fixes:**
@@ -19,7 +19,7 @@ config = {
"embedder": {
"provider": "gemini",
"config": {
"model": "models/text-embedding-004",
"model": "models/gemini-embedding-001",
}
}
}
@@ -66,7 +66,7 @@ Here are the parameters available for configuring Gemini embedder:
<Tab title="Python">
| Parameter | Description | Default Value |
| ---------------- | ------------------------------------ | ----------------------- |
| `model` | The name of the embedding model to use| `models/text-embedding-004` |
| `model` | The name of the embedding model to use| `models/gemini-embedding-001` |
| `embedding_dims` | Dimensions of the embedding model | `1536` |
| `api_key` | The Google API key | `None` |
</Tab>
@@ -20,7 +20,7 @@ config = {
"embedder": {
"provider": "vertexai",
"config": {
"model": "text-embedding-004",
"model": "gemini-embedding-001",
"memory_add_embedding_type": "RETRIEVAL_DOCUMENT",
"memory_update_embedding_type": "RETRIEVAL_DOCUMENT",
"memory_search_embedding_type": "RETRIEVAL_QUERY"
@@ -51,7 +51,7 @@ Here are the parameters available for configuring the Vertex AI embedder:
| Parameter | Description | Default Value |
| ------------------------- | ------------------------------------------------ | -------------------- |
| `model` | The name of the Vertex AI embedding model to use | `text-embedding-004` |
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
| `embedding_dims` | Dimensions of the embedding model | `256` |
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | `RETRIEVAL_DOCUMENT` |
+67 -87
View File
@@ -7,58 +7,51 @@ When using LLM rerankers, you can customize the prompts used for ranking to bett
## Default Prompt
The default LLM reranker prompt is designed to be general-purpose:
The default LLM reranker prompt scores each memory individually on a 0.0-1.0 scale:
```
Given a query and a list of memory entries, rank the memory entries based on their relevance to the query.
Rate each memory on a scale of 1-10 where 10 is most relevant.
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
Query: {query}
Score the relevance on a scale from 0.0 to 1.0, where:
- 1.0 = Perfectly relevant and directly answers the query
- 0.8-0.9 = Highly relevant with good information
- 0.6-0.7 = Moderately relevant with some useful information
- 0.4-0.5 = Slightly relevant with limited useful information
- 0.0-0.3 = Not relevant or no useful information
Memory entries:
{memories}
Query: "{query}"
Document: "{document}"
Provide your ranking as a JSON array with scores for each memory.
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
```
## Custom Prompt Configuration
You can provide a custom prompt template when configuring the LLM reranker:
You can provide a custom prompt template using the `scoring_prompt` parameter:
```python
from mem0 import Memory
custom_prompt = """
You are an expert at ranking memories for a personal AI assistant.
Given a user query and a list of memory entries, rank each memory based on:
1. Direct relevance to the query
2. Temporal relevance (recent memories may be more important)
3. Emotional significance
4. Actionability
You are an expert at evaluating memories for a personal AI assistant.
Given a user query and a memory entry, score how relevant the memory is.
Consider direct relevance, temporal relevance, and actionability.
Query: {query}
User Context: {user_context}
Query: "{query}"
Memory: "{document}"
Memory entries:
{memories}
Rate each memory from 1-10 and provide reasoning.
Return as JSON: {{"rankings": [{{"index": 0, "score": 8, "reason": "..."}}]}}
Provide only a single numerical score between 0.0 and 1.0.
"""
config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4.1-nano-2025-04-14",
"api_key": "your-openai-key"
}
},
"custom_prompt": custom_prompt,
"top_n": 5
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-openai-key",
"scoring_prompt": custom_prompt,
"top_k": 5
}
}
}
@@ -70,12 +63,14 @@ memory = Memory.from_config(config)
Your custom prompt can use the following variables:
| Variable | Description |
| ---------------- | ------------------------------------- |
| `{query}` | The search query |
| `{memories}` | The list of memory entries to rank |
| `{user_id}` | The user ID (if available) |
| `{user_context}` | Additional user context (if provided) |
| Variable | Description |
| ------------ | ----------------------------- |
| `{query}` | The search query |
| `{document}` | The memory entry being scored |
<Note>
Both `{query}` and `{document}` are required in your custom prompt. The LLM reranker scores each memory individually against the query, so the prompt is called once per candidate memory.
</Note>
## Domain-Specific Examples
@@ -89,13 +84,10 @@ Prioritize memories that:
- Show previous resolution patterns
- Indicate customer preferences or constraints
Query: {query}
Customer Context: Previous interactions with this customer
Query: "{query}"
Memory: "{document}"
Memories:
{memories}
Rank each memory 1-10 based on support relevance.
Score relevance from 0.0 to 1.0.
"""
```
@@ -103,19 +95,16 @@ Rank each memory 1-10 based on support relevance.
```python
educational_prompt = """
Rank these learning memories for a student query.
Score this learning memory for relevance to a student query.
Consider:
- Prerequisite knowledge requirements
- Learning progression and difficulty
- Relevance to current learning objectives
Student Query: {query}
Learning Context: {user_context}
Student Query: "{query}"
Memory: "{document}"
Available memories:
{memories}
Score each memory for educational value (1-10).
Score educational relevance from 0.0 to 1.0.
"""
```
@@ -123,73 +112,64 @@ Score each memory for educational value (1-10).
```python
personal_assistant_prompt = """
Rank personal memories for relevance to the user's query.
Score this personal memory for relevance to the user's query.
Consider:
- Recent vs. historical importance
- Personal preferences and habits
- Contextual relationships between memories
- Contextual relationships
Query: {query}
Personal context: {user_context}
Query: "{query}"
Memory: "{document}"
Memories to rank:
{memories}
Provide relevance scores (1-10) with brief explanations.
Provide relevance score from 0.0 to 1.0.
"""
```
## Advanced Prompt Techniques
### Multi-Criteria Ranking
### Multi-Criteria Scoring
```python
multi_criteria_prompt = """
Evaluate memories using multiple criteria:
Evaluate this memory using multiple criteria:
1. RELEVANCE (40%): How directly related to the query
2. RECENCY (20%): How recent the memory is
2. RECENCY (20%): How recent the memory appears to be
3. IMPORTANCE (25%): Personal or business significance
4. ACTIONABILITY (15%): How useful for next steps
Query: {query}
Context: {user_context}
Query: "{query}"
Memory: "{document}"
Memories:
{memories}
For each memory, provide:
- Overall score (1-10)
- Breakdown by criteria
- Final ranking recommendation
Format: JSON with detailed scoring
Compute a weighted score from 0.0 to 1.0 based on these criteria.
Provide only the final numerical score.
"""
```
### Contextual Ranking
### Chain-of-Thought Scoring
```python
contextual_prompt = """
Consider the following context when ranking memories:
- Current user situation: {user_context}
- Time of day: {current_time}
- Recent activities: {recent_activities}
reasoning_prompt = """
Evaluate this memory's relevance step by step:
Query: {query}
1. What is the main intent of the query?
2. What key information does the memory contain?
3. How directly does the memory address the query?
Rank these memories considering both direct relevance and contextual appropriateness:
{memories}
Based on this analysis, provide a single relevance score from 0.0 to 1.0.
Provide contextually-aware relevance scores (1-10).
Query: "{query}"
Memory: "{document}"
Score:
"""
```
## Best Practices
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
2. **Use Examples**: Include examples in your prompt for better model understanding
3. **Structure Output**: Specify the exact JSON format you want returned
2. **Use 0.0-1.0 Scale**: The score extractor expects values between 0.0 and 1.0
3. **Request Only the Score**: Ask for just the numerical score to improve extraction reliability
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
@@ -206,7 +186,7 @@ prompts = [
]
for i, prompt in enumerate(prompts):
config["reranker"]["config"]["custom_prompt"] = prompt
config["reranker"]["config"]["scoring_prompt"] = prompt
memory = Memory.from_config(config)
results = memory.search("test query", user_id="test_user")
@@ -216,6 +196,6 @@ for i, prompt in enumerate(prompts):
## Common Issues
- **Too Long**: Keep prompts under token limits for your chosen LLM
- **Too Vague**: Be specific about ranking criteria
- **Inconsistent Format**: Ensure JSON output format is clearly specified
- **Missing Context**: Include relevant variables for your use case
- **Too Vague**: Be specific about scoring criteria
- **Wrong Scale**: Use 0.0-1.0 scale to match the default score extractor
- **Extra Output**: Ask for only the numeric score — extra text can confuse score extraction
+101 -132
View File
@@ -18,13 +18,9 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"api_key": "your-openai-api-key"
}
}
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-openai-api-key"
}
}
}
@@ -36,11 +32,14 @@ m = Memory.from_config(config)
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `llm` | dict | Required | LLM configuration object |
| `top_k` | int | 10 | Number of results to rerank |
| `provider` | str | `"openai"` | LLM provider (openai, anthropic, etc.) |
| `model` | str | `"gpt-4o-mini"` | LLM model to use for reranking |
| `api_key` | str | None | API key for the LLM provider |
| `top_k` | int | None | Number of top documents to return after reranking |
| `temperature` | float | 0.0 | LLM temperature for consistency |
| `custom_prompt` | str | None | Custom reranking prompt |
| `score_range` | tuple | (0, 10) | Score range for relevance |
| `max_tokens` | int | 100 | Maximum tokens for LLM response |
| `scoring_prompt` | str | None | Custom prompt template for scoring documents |
| `llm` | dict | None | Optional nested LLM config for provider-specific fields (e.g., `ollama_base_url`, `azure_endpoint`). Overrides top-level `provider`/`model`/`api_key` when provided. |
### Advanced Configuration
@@ -49,20 +48,19 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "anthropic",
"config": {
"model": "claude-3-sonnet-20240229",
"api_key": "your-anthropic-api-key"
}
},
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"api_key": "your-anthropic-api-key",
"top_k": 15,
"temperature": 0.0,
"score_range": (1, 5),
"custom_prompt": """
Rate the relevance of each memory to the query on a scale of 1-5.
"scoring_prompt": """
Rate the relevance of each memory to the query on a scale of 0.0-1.0.
Consider semantic similarity, context, and practical utility.
Only provide the numeric score.
Query: "{query}"
Document: "{document}"
Score:
"""
}
}
@@ -78,14 +76,10 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"api_key": "your-openai-api-key",
"temperature": 0.0
}
}
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-openai-api-key",
"temperature": 0.0
}
}
}
@@ -98,13 +92,9 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "anthropic",
"config": {
"model": "claude-3-sonnet-20240229",
"api_key": "your-anthropic-api-key"
}
}
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"api_key": "your-anthropic-api-key"
}
}
}
@@ -117,10 +107,12 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"provider": "ollama",
"model": "llama3.2",
"llm": {
"provider": "ollama",
"config": {
"model": "llama2",
"model": "llama3.2",
"ollama_base_url": "http://localhost:11434"
}
}
@@ -129,6 +121,10 @@ config = {
}
```
<Note>
For providers like Ollama that need extra fields (e.g., `ollama_base_url`), use the optional nested `llm` key to pass provider-specific configuration. The nested `llm` config overrides top-level `provider`/`model`/`api_key` when provided.
</Note>
### Azure OpenAI
```python
@@ -136,13 +132,16 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"provider": "azure_openai",
"model": "gpt-4o-mini",
"api_key": "your-azure-api-key",
"llm": {
"provider": "azure_openai",
"config": {
"model": "gpt-4",
"model": "gpt-4o-mini",
"api_key": "your-azure-api-key",
"azure_endpoint": "https://your-resource.openai.azure.com/",
"azure_deployment": "gpt-4-deployment"
"azure_deployment": "gpt-4o-mini-deployment"
}
}
}
@@ -154,16 +153,22 @@ config = {
### Default Prompt Behavior
The default prompt asks the LLM to score relevance on a 0-10 scale:
The default prompt asks the LLM to score relevance on a 0.0-1.0 scale:
```
Given a query and a memory, rate how relevant the memory is to answering the query.
Score from 0 (completely irrelevant) to 10 (perfectly relevant).
Only provide the numeric score.
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
Query: {query}
Memory: {memory}
Score:
Score the relevance on a scale from 0.0 to 1.0, where:
- 1.0 = Perfectly relevant and directly answers the query
- 0.8-0.9 = Highly relevant with good information
- 0.6-0.7 = Moderately relevant with some useful information
- 0.4-0.5 = Slightly relevant with limited useful information
- 0.0-0.3 = Not relevant or no useful information
Query: "{query}"
Document: "{document}"
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
```
### Custom Prompt Examples
@@ -174,14 +179,14 @@ Score:
custom_prompt = """
You are a medical information specialist. Rate how relevant each memory is for answering the medical query.
Consider clinical accuracy, specificity, and practical applicability.
Rate from 1-10 where:
- 1-3: Irrelevant or potentially harmful
- 4-6: Somewhat relevant but incomplete
- 7-8: Relevant and helpful
- 9-10: Highly relevant and clinically useful
Rate from 0.0 to 1.0 where:
- 0.0-0.3: Irrelevant or potentially harmful
- 0.4-0.6: Somewhat relevant but incomplete
- 0.7-0.8: Relevant and helpful
- 0.9-1.0: Highly relevant and clinically useful
Query: {query}
Memory: {memory}
Query: "{query}"
Document: "{document}"
Score:
"""
@@ -189,14 +194,10 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"api_key": "your-api-key"
}
},
"custom_prompt": custom_prompt
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-api-key",
"scoring_prompt": custom_prompt
}
}
}
@@ -213,15 +214,15 @@ Consider:
- Recency and accuracy
- Practical usefulness
Rate 1-5:
1 = Not relevant
2 = Slightly relevant
3 = Moderately relevant
4 = Very relevant
5 = Perfectly answers the question
Rate 0.0-1.0:
0.0 = Not relevant
0.25 = Slightly relevant
0.5 = Moderately relevant
0.75 = Very relevant
1.0 = Perfectly answers the question
Query: {query}
Memory: {memory}
Query: "{query}"
Document: "{document}"
Score:
"""
```
@@ -239,13 +240,17 @@ Consider:
- Factual accuracy
- Conversation flow
Rate 0-10:
Query: {query}
Memory: {memory}
Rate 0.0-1.0:
Query: "{query}"
Document: "{document}"
Score:
"""
```
<Note>
Custom prompts must include `{query}` and `{document}` placeholders. The LLM response should contain a numerical score which is automatically extracted.
</Note>
## Usage Examples
### Basic Usage
@@ -295,9 +300,9 @@ results = safe_llm_rerank_search("What are my preferences?", "alice")
| Model Type | Speed | Quality | Cost | Best For |
|------------|-------|---------|------|----------|
| GPT-3.5 Turbo | Fast | Good | Low | High-volume applications |
| GPT-4 | Medium | Excellent | Medium | Quality-critical applications |
| Claude 3 Sonnet | Medium | Excellent | Medium | Balanced performance |
| GPT-4o mini | Fast | Good | Low | High-volume applications |
| GPT-4o | Medium | Excellent | Medium | Quality-critical applications |
| Claude Sonnet | Medium | Excellent | Medium | Balanced performance |
| Ollama Local | Variable | Good | Free | Privacy-sensitive applications |
### Optimization Strategies
@@ -308,14 +313,10 @@ fast_config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-3.5-turbo",
"api_key": "your-api-key"
}
},
"top_k": 5, # Limit candidates
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-api-key",
"top_k": 5,
"temperature": 0.0
}
}
@@ -326,13 +327,9 @@ quality_config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"api_key": "your-api-key"
}
},
"provider": "openai",
"model": "gpt-4o",
"api_key": "your-api-key",
"top_k": 15,
"temperature": 0.0
}
@@ -353,10 +350,10 @@ Evaluate this memory's relevance using multi-step reasoning:
3. How directly does the memory address the query?
4. What additional context might be needed?
Based on this analysis, rate relevance 1-10:
Based on this analysis, rate relevance 0.0-1.0:
Query: {query}
Memory: {memory}
Query: "{query}"
Document: "{document}"
Analysis:
Step 1 (Intent):
@@ -367,42 +364,6 @@ Final Score:
"""
```
### Comparative Ranking
```python
comparative_prompt = """
You will see a query and multiple memories. Rank them in order of relevance.
Consider which memories best answer the question and would be most helpful.
Query: {query}
Memories to rank:
{memories}
Provide scores 1-10 for each memory, considering their relative usefulness.
"""
```
### Emotional Intelligence
```python
emotional_prompt = """
Consider both factual relevance and emotional appropriateness.
Rate how suitable this memory is for responding to the user's query.
Factors to consider:
- Factual accuracy and relevance
- Emotional tone and sensitivity
- User's likely emotional state
- Appropriateness of response
Query: {query}
Memory: {memory}
Emotional Context: {context}
Score (1-10):
"""
```
## Error Handling and Fallbacks
```python
@@ -433,14 +394,22 @@ class RobustLLMReranker:
primary_config = {
"reranker": {
"provider": "llm_reranker",
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-4"}}}
"config": {
"provider": "openai",
"model": "gpt-4o",
"api_key": "your-api-key"
}
}
}
fallback_config = {
"reranker": {
"provider": "llm_reranker",
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-3.5-turbo"}}}
"config": {
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-api-key"
}
}
}
@@ -486,4 +455,4 @@ results = reranker.search("What are my preferences?", "alice")
<Card title="Performance Optimization" icon="bolt" href="/components/rerankers/optimization">
Optimize LLM reranker performance
</Card>
</CardGroup>
</CardGroup>
@@ -10,6 +10,32 @@ Problem: LLMs are stateless. GPT doesn't remember conversations. You could stuff
The solution: Mem0. It extracts and stores what matters from conversations, then retrieves it when needed. Your companion remembers user preferences, past events, and history.
<Tabs>
<Tab title="Platform">
</Tab>
<Tab title="Open Source">
Here we use **Mem0 open source** (`Memory`): all local, no API keys needed for memory. Vectors in **Qdrant**, LLM and embeddings via **Ollama**. The **OpenAI** Python SDK calls Ollama's **OpenAI-compatible** `/v1` endpoint for Ray's chat replies.
## Installation
Install the required dependencies:
```bash
pip install mem0ai qdrant-client openai ollama
```
Then start Qdrant and pull the Ollama models:
```bash
docker run -d -p 6333:6333 qdrant/qdrant
ollama pull llama3.1:latest
ollama pull nomic-embed-text:latest
```
<Note>You can swap `nomic-embed-text` for any Ollama-supported embedding model (e.g., `snowflake-arctic-embed`, `mxbai-embed-large`). Just update the `model` in the `embedder` config and set `embedding_model_dims` in the Qdrant config to match the model's output dimensions (768 for `nomic-embed-text`).</Note>
</Tab>
</Tabs>
In this cookbook we'll build a **fitness companion** that:
- Remembers user goals across sessions
@@ -25,6 +51,8 @@ By the end, you'll have a working fitness companion and know how to handle commo
Max wants to train for a marathon. He starts chatting with Ray, an AI running coach.
<Tabs>
<Tab title="Platform">
```python
from openai import OpenAI
from mem0 import MemoryClient
@@ -53,8 +81,73 @@ def chat(user_input, user_id):
], user_id=user_id)
return response
```
</Tab>
<Tab title="Open Source">
```python
from openai import OpenAI
from mem0 import Memory
OLLAMA_URL = "http://localhost:11434"
CHAT_MODEL = "llama3.1:latest"
memory = Memory.from_config({
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "fitness_companion",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768,
},
},
"llm": {
"provider": "ollama",
"config": {
"model": CHAT_MODEL,
"temperature": 0,
"max_tokens": 2000,
"ollama_base_url": OLLAMA_URL,
},
},
"embedder": {
"provider": "ollama",
"config": {
"model": "nomic-embed-text:latest",
"ollama_base_url": OLLAMA_URL,
},
},
})
ollama_chat = OpenAI(base_url=f"{OLLAMA_URL}/v1", api_key="ollama")
def chat(user_input, user_id):
# Retrieve relevant memories
memories = memory.search(user_input, user_id=user_id, limit=5)
context = "\n".join(m["memory"] for m in memories["results"])
# Call LLM with memory context (Ollama via OpenAI-compatible API)
response = ollama_chat.chat.completions.create(
model=CHAT_MODEL,
messages=[
{"role": "system", "content": f"You're Ray, a running coach. Memories:\n{context}"},
{"role": "user", "content": user_input},
],
).choices[0].message.content
# Store the exchange
memory.add(
[
{"role": "user", "content": user_input},
{"role": "assistant", "content": response},
],
user_id=user_id,
)
return response
```
</Tab>
</Tabs>
**Session 1:**
@@ -62,7 +155,6 @@ def chat(user_input, user_id):
chat("I want to run a marathon in under 4 hours", user_id="max")
# Output: "That's a solid goal. What's your current weekly mileage?"
# Stored in Mem0: "Max wants to run sub-4 marathon"
```
**Session 2 (next day, app restarted):**
@@ -70,7 +162,6 @@ chat("I want to run a marathon in under 4 hours", user_id="max")
```python
chat("What should I focus on today?", user_id="max")
# Output: "Based on your sub-4 marathon goal, let's work on building your aerobic base..."
```
<Info>
@@ -87,6 +178,8 @@ Ray remembers. Restart the app, and the goal persists. From here on, we'll focus
Max mentions his knee hurts. That's different from his marathon goal - one is temporary, the other is long-term.
<Tabs>
<Tab title="Platform">
**Categories vs Metadata:**
- **Categories**: AI-assigned by Mem0 based on content (you can't force them)
@@ -100,7 +193,6 @@ mem0_client.project.update(custom_categories=[
{"constraints": "Injuries, limitations, recovery needs"},
{"preferences": "Training style, surfaces, schedules"}
])
```
<Note>
@@ -121,7 +213,6 @@ mem0_client.add(
[{"role": "user", "content": "My right knee flares up on downhills"}],
user_id="max"
)
```
Mem0 reads the content and intelligently picks which categories apply. You define the palette, it handles the tagging.
@@ -135,13 +226,50 @@ mem0_client.add(
user_id="max",
metadata={"workout_type": "speed", "forced_tag": "custom_label"}
)
```
</Tab>
<Tab title="Open Source">
**Categories via Metadata:**
In open source, model categories with a stable field in `metadata`—here we use `memory_bucket`:
```python
# Add goal
memory.add(
[{"role": "user", "content": "Sub-4 marathon is my A-race"}],
user_id="max",
metadata={"memory_bucket": "goals"},
)
# Add constraint
memory.add(
[{"role": "user", "content": "My right knee flares up on downhills"}],
user_id="max",
metadata={"memory_bucket": "constraints"},
)
```
<Note>
**Categories vs Metadata:** In open source, categories are modeled as `metadata` fields you set on each `add`. Filters only see what you put on `add`.
</Note>
```python
# Force tag using metadata
memory.add(
[{"role": "user", "content": "Some workout note"}],
user_id="max",
metadata={"memory_bucket": "goals", "workout_type": "speed", "forced_tag": "custom_label"},
)
```
</Tab>
</Tabs>
### Filtering by Category
Retrieve just constraints for workout planning:
<Tabs>
<Tab title="Platform">
```python
constraints = mem0_client.search(
query="injury concerns",
@@ -155,8 +283,21 @@ constraints = mem0_client.search(
)
print([m["memory"] for m in constraints["results"]])
# Output: ["Max's right knee flares up on downhills"]
```
</Tab>
<Tab title="Open Source">
```python
constraints = memory.search(
query="injury concerns",
user_id="max",
filters={"memory_bucket": {"in": ["constraints"]}},
threshold=0.0 # optional: widen recall for short phrases
)
print([m["memory"] for m in constraints["results"]])
# Output: ["Max's right knee flares up on downhills"]
```
</Tab>
</Tabs>
Ray can plan workouts that avoid aggravating Max's knee, without pulling in race goals or other unrelated memories.
@@ -168,12 +309,22 @@ Ray can plan workouts that avoid aggravating Max's knee, without pulling in race
Run the basic loop for a week and check what's stored:
<Tabs>
<Tab title="Platform">
```python
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
print([m["memory"] for m in memories["results"]])
# Output: ["Max wants to run marathon under 4 hours", "hey", "lol ok", "cool thanks", "gtg bye"]
```
</Tab>
<Tab title="Open Source">
```python
memories = memory.get_all(user_id="max")
print([m["memory"] for m in memories["results"]])
# Output: ["Max wants to run marathon under 4 hours", "hey", "lol ok", "cool thanks", "gtg bye"]
```
</Tab>
</Tabs>
<Warning>
Without filters, Mem0 stores everything—greetings, filler, and casual chat. This pollutes retrieval: instead of pulling "marathon goal," you get "lol ok." Set custom instructions to keep memory clean.
@@ -183,6 +334,8 @@ Noise. Greetings and filler clutter the memory.
### Custom Instructions
<Tabs>
<Tab title="Platform">
Tell Mem0 what matters:
```python
@@ -198,11 +351,38 @@ Exclude:
- Casual chatter
- Hypotheticals unless planning related
""")
```
</Tab>
<Tab title="Open Source">
Tell Mem0 what matters by including `custom_fact_extraction_prompt` in the config dict:
```python
MEMORY_CONFIG["custom_fact_extraction_prompt"] = """
Extract from running coach conversations:
- Training goals and race targets
- Physical constraints or injuries
- Training preferences (time of day, surfaces, weather)
- Progress milestones
Exclude:
- Greetings and filler
- Casual chatter
- Hypotheticals unless planning related
Return JSON with key "facts" as a list of strings (use [] if nothing to store).
"""
memory = Memory.from_config(MEMORY_CONFIG)
```
<Note>`custom_fact_extraction_prompt` is a top-level key in the config dictionary passed to `Memory.from_config()`. Make sure it's set before creating the Memory instance — not after.</Note>
</Tab>
</Tabs>
Now chat again:
<Tabs>
<Tab title="Platform">
```python
chat("hey how's it going", user_id="max")
chat("I prefer trail running over roads", user_id="max")
@@ -210,8 +390,19 @@ chat("I prefer trail running over roads", user_id="max")
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
print([m["memory"] for m in memories["results"]])
# Output: ["Max wants to run marathon under 4 hours", "Max prefers trail running over roads"]
```
</Tab>
<Tab title="Open Source">
```python
chat("hey how's it going", user_id="max")
chat("I prefer trail running over roads", user_id="max")
memories = memory.get_all(user_id="max")
print([m["memory"] for m in memories["results"]])
# Output: ["Max wants to run marathon under 4 hours", "Max prefers trail running over roads"]
```
</Tab>
</Tabs>
<Info>
**Expected output:** Only 2 memories stored—the marathon goal and trail preference. The greeting "hey how's it going" was filtered out automatically. Custom instructions are working.
@@ -221,8 +412,6 @@ Only meaningful facts. Filler gets dropped automatically.
---
---
## Agent Memory for Personality
### Why Agents Need Memory Too
@@ -231,16 +420,30 @@ Max prefers direct feedback, not motivational fluff. Ray needs to remember how t
Store agent personality:
<Tabs>
<Tab title="Platform">
```python
mem0_client.add(
[{"role": "system", "content": "Max wants direct, data-driven feedback. Skip motivational language."}],
agent_id="ray_coach"
)
```
</Tab>
<Tab title="Open Source">
```python
memory.add(
[{"role": "user", "content": "Max wants direct, data-driven feedback. Skip motivational language."}],
agent_id="ray_coach",
infer=False,
)
```
</Tab>
</Tabs>
Retrieve agent style alongside user memories:
<Tabs>
<Tab title="Platform">
```python
# Get coach personality
agent_memories = mem0_client.search("coaching style", agent_id="ray_coach")
@@ -251,8 +454,26 @@ mem0_client.add([
{"role": "user", "content": "How'd my run look today?"},
{"role": "assistant", "content": "Pace was 8:15/mile. Heart rate 152, zone 2."}
], user_id="max", agent_id="ray_coach")
```
</Tab>
<Tab title="Open Source">
```python
# Get coach personality
agent_memories = memory.search("coaching style", agent_id="ray_coach")
# Output: ["Max wants direct, data-driven feedback. Skip motivational language."]
# Store conversations with agent_id
memory.add(
[
{"role": "user", "content": "How'd my run look today?"},
{"role": "assistant", "content": "Pace was 8:15/mile. Heart rate 152, zone 2."},
],
user_id="max",
agent_id="ray_coach",
)
```
</Tab>
</Tabs>
<Info>
**Expected behavior:** Ray's responses are now data-driven and direct. The agent memory stored the coaching style preference, so future responses adapt automatically without Max having to repeat his preference.
@@ -268,6 +489,8 @@ No "Great job!" or "Keep it up!" - just data. Ray adapts to Max's preference.
Don't send every single message to Mem0. Keep recent context in memory, let Mem0 handle the important long-term facts.
<Tabs>
<Tab title="Platform">
```python
# Store only meaningful exchanges in Mem0
mem0_client.add([
@@ -280,8 +503,27 @@ mem0_client.add([
# "cool thanks" → don't store
# Or rely on custom_instructions to filter automatically
```
</Tab>
<Tab title="Open Source">
```python
# Store only meaningful exchanges in Mem0
memory.add(
[
{"role": "user", "content": "I want to run a marathon"},
{"role": "assistant", "content": "Let's build a training plan"},
],
user_id="max",
)
# Skip storing filler
# "hey" → don't store
# "cool thanks" → don't store
# Or rely on custom_fact_extraction_prompt to filter automatically
```
</Tab>
</Tabs>
Last 10 messages in your app's buffer. Important facts in Mem0. Faster, cheaper, still works.
@@ -293,6 +535,8 @@ Last 10 messages in your app's buffer. Important facts in Mem0. Faster, cheaper,
Max tweaks his ankle. It'll heal in two weeks - the memory should expire too.
<Tabs>
<Tab title="Platform">
```python
from datetime import datetime, timedelta
@@ -303,10 +547,26 @@ mem0_client.add(
user_id="max",
expiration_date=expiration
)
```
In 14 days, this memory disappears automatically. Ray stops asking about the ankle.
</Tab>
<Tab title="Open Source">
```python
from datetime import datetime, timedelta
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
memory.add(
[{"role": "user", "content": "Rolled my left ankle, needs rest"}],
user_id="max",
metadata={"memory_bucket": "constraints", "expires_on": expiration},
)
```
Store `expires_on` in metadata and prune expired memories in your app. Ray stops asking about the ankle once it's removed.
</Tab>
</Tabs>
---
@@ -314,6 +574,8 @@ In 14 days, this memory disappears automatically. Ray stops asking about the ank
Here's the Mem0 setup combining everything:
<Tabs>
<Tab title="Platform">
```python
from mem0 import MemoryClient
from datetime import datetime, timedelta
@@ -332,11 +594,55 @@ mem0_client.project.update(
{"name": "preferences", "description": "Training style"}
]
)
```
</Tab>
<Tab title="Open Source">
```python
from mem0 import Memory
from datetime import datetime, timedelta
MEMORY_CONFIG = {
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "fitness_companion",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768,
},
},
"llm": {
"provider": "ollama",
"config": {
"model": "llama3.1:latest",
"temperature": 0,
"max_tokens": 2000,
"ollama_base_url": "http://localhost:11434",
},
},
"embedder": {
"provider": "ollama",
"config": {
"model": "nomic-embed-text:latest",
"ollama_base_url": "http://localhost:11434",
},
},
"custom_fact_extraction_prompt": """
Extract: goals, constraints, preferences, progress
Exclude: greetings, filler, casual chat
Return JSON with key "facts" as a list of strings.
""",
}
memory = Memory.from_config(MEMORY_CONFIG)
```
</Tab>
</Tabs>
**Week 1 - Store goals and preferences:**
<Tabs>
<Tab title="Platform">
```python
mem0_client.add([
{"role": "user", "content": "I want to run a sub-4 marathon"},
@@ -346,11 +652,33 @@ mem0_client.add([
mem0_client.add([
{"role": "user", "content": "I prefer trail running over roads"}
], user_id="max", categories=["preferences"])
```
</Tab>
<Tab title="Open Source">
```python
memory.add(
[
{"role": "user", "content": "I want to run a sub-4 marathon"},
{"role": "assistant", "content": "Got it. Let's build a training plan."},
],
user_id="max",
agent_id="ray",
metadata={"memory_bucket": "goals"},
)
memory.add(
[{"role": "user", "content": "I prefer trail running over roads"}],
user_id="max",
metadata={"memory_bucket": "preferences"},
)
```
</Tab>
</Tabs>
**Week 3 - Temporary injury with expiration:**
<Tabs>
<Tab title="Platform">
```python
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
mem0_client.add(
@@ -359,16 +687,36 @@ mem0_client.add(
categories=["constraints"],
expiration_date=expiration
)
```
</Tab>
<Tab title="Open Source">
```python
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
memory.add(
[{"role": "user", "content": "Rolled ankle, need light workouts"}],
user_id="max",
metadata={"memory_bucket": "constraints", "expires_on": expiration},
)
```
</Tab>
</Tabs>
**Retrieve for context:**
<Tabs>
<Tab title="Platform">
```python
memories = mem0_client.search("training plan", user_id="max", limit=5)
# Gets: marathon goal, trail preference, ankle injury (if still valid)
```
</Tab>
<Tab title="Open Source">
```python
memories = memory.search("training plan", user_id="max", limit=5)
# Gets: marathon goal, trail preference, ankle injury (if still valid / not pruned)
```
</Tab>
</Tabs>
Ray remembers goals, preferences, and personality. Handles temporary injuries. Works across sessions.
@@ -380,6 +728,8 @@ Ray remembers goals, preferences, and personality. Handles temporary injuries. W
Training for Boston is different from training for New York. Separate the memory threads:
<Tabs>
<Tab title="Platform">
```python
mem0_client.add(messages, user_id="max", run_id="boston-2025")
mem0_client.add(messages, user_id="max", run_id="nyc-2025")
@@ -390,8 +740,22 @@ boston_memories = mem0_client.search(
user_id="max",
run_id="boston-2025"
)
```
</Tab>
<Tab title="Open Source">
```python
memory.add(messages, user_id="max", run_id="boston-2025")
memory.add(messages, user_id="max", run_id="nyc-2025")
# Retrieve only Boston memories
boston_memories = memory.search(
"training plan",
user_id="max",
run_id="boston-2025",
)
```
</Tab>
</Tabs>
Each race gets its own episodic boundary. No cross-contamination.
@@ -399,6 +763,8 @@ Each race gets its own episodic boundary. No cross-contamination.
Max has 6 months of training logs to backfill:
<Tabs>
<Tab title="Platform">
```python
old_logs = [
[{"role": "user", "content": "Completed 20-mile long run"}],
@@ -407,13 +773,27 @@ old_logs = [
for log in old_logs:
mem0_client.add(log, user_id="max")
```
</Tab>
<Tab title="Open Source">
```python
old_logs = [
[{"role": "user", "content": "Completed 20-mile long run"}],
[{"role": "user", "content": "Hit 8:00 pace on tempo run"}],
]
for log in old_logs:
memory.add(log, user_id="max")
```
</Tab>
</Tabs>
### Handling Contradictions
Max changes his goal from sub-4 to sub-3:45:
<Tabs>
<Tab title="Platform">
```python
# Find the old memory
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
@@ -421,8 +801,19 @@ goal_memory = [m for m in memories["results"] if "sub-4" in m["memory"]][0]
# Update it
mem0_client.update(goal_memory["id"], "Max wants to run sub-3:45 marathon")
```
</Tab>
<Tab title="Open Source">
```python
# Find the old memory
memories = memory.get_all(user_id="max")
goal_memory = [m for m in memories["results"] if "sub-4" in m["memory"]][0]
# Update it
memory.update(goal_memory["id"], "Max wants to run sub-3:45 marathon")
```
</Tab>
</Tabs>
Update instead of creating duplicates.
@@ -430,11 +821,20 @@ Update instead of creating duplicates.
Max works with Ray for running and Jordan for strength training:
<Tabs>
<Tab title="Platform">
```python
chat("easy run today", user_id="max", agent_id="ray")
chat("leg day workout", user_id="max", agent_id="jordan")
```
</Tab>
<Tab title="Open Source">
```python
chat("easy run today", user_id="max", agent_id="ray")
chat("leg day workout", user_id="max", agent_id="jordan")
```
</Tab>
</Tabs>
Each coach maintains separate personality memory while sharing user context.
@@ -442,19 +842,44 @@ Each coach maintains separate personality memory while sharing user context.
Prioritize recent training over old data:
<Tabs>
<Tab title="Platform">
```python
recent = mem0_client.search(
"training progress",
user_id="max",
filters={"created_at": {"gte": "2025-10-01"}}
)
```
</Tab>
<Tab title="Open Source">
```python
# Qdrant range filters require numbers — store an epoch timestamp in metadata
from datetime import datetime
epoch = int(datetime(2025, 10, 15).timestamp())
memory.add(
[{"role": "user", "content": "Completed 18-mile long run"}],
user_id="max",
metadata={"logged_epoch": epoch},
)
cutoff = int(datetime(2025, 10, 1).timestamp())
recent = memory.search(
"training progress",
user_id="max",
filters={"logged_epoch": {"gte": cutoff}},
)
```
</Tab>
</Tabs>
### Metadata Tagging
Tag workouts by type:
<Tabs>
<Tab title="Platform">
```python
mem0_client.add(
[{"role": "user", "content": "10x400m intervals"}],
@@ -468,20 +893,48 @@ speed_sessions = mem0_client.search(
user_id="max",
filters={"metadata": {"workout_type": "speed"}}
)
```
</Tab>
<Tab title="Open Source">
```python
memory.add(
[{"role": "user", "content": "10x400m intervals"}],
user_id="max",
metadata={"workout_type": "speed", "intensity": "high"},
)
# Later, find all speed workouts
speed_sessions = memory.search(
"speed work",
user_id="max",
filters={"workout_type": "speed"},
)
```
</Tab>
</Tabs>
### Pruning Old Memories
Delete irrelevant memories:
<Tabs>
<Tab title="Platform">
```python
mem0_client.delete(memory_id="mem_xyz")
# Or clear an entire run_id
mem0_client.delete_all(user_id="max", run_id="old-training-cycle")
```
</Tab>
<Tab title="Open Source">
```python
memory.delete(memory_id="mem_xyz")
# Or clear an entire run_id
memory.delete_all(user_id="max", run_id="old-training-cycle")
```
</Tab>
</Tabs>
---
@@ -515,7 +968,7 @@ Before launching:
- Define 2-3 categories (goals, constraints, preferences)
- Add expiration strategy for time-bound facts
- Implement error handling for API calls
- Monitor memory quality in Mem0 dashboard
- Monitor memory quality (Mem0 dashboard or `get_all` / Qdrant when local)
- Clear test data from production project
---
@@ -3,7 +3,7 @@ title: "Gemini 3 with Mem0 MCP"
description: "Create snappy, smart, memory-aware agents by pairing Gemini 3 with Mem0 MCP server."
---
Gemini 3, when paired with mem0-mcp-server, works in synergy to create snappy, smart, memory-aware agents.
Gemini 3, when paired with Mem0's cloud MCP server, works in synergy to create snappy, smart, memory-aware agents.
<Callout type="info" icon="sparkles" color="#8B5CF6">
This is the primary example of MCP integration - the same patterns work with Claude Desktop, Cursor, or any MCP-compatible client.
@@ -27,10 +27,22 @@ The Mem0 MCP server provides these tools to Gemini:
## Setup
### Configure Mem0 MCP
Add Mem0 MCP to your MCP client:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
### Install dependencies
```bash
pip install pydantic-ai nest-asyncio python-dotenv uv google-genai
pip install pydantic-ai nest-asyncio python-dotenv google-genai
```
### Environment Setup
@@ -40,7 +52,6 @@ Create a file named `.env`:
```bash
MEM0_API_KEY=m0-xxxxxxxxxxxxxxxxx
GEMINI_API_KEY=your-gemini-api-key-here
MEM0_DEFAULT_USER_ID=demo-user
```
<Note>
@@ -60,7 +71,7 @@ import asyncio
import os
from dotenv import load_dotenv
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio
from pydantic_ai.mcp import MCPServerHTTP
# Load environment variables
load_dotenv()
@@ -75,11 +86,9 @@ class MemoryAgent:
def _setup(self):
"""Initialize the agent with MCP tools"""
# Create MCP server directly
self.server = MCPServerStdio(
command="uvx",
args=["mem0-mcp-server"],
env=os.environ
# Connect to Mem0's cloud MCP server
self.server = MCPServerHTTP(
url="https://mcp.mem0.ai/mcp"
)
# Create agent with Gemini and memory tools
+158 -12
View File
@@ -16,6 +16,8 @@ Integrating Mem0 into your writing workflow helps you:
## Setup
<Tabs>
<Tab title="Platform">
```python
import os
from openai import OpenAI
@@ -32,13 +34,81 @@ openai = OpenAI()
USER_ID = "content_writer"
RUN_ID = "smart_editing_session"
```
</Tab>
<Tab title="Open Source">
Here we use **Mem0 open source** (`Memory`): all local, no API keys needed for memory. Vectors in **Qdrant**, LLM and embeddings via **Ollama**. The **OpenAI** Python SDK calls Ollama's **OpenAI-compatible** `/v1` endpoint for content rewriting.
### Installation
Install the required dependencies:
```bash
pip install mem0ai qdrant-client openai ollama
```
Then start Qdrant and pull the Ollama models:
```bash
docker run -d -p 6333:6333 qdrant/qdrant
ollama pull llama3.1:latest
ollama pull nomic-embed-text:latest
```
<Note>You can swap `nomic-embed-text` for any Ollama-supported embedding model (e.g., `snowflake-arctic-embed`, `mxbai-embed-large`). Just update the `model` in the `embedder` config and set `embedding_model_dims` in the Qdrant config to match the model's output dimensions (768 for `nomic-embed-text`).</Note>
```python
from openai import OpenAI
from mem0 import Memory
OLLAMA_URL = "http://localhost:11434"
CHAT_MODEL = "llama3.1:latest"
# Set up Mem0 with local providers
memory = Memory.from_config({
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "content_writing",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768,
},
},
"llm": {
"provider": "ollama",
"config": {
"model": CHAT_MODEL,
"temperature": 0,
"max_tokens": 2000,
"ollama_base_url": OLLAMA_URL,
},
},
"embedder": {
"provider": "ollama",
"config": {
"model": "nomic-embed-text:latest",
"ollama_base_url": OLLAMA_URL,
},
},
})
# OpenAI SDK pointed at Ollama for content rewriting
ollama_chat = OpenAI(base_url=f"{OLLAMA_URL}/v1", api_key="ollama")
USER_ID = "content_writer"
RUN_ID = "smart_editing_session"
```
</Tab>
</Tabs>
## Storing Your Writing Preferences in Mem0
<Tabs>
<Tab title="Platform">
```python
def store_writing_preferences():
"""Store your writing preferences in Mem0."""
preferences = """My writing preferences:
1. Use headings and sub-headings for structure.
2. Keep paragraphs concise (8–10 sentences max).
@@ -61,9 +131,41 @@ def store_writing_preferences():
return response
```
</Tab>
<Tab title="Open Source">
```python
def store_writing_preferences():
"""Store your writing preferences in Mem0."""
preferences = """My writing preferences:
1. Use headings and sub-headings for structure.
2. Keep paragraphs concise (8–10 sentences max).
3. Incorporate specific numbers and statistics.
4. Provide concrete examples.
5. Use bullet points for clarity.
6. Avoid jargon and buzzwords."""
messages = [
{"role": "user", "content": "Here are my writing style preferences."},
{"role": "assistant", "content": preferences},
]
response = memory.add(
messages,
user_id=USER_ID,
run_id=RUN_ID,
metadata={"type": "preferences", "category": "writing_style"},
)
return response
```
</Tab>
</Tabs>
## Editing Content Using Stored Preferences
<Tabs>
<Tab title="Platform">
```python
def apply_writing_style(original_content):
"""Use preferences stored in Mem0 to guide content rewriting."""
@@ -107,29 +209,73 @@ Preferences:
return clean_response
```
</Tab>
<Tab title="Open Source">
```python
def apply_writing_style(original_content):
"""Use preferences stored in Mem0 to guide content rewriting."""
results = memory.search(
query="What are my writing style preferences?",
user_id=USER_ID,
run_id=RUN_ID,
)
if not results:
print("No preferences found.")
return None
preferences = "\n".join(r["memory"] for r in results.get("results", []))
system_prompt = f"""
You are a writing assistant.
Apply the following writing style preferences to improve the user's content:
Preferences:
{preferences}
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"""Original Content:
{original_content}"""},
]
# Ollama via OpenAI-compatible API
response = ollama_chat.chat.completions.create(
model=CHAT_MODEL,
messages=messages,
)
clean_response = response.choices[0].message.content.strip()
return clean_response
```
</Tab>
</Tabs>
## Complete Workflow: Content Editing
```python
def content_writing_workflow(content):
"""Automated workflow for editing a document based on writing preferences."""
# Store writing preferences (if not already stored)
store_writing_preferences() # Ideally done once, or with a conditional check
# Edit the document with Mem0 preferences
edited_content = apply_writing_style(content)
if not edited_content:
return "Failed to edit document."
# Display results
print("\n=== ORIGINAL DOCUMENT ===\n")
print(content)
print("\n=== EDITED DOCUMENT ===\n")
print(edited_content)
return edited_content
```
@@ -138,8 +284,8 @@ def content_writing_workflow(content):
```python
# Define your document
original_content = """Project Proposal
The following proposal outlines our strategy for the Q3 marketing campaign.
The following proposal outlines our strategy for the Q3 marketing campaign.
We believe this approach will significantly increase our market share.
Increase brand awareness
@@ -160,8 +306,8 @@ Your document will be transformed into a structured, well-formatted version base
### Original Document
```
Project Proposal
The following proposal outlines our strategy for the Q3 marketing campaign.
The following proposal outlines our strategy for the Q3 marketing campaign.
We believe this approach will significantly increase our market share.
Increase brand awareness
@@ -204,7 +350,7 @@ This proposal outlines our strategy for the Q3 marketing campaign. We aim to sig
### Conclusion
We believe this strategy will effectively increase our market share. To achieve these goals, we need your support and collaboration. Let’s work together to make this campaign a success. Please review the proposal and provide your feedback by the end of the week.
We believe this strategy will effectively increase our market share. To achieve these goals, we need your support and collaboration. Let's work together to make this campaign a success. Please review the proposal and provide your feedback by the end of the week.
```
Mem0 enables a seamless, intelligent content-writing workflow, perfect for content creators, marketers, and technical writers looking to scale their personal tone and structure across work.
+240 -11
View File
@@ -18,11 +18,36 @@ Email overload is a common challenge for many professionals. By leveraging Mem0'
## Setup
<Tabs>
<Tab title="Platform">
Before you begin, ensure you have the required dependencies installed:
```bash
pip install mem0ai openai
```
</Tab>
<Tab title="Open Source">
Here we use **Mem0 open source** (`Memory`): all local, no API keys needed for memory. Vectors in **Qdrant**, LLM and embeddings via **Ollama**.
### Installation
Install the required dependencies:
```bash
pip install mem0ai qdrant-client openai ollama
```
Then start Qdrant and pull the Ollama models:
```bash
docker run -d -p 6333:6333 qdrant/qdrant
ollama pull llama3.1:latest
ollama pull nomic-embed-text:latest
```
<Note>You can swap `nomic-embed-text` for any Ollama-supported embedding model (e.g., `snowflake-arctic-embed`, `mxbai-embed-large`). Just update the `model` in the `embedder` config and set `embedding_model_dims` in the Qdrant config to match the model's output dimensions (768 for `nomic-embed-text`).</Note>
</Tab>
</Tabs>
## Implementation
@@ -30,6 +55,8 @@ pip install mem0ai openai
The following example shows how to create a basic email processing system with Mem0:
<Tabs>
<Tab title="Platform">
```python
import os
from mem0 import MemoryClient
@@ -45,11 +72,11 @@ class EmailProcessor:
def __init__(self):
"""Initialize the Email Processor with Mem0 memory client"""
self.client = client
def process_email(self, email_content, user_id):
"""
Process an email and store it in Mem0 memory
Args:
email_content (str): Raw email content
user_id (str): User identifier for memory association
@@ -57,20 +84,20 @@ class EmailProcessor:
# Parse email
parser = Parser()
email = parser.parsestr(email_content)
# Extract email details
sender = email['from']
recipient = email['to']
subject = email['subject']
date = email['date']
body = self._get_email_body(email)
# Create message object for Mem0
message = {
"role": "user",
"content": f"Email from {sender}: {subject}\n\n{body}"
}
# Create metadata for better retrieval
metadata = {
"email_type": "incoming",
@@ -79,18 +106,18 @@ class EmailProcessor:
"subject": subject,
"date": date
}
# Store in Mem0 with appropriate categories
response = self.client.add(
messages=[message],
user_id=user_id,
metadata=metadata,
categories=["email", "correspondence"],
)
return response
def _get_email_body(self, email):
"""Extract the body content from an email"""
# Simplified extraction - in real-world, handle multipart emails
@@ -100,7 +127,7 @@ class EmailProcessor:
return part.get_payload(decode=True).decode()
else:
return email.get_payload(decode=True).decode()
def search_emails(self, query, user_id, sender=None):
"""
Search through stored emails
@@ -132,7 +159,7 @@ class EmailProcessor:
results = self.client.search(query=query, filters=filters)
return results
def get_email_thread(self, subject, user_id):
"""
Retrieve all emails in a thread based on subject
@@ -182,6 +209,208 @@ processor.process_email(sample_email, user_id)
meeting_emails = processor.search_emails("meeting schedule", user_id)
print(f"Found {len(meeting_emails['results'])} relevant emails")
```
</Tab>
<Tab title="Open Source">
```python
from mem0 import Memory
from email.parser import Parser
OLLAMA_URL = "http://localhost:11434"
# Set up Mem0 with local providers
memory = Memory.from_config({
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "email_intelligence",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768,
},
},
"llm": {
"provider": "ollama",
"config": {
"model": "llama3.1:latest",
"temperature": 0,
"max_tokens": 2000,
"ollama_base_url": OLLAMA_URL,
},
},
"embedder": {
"provider": "ollama",
"config": {
"model": "nomic-embed-text:latest",
"ollama_base_url": OLLAMA_URL,
},
},
})
class EmailProcessor:
def __init__(self):
"""Initialize the Email Processor with Mem0 memory"""
self.memory = memory
def process_email(self, email_content, user_id):
"""
Process an email and store it in Mem0 memory
Args:
email_content (str): Raw email content
user_id (str): User identifier for memory association
"""
# Parse email
parser = Parser()
email = parser.parsestr(email_content)
# Extract email details
sender = email["from"]
recipient = email["to"]
subject = email["subject"]
date = email["date"]
body = self._get_email_body(email)
# Create message object for Mem0
message = {
"role": "user",
"content": f"Email from {sender}: {subject}\n\n{body}",
}
# Create metadata for better retrieval
# In OSS, categories are modeled as metadata fields
metadata = {
"email_type": "incoming",
"memory_category": "email",
"sender": sender,
"recipient": recipient,
"subject": subject,
"date": date,
}
# Store in Mem0
response = self.memory.add(
message,
user_id=user_id,
metadata=metadata,
)
return response
def _get_email_body(self, email):
"""Extract the body content from an email"""
if email.is_multipart():
for part in email.walk():
if part.get_content_type() == "text/plain":
return part.get_payload(decode=True).decode()
else:
return email.get_payload(decode=True).decode()
def search_emails(self, query, user_id, sender=None):
"""
Search through stored emails
Args:
query (str): Search query
user_id (str): User identifier
sender (str, optional): Filter by sender email address
"""
# In OSS, user_id is an explicit parameter (not inside filters)
if not sender:
results = self.memory.search(
query=query,
user_id=user_id,
filters={"memory_category": "email"},
)
else:
results = self.memory.search(
query=query,
user_id=user_id,
filters={
"AND": [
{"memory_category": "email"},
{"sender": sender},
]
},
)
return results
def get_email_thread(self, subject, user_id):
"""
Retrieve all emails in a thread based on subject
Args:
subject (str): Email subject to match
user_id (str): User identifier
"""
# In OSS, user_id is an explicit parameter
thread = self.memory.get_all(
user_id=user_id,
filters={
"AND": [
{"memory_category": "email"},
{"subject": {"icontains": subject}},
]
},
)
return thread
# Initialize the processor
processor = EmailProcessor()
# Example raw email
sample_email = """From: alice@example.com
To: bob@example.com
Subject: Meeting Schedule Update
Date: Mon, 15 Jul 2024 14:22:05 -0700
Hi Bob,
I wanted to update you on the schedule for our upcoming project meeting.
We'll be meeting this Thursday at 2pm instead of Friday.
Could you please prepare your section of the presentation?
Thanks,
Alice
"""
# Process and store the email
user_id = "bob@example.com"
processor.process_email(sample_email, user_id)
# Later, search for emails about meetings
meeting_emails = processor.search_emails("meeting schedule", user_id)
print(f"Found {len(meeting_emails['results'])} relevant emails")
```
<Note>
**Categories vs Metadata:** The Platform version uses `categories=["email"]` which are AI-assigned by Mem0. In open source, categories are modeled as `metadata` fields (e.g., `memory_category`) that you set on each `add` call and filter on during search.
</Note>
</Tab>
</Tabs>
### Fetching Memories
You can fetch all the memories at any point in time using the following code:
<Tabs>
<Tab title="Platform">
```python
meeting_emails = processor.search_emails("meeting schedule", user_id)
for m in meeting_emails['results']:
print(m['memory'])
```
</Tab>
<Tab title="Open Source">
```python
meeting_emails = processor.search_emails("meeting schedule", user_id)
for m in meeting_emails["results"]:
print(m["memory"])
```
</Tab>
</Tabs>
## Key Features and Benefits
+16 -18
View File
@@ -4,9 +4,9 @@
"description": "Mem0 is a self-improving memory layer for LLM applications, enabling personalized AI experiences that save costs and delight users.",
"theme": "aspen",
"colors": {
"primary": "#9C58FA",
"light": "#9C58FA",
"dark": "#9C58FA"
"primary": "#8F74E0",
"light": "#8F74E0",
"dark": "#8F74E0"
},
"favicon": "/logo/favicon.png",
"logo": {
@@ -42,6 +42,7 @@
"platform/overview",
"vibecoding",
"platform/mem0-mcp",
"platform/cli",
"platform/platform-vs-oss",
"platform/quickstart"
]
@@ -296,21 +297,6 @@
}
]
},
{
"tab": "OpenMemory",
"groups": [
{
"group": "Overview & Quickstart",
"icon": "square-terminal",
"pages": [
"openmemory/overview",
"vibecoding",
"openmemory/quickstart",
"openmemory/integrations"
]
}
]
},
{
"tab": "Cookbooks",
"groups": [
@@ -1056,6 +1042,18 @@
{
"source": "/cookbooks/customer-support-agent",
"destination": "/cookbooks/operations/support-inbox"
},
{
"source": "/openmemory/overview",
"destination": "/introduction"
},
{
"source": "/openmemory/quickstart",
"destination": "/introduction"
},
{
"source": "/openmemory/integrations",
"destination": "/introduction"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

+51 -67
View File
@@ -4,16 +4,16 @@ description: "Mem0 is a universal, self-improving memory layer for LLM applicati
mode: "custom"
---
{/* debug: welcome-layout-v2 */}
{/* debug: welcome-layout-v3-grid */}
<div className="px-4 pt-16 pb-12 lg:pt-20 max-w-4xl mx-auto text-center space-y-6">
<h1 className="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-zinc-50 tracking-tight mb-3">
Build with <span className="text-primary">mem0</span>
<div className="px-4 pt-8 pb-4 lg:pt-10 max-w-4xl mx-auto text-center space-y-3">
<h1 className="text-3xl lg:text-4xl font-bold text-gray-900 dark:text-zinc-50 tracking-tight mb-2">
Build with <span className="text-primary">Mem0</span>
</h1>
<p className="max-w-2xl mx-auto text-base text-gray-600 dark:text-zinc-400 leading-relaxed">
Universal, Self-improving memory layer for LLM applications.
</p>
<p className="max-w-2xl mx-auto text-base text-gray-600 dark:text-zinc-400 leading-relaxed">
Universal, Self-improving memory layer for LLM applications.
</p>
<a
href="/platform/quickstart"
@@ -24,31 +24,25 @@ mode: "custom"
</a>
</div>
<section className="px-4 max-w-6xl mx-auto space-y-4">
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-900 dark:text-zinc-100">
Mem0 Products
</h2>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<section className="px-4 pt-4 pb-6 max-w-6xl mx-auto">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<a
href="/platform/overview"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/mem0_platform.png"
alt="Mem0 Platform thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/mem0_platform.png"
alt="Mem0 Platform thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
Mem0 Platform
</h3>
@@ -63,18 +57,18 @@ mode: "custom"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/mem0_open_source.png"
alt="Mem0 Open Source thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/mem0_open_source.png"
alt="Mem0 Open Source thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
Mem0 Open Source
</h3>
@@ -84,59 +78,23 @@ mode: "custom"
</div>
</a>
<a
href="/openmemory/overview"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
src="/images/docs thumbnails/light/mem0_openmemory.png"
alt="OpenMemory thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
src="/images/docs thumbnails/dark/mem0_openmemory.png"
alt="OpenMemory thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
OpenMemory
</h3>
<p className="text-sm text-gray-600 dark:text-zinc-400">
Workspace-based memory for teams collaborating across agents and projects.
</p>
</div>
</a>
</div>
</section>
<section className="px-4 pt-12 pb-20 max-w-6xl mx-auto space-y-4">
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-900 dark:text-zinc-100">
Developer Resources
</h2>
</div>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
<a
href="/cookbooks/overview"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/Cookbooks.png"
alt="Cookbooks thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/Cookbooks.png"
alt="Cookbooks thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
Cookbooks
</h3>
@@ -151,18 +109,18 @@ mode: "custom"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/Integrations.png"
alt="Integrations thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/Integrations.png"
alt="Integrations thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
Integrations
</h3>
@@ -177,25 +135,51 @@ mode: "custom"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[4/3] w-full object-cover"
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/API.png"
alt="API reference thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[4/3] w-full object-cover"
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/API.png"
alt="API reference thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
API reference
API Reference
</h3>
<p className="text-sm text-gray-600 dark:text-zinc-400">
Explore every REST endpoint with payload examples and usage guidance.
</p>
</div>
</a>
<a
href="/platform/cli"
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
>
<img
className="block dark:hidden aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/light/CLI.png"
alt="CLI thumbnail"
style={{pointerEvents: "none"}}
/>
<img
className="hidden dark:block aspect-[2/1] w-full object-cover"
src="/images/docs thumbnails/dark/CLI.png"
alt="CLI thumbnail"
style={{pointerEvents: "none"}}
/>
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
CLI
</h3>
<p className="text-sm text-gray-600 dark:text-zinc-400">
Manage memories directly from your terminal. Built for developers and AI agents.
</p>
</div>
</a>
</div>
</section>
+161 -69
View File
@@ -14,11 +14,14 @@ Key differentiators:
## Getting Started
- [Introduction](https://docs.mem0.ai/introduction): Overview of Mem0's memory layer for AI agents, including stateless vs stateful agents and how memory fits in the agent stack
- [Platform Overview](https://docs.mem0.ai/platform/overview): Managed solution with 4-line integration, sub-50ms latency, and intuitive dashboard
- [Vibe Code with Mem0](https://docs.mem0.ai/vibecoding): Single entry point for developers using AI coding tools (Claude Code, Cursor, Windsurf) with Mem0
- [Mem0 MCP Server](https://docs.mem0.ai/platform/mem0-mcp): Model Context Protocol server for integrating Mem0 with AI coding assistants
- [Platform vs Open Source](https://docs.mem0.ai/platform/platform-vs-oss): Compare managed platform vs self-hosted options
- [Platform Quickstart](https://docs.mem0.ai/platform/quickstart): Get started with Mem0 Platform (managed) in minutes
- [Open Source Overview](https://docs.mem0.ai/open-source/overview): Self-hosted solution with full infrastructure control and customization
- [Open Source Python Quickstart](https://docs.mem0.ai/open-source/python-quickstart): Get started with Mem0 Open Source using Python
- [Open Source Node.js Quickstart](https://docs.mem0.ai/open-source/node-quickstart): Get started with Mem0 Open Source using Node.js
- [Platform Overview](https://docs.mem0.ai/platform/overview): Managed solution with 4-line integration, sub-50ms latency, and intuitive dashboard
- [Open Source Overview](https://docs.mem0.ai/open-source/overview): Self-hosted solution with full infrastructure control and customization
## Core Concepts
@@ -28,20 +31,20 @@ Key differentiators:
- [Memory Operations - Update](https://docs.mem0.ai/core-concepts/memory-operations/update): Modifying existing memories when new information conflicts or supplements stored data
- [Memory Operations - Delete](https://docs.mem0.ai/core-concepts/memory-operations/delete): Removing outdated or irrelevant memories to maintain memory quality
## Platform (Managed Solution)
## Platform Features
- [Platform Quickstart](https://docs.mem0.ai/platform/quickstart): Complete guide to using Mem0 Platform with Python, JavaScript, and cURL examples
- [Platform vs Open Source](https://docs.mem0.ai/platform/platform-vs-oss): Compare managed platform vs self-hosted options
- [Platform Features Overview](https://docs.mem0.ai/platform/features/platform-overview): High-level overview of all Mem0 Platform capabilities
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations): Sophisticated memory management techniques for complex applications
### Essential Platform Features
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters): Advanced filtering and querying capabilities
### Essential Features
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters): Advanced filtering and querying capabilities for memories
- [Entity-Scoped Memory](https://docs.mem0.ai/platform/features/entity-scoped-memory): Organize memories by user, agent, app, and session identifiers
- [Async Client](https://docs.mem0.ai/platform/features/async-client): Non-blocking operations for high-concurrency applications
- [Async Mode Default Changes](https://docs.mem0.ai/platform/features/async-mode-default-change): Understanding new async behavior defaults
- [Multimodal Support](https://docs.mem0.ai/platform/features/multimodal-support): Integration of images and documents (JPG, PNG, MDX, TXT, PDF) via URLs or Base64
- [Custom Categories](https://docs.mem0.ai/platform/features/custom-categories): Define domain-specific categories to improve memory organization
- [Async Mode Default Changes](https://docs.mem0.ai/platform/features/async-mode-default-change): Understanding new async behavior defaults
### Advanced Platform Features
### Advanced Features
- [Graph Memory](https://docs.mem0.ai/platform/features/graph-memory): Build and query relationships between entities for contextually relevant retrieval
- [Graph Threshold](https://docs.mem0.ai/platform/features/graph-threshold): Configure graph relationship sensitivity and strength
- [Advanced Retrieval](https://docs.mem0.ai/platform/features/advanced-retrieval): Enhanced search with keyword search, reranking, and filtering capabilities
@@ -59,10 +62,15 @@ Key differentiators:
- [Webhooks](https://docs.mem0.ai/platform/features/webhooks): Real-time notifications for memory events
- [Feedback Mechanism](https://docs.mem0.ai/platform/features/feedback-mechanism): Improve memory quality through user feedback
- [Group Chat Support](https://docs.mem0.ai/platform/features/group-chat): Multi-conversation memory management
- [MCP Integration](https://docs.mem0.ai/platform/features/mcp-integration): Model Context Protocol integration for AI coding tools
### Platform Support
### Support & Migration
- [FAQs](https://docs.mem0.ai/platform/faqs): Frequently asked questions about Mem0 Platform
- [Contribute Guide](https://docs.mem0.ai/platform/contribute): Contributing to Mem0 Platform development
- [OSS to Platform Migration](https://docs.mem0.ai/migration/oss-to-platform): Guide for migrating from open-source to managed platform
- [V0 to V1 Migration](https://docs.mem0.ai/migration/v0-to-v1): Upgrading from Mem0 v0 to v1
- [Breaking Changes](https://docs.mem0.ai/migration/breaking-changes): List of breaking changes across versions
- [API Changes](https://docs.mem0.ai/migration/api-changes): Detailed API changes and migration paths
## Open Source
@@ -72,93 +80,130 @@ Key differentiators:
- [Configuration Guide](https://docs.mem0.ai/open-source/configuration): Complete configuration options for self-hosted deployment
### Open Source Features
- [OpenAI Compatibility](https://docs.mem0.ai/open-source/features/openai_compatibility): Seamless integration with OpenAI-compatible APIs
- [REST API Server](https://docs.mem0.ai/open-source/features/rest-api): FastAPI-based server with core operations and OpenAPI documentation
- [Features Overview](https://docs.mem0.ai/open-source/features/overview): Overview of all open-source features
- [Graph Memory](https://docs.mem0.ai/open-source/features/graph-memory): Build and query entity relationships using graph stores like Neo4j
- [Metadata Filtering](https://docs.mem0.ai/open-source/features/metadata-filtering): Advanced filtering using custom metadata fields
- [Reranker Search](https://docs.mem0.ai/open-source/features/reranker-search): Enhanced search results with reranking models
- [Async Memory](https://docs.mem0.ai/open-source/features/async-memory): Asynchronous memory operations for better performance
- [Multimodal Support](https://docs.mem0.ai/open-source/features/multimodal-support): Handle text, images, and documents in self-hosted setup
### Customization
- [Custom Fact Extraction](https://docs.mem0.ai/open-source/features/custom-fact-extraction-prompt): Tailor information extraction for specific use cases
- [Custom Memory Update Prompt](https://docs.mem0.ai/open-source/features/custom-update-memory-prompt): Customize how memories are updated and merged
- [REST API Server](https://docs.mem0.ai/open-source/features/rest-api): FastAPI-based server with core operations and OpenAPI documentation
- [OpenAI Compatibility](https://docs.mem0.ai/open-source/features/openai_compatibility): Seamless integration with OpenAI-compatible APIs
## Components
### LLMs
- [LLM Overview](https://docs.mem0.ai/components/llms/overview): Comprehensive guide to Large Language Model integration and configuration options
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview): Guide to supported vector databases for semantic memory storage
- [Embeddings Overview](https://docs.mem0.ai/components/embedders/overview): Embedding model configuration for semantic understanding
### Supported LLMs
- [OpenAI](https://docs.mem0.ai/components/llms/models/openai): Integration with OpenAI models including GPT-4 and structured outputs
- [LLM Configuration](https://docs.mem0.ai/components/llms/config): Configuration reference for LLM providers
- [OpenAI](https://docs.mem0.ai/components/llms/models/openai): Integration with OpenAI models including GPT-4
- [Anthropic](https://docs.mem0.ai/components/llms/models/anthropic): Claude model integration with advanced reasoning capabilities
- [Google AI](https://docs.mem0.ai/components/llms/models/google_AI): Gemini model integration for multimodal applications
- [Groq](https://docs.mem0.ai/components/llms/models/groq): High-performance LPU optimized models for fast inference
- [AWS Bedrock](https://docs.mem0.ai/components/llms/models/aws_bedrock): Enterprise-grade AWS managed model integration
- [Azure OpenAI](https://docs.mem0.ai/components/llms/models/azure_openai): Microsoft Azure hosted OpenAI models for enterprise environments
- [Ollama](https://docs.mem0.ai/components/llms/models/ollama): Local model deployment for privacy-focused applications
- [vLLM](https://docs.mem0.ai/components/llms/models/vllm): High-performance inference framework
- [LM Studio](https://docs.mem0.ai/components/llms/models/lmstudio): Local model management and deployment
- [Together](https://docs.mem0.ai/components/llms/models/together): Open-source model inference platform
- [DeepSeek](https://docs.mem0.ai/components/llms/models/deepseek): Advanced reasoning models
- [Sarvam](https://docs.mem0.ai/components/llms/models/sarvam): Indian language models
- [XAI](https://docs.mem0.ai/components/llms/models/xAI): xAI models integration
- [Groq](https://docs.mem0.ai/components/llms/models/groq): High-performance LPU optimized models for fast inference
- [LiteLLM](https://docs.mem0.ai/components/llms/models/litellm): Unified LLM interface and proxy
- [LangChain](https://docs.mem0.ai/components/llms/models/langchain): LangChain LLM integration
- [OpenAI Structured](https://docs.mem0.ai/components/llms/models/openai_structured): OpenAI with structured output support
- [Azure OpenAI Structured](https://docs.mem0.ai/components/llms/models/azure_openai_structured): Azure OpenAI with structured outputs
### Supported Vector Databases
- [Mistral AI](https://docs.mem0.ai/components/llms/models/mistral_AI): Mistral model integration
- [Google AI](https://docs.mem0.ai/components/llms/models/google_AI): Gemini model integration for multimodal applications
- [AWS Bedrock](https://docs.mem0.ai/components/llms/models/aws_bedrock): Enterprise-grade AWS managed model integration
- [DeepSeek](https://docs.mem0.ai/components/llms/models/deepseek): Advanced reasoning models
- [MiniMax](https://docs.mem0.ai/components/llms/models/minimax): MiniMax model integration
- [xAI](https://docs.mem0.ai/components/llms/models/xAI): xAI Grok models integration
- [Sarvam](https://docs.mem0.ai/components/llms/models/sarvam): Indian language models
- [LM Studio](https://docs.mem0.ai/components/llms/models/lmstudio): Local model management and deployment
- [LangChain LLM](https://docs.mem0.ai/components/llms/models/langchain): LangChain LLM integration
- [vLLM](https://docs.mem0.ai/components/llms/models/vllm): High-performance inference framework
### Vector Databases
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview): Guide to supported vector databases for semantic memory storage
- [Vector Database Configuration](https://docs.mem0.ai/components/vectordbs/config): Configuration reference for vector database providers
- [Qdrant](https://docs.mem0.ai/components/vectordbs/dbs/qdrant): High-performance vector similarity search engine
- [Pinecone](https://docs.mem0.ai/components/vectordbs/dbs/pinecone): Managed vector database with serverless and pod deployment options
- [Chroma](https://docs.mem0.ai/components/vectordbs/dbs/chroma): AI-native open-source vector database optimized for speed
- [Weaviate](https://docs.mem0.ai/components/vectordbs/dbs/weaviate): Open-source vector search engine with built-in ML capabilities
- [PGVector](https://docs.mem0.ai/components/vectordbs/dbs/pgvector): PostgreSQL extension for vector similarity search
- [Milvus](https://docs.mem0.ai/components/vectordbs/dbs/milvus): Open-source vector database for AI applications at scale
- [Redis](https://docs.mem0.ai/components/vectordbs/dbs/redis): Real-time vector storage and search with Redis Stack
- [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase): Open-source Firebase alternative with vector support
- [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector): Serverless vector database
- [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch): Distributed search and analytics engine
- [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch): Open-source search and analytics platform
- [FAISS](https://docs.mem0.ai/components/vectordbs/dbs/faiss): Facebook AI Similarity Search library
- [Pinecone](https://docs.mem0.ai/components/vectordbs/dbs/pinecone): Managed vector database with serverless and pod deployment options
- [MongoDB](https://docs.mem0.ai/components/vectordbs/dbs/mongodb): Document database with vector search capabilities
- [Azure AI Search](https://docs.mem0.ai/components/vectordbs/dbs/azure): Microsoft's enterprise search service
- [Azure MySQL](https://docs.mem0.ai/components/vectordbs/dbs/azure_mysql): Azure Database for MySQL with vector search
- [Redis](https://docs.mem0.ai/components/vectordbs/dbs/redis): Real-time vector storage and search with Redis Stack
- [Valkey](https://docs.mem0.ai/components/vectordbs/dbs/valkey): Open-source Redis alternative with vector search
- [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch): Distributed search and analytics engine
- [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch): Open-source search and analytics platform
- [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase): Open-source Firebase alternative with vector support
- [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector): Serverless vector database
- [Vectorize](https://docs.mem0.ai/components/vectordbs/dbs/vectorize): Vectorize vector database integration
- [Vertex AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/vertex_ai): Google Cloud's vector search service
- [Databricks](https://docs.mem0.ai/components/vectordbs/dbs/databricks): Delta Lake integration for vector search
- [Weaviate](https://docs.mem0.ai/components/vectordbs/dbs/weaviate): Open-source vector search engine with built-in ML capabilities
- [FAISS](https://docs.mem0.ai/components/vectordbs/dbs/faiss): Facebook AI Similarity Search library
- [LangChain Vector Store](https://docs.mem0.ai/components/vectordbs/dbs/langchain): LangChain vector store integration
- [Baidu](https://docs.mem0.ai/components/vectordbs/dbs/baidu): Baidu vector database integration
- [LangChain](https://docs.mem0.ai/components/vectordbs/dbs/langchain): LangChain vector store integration
- [Cassandra](https://docs.mem0.ai/components/vectordbs/dbs/cassandra): Apache Cassandra with vector search capabilities
- [S3 Vectors](https://docs.mem0.ai/components/vectordbs/dbs/s3_vectors): Amazon S3 Vectors integration
- [Databricks](https://docs.mem0.ai/components/vectordbs/dbs/databricks): Delta Lake integration for vector search
- [Neptune Analytics](https://docs.mem0.ai/components/vectordbs/dbs/neptune_analytics): AWS Neptune Analytics for graph and vector search
- [Turbopuffer](https://docs.mem0.ai/components/vectordbs/dbs/turbopuffer): High-performance serverless vector database
### Supported Embeddings
### Embedding Models
- [Embeddings Overview](https://docs.mem0.ai/components/embedders/overview): Embedding model configuration for semantic understanding
- [Embeddings Configuration](https://docs.mem0.ai/components/embedders/config): Configuration reference for embedding providers
- [OpenAI Embeddings](https://docs.mem0.ai/components/embedders/models/openai): High-quality text embeddings with customizable dimensions
- [Azure OpenAI Embeddings](https://docs.mem0.ai/components/embedders/models/azure_openai): Enterprise Azure-hosted embedding models
- [Google AI](https://docs.mem0.ai/components/embedders/models/google_AI): Gemini embedding models
- [AWS Bedrock](https://docs.mem0.ai/components/embedders/models/aws_bedrock): Amazon embedding models through Bedrock
- [Hugging Face](https://docs.mem0.ai/components/embedders/models/huggingface): Open-source embedding models for local deployment
- [Vertex AI](https://docs.mem0.ai/components/embedders/models/vertexai): Google Cloud's enterprise embedding models
- [Ollama](https://docs.mem0.ai/components/embedders/models/ollama): Local embedding models for privacy-focused applications
- [Together](https://docs.mem0.ai/components/embedders/models/together): Open-source model embeddings
- [LM Studio](https://docs.mem0.ai/components/embedders/models/lmstudio): Local model embeddings
- [LangChain](https://docs.mem0.ai/components/embedders/models/langchain): LangChain embedder integration
- [Ollama Embeddings](https://docs.mem0.ai/components/embedders/models/ollama): Local embedding models for privacy-focused applications
- [Hugging Face Embeddings](https://docs.mem0.ai/components/embedders/models/huggingface): Open-source embedding models for local deployment
- [Vertex AI Embeddings](https://docs.mem0.ai/components/embedders/models/vertexai): Google Cloud's enterprise embedding models
- [Google AI Embeddings](https://docs.mem0.ai/components/embedders/models/google_AI): Gemini embedding models
- [LM Studio Embeddings](https://docs.mem0.ai/components/embedders/models/lmstudio): Local model embeddings
- [Together Embeddings](https://docs.mem0.ai/components/embedders/models/together): Open-source model embeddings
- [LangChain Embeddings](https://docs.mem0.ai/components/embedders/models/langchain): LangChain embedder integration
- [AWS Bedrock Embeddings](https://docs.mem0.ai/components/embedders/models/aws_bedrock): Amazon embedding models through Bedrock
### Rerankers
- [Reranker Overview](https://docs.mem0.ai/components/rerankers/overview): Guide to reranking models for improving search result quality
- [Reranker Configuration](https://docs.mem0.ai/components/rerankers/config): Configuration reference for reranker providers
- [Reranker Optimization](https://docs.mem0.ai/components/rerankers/optimization): Performance tuning and optimization strategies for rerankers
- [Custom Reranker Prompts](https://docs.mem0.ai/components/rerankers/custom-prompts): Customize reranker behavior with custom prompts
- [Cohere Reranker](https://docs.mem0.ai/components/rerankers/models/cohere): Cohere reranking model integration
- [Sentence Transformer Reranker](https://docs.mem0.ai/components/rerankers/models/sentence_transformer): Cross-encoder reranking with sentence transformers
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface): Hugging Face reranking models
- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker): Use LLMs as rerankers for flexible relevance scoring
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy): Zero Entropy reranking model
## Integrations
- [Integrations Overview](https://docs.mem0.ai/integrations): Overview of all available Mem0 integrations
### Agent Frameworks
- [LangChain](https://docs.mem0.ai/integrations/langchain): Seamless integration with LangChain framework for enhanced agent capabilities
- [LangGraph](https://docs.mem0.ai/integrations/langgraph): Build stateful, multi-actor applications with persistent memory
- [LlamaIndex](https://docs.mem0.ai/integrations/llama-index): Enhanced RAG applications with intelligent memory layer
- [CrewAI](https://docs.mem0.ai/integrations/crewai): Multi-agent systems with shared and individual memory capabilities
- [AutoGen](https://docs.mem0.ai/integrations/autogen): Microsoft's multi-agent conversation framework with memory
- [Agno](https://docs.mem0.ai/integrations/agno): Agno framework integration with persistent memory
- [Camel AI](https://docs.mem0.ai/integrations/camel-ai): Camel AI multi-agent framework with memory support
- [OpenClaw](https://docs.mem0.ai/integrations/openclaw): OpenClaw framework integration
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk): OpenAI's agent framework with Mem0 memory
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk): Google AI Agent Development Kit with persistent memory
- [Mastra](https://docs.mem0.ai/integrations/mastra): Mastra TypeScript agent framework integration
- [Vercel AI SDK](https://docs.mem0.ai/integrations/vercel-ai-sdk): Build AI-powered web applications with persistent memory
- [Flowise](https://docs.mem0.ai/integrations/flowise): No-code LLM workflow builder with memory capabilities
### Voice & Real-time
- [LiveKit](https://docs.mem0.ai/integrations/livekit): Real-time voice and video AI with persistent memory
- [Pipecat](https://docs.mem0.ai/integrations/pipecat): Voice AI pipeline framework with memory capabilities
- [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs): Voice synthesis integration with conversational memory
### Cloud & Infrastructure
- [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock): Enterprise AWS integration for managed AI services
### Developer Tools
- [Dify](https://docs.mem0.ai/integrations/dify): LLMOps platform integration for production AI applications
- [Flowise](https://docs.mem0.ai/integrations/flowise): No-code LLM workflow builder with memory capabilities
- [LangChain Tools](https://docs.mem0.ai/integrations/langchain-tools): Use Mem0 as a LangChain tool for agents
- [AgentOps](https://docs.mem0.ai/integrations/agentops): Agent observability and monitoring with memory tracking
- [Keywords AI](https://docs.mem0.ai/integrations/keywords): Keywords AI integration for LLM monitoring
- [Raycast](https://docs.mem0.ai/integrations/raycast): Raycast extension for quick memory access
## Cookbooks and Examples
### Cookbooks Overview
- [Cookbooks Overview](https://docs.mem0.ai/cookbooks/overview): Complete guide to Mem0 examples and implementation patterns
### Essential Guides
@@ -171,12 +216,13 @@ Key differentiators:
- [Choosing Memory Architecture](https://docs.mem0.ai/cookbooks/essentials/choosing-memory-architecture-vector-vs-graph): Vector vs Graph memory architectures comparison
### AI Companion Examples
- [Quickstart Demo](https://docs.mem0.ai/cookbooks/companions/quickstart-demo): Quick demo of building an AI companion with memory
- [Node.js Companion](https://docs.mem0.ai/cookbooks/companions/nodejs-companion): JavaScript-based AI companion applications
- [AI Tutor](https://docs.mem0.ai/cookbooks/companions/ai-tutor): Educational AI that adapts to learning progress
- [Travel Assistant](https://docs.mem0.ai/cookbooks/companions/travel-assistant): Travel planning agent that learns preferences
- [YouTube Research Assistant](https://docs.mem0.ai/cookbooks/companions/youtube-research): AI that researches and learns from video content
- [Voice Companion](https://docs.mem0.ai/cookbooks/companions/voice-companion-openai): Voice-enabled AI with conversational memory
- [Local Companion](https://docs.mem0.ai/cookbooks/companions/local-companion-ollama): Privacy-focused companion using local models
- [Node.js Companion](https://docs.mem0.ai/cookbooks/companions/nodejs-companion): JavaScript-based AI companion applications
- [YouTube Research Assistant](https://docs.mem0.ai/cookbooks/companions/youtube-research): AI that researches and learns from video content
### Operations & Automation
- [Support Inbox](https://docs.mem0.ai/cookbooks/operations/support-inbox): Customer service agents with conversation history
@@ -186,31 +232,77 @@ Key differentiators:
- [Team Task Agent](https://docs.mem0.ai/cookbooks/operations/team-task-agent): Collaborative AI agents with shared project memory
### Integration Examples
- [Agents SDK Tool](https://docs.mem0.ai/cookbooks/integrations/agents-sdk-tool): Using Mem0 as a tool with OpenAI Agents SDK
- [OpenAI Tool Calls](https://docs.mem0.ai/cookbooks/integrations/openai-tool-calls): Mem0 integrated with OpenAI function calling
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock): Enterprise memory with AWS managed services
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search): Web search with persistent memory of results
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk): Medical AI applications with memory
- [Mastra Agent](https://docs.mem0.ai/cookbooks/integrations/mastra-agent): Mastra framework integration with memory
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk): Medical AI applications with memory
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock): Enterprise memory with AWS managed services
- [Neptune Analytics](https://docs.mem0.ai/cookbooks/integrations/neptune-analytics): Graph and vector search with AWS Neptune
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search): Web search with persistent memory of results
### Framework Examples
- [LlamaIndex React](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-react): React applications with LlamaIndex and memory
- [LlamaIndex Multiagent](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-multiagent): Multi-agent systems with shared memory
- [Multimodal Retrieval](https://docs.mem0.ai/cookbooks/frameworks/multimodal-retrieval): Memory systems handling text, images, and documents
- [Eliza OS Character](https://docs.mem0.ai/cookbooks/frameworks/eliza-os-character): Character-based AI with persistent personality
- [Chrome Extension](https://docs.mem0.ai/cookbooks/frameworks/chrome-extension): Browser extensions that remember user interactions
- [Multimodal Retrieval](https://docs.mem0.ai/cookbooks/frameworks/multimodal-retrieval): Memory systems handling text, images, and documents
- [Gemini with Mem0 MCP](https://docs.mem0.ai/cookbooks/frameworks/gemini-3-with-mem0-mcp): Google Gemini integration using MCP server
- [Mirofish Swarm Memory](https://docs.mem0.ai/cookbooks/frameworks/mirofish-swarm-memory): Swarm-based multi-agent memory patterns
## API Reference
- [Memory APIs](https://docs.mem0.ai/api-reference/memory/add-memories): Comprehensive API documentation for memory operations
- [API Reference Overview](https://docs.mem0.ai/api-reference): REST API overview with authentication and quick start guide
- [Organizations & Projects](https://docs.mem0.ai/api-reference/organizations-projects): Managing organizations and projects for multi-tenant setups
### Core Memory APIs
- [Add Memories](https://docs.mem0.ai/api-reference/memory/add-memories): REST API for storing new memories with detailed request/response formats
- [Search Memories](https://docs.mem0.ai/api-reference/memory/search-memories): Advanced search API with filtering and ranking capabilities
- [Get All Memories](https://docs.mem0.ai/api-reference/memory/get-memories): Retrieve all memories with pagination and filtering options
- [Search Memories](https://docs.mem0.ai/api-reference/memory/search-memories): Advanced search API with filtering and ranking capabilities
- [Update Memory](https://docs.mem0.ai/api-reference/memory/update-memory): Modify existing memories with conflict resolution
- [Delete Memory](https://docs.mem0.ai/api-reference/memory/delete-memory): Remove memories individually or in batches
- [Delete Memory](https://docs.mem0.ai/api-reference/memory/delete-memory): Remove a specific memory by ID
## Optional
### Additional Memory APIs
- [Create Memory Export](https://docs.mem0.ai/api-reference/memory/create-memory-export): Export memories in bulk
- [Feedback](https://docs.mem0.ai/api-reference/memory/feedback): Submit feedback on memory quality
- [Get Memory](https://docs.mem0.ai/api-reference/memory/get-memory): Retrieve a single memory by ID
- [Memory History](https://docs.mem0.ai/api-reference/memory/history-memory): View the history of changes to a memory
- [Get Memory Export](https://docs.mem0.ai/api-reference/memory/get-memory-export): Retrieve a previously created memory export
- [Batch Update](https://docs.mem0.ai/api-reference/memory/batch-update): Update multiple memories in a single request
- [Batch Delete](https://docs.mem0.ai/api-reference/memory/batch-delete): Delete multiple memories in a single request
- [Delete All Memories](https://docs.mem0.ai/api-reference/memory/delete-memories): Remove all memories matching criteria
- [FAQs](https://docs.mem0.ai/platform/faqs): Frequently asked questions about Mem0's Platform capabilities and implementation details
- [Changelog](https://docs.mem0.ai/changelog): Detailed product updates and version history for tracking new features and improvements
- [Contributing Guide](https://docs.mem0.ai/contributing/development): Guidelines for contributing to Mem0's open-source development
- [OpenMemory](https://docs.mem0.ai/openmemory/overview): Open-source memory infrastructure for research and experimentation
### Events APIs
- [Get Events](https://docs.mem0.ai/api-reference/events/get-events): List asynchronous memory operation events
- [Get Event](https://docs.mem0.ai/api-reference/events/get-event): Retrieve details of a specific event
### Entities APIs
- [Get Users](https://docs.mem0.ai/api-reference/entities/get-users): List all entities (users, agents, apps)
- [Delete User](https://docs.mem0.ai/api-reference/entities/delete-user): Remove an entity and all associated memories
### Organizations APIs
- [Create Organization](https://docs.mem0.ai/api-reference/organization/create-org): Create a new organization
- [Get Organizations](https://docs.mem0.ai/api-reference/organization/get-orgs): List all organizations
- [Get Organization](https://docs.mem0.ai/api-reference/organization/get-org): Retrieve organization details
- [Get Organization Members](https://docs.mem0.ai/api-reference/organization/get-org-members): List organization members
- [Add Organization Member](https://docs.mem0.ai/api-reference/organization/add-org-member): Add a member to an organization
- [Delete Organization](https://docs.mem0.ai/api-reference/organization/delete-org): Remove an organization
### Project APIs
- [Create Project](https://docs.mem0.ai/api-reference/project/create-project): Create a new project within an organization
- [Get Projects](https://docs.mem0.ai/api-reference/project/get-projects): List all projects
- [Get Project](https://docs.mem0.ai/api-reference/project/get-project): Retrieve project details
- [Get Project Members](https://docs.mem0.ai/api-reference/project/get-project-members): List project members
- [Add Project Member](https://docs.mem0.ai/api-reference/project/add-project-member): Add a member to a project
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project): Remove a project
### Webhook APIs
- [Create Webhook](https://docs.mem0.ai/api-reference/webhook/create-webhook): Register a new webhook endpoint
- [Get Webhook](https://docs.mem0.ai/api-reference/webhook/get-webhook): Retrieve webhook configuration
- [Update Webhook](https://docs.mem0.ai/api-reference/webhook/update-webhook): Modify webhook settings
- [Delete Webhook](https://docs.mem0.ai/api-reference/webhook/delete-webhook): Remove a webhook
## Community & Support
- [Contributing - Development](https://docs.mem0.ai/contributing/development): Guidelines for contributing to Mem0's open-source development
- [Contributing - Documentation](https://docs.mem0.ai/contributing/documentation): Guidelines for contributing to Mem0's documentation
- [Changelog](https://docs.mem0.ai/changelog): Detailed product updates and version history
+22 -7
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+22 -7
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -123,13 +123,9 @@ config = {
"reranker": {
"provider": "llm_reranker",
"config": {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"api_key": "your-openai-api-key"
}
},
"provider": "openai",
"model": "gpt-4o-mini",
"api_key": "your-openai-api-key",
"top_k": 5
}
}
+31 -3
View File
@@ -13,6 +13,10 @@ The Mem0 REST API server exposes every OSS memory operation over HTTP. Run it al
- You plan to explore or debug endpoints through the built-in OpenAPI page at `/docs`.
</Info>
<Warning>
**OSS vs Platform API paths:** The self-hosted OSS server does **not** use the `/v1/` prefix. For example, the endpoint is `POST /memories`, not `POST /v1/memories/`. The [API Reference](/api-reference) documents the hosted platform at `api.mem0.ai` which uses `/v1/` paths — those do not apply to the OSS server.
</Warning>
<Warning>
Enable API key authentication (see below) and HTTPS before exposing the server to anything beyond your internal network.
</Warning>
@@ -147,13 +151,18 @@ curl -X POST http://localhost:8000/memories \
</Info>
```bash
curl "http://localhost:8000/memories/search?user_id=alice&query=vegetable"
curl -X POST http://localhost:8000/search \
-H "Content-Type: application/json" \
-d '{
"query": "vegetable",
"user_id": "alice"
}'
```
### Explore with OpenAPI docs
1. Navigate to `http://localhost:8000/docs`.
2. Pick an endpoint (e.g., `POST /memories/search`).
1. Navigate to `http://localhost:8000/docs`.
2. Pick an endpoint (e.g., `POST /search`).
3. Fill in parameters and click **Execute** to try requests in-browser.
<Tip>
@@ -162,6 +171,25 @@ curl "http://localhost:8000/memories/search?user_id=alice&query=vegetable"
---
## Endpoint reference
The OSS REST server exposes the following endpoints. None use the `/v1/` prefix.
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/configure` | Set memory configuration |
| `POST` | `/memories` | Create memories |
| `GET` | `/memories` | Get all memories (filter by `user_id`, `agent_id`, or `run_id`) |
| `GET` | `/memories/{memory_id}` | Get a specific memory |
| `PUT` | `/memories/{memory_id}` | Update a memory |
| `DELETE` | `/memories/{memory_id}` | Delete a specific memory |
| `DELETE` | `/memories` | Delete all memories for an identifier |
| `GET` | `/memories/{memory_id}/history` | Get memory history |
| `POST` | `/search` | Search memories |
| `POST` | `/reset` | Reset all memories |
---
## Verify the feature is working
- Hit the root route and `/docs` to confirm the server is reachable.
+1 -1
View File
@@ -52,7 +52,7 @@ m.add(messages, user_id="alex")
<Step title="Search memories">
```python
results = m.search("What do you know about me?", filters={"user_id": "alex"})
results = m.search("What do you know about me?", user_id="alex")
print(results)
```
+55 -3
View File
@@ -4,7 +4,7 @@
"title": "Mem0 API Docs",
"description": "mem0.ai API Docs",
"contact": {
"email": "deshraj@mem0.ai"
"email": "support@mem0.ai"
},
"license": {
"name": "Apache 2.0"
@@ -457,7 +457,33 @@
}
}
}
}
},
"x-code-samples": [
{
"lang": "Python",
"source": "import requests\n\nurl = \"https://api.mem0.ai/v1/events/\"\n\nheaders = {\"Authorization\": \"Token <api-key>\"}\n\nresponse = requests.request(\"GET\", url, headers=headers)\n\nprint(response.text)"
},
{
"lang": "JavaScript",
"source": "const options = {method: 'GET', headers: {Authorization: 'Token <api-key>'}};\n\nfetch('https://api.mem0.ai/v1/events/', options)\n .then(response => response.json())\n .then(response => console.log(response))\n .catch(err => console.error(err));"
},
{
"lang": "cURL",
"source": "curl --request GET \\\n --url https://api.mem0.ai/v1/events/ \\\n --header 'Authorization: Token <api-key>'"
},
{
"lang": "Go",
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/events/\"\n\n\treq, _ := http.NewRequest(\"GET\", url, nil)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
},
{
"lang": "PHP",
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/events/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"GET\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\"\n ],\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
},
{
"lang": "Java",
"source": "HttpResponse<String> response = Unirest.get(\"https://api.mem0.ai/v1/events/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .asString();"
}
]
}
},
"/v1/event/{event_id}/": {
@@ -563,7 +589,33 @@
}
}
}
}
},
"x-code-samples": [
{
"lang": "Python",
"source": "import requests\n\nurl = \"https://api.mem0.ai/v1/event/{event_id}/\"\n\nheaders = {\"Authorization\": \"Token <api-key>\"}\n\nresponse = requests.request(\"GET\", url, headers=headers)\n\nprint(response.text)"
},
{
"lang": "JavaScript",
"source": "const options = {method: 'GET', headers: {Authorization: 'Token <api-key>'}};\n\nfetch('https://api.mem0.ai/v1/event/{event_id}/', options)\n .then(response => response.json())\n .then(response => console.log(response))\n .catch(err => console.error(err));"
},
{
"lang": "cURL",
"source": "curl --request GET \\\n --url https://api.mem0.ai/v1/event/{event_id}/ \\\n --header 'Authorization: Token <api-key>'"
},
{
"lang": "Go",
"source": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"io/ioutil\"\n)\n\nfunc main() {\n\n\turl := \"https://api.mem0.ai/v1/event/{event_id}/\"\n\n\treq, _ := http.NewRequest(\"GET\", url, nil)\n\n\treq.Header.Add(\"Authorization\", \"Token <api-key>\")\n\n\tres, _ := http.DefaultClient.Do(req)\n\n\tdefer res.Body.Close()\n\tbody, _ := ioutil.ReadAll(res.Body)\n\n\tfmt.Println(res)\n\tfmt.Println(string(body))\n\n}"
},
{
"lang": "PHP",
"source": "<?php\n\n$curl = curl_init();\n\ncurl_setopt_array($curl, [\n CURLOPT_URL => \"https://api.mem0.ai/v1/event/{event_id}/\",\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_ENCODING => \"\",\n CURLOPT_MAXREDIRS => 10,\n CURLOPT_TIMEOUT => 30,\n CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n CURLOPT_CUSTOMREQUEST => \"GET\",\n CURLOPT_HTTPHEADER => [\n \"Authorization: Token <api-key>\"\n ],\n]);\n\n$response = curl_exec($curl);\n$err = curl_error($curl);\n\ncurl_close($curl);\n\nif ($err) {\n echo \"cURL Error #:\" . $err;\n} else {\n echo $response;\n}"
},
{
"lang": "Java",
"source": "HttpResponse<String> response = Unirest.get(\"https://api.mem0.ai/v1/event/{event_id}/\")\n .header(\"Authorization\", \"Token <api-key>\")\n .asString();"
}
]
}
},
"/v1/exports/": {
-56
View File
@@ -1,56 +0,0 @@
---
title: MCP Client Integration Guide
description: "Connect MCP-compatible clients to a locally running OpenMemory server for seamless memory integration."
icon: "plug"
iconType: "solid"
---
## Connecting an MCP Client
Once your OpenMemory server is running locally, you can connect any compatible MCP client to your personal memory stream. This enables a seamless memory layer integration for AI tools and agents.
Ensure the following environment variables are correctly set in your configuration files:
**In `/ui/.env`:**
```env
NEXT_PUBLIC_API_URL=http://localhost:8765
NEXT_PUBLIC_USER_ID=<user-id>
```
**In `/api/.env`:**
```env
OPENAI_API_KEY=sk-xxx
USER=<user-id>
```
These values define where your MCP server is running and which user's memory is accessed.
### MCP Client Setup
Use the following one-step command to configure OpenMemory Local MCP to a client. The general command format is as follows:
```bash
npx @openmemory/install local http://localhost:8765/mcp/<client-name>/sse/<user-id> --client <client-name>
```
Replace `<client-name>` with the desired client name and `<user-id>` with the value specified in your environment variables.
### Example Commands for Supported Clients
| Client | Command |
|-------------|---------|
| Claude | `npx install-mcp http://localhost:8765/mcp/claude/sse/<user-id> --client claude` |
| Cursor | `npx install-mcp http://localhost:8765/mcp/cursor/sse/<user-id> --client cursor` |
| Cline | `npx install-mcp http://localhost:8765/mcp/cline/sse/<user-id> --client cline` |
| RooCline | `npx install-mcp http://localhost:8765/mcp/roocline/sse/<user-id> --client roocline` |
| Windsurf | `npx install-mcp http://localhost:8765/mcp/windsurf/sse/<user-id> --client windsurf` |
| Witsy | `npx install-mcp http://localhost:8765/mcp/witsy/sse/<user-id> --client witsy` |
| Enconvo | `npx install-mcp http://localhost:8765/mcp/enconvo/sse/<user-id> --client enconvo` |
| Augment | `npx install-mcp http://localhost:8765/mcp/augment/sse/<user-id> --client augment` |
| Claude Code | `claude mcp add -s user -t sse OpenMemory http://localhost:8765/mcp/openmemory/sse/<user-id>` |
### What This Does
Running one of the above commands registers the specified MCP client and connects it to your OpenMemory server. This enables the client to stream and store contextual memory for the provided user ID.
The connection status and memory activity can be monitored via the OpenMemory UI at [http://localhost:3000](http://localhost:3000).
-124
View File
@@ -1,124 +0,0 @@
---
title: Overview
description: "Overview of OpenMemory, a local and hosted memory infrastructure powered by Mem0 with MCP server support."
icon: "info"
iconType: "solid"
---
## Hosted OpenMemory MCP Now Available
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
Everything you love about OpenMemory MCP but with zero setup.
- Works with all MCP-compatible tools (Claude Desktop, Cursor, etc.)
- Same standard memory operations: `add_memories`, `search_memory`, etc.
- One-click provisioning, no Docker required
- Powered by Mem0
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
### Get Started Now
Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev).
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
OpenMemory is a local memory infrastructure powered by Mem0 that lets you carry your memory across any AI app. It provides a unified memory layer that stays with you, enabling agents and assistants to remember what matters across applications.
<img src="https://github.com/user-attachments/assets/3c701757-ad82-4afa-bfbe-e049c2b4320b" alt="OpenMemory UI" />
## What is the OpenMemory MCP Server
The OpenMemory MCP Server is a private, local-first memory server that creates a shared, persistent memory layer for your MCP-compatible tools. It runs entirely on your machine, enabling seamless context handoff across tools. Whether you're switching between development, planning, or debugging environments, your AI assistants can access relevant memory without needing repeated instructions.
The OpenMemory MCP Server ensures all memory stays local, structured, and under your control with no cloud sync or external storage.
## OpenMemory Easy Setup
### Prerequisites
- Docker
- OpenAI API Key
You can quickly run OpenMemory by running the following command:
```bash
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | bash
```
You should set the `OPENAI_API_KEY` as a global environment variable:
```bash
export OPENAI_API_KEY=your_api_key
```
You can also set the `OPENAI_API_KEY` as a parameter to the script:
```bash
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | OPENAI_API_KEY=your_api_key bash
```
This will start the OpenMemory server and the OpenMemory UI. Deleting the container will lead to the deletion of the memory store. We suggest you follow the instructions [here](/openmemory/quickstart#setting-up-openmemory) to set up OpenMemory on your local machine with a more persistent memory store.
## How the OpenMemory MCP Server Works
Built around the Model Context Protocol (MCP), the OpenMemory MCP Server exposes a standardized set of memory tools:
- `add_memories`: Store new memory objects
- `search_memory`: Retrieve relevant memories
- `list_memories`: View all stored memory
- `delete_all_memories`: Clear memory entirely
Any MCP-compatible tool can connect to the server and use these APIs to persist and access memory.
## What It Enables
### Cross-Client Memory Access
Store context in Cursor and retrieve it later in Claude or Windsurf without repeating yourself.
### Fully Local Memory Store
All memory is stored on your machine. Nothing goes to the cloud. You maintain full ownership and control.
### Unified Memory UI
The built-in OpenMemory dashboard provides a central view of everything stored. Add, browse, delete, and control memory access to clients directly from the dashboard.
## Supported Clients
The OpenMemory MCP Server is compatible with any client that supports the Model Context Protocol. This includes:
- Cursor
- Claude Desktop
- Windsurf
- Cline
- And more
As more AI systems adopt MCP, your private memory becomes more valuable.
## Real-World Examples
### Scenario 1: Cross-Tool Project Flow
Define technical requirements of a project in Claude Desktop. Build in Cursor. Debug issues in Windsurf - all with shared context passed through OpenMemory.
### Scenario 2: Preferences That Persist
Set your preferred code style or tone in one tool. When you switch to another MCP client, it can access those same preferences without redefining them.
### Scenario 3: Project Knowledge
Save important project details once, then access them from any compatible AI tool - no more repetitive explanations.
## Conclusion
The OpenMemory MCP Server brings memory to MCP-compatible tools without giving up control or privacy. It solves a foundational limitation in modern LLM workflows: the loss of context across tools, sessions, and environments.
By standardizing memory operations and keeping all data local, it reduces token overhead, improves performance, and unlocks more intelligent interactions across the growing ecosystem of AI assistants.
This is just the beginning. The MCP server is the first core layer in the OpenMemory platform, a broader effort to make memory portable, private, and interoperable across AI systems.
## Getting Started Today
- Repository: [GitHub](https://github.com/mem0ai/mem0/tree/main/openmemory)
- Join our community: [Discord](https://discord.gg/6PzXDgEjG5)
With OpenMemory, your AI memories stay private, portable, and under your control, exactly where they belong.
OpenMemory: Your memories, your control.
## Contributing
OpenMemory is open source and we welcome contributions. Please see the [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/openmemory/CONTRIBUTING.md) file for more information.
-197
View File
@@ -1,197 +0,0 @@
---
title: Quickstart
description: "Get started with hosted or self-hosted OpenMemory MCP, including API key setup and client installation."
icon: "terminal"
iconType: "solid"
---
## Hosted OpenMemory MCP Now Available
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
Everything you love about OpenMemory MCP but with zero setup.
- Works with all MCP-compatible tools (Claude Desktop, Cursor, etc.)
- Same standard memory operations: `add_memories`, `search_memory`, etc.
- One-click provisioning, no Docker required
- Powered by Mem0
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
### Get Started Now
Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev).
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
## Getting Started with Hosted OpenMemory
The fastest way to get started is with our hosted version - no setup required.
### 1. Get Your API Key
Visit [app.openmemory.dev](https://app.openmemory.dev) to sign up and get your `OPENMEMORY_API_KEY`.
### 2. Install and Connect to Your Preferred Client
Example commands (replace `your-key` with your actual API key):
**For Claude Desktop:**
```bash
npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key
```
**For Cursor:**
```bash
npx @openmemory/install --client cursor --env OPENMEMORY_API_KEY=your-key
```
**For Windsurf:**
```bash
npx @openmemory/install --client windsurf --env OPENMEMORY_API_KEY=your-key
```
That's it! Your AI client now has persistent memory across sessions.
## Local Setup (Self-Hosted)
Prefer to run OpenMemory locally? Follow the instructions below for a self-hosted setup.
## OpenMemory Easy Setup
### Prerequisites
- Docker
- OpenAI API Key
You can quickly run OpenMemory by running the following command:
```bash
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | bash
```
You should set the `OPENAI_API_KEY` as a global environment variable:
```bash
export OPENAI_API_KEY=your_api_key
```
You can also set the `OPENAI_API_KEY` as a parameter to the script:
```bash
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | OPENAI_API_KEY=your_api_key bash
```
This will start the OpenMemory server and the OpenMemory UI. Deleting the container will lead to the deletion of the memory store. We suggest you follow the instructions below to set up OpenMemory on your local machine with a more persistent memory store.
## Setting Up OpenMemory
Getting started with OpenMemory is straightforward and takes just a few minutes to set up on your local machine. Follow these steps:
### 1. Clone the Repository
```bash
# Clone the repository
git clone https://github.com/mem0ai/mem0.git
cd mem0/openmemory
```
### 2. Set Up Environment Variables
Before running the project, you need to configure environment variables for both the API and the UI.
You can do this in one of the following ways:
- **Manually:** Create a `.env` file in each of the following directories:
- `/api/.env`
- `/ui/.env`
- **Using `.env.example` files:** Copy and rename the example files:
```bash
cp api/.env.example api/.env
cp ui/.env.example ui/.env
```
- **Using Makefile** (if supported): Run:
```bash
make env
```
#### Example `/api/.env`
```bash
OPENAI_API_KEY=sk-xxx
USER=<user-id> # The User ID you want to associate the memories with
```
#### LLM Configuration (optional)
By default, OpenMemory uses OpenAI (`gpt-4o-mini`) for the LLM and embedder. You can configure a different provider by adding these variables to `/api/.env`:
| Variable | Description | Default |
|---|---|---|
| `LLM_PROVIDER` | LLM provider (`openai`, `ollama`, `anthropic`, `groq`, `together`, `deepseek`, etc.) | `openai` |
| `LLM_MODEL` | Model name for the LLM provider | `gpt-4o-mini` (OpenAI) / `llama3.1:latest` (Ollama) |
| `LLM_API_KEY` | API key for the LLM provider | `OPENAI_API_KEY` env var |
| `LLM_BASE_URL` | Custom base URL for the LLM API | Provider default |
| `OLLAMA_BASE_URL` | Ollama-specific base URL (takes precedence over `LLM_BASE_URL` for Ollama) | `http://localhost:11434` |
| `EMBEDDER_PROVIDER` | Embedder provider (defaults to `ollama` when LLM is Ollama, otherwise `openai`) | `openai` |
| `EMBEDDER_MODEL` | Model name for the embedder | `text-embedding-3-small` (OpenAI) / `nomic-embed-text` (Ollama) |
| `EMBEDDER_API_KEY` | API key for the embedder provider | `OPENAI_API_KEY` env var |
| `EMBEDDER_BASE_URL` | Custom base URL for the embedder API | Provider default |
**Example: Using Ollama (fully local)**
```bash
LLM_PROVIDER=ollama
LLM_MODEL=llama3.1:latest
EMBEDDER_PROVIDER=ollama
EMBEDDER_MODEL=nomic-embed-text
OLLAMA_BASE_URL=http://localhost:11434
```
**Example: Using Anthropic**
```bash
LLM_PROVIDER=anthropic
LLM_MODEL=claude-sonnet-4-20250514
LLM_API_KEY=sk-ant-xxx
```
#### Example `/ui/.env`
```bash
NEXT_PUBLIC_API_URL=http://localhost:8765
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user ID for environment variable in api
```
### 3. Build and Run the Project
You can run the project using the following two commands:
```bash
make build # Builds the MCP server and UI
make up # Runs OpenMemory MCP server and UI
```
After running these commands, you will have:
- OpenMemory MCP server running at http://localhost:8765 (API documentation available at http://localhost:8765/docs)
- OpenMemory UI running at http://localhost:3000
#### UI Not Working on http://localhost:3000?
If the UI does not start properly on http://localhost:3000, try running it manually:
```bash
cd ui
pnpm install
pnpm dev
```
You can configure the MCP client using the following command (replace `username` with your username):
```bash
npx @openmemory/install local "http://localhost:8765/mcp/cursor/sse/username" --client cursor
```
**For Claude Code (CLI):**
```bash
claude mcp add -s user -t sse OpenMemory "http://localhost:8765/mcp/openmemory/sse/username"
```
The OpenMemory dashboard will be available at http://localhost:3000. From here, you can view and manage your memories and check connection status with your MCP clients.
Once set up, OpenMemory runs locally on your machine, ensuring all your AI memories remain private and secure while being accessible across any compatible MCP client.
## Getting Started Today
GitHub Repository: https://github.com/mem0ai/mem0/tree/main/openmemory
+303
View File
@@ -0,0 +1,303 @@
---
title: CLI
description: "Manage memories from your terminal. Available for Node.js and Python."
icon: "terminal"
iconType: "solid"
---
The mem0 CLI lets you add, search, list, update, and delete memories directly from the terminal. It works with the Mem0 Platform API and is available as both an npm package and a Python package.
Both implementations provide identical behavior — same commands, same options, same output formats.
## Installation
<CodeGroup>
```bash npm
npm install -g @mem0/cli
```
```bash pip
pip install mem0-cli
```
</CodeGroup>
## Authentication
Run the interactive setup wizard to configure your API key:
```bash
mem0 init
```
This prompts for your API key and a default user ID, validates the connection, and saves the configuration locally.
For CI/CD or non-interactive environments, pass both values as flags:
```bash
mem0 init --api-key m0-xxx --user-id alice
```
You can also set your API key via environment variable:
```bash
export MEM0_API_KEY="m0-xxx"
```
## Quick start
```bash
# 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 prefer light mode now"
# 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 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 entities`
List or delete entities (users, agents, apps).
```bash
mem0 entities list
mem0 entities list --type agent --output json
mem0 entities delete --user-id alice --force
```
### `mem0 status`
Verify your API connection and display the current project.
```bash
mem0 status
```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Output formats
All commands support the `--output` flag to control how results are displayed:
| Format | Description |
|--------|-------------|
| `text` | Human-readable output with colors and formatting (default for most commands) |
| `json` | Structured JSON, suitable for piping to `jq` or consumption by AI agents |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal output — just IDs or status codes |
Example with JSON output:
```bash
mem0 search "user preferences" --user-id alice --output json | jq '.data.results[].memory'
```
## Use with AI agents
The CLI is designed to be used by AI agents and automation tools. Two features make this straightforward:
- **`--output json`** returns structured data that agents can parse directly
- **`mem0 help --json`** returns the full command tree as JSON, so agents can discover available commands programmatically
```bash
# Agent adds a memory
mem0 add "User prefers concise responses" --user-id user-42 --output quiet
# Agent searches memories and parses results
mem0 search "response preferences" --user-id user-42 --output json
```
For non-interactive environments (CI, agent runtimes), use `mem0 init --api-key --user-id` or set the `MEM0_API_KEY` environment variable to skip interactive prompts.
## Environment variables
| 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.
## Global flags
These flags are available on all commands that interact with the API:
| Flag | Description |
|------|-------------|
| `--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 |
## What's next
<CardGroup cols={3}>
<Card title="Quickstart" icon="bolt" href="/platform/quickstart">
Store your first memory in under five minutes using the SDK or CLI
</Card>
<Card title="Memory Operations" icon="database" href="/core-concepts/memory-operations/add">
Learn about add, search, update, and delete operations in depth
</Card>
<Card title="API Reference" icon="code" href="/api-reference/memory/add-memories">
See the complete REST API documentation
</Card>
</CardGroup>
+46 -139
View File
@@ -9,11 +9,48 @@ description: "Connect any AI client to Mem0 using Model Context Protocol for uni
When building AI applications, memory management often requires manual integration. MCP eliminates this complexity by:
- **Universal compatibility**: Works with any MCP-compatible client (Claude Desktop, Cursor, custom agents)
- **Universal compatibility**: Works with any MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
- **Agent autonomy**: AI agents decide when to save, search, or update memories
- **Zero infrastructure**: No servers to maintain - Mem0 handles everything
- **Zero infrastructure**: No servers to maintain - Mem0's cloud MCP handles everything
- **Standardized protocol**: One integration works across all your AI tools
## Setup
Add Mem0 MCP to all supported clients with a single command:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
Or configure a specific client:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "cursor"
```
For manual configuration, add this to your MCP client config:
```json
{
"mcpServers": {
"mem0-mcp": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp"
}
}
}
```
For detailed per-client instructions, see the [Mem0 MCP Quickstart](/platform/mem0-mcp).
## Available tools
The MCP server exposes 9 memory tools to your AI client:
@@ -30,139 +67,12 @@ The MCP server exposes 9 memory tools to your AI client:
| `get_memory` | Retrieve single memory by ID |
| `list_entities` | View stored entities |
## Deployment options
## How it works
Choose the deployment method that fits your workflow:
<AccordionGroup>
<Accordion title="Python package (recommended)">
Install and run locally with uvx:
```bash
uv pip install mem0-mcp-server
```
Configure your client:
```json
{
"mcpServers": {
"mem0": {
"command": "uvx",
"args": ["mem0-mcp-server"],
"env": {
"MEM0_API_KEY": "m0-...",
"MEM0_DEFAULT_USER_ID": "your-handle"
}
}
}
}
```
</Accordion>
<Accordion title="Docker container">
Containerized deployment with HTTP endpoint:
```bash
docker build -t mem0-mcp-server https://github.com/mem0ai/mem0-mcp.git
docker run --rm -d -e MEM0_API_KEY="m0-..." -p 8080:8081 mem0-mcp-server
```
Configure for HTTP:
```json
{
"mcpServers": {
"mem0-docker": {
"command": "curl",
"args": ["-X", "POST", "http://localhost:8080/mcp", "--data-binary", "@"],
"env": {
"MEM0_API_KEY": "m0-..."
}
}
}
}
```
</Accordion>
<Accordion title="Smithery">
One-click setup with managed service:
Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) and:
1. Select your AI client (Cursor, Claude Desktop, etc.)
2. Configure your Mem0 API key
3. Set your default user ID
4. Enable graph memory (optional)
5. Copy the generated configuration
Your client connects automatically - no installation required.
</Accordion>
</AccordionGroup>
## Configuration
### Required environment variables
```bash
MEM0_API_KEY="m0-..." # Your Mem0 API key
MEM0_DEFAULT_USER_ID="your-handle" # Default user ID
```
### Optional variables
```bash
MEM0_ENABLE_GRAPH_DEFAULT="true" # Enable graph memories
MEM0_MCP_AGENT_MODEL="gpt-4o-mini" # LLM for bundled examples
```
<AccordionGroup>
<Accordion title="Test your setup with the Python agent">
The included Pydantic AI agent provides an interactive REPL to test memory operations:
```bash
# Install the package
pip install mem0-mcp-server
# Set your API keys
export MEM0_API_KEY="m0-..."
export OPENAI_API_KEY="sk-openai-..."
# Clone and test with the agent
git clone https://github.com/mem0ai/mem0-mcp.git
cd mem0-mcp-server
python example/pydantic_ai_repl.py
```
**Testing different server configurations:**
- **Local server** (default): `python example/pydantic_ai_repl.py`
- **Docker container**:
```bash
export MEM0_MCP_CONFIG_PATH=example/docker-config.json
export MEM0_MCP_CONFIG_SERVER=mem0-docker
python example/pydantic_ai_repl.py
```
- **Smithery remote**:
```bash
export MEM0_MCP_CONFIG_PATH=example/config-smithery.json
export MEM0_MCP_CONFIG_SERVER=mem0-memory-mcp
python example/pydantic_ai_repl.py
```
Try these test prompts:
- "Remember that I love tiramisu"
- "Search for my food preferences"
- "Update my project: the mobile app is now 80% complete"
- "Show me all memories about project Phoenix"
- "Delete memories from 2023"
</Accordion>
</AccordionGroup>
## How the testing works
1. **Configuration loads** - Reads from `example/config.json` by default
2. **Server starts** - Launches or connects to the Mem0 MCP server
3. **Agent connects** - Pydantic AI agent (Mem0Guide) attaches to the server
4. **Interactive REPL** - You get a chat interface to test all memory operations
1. **Configure the MCP server** - Add Mem0 MCP to your AI client using the setup command above
2. **Agent connects** - Your AI client connects to Mem0's cloud MCP server over HTTP
3. **Autonomous memory** - The agent decides when to store/retrieve memories as part of its reasoning
4. **No manual API calls** - The agent manages memory automatically through MCP tools
## Example interactions
@@ -225,14 +135,11 @@ The Mem0 MCP server enables powerful memory capabilities for your AI application
## Best practices
- **Start simple**: Use the Python package for development
- **Use the cloud MCP**: The hosted MCP server at `https://mcp.mem0.ai/mcp` handles infrastructure for you
- **Use wildcards**: `user_id: "*"` to search across all users
- **Test locally**: Use the bundled Python agent to verify setup
- **Monitor usage**: Track memory operations in the dashboard
- **Document patterns**: Share successful prompt patterns with your team
{/* DEBUG: verify CTA targets */}
<CardGroup cols={2}>
<Card
title="Memory Filters"
@@ -246,4 +153,4 @@ The Mem0 MCP server enables powerful memory capabilities for your AI application
icon="book-open"
href="/cookbooks/frameworks/gemini-3-with-mem0-mcp"
/>
</CardGroup>
</CardGroup>
+108 -141
View File
@@ -2,28 +2,34 @@
title: "Mem0 MCP"
description: "Connect any AI client to Mem0 using Model Context Protocol in minutes"
icon: "puzzle-piece"
estimatedTime: "~5 minutes"
estimatedTime: "~2 minutes"
---
<Info>
**Prerequisites**
- Mem0 Platform account ([Sign up here](https://app.mem0.ai))
- API key ([Get one from dashboard](https://app.mem0.ai/settings/api-keys))
- Python 3.10+, Docker, or Node.js 14+
- An MCP-compatible client (Claude Desktop, Cursor, or custom agent)
- Node.js 14+ (for npx)
- An MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
</Info>
## What is Mem0 MCP?
Mem0 MCP Server exposes Mem0's memory capabilities as MCP tools, letting AI agents decide when to save, search, or update information.
Mem0 MCP Server exposes Mem0's memory capabilities as MCP tools, letting AI agents decide when to save, search, or update information. The cloud-hosted MCP server requires no local installation — just connect and start using memory.
## Deployment Options
## Quick Setup
Choose from three deployment methods:
Add Mem0 MCP to your preferred clients with a single command:
1. **Python Package (Recommended)** - Install locally with `uvx` for instant setup
2. **Docker Container** - Isolated deployment with HTTP endpoint
3. **Smithery** - Remote hosted service for managed deployments
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
This automatically configures Mem0 MCP for all supported clients at once.
## Available Tools
@@ -43,54 +49,105 @@ The MCP server exposes these memory tools to your AI client:
---
## Quickstart with Python (UVX)
## Client-Specific Setup
<Steps>
<Step title="Install the MCP Server">
```bash
uv pip install mem0-mcp-server
```
</Step>
You can also configure individual clients:
<Step title="Configure your MCP client">
Add this to your MCP client (e.g., Claude Desktop):
<AccordionGroup>
<Accordion title="Claude Desktop">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude"
```
```json
{
"mcpServers": {
"mem0": {
"command": "uvx",
"args": ["mem0-mcp-server"],
"env": {
"MEM0_API_KEY": "m0-...",
"MEM0_DEFAULT_USER_ID": "your-handle"
Or manually add to your Claude Desktop configuration (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"mem0-mcp": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp"
}
}
}
}
}
```
```
</Accordion>
Set your environment variables:
<Accordion title="Claude Code">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude code"
```
</Accordion>
```bash
export MEM0_API_KEY="m0-..."
export MEM0_DEFAULT_USER_ID="your-handle"
```
</Step>
<Accordion title="Cursor">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "cursor"
```
<Step title="Test with the Python agent">
```bash
# Clone the mem0-mcp repository
git clone https://github.com/mem0ai/mem0-mcp.git
cd mem0-mcp
Or go to Cursor → Settings → MCP and add:
```json
{
"mcpServers": {
"mem0-mcp": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp"
}
}
}
```
</Accordion>
# Set your API keys
export MEM0_API_KEY="m0-..."
export OPENAI_API_KEY="sk-openai-..."
<Accordion title="Windsurf">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "windsurf"
```
</Accordion>
# Run the interactive agent
python example/pydantic_ai_repl.py
```
<Accordion title="VS Code">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "vscode"
```
</Accordion>
<Accordion title="OpenCode">
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "opencode"
```
</Accordion>
</AccordionGroup>
---
## Verify Your Setup
Once configured, your AI client can:
- Automatically save information with `add_memory`
- Search memories with `search_memories`
- Update memories with `update_memory`
- Delete memories with `delete_memory`
**Sample Interactions:**
@@ -104,107 +161,18 @@ Agent: Based on your memories, you love tiramisu.
User: Update my project: the mobile app is now 80% complete
Agent: Updated your project status successfully.
```
</Step>
<Step title="Verify the setup">
Your AI client can now:
- Automatically save information with `add_memory`
- Search memories with `search_memories`
- Update memories with `update_memory`
- Delete memories with `delete_memory`
<Info icon="check">
If you get "Connection failed", ensure your API key is valid and the server is running.
If you get "Connection failed", ensure you have a valid API key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys).
</Info>
</Step>
</Steps>
---
## Quickstart with Docker
<Steps>
<Step title="Build the Docker image">
```bash
docker build -t mem0-mcp-server https://github.com/mem0ai/mem0-mcp.git
```
</Step>
<Step title="Run the container">
```bash
docker run --rm -d \
--name mem0-mcp \
-e MEM0_API_KEY="m0-..." \
-p 8080:8081 \
mem0-mcp-server
```
</Step>
<Step title="Configure your client for HTTP">
For clients that connect via HTTP (instead of stdio):
```json
{
"mcpServers": {
"mem0-docker": {
"command": "curl",
"args": ["-X", "POST", "http://localhost:8080/mcp", "--data-binary", "@-"],
"env": {
"MEM0_API_KEY": "m0-..."
}
}
}
}
```
</Step>
<Step title="Verify the setup">
```bash
# Check container logs
docker logs mem0-mcp
# Test HTTP endpoint
curl http://localhost:8080/health
```
<Info icon="check">
The container should start successfully and respond to HTTP requests. If port 8080 is occupied, change it with `-p 8081:8081`.
</Info>
</Step>
</Steps>
---
## Quickstart with Smithery (Hosted)
For the simplest integration, use Smithery's hosted Mem0 MCP server - no installation required.
**Example: One-click setup in Cursor**
1. Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) and select Cursor as your client
![Smithery Mem0 MCP Configuration](/images/smithery-mem0-mcp.png)
2. Open Cursor → Settings → MCP
3. Click `mem0-mcp` → Initiate authorization
4. Configure Smithery with your environment:
- `MEM0_API_KEY`: Your Mem0 API key
- `MEM0_DEFAULT_USER_ID`: Your user ID
- `MEM0_ENABLE_GRAPH_DEFAULT`: Optional, set to `true` for graph memories
5. Return to Cursor settings and wait for tools to load
6. Start chatting with Cursor and begin storing preferences
**For other clients:**
Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) to connect any MCP-compatible client with your Mem0 credentials.
---
## Quick Recovery
- **"uvx command not found"** → Install with `pip install uv` or use `pip install mem0-mcp-server` instead. Make sure your Python environment has `uv` installed (or system-wide).
- **"Connection refused"** → Check that the server is running and the correct port is configured
- **"Connection refused"** → Check your internet connection and ensure the MCP client is correctly configured
- **"Invalid API key"** → Get a new key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys)
- **"Permission denied"** → Ensure Docker has access to bind ports (try with `sudo` on Linux)
- **"npx command not found"** → Install Node.js from [nodejs.org](https://nodejs.org)
---
@@ -227,6 +195,5 @@ Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@m
## Additional Resources
- **[Mem0 MCP Repository](https://github.com/mem0ai/mem0-mcp)** - Source code and examples
- **[Platform Quickstart](/platform/quickstart)** - Direct API integration guide
- **[MCP Specification](https://modelcontextprotocol.io)** - Learn about MCP protocol
- **[MCP Specification](https://modelcontextprotocol.io)** - Learn about MCP protocol
+12
View File
@@ -46,6 +46,10 @@ const client = new MemoryClient({ apiKey: 'your-api-key' });
export MEM0_API_KEY="your-api-key"
```
```bash CLI
mem0 init --api-key "your-api-key"
```
</CodeGroup>
</Step>
@@ -80,6 +84,10 @@ curl -X POST https://api.mem0.ai/v1/memories/add \
}'
```
```bash CLI
mem0 add "I'm a vegetarian and allergic to nuts." --user-id user123
```
</CodeGroup>
</Step>
@@ -105,6 +113,10 @@ curl -X POST https://api.mem0.ai/v1/memories/search \
}'
```
```bash CLI
mem0 search "What are my dietary restrictions?" --user-id user123
```
</CodeGroup>
**Output:**
+1 -1
View File
@@ -1,6 +1,6 @@
---
title: Integration Guide Template
description: "Pattern for pairing Mem0 with third-party tools."
description: "A reusable template for writing integration guides that pair Mem0 with third-party tools and services."
icon: "plug"
---
+16
View File
@@ -30,6 +30,22 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0
Works with Claude Code, Cursor, Windsurf, and any assistant that supports skills. Once installed, your assistant understands Mem0's full API, framework integrations, and common patterns.
## MCP Server Setup
Connect Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode, or any MCP-compatible client to Mem0.
Get your API key from [app.mem0.ai](https://app.mem0.ai), then add Mem0 MCP with a single command:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
For per-client setup and advanced options, see [Mem0 MCP Setup](/platform/mem0-mcp).
## Universal Starter Prompt
Copy this into any AI tool to start building with Mem0:
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mem0",
"version": "0.1.0",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
"email": "support@mem0.ai"
},
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"logo": "logo.svg",
"license": "Apache-2.0"
}

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