Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0fb194c76 | ||
|
|
215d8b5a71 | ||
|
|
82525dbf0f | ||
|
|
32c1ccba5b | ||
|
|
9bebcf45f7 | ||
|
|
93bd4e248c | ||
|
|
af19495f66 | ||
|
|
dbac83218f | ||
|
|
3618aeff22 | ||
|
|
25e25aaa2f | ||
|
|
5d30af9560 | ||
|
|
47969aaa6b | ||
|
|
7213e1b1a6 | ||
|
|
97291aa119 | ||
|
|
b8a5ca1b70 | ||
|
|
431cba20e9 | ||
|
|
4482228383 | ||
|
|
ae49edec44 | ||
|
|
4b7f51d194 | ||
|
|
41abb571a2 | ||
|
|
e280665578 | ||
|
|
27a6e7863e | ||
|
|
ae8e03c6b7 | ||
|
|
376be3b6d4 | ||
|
|
cd2dd7cc54 | ||
|
|
ece654811a | ||
|
|
13d42a99e9 | ||
|
|
3225e30859 | ||
|
|
88fd0e77d0 | ||
|
|
c2698f2b71 | ||
|
|
e87240d4d9 | ||
|
|
68cf4e118d | ||
|
|
12624555b4 | ||
|
|
e8004b93db | ||
|
|
4e414e4015 | ||
|
|
16455789d4 | ||
|
|
3ac4e047de | ||
|
|
a2ffca3266 | ||
|
|
3a9fcdbec2 | ||
|
|
515f87b6bd | ||
|
|
5edc0cc99f | ||
|
|
7fff26f374 | ||
|
|
1aecfadf45 | ||
|
|
7e06aeeada | ||
|
|
2a59c9fd99 | ||
|
|
669ed184e4 | ||
|
|
3c2683c1b5 | ||
|
|
f06e2d744d | ||
|
|
f9e30304d7 | ||
|
|
7e3b727528 | ||
|
|
13c7f84eec | ||
|
|
924ac00c52 | ||
|
|
2a36960f4c | ||
|
|
2e0f91e70d | ||
|
|
d1b4b304c7 | ||
|
|
2868bfe749 | ||
|
|
5431badfd4 |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -12,8 +12,6 @@ component:
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openmemory"
|
||||
matcher: "OpenMemory"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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)`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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("");
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 },
|
||||
]),
|
||||
};
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
@@ -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),
|
||||
},
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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]"
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Allow running with `python -m mem0_cli`."""
|
||||
|
||||
from mem0_cli.app import main
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Backend abstraction layer for mem0 CLI."""
|
||||
|
||||
from mem0_cli.backend.base import Backend, get_backend
|
||||
|
||||
__all__ = ["Backend", "get_backend"]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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)")
|
||||
@@ -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.")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 426 KiB |
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
|
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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/": {
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
@@ -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,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"
|
||||
---
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||