Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
406110460d | ||
|
|
29b9059cea | ||
|
|
a84a565422 | ||
|
|
0b397eaf06 | ||
|
|
051551edb7 |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -93,20 +93,6 @@ 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).
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# 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
|
||||
# Authenticate and save config
|
||||
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 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` | Interactive setup wizard — configures API key and default user ID |
|
||||
| `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
|
||||
@@ -1,543 +0,0 @@
|
||||
{
|
||||
"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": "Interactive setup wizard for mem0 CLI.",
|
||||
"usage": "mem0 init",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
"resolveIds": false,
|
||||
"resolveGraph": false,
|
||||
"confirmDangerous": false,
|
||||
"arguments": [],
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"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." }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
# 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
|
||||
@@ -1,91 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"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 src/index.ts --format esm --dts --clean",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* 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";
|
||||
@@ -1,303 +0,0 @@
|
||||
/**
|
||||
* Platform (SaaS) backend — communicates with api.mem0.ai.
|
||||
*/
|
||||
|
||||
import type { PlatformConfig } from "../config.js";
|
||||
import {
|
||||
type AddOptions,
|
||||
APIError,
|
||||
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();
|
||||
detail = (body as Record<string, string>).detail ?? resp.statusText;
|
||||
} catch {
|
||||
detail = resp.statusText;
|
||||
}
|
||||
throw new APIError(path, detail);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
||||
}
|
||||
if (resp.status === 204) {
|
||||
return {};
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/**
|
||||
* Branding and ASCII art for mem0 CLI.
|
||||
*/
|
||||
|
||||
import chalk from "chalk";
|
||||
import ora, { type Ora } from "ora";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const PKG_VERSION: string = _require("../package.json").version;
|
||||
|
||||
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${PKG_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.log(`${brand(sym("◆", "*"))} ${message}`);
|
||||
}
|
||||
|
||||
export function printScope(ids: Record<string, string | undefined>): void {
|
||||
const parts: string[] = [];
|
||||
for (const [key, val] of Object.entries(ids)) {
|
||||
if (val) {
|
||||
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
|
||||
parts.push(`${label}=${val}`);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -1,90 +0,0 @@
|
||||
/**
|
||||
* Config management commands: show, set, get.
|
||||
*/
|
||||
|
||||
import Table from "cli-table3";
|
||||
import { printError, printSuccess, colors } 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}`);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Entity management commands.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import Table from "cli-table3";
|
||||
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { formatJson } from "../output.js";
|
||||
|
||||
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)`);
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* mem0 init — interactive setup wizard.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import {
|
||||
printBanner,
|
||||
printError,
|
||||
printInfo,
|
||||
printSuccess,
|
||||
colors,
|
||||
} from "../branding.js";
|
||||
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
|
||||
import { PlatformBackend } from "../backend/platform.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
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 } = {}): Promise<void> {
|
||||
const config = createDefaultConfig();
|
||||
|
||||
// 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 {
|
||||
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();
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
/**
|
||||
* Memory CRUD commands: add, search, get, list, update, delete.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import { printError, printInfo, printScope, printSuccess, timedStatus } from "../branding.js";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import {
|
||||
formatAddResult,
|
||||
formatJson,
|
||||
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 && !msgs) {
|
||||
printError("No content provided. Pass text, --messages, --file, or pipe via stdin.");
|
||||
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;
|
||||
|
||||
const start = performance.now();
|
||||
let results: Record<string, unknown>[];
|
||||
try {
|
||||
results = await timedStatus("Searching memories...", async () => {
|
||||
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 === "json") {
|
||||
formatJson(results);
|
||||
} else if (opts.output === "table") {
|
||||
if (results.length > 0) {
|
||||
formatMemoriesTable(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();
|
||||
}
|
||||
} 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> {
|
||||
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 === "json") {
|
||||
formatJson(results);
|
||||
} 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("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)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/**
|
||||
* Utility commands: status, version, import.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
|
||||
import type { Backend } from "../backend/base.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
import boxen from "boxen";
|
||||
|
||||
const { brand, dim, success, error: errorColor } = colors;
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const VERSION: string = _require("../../package.json").version;
|
||||
|
||||
export async function cmdStatus(
|
||||
backend: Backend,
|
||||
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||
): 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${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: 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.`);
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* 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"],
|
||||
};
|
||||
|
||||
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] = parseInt(value, 10);
|
||||
} else {
|
||||
obj[field] = value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,374 +0,0 @@
|
||||
/**
|
||||
* 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 { Command, Help, Option, Argument } 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 ───────────────────────────────────────────────────────────
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -1,501 +0,0 @@
|
||||
#!/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 { createRequire } from "node:module";
|
||||
import { Command } from "commander";
|
||||
import type { Mem0Config } from "./config.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { getBackend, type Backend } from "./backend/index.js";
|
||||
import { printError, colors } from "./branding.js";
|
||||
import { richFormatHelp } from "./help.js";
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
const VERSION: string = _require("../package.json").version;
|
||||
|
||||
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${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${VERSION}`);
|
||||
process.exit(0);
|
||||
})
|
||||
.usage("<command> [options]")
|
||||
.helpOption("--help", "Show this message and exit.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
.command("init")
|
||||
.description("Interactive setup wizard for mem0 CLI.")
|
||||
.option("--api-key <key>", "API key (skip prompt).")
|
||||
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice")
|
||||
.action(async (opts) => {
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
await runInit({ apiKey: opts.apiKey, userId: opts.userId });
|
||||
});
|
||||
|
||||
// ── 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) => parseInt(v), 10)
|
||||
.option("--threshold <n>", "Minimum similarity score.", (v) => 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) => parseInt(v), 1)
|
||||
.option("--page-size <n>", "Results per page.", (v) => 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: VERSION, description: "The Memory Layer for AI Agents" }, null, 2));
|
||||
}
|
||||
} else {
|
||||
const { brand: b } = colors;
|
||||
console.log(`${b("◆ Mem0 CLI")} v${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();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
program.parse();
|
||||
@@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||
*/
|
||||
|
||||
import Table from "cli-table3";
|
||||
import boxen from "boxen";
|
||||
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>[]): void {
|
||||
const table = new Table({
|
||||
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
|
||||
colWidths: [12, 52, 16, 14],
|
||||
wordWrap: true,
|
||||
style: { head: [], border: [] },
|
||||
});
|
||||
|
||||
for (const mem of memories) {
|
||||
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
||||
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||
if (memoryText.length > 60) {
|
||||
memoryText = memoryText.slice(0, 57) + "...";
|
||||
}
|
||||
const categories = mem.categories;
|
||||
const cat =
|
||||
Array.isArray(categories) && categories.length > 0
|
||||
? (categories[0] as string)
|
||||
: "—";
|
||||
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
||||
table.push([dim(memId), memoryText, cat, created]);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* 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(output).toContain("Important note");
|
||||
});
|
||||
});
|
||||
|
||||
describe("printScope", () => {
|
||||
it("prints scope when IDs present", () => {
|
||||
printScope({ user_id: "alice", agent_id: "bot" });
|
||||
expect(output).toContain("alice");
|
||||
expect(output).toContain("bot");
|
||||
});
|
||||
|
||||
it("prints nothing when no IDs", () => {
|
||||
printScope({});
|
||||
expect(output).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* 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 });
|
||||
});
|
||||
});
|
||||
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* 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(output).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(output).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");
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* 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");
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* 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 },
|
||||
]),
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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)
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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
|
||||
@@ -1,107 +0,0 @@
|
||||
# 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]"
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
[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
|
||||
@@ -1,3 +0,0 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Allow running with `python -m mem0_cli`."""
|
||||
|
||||
from mem0_cli.app import main
|
||||
|
||||
main()
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Backend abstraction layer for mem0 CLI."""
|
||||
|
||||
from mem0_cli.backend.base import Backend, get_backend
|
||||
|
||||
__all__ = ["Backend", "get_backend"]
|
||||
@@ -1,113 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,325 +0,0 @@
|
||||
"""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
|
||||
@@ -1,130 +0,0 @@
|
||||
"""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__
|
||||
@@ -1 +0,0 @@
|
||||
"""CLI command modules."""
|
||||
@@ -1,108 +0,0 @@
|
||||
"""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}")
|
||||
@@ -1,133 +0,0 @@
|
||||
"""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)")
|
||||
@@ -1,194 +0,0 @@
|
||||
"""mem0 init — interactive setup wizard."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
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 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)
|
||||
|
||||
|
||||
def run_init(*, api_key: str | None = None, user_id: 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()
|
||||
|
||||
# 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")
|
||||
|
||||
# Use provided flags or prompt
|
||||
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}")
|
||||
@@ -1,469 +0,0 @@
|
||||
"""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(",")]
|
||||
|
||||
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)
|
||||
print_error(err_console, 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(",")]
|
||||
|
||||
_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 == "json":
|
||||
format_json(console, results)
|
||||
elif output == "table":
|
||||
if results:
|
||||
format_memories_table(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()
|
||||
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."""
|
||||
_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 == "json":
|
||||
format_json(console, results)
|
||||
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, "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)")
|
||||
@@ -1,141 +0,0 @@
|
||||
"""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.")
|
||||
@@ -1,176 +0,0 @@
|
||||
"""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)
|
||||
|
||||
|
||||
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'."""
|
||||
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."""
|
||||
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
|
||||
@@ -1,242 +0,0 @@
|
||||
"""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]) -> 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=10)
|
||||
table.add_column("Memory", max_width=50, no_wrap=False)
|
||||
table.add_column("Category", max_width=14)
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "")[:8]
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
if len(memory_text) > 60:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
cat = categories[0] if isinstance(categories, list) and categories else "—"
|
||||
created = _format_date(mem.get("created_at")) or "—"
|
||||
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
|
||||
@@ -1,109 +0,0 @@
|
||||
"""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
|
||||
@@ -1,54 +0,0 @@
|
||||
"""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
|
||||
@@ -1,238 +0,0 @@
|
||||
"""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 subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
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]
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
env.update(env_override)
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
@@ -1,240 +0,0 @@
|
||||
"""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
|
||||
@@ -1,136 +0,0 @@
|
||||
"""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,4 +10,5 @@ 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,10 +52,6 @@ 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,74 +8,20 @@ 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](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))
|
||||
- **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)
|
||||
|
||||
**Improvements:**
|
||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling ([#4403](https://github.com/mem0ai/mem0/pull/4403))
|
||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403)
|
||||
|
||||
</Update>
|
||||
|
||||
@@ -817,27 +763,6 @@ 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/gemini-embedding-001",
|
||||
"model": "models/text-embedding-004",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/gemini-embedding-001` |
|
||||
| `model` | The name of the embedding model to use| `models/text-embedding-004` |
|
||||
| `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": "gemini-embedding-001",
|
||||
"model": "text-embedding-004",
|
||||
"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 | `gemini-embedding-001` |
|
||||
| `model` | The name of the Vertex AI embedding model to use | `text-embedding-004` |
|
||||
| `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,51 +7,58 @@ When using LLM rerankers, you can customize the prompts used for ranking to bett
|
||||
|
||||
## Default Prompt
|
||||
|
||||
The default LLM reranker prompt scores each memory individually on a 0.0-1.0 scale:
|
||||
The default LLM reranker prompt is designed to be general-purpose:
|
||||
|
||||
```
|
||||
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||
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.
|
||||
|
||||
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}
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Memory entries:
|
||||
{memories}
|
||||
|
||||
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||
Provide your ranking as a JSON array with scores for each memory.
|
||||
```
|
||||
|
||||
## Custom Prompt Configuration
|
||||
|
||||
You can provide a custom prompt template using the `scoring_prompt` parameter:
|
||||
You can provide a custom prompt template when configuring the LLM reranker:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
custom_prompt = """
|
||||
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.
|
||||
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
|
||||
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
Query: {query}
|
||||
User Context: {user_context}
|
||||
|
||||
Provide only a single numerical score between 0.0 and 1.0.
|
||||
Memory entries:
|
||||
{memories}
|
||||
|
||||
Rate each memory from 1-10 and provide reasoning.
|
||||
Return as JSON: {{"rankings": [{{"index": 0, "score": 8, "reason": "..."}}]}}
|
||||
"""
|
||||
|
||||
config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-key",
|
||||
"scoring_prompt": custom_prompt,
|
||||
"top_k": 5
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4.1-nano-2025-04-14",
|
||||
"api_key": "your-openai-key"
|
||||
}
|
||||
},
|
||||
"custom_prompt": custom_prompt,
|
||||
"top_n": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,14 +70,12 @@ memory = Memory.from_config(config)
|
||||
|
||||
Your custom prompt can use the following variables:
|
||||
|
||||
| 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>
|
||||
| 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) |
|
||||
|
||||
## Domain-Specific Examples
|
||||
|
||||
@@ -84,10 +89,13 @@ Prioritize memories that:
|
||||
- Show previous resolution patterns
|
||||
- Indicate customer preferences or constraints
|
||||
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
Query: {query}
|
||||
Customer Context: Previous interactions with this customer
|
||||
|
||||
Score relevance from 0.0 to 1.0.
|
||||
Memories:
|
||||
{memories}
|
||||
|
||||
Rank each memory 1-10 based on support relevance.
|
||||
"""
|
||||
```
|
||||
|
||||
@@ -95,16 +103,19 @@ Score relevance from 0.0 to 1.0.
|
||||
|
||||
```python
|
||||
educational_prompt = """
|
||||
Score this learning memory for relevance to a student query.
|
||||
Rank these learning memories for a student query.
|
||||
Consider:
|
||||
- Prerequisite knowledge requirements
|
||||
- Learning progression and difficulty
|
||||
- Relevance to current learning objectives
|
||||
|
||||
Student Query: "{query}"
|
||||
Memory: "{document}"
|
||||
Student Query: {query}
|
||||
Learning Context: {user_context}
|
||||
|
||||
Score educational relevance from 0.0 to 1.0.
|
||||
Available memories:
|
||||
{memories}
|
||||
|
||||
Score each memory for educational value (1-10).
|
||||
"""
|
||||
```
|
||||
|
||||
@@ -112,64 +123,73 @@ Score educational relevance from 0.0 to 1.0.
|
||||
|
||||
```python
|
||||
personal_assistant_prompt = """
|
||||
Score this personal memory for relevance to the user's query.
|
||||
Rank personal memories for relevance to the user's query.
|
||||
Consider:
|
||||
- Recent vs. historical importance
|
||||
- Personal preferences and habits
|
||||
- Contextual relationships
|
||||
- Contextual relationships between memories
|
||||
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
Query: {query}
|
||||
Personal context: {user_context}
|
||||
|
||||
Provide relevance score from 0.0 to 1.0.
|
||||
Memories to rank:
|
||||
{memories}
|
||||
|
||||
Provide relevance scores (1-10) with brief explanations.
|
||||
"""
|
||||
```
|
||||
|
||||
## Advanced Prompt Techniques
|
||||
|
||||
### Multi-Criteria Scoring
|
||||
### Multi-Criteria Ranking
|
||||
|
||||
```python
|
||||
multi_criteria_prompt = """
|
||||
Evaluate this memory using multiple criteria:
|
||||
Evaluate memories using multiple criteria:
|
||||
|
||||
1. RELEVANCE (40%): How directly related to the query
|
||||
2. RECENCY (20%): How recent the memory appears to be
|
||||
2. RECENCY (20%): How recent the memory is
|
||||
3. IMPORTANCE (25%): Personal or business significance
|
||||
4. ACTIONABILITY (15%): How useful for next steps
|
||||
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
Query: {query}
|
||||
Context: {user_context}
|
||||
|
||||
Compute a weighted score from 0.0 to 1.0 based on these criteria.
|
||||
Provide only the final numerical score.
|
||||
Memories:
|
||||
{memories}
|
||||
|
||||
For each memory, provide:
|
||||
- Overall score (1-10)
|
||||
- Breakdown by criteria
|
||||
- Final ranking recommendation
|
||||
|
||||
Format: JSON with detailed scoring
|
||||
"""
|
||||
```
|
||||
|
||||
### Chain-of-Thought Scoring
|
||||
### Contextual Ranking
|
||||
|
||||
```python
|
||||
reasoning_prompt = """
|
||||
Evaluate this memory's relevance step by step:
|
||||
contextual_prompt = """
|
||||
Consider the following context when ranking memories:
|
||||
- Current user situation: {user_context}
|
||||
- Time of day: {current_time}
|
||||
- Recent activities: {recent_activities}
|
||||
|
||||
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?
|
||||
Query: {query}
|
||||
|
||||
Based on this analysis, provide a single relevance score from 0.0 to 1.0.
|
||||
Rank these memories considering both direct relevance and contextual appropriateness:
|
||||
{memories}
|
||||
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Score:
|
||||
Provide contextually-aware relevance scores (1-10).
|
||||
"""
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
||||
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
|
||||
2. **Use Examples**: Include examples in your prompt for better model understanding
|
||||
3. **Structure Output**: Specify the exact JSON format you want returned
|
||||
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
||||
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
||||
|
||||
@@ -186,7 +206,7 @@ prompts = [
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(prompts):
|
||||
config["reranker"]["config"]["scoring_prompt"] = prompt
|
||||
config["reranker"]["config"]["custom_prompt"] = prompt
|
||||
memory = Memory.from_config(config)
|
||||
|
||||
results = memory.search("test query", user_id="test_user")
|
||||
@@ -196,6 +216,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 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
|
||||
- **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
|
||||
|
||||
@@ -18,9 +18,13 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key"
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,14 +36,11 @@ m = Memory.from_config(config)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `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 |
|
||||
| `llm` | dict | Required | LLM configuration object |
|
||||
| `top_k` | int | 10 | Number of results to rerank |
|
||||
| `temperature` | float | 0.0 | LLM temperature for consistency |
|
||||
| `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. |
|
||||
| `custom_prompt` | str | None | Custom reranking prompt |
|
||||
| `score_range` | tuple | (0, 10) | Score range for relevance |
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
@@ -48,19 +49,20 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"api_key": "your-anthropic-api-key",
|
||||
"llm": {
|
||||
"provider": "anthropic",
|
||||
"config": {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
}
|
||||
},
|
||||
"top_k": 15,
|
||||
"temperature": 0.0,
|
||||
"scoring_prompt": """
|
||||
Rate the relevance of each memory to the query on a scale of 0.0-1.0.
|
||||
"score_range": (1, 5),
|
||||
"custom_prompt": """
|
||||
Rate the relevance of each memory to the query on a scale of 1-5.
|
||||
Consider semantic similarity, context, and practical utility.
|
||||
Only provide the numeric score.
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Score:
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -76,10 +78,14 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key",
|
||||
"temperature": 0.0
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key",
|
||||
"temperature": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,9 +98,13 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
"llm": {
|
||||
"provider": "anthropic",
|
||||
"config": {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,12 +117,10 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "ollama",
|
||||
"model": "llama3.2",
|
||||
"llm": {
|
||||
"provider": "ollama",
|
||||
"config": {
|
||||
"model": "llama3.2",
|
||||
"model": "llama2",
|
||||
"ollama_base_url": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
@@ -121,10 +129,6 @@ 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
|
||||
@@ -132,16 +136,13 @@ 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-4o-mini",
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-azure-api-key",
|
||||
"azure_endpoint": "https://your-resource.openai.azure.com/",
|
||||
"azure_deployment": "gpt-4o-mini-deployment"
|
||||
"azure_deployment": "gpt-4-deployment"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,22 +154,16 @@ config = {
|
||||
|
||||
### Default Prompt Behavior
|
||||
|
||||
The default prompt asks the LLM to score relevance on a 0.0-1.0 scale:
|
||||
The default prompt asks the LLM to score relevance on a 0-10 scale:
|
||||
|
||||
```
|
||||
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||
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.
|
||||
|
||||
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.
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Score:
|
||||
```
|
||||
|
||||
### Custom Prompt Examples
|
||||
@@ -179,14 +174,14 @@ Provide only a single numerical score between 0.0 and 1.0. Do not include any ex
|
||||
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 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
|
||||
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
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Score:
|
||||
"""
|
||||
|
||||
@@ -194,10 +189,14 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key",
|
||||
"scoring_prompt": custom_prompt
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"custom_prompt": custom_prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,15 +213,15 @@ Consider:
|
||||
- Recency and accuracy
|
||||
- Practical usefulness
|
||||
|
||||
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
|
||||
Rate 1-5:
|
||||
1 = Not relevant
|
||||
2 = Slightly relevant
|
||||
3 = Moderately relevant
|
||||
4 = Very relevant
|
||||
5 = Perfectly answers the question
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Score:
|
||||
"""
|
||||
```
|
||||
@@ -240,17 +239,13 @@ Consider:
|
||||
- Factual accuracy
|
||||
- Conversation flow
|
||||
|
||||
Rate 0.0-1.0:
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Rate 0-10:
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
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
|
||||
@@ -300,9 +295,9 @@ results = safe_llm_rerank_search("What are my preferences?", "alice")
|
||||
|
||||
| Model Type | Speed | Quality | Cost | Best For |
|
||||
|------------|-------|---------|------|----------|
|
||||
| GPT-4o mini | Fast | Good | Low | High-volume applications |
|
||||
| GPT-4o | Medium | Excellent | Medium | Quality-critical applications |
|
||||
| Claude Sonnet | Medium | Excellent | Medium | Balanced performance |
|
||||
| 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 |
|
||||
| Ollama Local | Variable | Good | Free | Privacy-sensitive applications |
|
||||
|
||||
### Optimization Strategies
|
||||
@@ -313,10 +308,14 @@ fast_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key",
|
||||
"top_k": 5,
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"top_k": 5, # Limit candidates
|
||||
"temperature": 0.0
|
||||
}
|
||||
}
|
||||
@@ -327,9 +326,13 @@ quality_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"api_key": "your-api-key",
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"top_k": 15,
|
||||
"temperature": 0.0
|
||||
}
|
||||
@@ -350,10 +353,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 0.0-1.0:
|
||||
Based on this analysis, rate relevance 1-10:
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
|
||||
Analysis:
|
||||
Step 1 (Intent):
|
||||
@@ -364,6 +367,42 @@ 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
|
||||
@@ -394,22 +433,14 @@ class RobustLLMReranker:
|
||||
primary_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-4"}}}
|
||||
}
|
||||
}
|
||||
|
||||
fallback_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-3.5-turbo"}}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,4 +486,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>
|
||||
@@ -52,7 +52,7 @@ Here are the parameters available for configuring FAISS:
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collection_name` | The name of the collection | `mem0` |
|
||||
| `path` | Path to store FAISS index and metadata | `/tmp/faiss/<collection_name>` |
|
||||
| `path` | Path to store FAISS index and metadata | `~/.local/share/mem0/faiss/<collection_name>` |
|
||||
| `distance_strategy` | Distance metric strategy to use (options: 'euclidean', 'inner_product', 'cosine') | `euclidean` |
|
||||
| `normalize_L2` | Whether to normalize L2 vectors (only applicable for euclidean distance) | `False` |
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `client` | Custom client for qdrant | `None` |
|
||||
| `host` | The host where the qdrant server is running | `None` |
|
||||
| `port` | The port where the qdrant server is running | `None` |
|
||||
| `path` | Path for the qdrant database | `/tmp/qdrant` |
|
||||
| `path` | Path for the qdrant database | `~/.local/share/mem0/qdrant` |
|
||||
| `url` | Full URL for the qdrant server | `None` |
|
||||
| `api_key` | API key for the qdrant server | `None` |
|
||||
| `on_disk` | For enabling persistent storage | `False` |
|
||||
@@ -85,7 +85,7 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `embeddingModelDims` | Dimensions of the embedding model | `1536` |
|
||||
| `host` | The host where the Qdrant server is running | `None` |
|
||||
| `port` | The port where the Qdrant server is running | `None` |
|
||||
| `path` | Path for the Qdrant database | `/tmp/qdrant` |
|
||||
| `path` | Path for the Qdrant database | `~/.local/share/mem0/qdrant` |
|
||||
| `url` | Full URL for the Qdrant server | `None` |
|
||||
| `apiKey` | API key for the Qdrant server | `None` |
|
||||
| `onDisk` | For enabling persistent storage | `False` |
|
||||
|
||||
@@ -10,32 +10,6 @@ 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
|
||||
@@ -51,8 +25,6 @@ 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
|
||||
@@ -81,73 +53,8 @@ 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:**
|
||||
|
||||
@@ -155,6 +62,7 @@ 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):**
|
||||
@@ -162,6 +70,7 @@ 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>
|
||||
@@ -178,8 +87,6 @@ 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)
|
||||
@@ -193,6 +100,7 @@ mem0_client.project.update(custom_categories=[
|
||||
{"constraints": "Injuries, limitations, recovery needs"},
|
||||
{"preferences": "Training style, surfaces, schedules"}
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
<Note>
|
||||
@@ -213,6 +121,7 @@ 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.
|
||||
@@ -226,50 +135,13 @@ 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",
|
||||
@@ -283,21 +155,8 @@ 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.
|
||||
|
||||
@@ -309,22 +168,12 @@ 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.
|
||||
@@ -334,8 +183,6 @@ Noise. Greetings and filler clutter the memory.
|
||||
|
||||
### Custom Instructions
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Platform">
|
||||
Tell Mem0 what matters:
|
||||
|
||||
```python
|
||||
@@ -351,38 +198,11 @@ 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")
|
||||
@@ -390,19 +210,8 @@ 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.
|
||||
@@ -412,6 +221,8 @@ Only meaningful facts. Filler gets dropped automatically.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Agent Memory for Personality
|
||||
|
||||
### Why Agents Need Memory Too
|
||||
@@ -420,30 +231,16 @@ 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")
|
||||
@@ -454,26 +251,8 @@ 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.
|
||||
@@ -489,8 +268,6 @@ 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([
|
||||
@@ -503,27 +280,8 @@ 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.
|
||||
|
||||
@@ -535,8 +293,6 @@ 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
|
||||
|
||||
@@ -547,26 +303,10 @@ 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>
|
||||
|
||||
---
|
||||
|
||||
@@ -574,8 +314,6 @@ Store `expires_on` in metadata and prune expired memories in your app. Ray stops
|
||||
|
||||
Here's the Mem0 setup combining everything:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Platform">
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from datetime import datetime, timedelta
|
||||
@@ -594,55 +332,11 @@ 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"},
|
||||
@@ -652,33 +346,11 @@ 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(
|
||||
@@ -687,36 +359,16 @@ 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.
|
||||
|
||||
@@ -728,8 +380,6 @@ 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")
|
||||
@@ -740,22 +390,8 @@ 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.
|
||||
|
||||
@@ -763,8 +399,6 @@ 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"}],
|
||||
@@ -773,27 +407,13 @@ 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"}]})
|
||||
@@ -801,19 +421,8 @@ 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.
|
||||
|
||||
@@ -821,20 +430,11 @@ 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.
|
||||
|
||||
@@ -842,44 +442,19 @@ 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"}],
|
||||
@@ -893,48 +468,20 @@ 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>
|
||||
|
||||
---
|
||||
|
||||
@@ -968,7 +515,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 (Mem0 dashboard or `get_all` / Qdrant when local)
|
||||
- Monitor memory quality in Mem0 dashboard
|
||||
- Clear test data from production project
|
||||
|
||||
---
|
||||
|
||||
@@ -16,8 +16,6 @@ Integrating Mem0 into your writing workflow helps you:
|
||||
|
||||
## Setup
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Platform">
|
||||
```python
|
||||
import os
|
||||
from openai import OpenAI
|
||||
@@ -34,81 +32,13 @@ 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).
|
||||
@@ -131,41 +61,9 @@ 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."""
|
||||
@@ -209,73 +107,29 @@ 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
|
||||
```
|
||||
|
||||
@@ -284,8 +138,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
|
||||
@@ -306,8 +160,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
|
||||
@@ -350,7 +204,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,36 +18,11 @@ 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
|
||||
|
||||
@@ -55,8 +30,6 @@ ollama pull nomic-embed-text:latest
|
||||
|
||||
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
|
||||
@@ -72,11 +45,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
|
||||
@@ -84,20 +57,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",
|
||||
@@ -106,18 +79,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
|
||||
@@ -127,7 +100,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
|
||||
@@ -159,7 +132,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
|
||||
@@ -209,208 +182,6 @@ 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": "#8F74E0",
|
||||
"light": "#8F74E0",
|
||||
"dark": "#8F74E0"
|
||||
"primary": "#9C58FA",
|
||||
"light": "#9C58FA",
|
||||
"dark": "#9C58FA"
|
||||
},
|
||||
"favicon": "/logo/favicon.png",
|
||||
"logo": {
|
||||
@@ -42,7 +42,6 @@
|
||||
"platform/overview",
|
||||
"vibecoding",
|
||||
"platform/mem0-mcp",
|
||||
"platform/cli",
|
||||
"platform/platform-vs-oss",
|
||||
"platform/quickstart"
|
||||
]
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 178 KiB |
@@ -4,16 +4,16 @@ description: "Mem0 is a universal, self-improving memory layer for LLM applicati
|
||||
mode: "custom"
|
||||
---
|
||||
|
||||
{/* debug: welcome-layout-v3-grid */}
|
||||
{/* debug: welcome-layout-v2 */}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</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,25 +24,31 @@ mode: "custom"
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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-[2/1] w-full object-cover"
|
||||
className="block dark:hidden aspect-[4/3] 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-[2/1] w-full object-cover"
|
||||
className="hidden dark:block aspect-[4/3] 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-2 px-4 pb-4 pt-3 text-left">
|
||||
<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">
|
||||
Mem0 Platform
|
||||
</h3>
|
||||
@@ -57,18 +63,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-[2/1] w-full object-cover"
|
||||
className="block dark:hidden aspect-[4/3] 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-[2/1] w-full object-cover"
|
||||
className="hidden dark:block aspect-[4/3] 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-2 px-4 pb-4 pt-3 text-left">
|
||||
<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">
|
||||
Mem0 Open Source
|
||||
</h3>
|
||||
@@ -78,23 +84,34 @@ mode: "custom"
|
||||
</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-[2/1] w-full object-cover"
|
||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
||||
src="/images/docs thumbnails/light/Cookbooks.png"
|
||||
alt="Cookbooks thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||
className="hidden dark:block aspect-[4/3] 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-2 px-4 pb-4 pt-3 text-left">
|
||||
<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">
|
||||
Cookbooks
|
||||
</h3>
|
||||
@@ -109,18 +126,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-[2/1] w-full object-cover"
|
||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
||||
src="/images/docs thumbnails/light/Integrations.png"
|
||||
alt="Integrations thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||
className="hidden dark:block aspect-[4/3] 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-2 px-4 pb-4 pt-3 text-left">
|
||||
<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">
|
||||
Integrations
|
||||
</h3>
|
||||
@@ -135,51 +152,25 @@ 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-[2/1] w-full object-cover"
|
||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
||||
src="/images/docs thumbnails/light/API.png"
|
||||
alt="API reference thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||
className="hidden dark:block aspect-[4/3] 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-2 px-4 pb-4 pt-3 text-left">
|
||||
<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">
|
||||
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,14 +14,11 @@ 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
|
||||
|
||||
@@ -31,20 +28,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 Features
|
||||
## Platform (Managed Solution)
|
||||
|
||||
- [Platform Features Overview](https://docs.mem0.ai/platform/features/platform-overview): High-level overview of all Mem0 Platform capabilities
|
||||
- [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
|
||||
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations): Sophisticated memory management techniques for complex applications
|
||||
|
||||
### 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
|
||||
### Essential Platform Features
|
||||
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters): Advanced filtering and querying capabilities
|
||||
- [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 Features
|
||||
### Advanced Platform 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
|
||||
@@ -62,15 +59,10 @@ 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
|
||||
|
||||
### Support & Migration
|
||||
### Platform Support
|
||||
- [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
|
||||
|
||||
@@ -80,130 +72,93 @@ Key differentiators:
|
||||
- [Configuration Guide](https://docs.mem0.ai/open-source/configuration): Complete configuration options for self-hosted deployment
|
||||
|
||||
### Open Source Features
|
||||
- [Features Overview](https://docs.mem0.ai/open-source/features/overview): Overview of all 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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [Together](https://docs.mem0.ai/components/llms/models/together): Open-source model inference platform
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
|
||||
### 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
|
||||
- [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
|
||||
- [Weaviate](https://docs.mem0.ai/components/vectordbs/dbs/weaviate): Open-source vector search engine with built-in ML capabilities
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [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
|
||||
- [S3 Vectors](https://docs.mem0.ai/components/vectordbs/dbs/s3_vectors): Amazon S3 Vectors integration
|
||||
|
||||
### 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
|
||||
- [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
|
||||
- [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
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
- [Dify](https://docs.mem0.ai/integrations/dify): LLMOps platform integration for production AI applications
|
||||
|
||||
## Cookbooks and Examples
|
||||
|
||||
### Cookbooks Overview
|
||||
- [Cookbooks Overview](https://docs.mem0.ai/cookbooks/overview): Complete guide to Mem0 examples and implementation patterns
|
||||
|
||||
### Essential Guides
|
||||
@@ -216,13 +171,12 @@ 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
|
||||
@@ -232,77 +186,30 @@ 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
|
||||
- [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
|
||||
- [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
|
||||
|
||||
### 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
|
||||
- [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
|
||||
- [Multimodal Retrieval](https://docs.mem0.ai/cookbooks/frameworks/multimodal-retrieval): Memory systems handling text, images, and documents
|
||||
|
||||
## API Reference
|
||||
|
||||
- [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
|
||||
- [Memory APIs](https://docs.mem0.ai/api-reference/memory/add-memories): Comprehensive API documentation for memory operations
|
||||
- [Add Memories](https://docs.mem0.ai/api-reference/memory/add-memories): REST API for storing new memories with detailed request/response formats
|
||||
- [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
|
||||
- [Get All Memories](https://docs.mem0.ai/api-reference/memory/get-memories): Retrieve all memories with pagination and filtering options
|
||||
- [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 a specific memory by ID
|
||||
- [Delete Memory](https://docs.mem0.ai/api-reference/memory/delete-memory): Remove memories individually or in batches
|
||||
|
||||
### 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
|
||||
## Optional
|
||||
|
||||
### 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
|
||||
- [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
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
@@ -123,9 +123,13 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key",
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
},
|
||||
"top_k": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ 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>
|
||||
@@ -151,18 +147,13 @@ curl -X POST http://localhost:8000/memories \
|
||||
</Info>
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "vegetable",
|
||||
"user_id": "alice"
|
||||
}'
|
||||
curl "http://localhost:8000/memories/search?user_id=alice&query=vegetable"
|
||||
```
|
||||
|
||||
### Explore with OpenAPI docs
|
||||
|
||||
1. Navigate to `http://localhost:8000/docs`.
|
||||
2. Pick an endpoint (e.g., `POST /search`).
|
||||
1. Navigate to `http://localhost:8000/docs`.
|
||||
2. Pick an endpoint (e.g., `POST /memories/search`).
|
||||
3. Fill in parameters and click **Execute** to try requests in-browser.
|
||||
|
||||
<Tip>
|
||||
@@ -171,25 +162,6 @@ curl -X POST http://localhost:8000/search \
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -77,7 +77,7 @@ Mem0 Open Source delivers the same adaptive memory engine as the platform, but p
|
||||
Mem0 OSS works out of the box with sensible defaults:
|
||||
- LLM: OpenAI `gpt-4.1-nano-2025-04-14` (via `OPENAI_API_KEY`)
|
||||
- Embeddings: OpenAI `text-embedding-3-small`
|
||||
- Vector store: Local Qdrant instance storing data at `/tmp/qdrant`
|
||||
- Vector store: Local Qdrant instance storing data at `~/.local/share/mem0/qdrant`
|
||||
- History store: SQLite database at `~/.mem0/history.db`
|
||||
- Reranker: Disabled until you configure a provider
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ m.add(messages, user_id="alex")
|
||||
|
||||
<Step title="Search memories">
|
||||
```python
|
||||
results = m.search("What do you know about me?", user_id="alex")
|
||||
results = m.search("What do you know about me?", filters={"user_id": "alex"})
|
||||
print(results)
|
||||
```
|
||||
|
||||
@@ -81,7 +81,7 @@ print(results)
|
||||
By default `Memory()` wires up:
|
||||
- OpenAI `gpt-4.1-nano-2025-04-14` for fact extraction and updates
|
||||
- OpenAI `text-embedding-3-small` embeddings (1536 dimensions)
|
||||
- Qdrant vector store with on-disk data at `/tmp/qdrant`
|
||||
- Qdrant vector store with on-disk data at `~/.local/share/mem0/qdrant`
|
||||
- SQLite history at `~/.mem0/history.db`
|
||||
- No reranker (add one in the config when you need it)
|
||||
</Note>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"title": "Mem0 API Docs",
|
||||
"description": "mem0.ai API Docs",
|
||||
"contact": {
|
||||
"email": "support@mem0.ai"
|
||||
"email": "deshraj@mem0.ai"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0"
|
||||
@@ -457,33 +457,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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}/": {
|
||||
@@ -589,33 +563,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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,303 +0,0 @@
|
||||
---
|
||||
title: CLI
|
||||
description: "Manage memories from your terminal. Available for Node.js and Python."
|
||||
icon: "terminal"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
The mem0 CLI lets you add, search, list, update, and delete memories directly from the terminal. It works with the Mem0 Platform API and is available as both an npm package and a Python package.
|
||||
|
||||
Both implementations provide identical behavior — same commands, same options, same output formats.
|
||||
|
||||
## Installation
|
||||
|
||||
<CodeGroup>
|
||||
```bash npm
|
||||
npm install -g @mem0/cli
|
||||
```
|
||||
|
||||
```bash pip
|
||||
pip install mem0-cli
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Authentication
|
||||
|
||||
Run the interactive setup wizard to configure your API key:
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
```
|
||||
|
||||
This prompts for your API key and a default user ID, validates the connection, and saves the configuration locally.
|
||||
|
||||
For CI/CD or non-interactive environments, pass both values as flags:
|
||||
|
||||
```bash
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
```
|
||||
|
||||
You can also set your API key via environment variable:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-xxx"
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# Add a memory
|
||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||
|
||||
# Search memories
|
||||
mem0 search "What are Alice's preferences?" --user-id alice
|
||||
|
||||
# List all memories for a user
|
||||
mem0 list --user-id alice
|
||||
|
||||
# Get a specific memory
|
||||
mem0 get <memory-id>
|
||||
|
||||
# Update a memory
|
||||
mem0 update <memory-id> "I prefer light mode now"
|
||||
|
||||
# Delete a memory
|
||||
mem0 delete <memory-id>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `mem0 init`
|
||||
|
||||
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||
|
||||
```bash
|
||||
mem0 init
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
```
|
||||
|
||||
### `mem0 add`
|
||||
|
||||
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||
|
||||
```bash
|
||||
mem0 add "I prefer dark mode" --user-id alice
|
||||
mem0 add --file conversation.json --user-id alice
|
||||
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Scope to a user |
|
||||
| `--agent-id` | Scope to an agent |
|
||||
| `--messages` | Conversation messages as JSON |
|
||||
| `-f, --file` | Read messages from a JSON file |
|
||||
| `-m, --metadata` | Custom metadata as JSON |
|
||||
| `--categories` | Categories (JSON array or comma-separated) |
|
||||
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||
|
||||
### `mem0 search`
|
||||
|
||||
Search memories using natural language.
|
||||
|
||||
```bash
|
||||
mem0 search "dietary restrictions" --user-id alice
|
||||
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `-k, --top-k` | Number of results (default: 10) |
|
||||
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||
| `--rerank` | Enable reranking |
|
||||
| `--keyword` | Use keyword search instead of semantic |
|
||||
| `--filter` | Advanced filter expression (JSON) |
|
||||
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 list`
|
||||
|
||||
List memories with optional filters and pagination.
|
||||
|
||||
```bash
|
||||
mem0 list --user-id alice
|
||||
mem0 list --user-id alice --category preferences --output json
|
||||
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-u, --user-id` | Filter by user |
|
||||
| `--page` | Page number (default: 1) |
|
||||
| `--page-size` | Results per page (default: 100) |
|
||||
| `--category` | Filter by category |
|
||||
| `--after` | Created after date (YYYY-MM-DD) |
|
||||
| `--before` | Created before date (YYYY-MM-DD) |
|
||||
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||
|
||||
### `mem0 get`
|
||||
|
||||
Retrieve a specific memory by ID.
|
||||
|
||||
```bash
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||
```
|
||||
|
||||
### `mem0 update`
|
||||
|
||||
Update the text or metadata of an existing memory.
|
||||
|
||||
```bash
|
||||
mem0 update <memory-id> "Updated preference text"
|
||||
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||
echo "new text" | mem0 update <memory-id>
|
||||
```
|
||||
|
||||
### `mem0 delete`
|
||||
|
||||
Delete a single memory, all memories for a scope, or an entire entity.
|
||||
|
||||
```bash
|
||||
# Delete a single memory
|
||||
mem0 delete <memory-id>
|
||||
|
||||
# Delete all memories for a user
|
||||
mem0 delete --all --user-id alice --force
|
||||
|
||||
# Delete all memories project-wide
|
||||
mem0 delete --all --project --force
|
||||
|
||||
# Preview what would be deleted
|
||||
mem0 delete --all --user-id alice --dry-run
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--all` | Delete all memories matching scope filters |
|
||||
| `--entity` | Delete the entity and all its memories |
|
||||
| `--project` | With `--all`: delete all memories project-wide |
|
||||
| `--dry-run` | Preview without deleting |
|
||||
| `--force` | Skip confirmation prompt |
|
||||
|
||||
### `mem0 import`
|
||||
|
||||
Bulk import memories from a JSON file.
|
||||
|
||||
```bash
|
||||
mem0 import data.json --user-id alice
|
||||
```
|
||||
|
||||
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||
|
||||
### `mem0 config`
|
||||
|
||||
View or modify the local CLI configuration.
|
||||
|
||||
```bash
|
||||
mem0 config show # Display current config (secrets redacted)
|
||||
mem0 config get api_key # Get a specific value
|
||||
mem0 config set user_id bob # Set a value
|
||||
```
|
||||
|
||||
### `mem0 entities`
|
||||
|
||||
List or delete entities (users, agents, apps).
|
||||
|
||||
```bash
|
||||
mem0 entities list
|
||||
mem0 entities list --type agent --output json
|
||||
mem0 entities delete --user-id alice --force
|
||||
```
|
||||
|
||||
### `mem0 status`
|
||||
|
||||
Verify your API connection and display the current project.
|
||||
|
||||
```bash
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Output formats
|
||||
|
||||
All commands support the `--output` flag to control how results are displayed:
|
||||
|
||||
| Format | Description |
|
||||
|--------|-------------|
|
||||
| `text` | Human-readable output with colors and formatting (default for most commands) |
|
||||
| `json` | Structured JSON, suitable for piping to `jq` or consumption by AI agents |
|
||||
| `table` | Tabular format (default for `list`) |
|
||||
| `quiet` | Minimal output — just IDs or status codes |
|
||||
|
||||
Example with JSON output:
|
||||
|
||||
```bash
|
||||
mem0 search "user preferences" --user-id alice --output json | jq '.data.results[].memory'
|
||||
```
|
||||
|
||||
## Use with AI agents
|
||||
|
||||
The CLI is designed to be used by AI agents and automation tools. Two features make this straightforward:
|
||||
|
||||
- **`--output json`** returns structured data that agents can parse directly
|
||||
- **`mem0 help --json`** returns the full command tree as JSON, so agents can discover available commands programmatically
|
||||
|
||||
```bash
|
||||
# Agent adds a memory
|
||||
mem0 add "User prefers concise responses" --user-id user-42 --output quiet
|
||||
|
||||
# Agent searches memories and parses results
|
||||
mem0 search "response preferences" --user-id user-42 --output json
|
||||
```
|
||||
|
||||
For non-interactive environments (CI, agent runtimes), use `mem0 init --api-key --user-id` or set the `MEM0_API_KEY` environment variable to skip interactive prompts.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||
| `MEM0_BASE_URL` | API base URL |
|
||||
| `MEM0_USER_ID` | Default user ID |
|
||||
| `MEM0_AGENT_ID` | Default agent ID |
|
||||
| `MEM0_APP_ID` | Default app ID |
|
||||
| `MEM0_RUN_ID` | Default run ID |
|
||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||
|
||||
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||
|
||||
## Global flags
|
||||
|
||||
These flags are available on all commands that interact with the API:
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-key` | Override the configured API key for this request |
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
## What's next
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Quickstart" icon="bolt" href="/platform/quickstart">
|
||||
Store your first memory in under five minutes using the SDK or CLI
|
||||
</Card>
|
||||
|
||||
<Card title="Memory Operations" icon="database" href="/core-concepts/memory-operations/add">
|
||||
Learn about add, search, update, and delete operations in depth
|
||||
</Card>
|
||||
|
||||
<Card title="API Reference" icon="code" href="/api-reference/memory/add-memories">
|
||||
See the complete REST API documentation
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -46,10 +46,6 @@ 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>
|
||||
|
||||
@@ -84,10 +80,6 @@ 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>
|
||||
|
||||
@@ -113,10 +105,6 @@ 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: "A reusable template for writing integration guides that pair Mem0 with third-party tools and services."
|
||||
description: "Pattern for pairing Mem0 with third-party tools."
|
||||
icon: "plug"
|
||||
---
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search 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",
|
||||
"keywords": ["mem0", "memory", "mcp", "personalization", "semantic-search"],
|
||||
"skills": "./skills/",
|
||||
"hooks": "./hooks/cursor-hooks.json",
|
||||
"mcpServers": ".cursor-mcp.json"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
# Mem0 Plugin for Claude Code, Claude Cowork & Cursor
|
||||
|
||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), and **Cursor**.
|
||||
|
||||
## Step 1: Set your API key
|
||||
|
||||
> **You must complete this step before installing the plugin.**
|
||||
|
||||
1. Sign up at [app.mem0.ai](https://app.mem0.ai) if you haven't already
|
||||
2. Go to [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys)
|
||||
3. Click **Create API Key** and copy the key (starts with `m0-`)
|
||||
4. Add it to your shell profile:
|
||||
|
||||
```bash
|
||||
# For zsh (default on macOS)
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
|
||||
# For bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
5. Confirm it's set:
|
||||
|
||||
```bash
|
||||
echo $MEM0_API_KEY
|
||||
# Should print: m0-your-api-key
|
||||
```
|
||||
|
||||
## Step 2: Install the plugin
|
||||
|
||||
Choose one of the options below. All require `MEM0_API_KEY` to be set first (see above).
|
||||
|
||||
### Claude Code (CLI) / Claude Cowork (Desktop)
|
||||
|
||||
Claude Code and Claude Cowork share the same plugin system.
|
||||
|
||||
**CLI:**
|
||||
|
||||
```
|
||||
/plugin marketplace add mem0ai/mem0
|
||||
/plugin install mem0@mem0-plugins
|
||||
```
|
||||
|
||||
**Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
|
||||
|
||||
This installs the full plugin including the MCP server, lifecycle hooks (automatic memory capture), and the Mem0 SDK skill.
|
||||
|
||||
### Cursor
|
||||
|
||||
> **Already have `mem0` configured as an MCP server?** Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
|
||||
**Option A — One-click deeplink** (installs MCP server only):
|
||||
|
||||
[Install Mem0 MCP in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=mem0&config=eyJtY3BTZXJ2ZXJzIjp7Im1lbTAiOnsidXJsIjoiaHR0cHM6Ly9tY3AubWVtMC5haS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiVG9rZW4gJHtlbnY6TUVNMF9BUElfS0VZfSJ9fX19)
|
||||
|
||||
**Option B — Manual configuration** (MCP server only):
|
||||
|
||||
Add the following to your `.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option C — Cursor Marketplace** (full plugin with hooks and skills):
|
||||
|
||||
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks and the Mem0 SDK skill.
|
||||
|
||||
## Verify it works
|
||||
|
||||
After installing, confirm the MCP server is connected:
|
||||
|
||||
1. Start a new session (or restart your current one)
|
||||
2. Ask: *"List my mem0 entities"* or *"Search my memories for hello"*
|
||||
3. If the `mem0` tools appear and respond, you're all set
|
||||
|
||||
## What's included
|
||||
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|
|
||||
| MCP Server | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No |
|
||||
|
||||
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end.
|
||||
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
Once installed, the following tools are available:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -1,37 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"sessionStart": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"matcher": "startup|resume|compact"
|
||||
}
|
||||
],
|
||||
"preToolUse": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write.sh",
|
||||
"matcher": "Write|Edit"
|
||||
}
|
||||
],
|
||||
"preCompact": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.sh"
|
||||
},
|
||||
{
|
||||
"command": "python3 ${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"timeout": 30
|
||||
}
|
||||
],
|
||||
"stop": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"beforeSubmitPrompt": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"description": "Mem0 memory capture hooks — automatic memory extraction at key lifecycle points",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"statusMessage": "Loading mem0 context..."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block_memory_write.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.sh",
|
||||
"statusMessage": "Preparing pre-compaction summary..."
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"statusMessage": "Saving session state to mem0...",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_stop.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"statusMessage": "Searching mem0 memories...",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"TaskCompleted": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_task_completed.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<svg width="307" height="307" viewBox="0 0 307 307" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M162.496 25.3505C165.003 25.3505 167.453 24.6071 169.538 23.2144C171.622 21.8216 173.247 19.8419 174.206 17.5258C175.165 15.2097 175.416 12.6612 174.927 10.2024C174.438 7.74365 173.231 5.48516 171.458 3.71249C169.686 1.93983 167.427 0.73263 164.968 0.243552C162.51 -0.245525 159.961 0.00550576 157.645 0.964866C155.329 1.92423 153.349 3.54885 151.956 5.63328C150.564 7.71772 149.82 10.1683 149.82 12.6753C149.818 14.3404 150.145 15.9895 150.781 17.5283C151.417 19.0671 152.351 20.4653 153.528 21.6427C154.706 22.8201 156.104 23.7537 157.643 24.39C159.181 25.0262 160.83 25.3526 162.496 25.3505Z" fill="white"/>
|
||||
<path d="M69.3342 56.559C71.1066 54.7862 72.3135 52.5277 72.8024 50.069C73.2913 47.6103 73.0401 45.0619 72.0807 42.7459C71.1213 40.43 69.4967 38.4505 67.4123 37.0579C65.3279 35.6652 62.8774 34.9219 60.3706 34.9219C57.8637 34.9219 55.4132 35.6652 53.3288 37.0579C51.2444 38.4505 49.6198 40.43 48.6604 42.7459C47.701 45.0619 47.4498 47.6103 47.9387 50.069C48.4276 52.5277 49.6345 54.7862 51.4069 56.559C52.5839 57.7363 53.9813 58.6701 55.5193 59.3073C57.0573 59.9444 58.7058 60.2724 60.3706 60.2724C62.0353 60.2724 63.6838 59.9444 65.2218 59.3073C66.7598 58.6701 68.1572 57.7363 69.3342 56.559Z" fill="white"/>
|
||||
<path d="M25.3505 144.504C25.3505 141.997 24.6071 139.547 23.2143 137.462C21.8216 135.378 19.842 133.753 17.5259 132.794C15.2098 131.835 12.6612 131.584 10.2024 132.073C7.74368 132.562 5.48513 133.769 3.71247 135.542C1.9398 137.314 0.732655 139.573 0.243578 142.032C-0.2455 144.49 0.00543354 147.039 0.964793 149.355C1.92415 151.671 3.54877 153.651 5.63321 155.044C7.71764 156.436 10.1683 157.18 12.6752 157.18C16.0369 157.18 19.261 155.844 21.638 153.467C24.0151 151.09 25.3505 147.866 25.3505 144.504Z" fill="white"/>
|
||||
<path d="M56.5589 237.749C54.7862 235.976 52.5277 234.769 50.069 234.28C47.6103 233.792 45.0619 234.043 42.7459 235.002C40.43 235.962 38.4505 237.586 37.0579 239.671C35.6652 241.755 34.9219 244.206 34.9219 246.712C34.9219 249.219 35.6652 251.67 37.0579 253.754C38.4505 255.838 40.43 257.463 42.7459 258.423C45.0619 259.382 47.6103 259.633 50.069 259.144C52.5277 258.655 54.7862 257.448 56.5589 255.676C57.7362 254.499 58.6701 253.102 59.3073 251.564C59.9444 250.026 60.2724 248.377 60.2724 246.712C60.2724 245.048 59.9444 243.399 59.3073 241.861C58.6701 240.323 57.7362 238.926 56.5589 237.749Z" fill="white"/>
|
||||
<path d="M144.488 281.648C141.981 281.648 139.53 282.392 137.446 283.785C135.361 285.177 133.737 287.157 132.777 289.473C131.818 291.789 131.567 294.338 132.056 296.797C132.545 299.255 133.752 301.514 135.525 303.286C137.298 305.059 139.556 306.266 142.015 306.755C144.474 307.244 147.022 306.993 149.338 306.034C151.655 305.075 153.634 303.45 155.027 301.366C156.42 299.281 157.163 296.831 157.163 294.324C157.159 290.963 155.822 287.742 153.446 285.366C151.07 282.989 147.848 281.653 144.488 281.648Z" fill="white"/>
|
||||
<path d="M237.751 250.487C235.978 252.26 234.771 254.518 234.282 256.977C233.794 259.435 234.045 261.984 235.004 264.3C235.964 266.616 237.588 268.595 239.673 269.988C241.757 271.381 244.207 272.124 246.714 272.124C249.221 272.124 251.672 271.381 253.756 269.988C255.84 268.595 257.465 266.616 258.424 264.3C259.384 261.984 259.635 259.435 259.146 256.977C258.657 254.518 257.45 252.26 255.678 250.487C254.501 249.31 253.104 248.376 251.566 247.739C250.028 247.101 248.379 246.773 246.714 246.773C245.05 246.773 243.401 247.101 241.863 247.739C240.325 248.376 238.928 249.31 237.751 250.487Z" fill="white"/>
|
||||
<path d="M281.648 162.512C281.648 165.019 282.392 167.469 283.785 169.554C285.177 171.638 287.157 173.263 289.473 174.222C291.789 175.181 294.338 175.432 296.797 174.943C299.255 174.454 301.514 173.247 303.286 171.474C305.059 169.702 306.266 167.443 306.755 164.984C307.244 162.526 306.993 159.977 306.034 157.661C305.075 155.345 303.45 153.365 301.366 151.973C299.281 150.58 296.831 149.836 294.324 149.836C290.962 149.836 287.738 151.172 285.361 153.549C282.984 155.926 281.648 159.15 281.648 162.512Z" fill="white"/>
|
||||
<path d="M250.471 69.3303C252.244 71.1027 254.503 72.3097 256.961 72.7985C259.42 73.2874 261.968 73.0363 264.284 72.0768C266.6 71.1174 268.58 69.4928 269.972 67.4084C271.365 65.324 272.108 62.8735 272.108 60.3667C272.108 57.8599 271.365 55.4093 269.972 53.3249C268.58 51.2406 266.6 49.616 264.284 48.6565C261.968 47.6971 259.42 47.4459 256.961 47.9348C254.503 48.4236 252.244 49.6306 250.471 51.403C249.294 52.58 248.36 53.9775 247.723 55.5155C247.086 57.0535 246.758 58.7019 246.758 60.3667C246.758 62.0314 247.086 63.6799 247.723 65.2179C248.36 66.7559 249.294 68.1533 250.471 69.3303Z" fill="white"/>
|
||||
<path d="M184.782 60.8054C180.168 63.4713 178.3 69.0427 177.63 74.3267C177.047 78.9358 175.033 83.2457 171.87 86.6488C168.707 90.052 164.556 92.3766 160.002 93.2951C155.448 94.2136 150.721 93.6796 146.487 91.7683C142.252 89.857 138.724 86.6649 136.401 82.642C134.077 78.6192 133.075 73.9684 133.535 69.3455C133.995 64.7226 135.895 60.3607 138.966 56.8748C142.037 53.389 146.125 50.9549 150.653 49.9159C155.181 48.8768 159.921 49.2852 164.204 51.0834C169.121 53.1428 174.884 54.2762 179.514 51.6422C184.143 49.0082 185.995 43.4049 186.665 38.1208C187.245 33.5107 189.257 29.1987 192.418 25.7931C195.579 22.3876 199.73 20.0603 204.284 19.1394C208.838 18.2185 213.567 18.7505 217.803 20.6605C222.039 22.5704 225.568 25.7618 227.893 29.7846C230.218 33.8075 231.222 38.4587 230.763 43.0824C230.303 47.7062 228.404 52.0691 225.333 55.5559C222.262 59.0426 218.173 61.4773 213.645 62.5165C209.116 63.5557 204.375 63.147 200.091 61.3481C195.174 59.3048 189.411 58.1554 184.782 60.8054Z" fill="white"/>
|
||||
<path d="M110.073 65.8178C108.7 70.9742 111.318 76.2422 114.575 80.4567C117.417 84.1261 119.036 88.595 119.204 93.2335C119.372 97.872 118.08 102.446 115.51 106.311C112.941 110.177 109.223 113.138 104.881 114.778C100.538 116.419 95.7912 116.655 91.3077 115.454C86.8242 114.253 82.8306 111.675 79.8898 108.084C76.9489 104.493 75.2091 100.07 74.9155 95.4379C74.6219 90.8057 75.7894 86.1981 78.2533 82.2645C80.7173 78.331 84.3534 75.2698 88.6494 73.5124C93.5822 71.485 98.4991 68.2444 99.8241 63.0881C101.149 57.9317 98.579 52.6637 95.3224 48.4493C92.4827 44.7781 90.8665 40.3083 90.7018 35.6699C90.537 31.0315 91.8319 26.4583 94.4039 22.5949C96.976 18.7314 100.695 15.7724 105.038 14.1349C109.381 12.4974 114.128 12.2639 118.611 13.4673C123.094 14.6708 127.085 17.2505 130.024 20.8429C132.963 24.4354 134.7 28.8594 134.991 33.4916C135.283 38.1237 134.113 42.7305 131.647 46.6627C129.182 50.5948 125.544 53.6541 121.248 55.4095C116.363 57.4209 111.462 60.6775 110.073 65.8178Z" fill="white"/>
|
||||
<path d="M60.7892 122.218C63.4552 126.831 69.0425 128.699 74.3265 129.37C78.9361 129.955 83.2455 131.973 86.6471 135.138C90.0487 138.304 92.3707 142.457 93.2857 147.013C94.2006 151.569 93.6625 156.296 91.747 160.53C89.8314 164.763 86.6353 168.288 82.6093 170.608C78.5833 172.928 73.9305 173.926 69.3073 173.46C64.6841 172.995 60.3236 171.09 56.841 168.014C53.3583 164.938 50.9292 160.846 49.8962 156.316C48.8631 151.785 49.2783 147.045 51.0832 142.763C53.1426 137.846 54.2759 132.083 51.6419 127.454C49.0079 122.824 43.4046 120.973 38.1047 120.302C33.4951 119.717 29.1856 117.699 25.7841 114.533C22.3825 111.368 20.0604 107.214 19.1454 102.659C18.2304 98.1032 18.7687 93.3752 20.6842 89.1418C22.5997 84.9084 25.7959 81.3832 29.8219 79.0632C33.8479 76.7433 38.5006 75.7457 43.1238 76.2113C47.7471 76.6768 52.1075 78.582 55.5902 81.658C59.0728 84.7341 61.502 88.8258 62.535 93.3561C63.568 97.8865 63.1528 102.627 61.3479 106.908C59.2886 111.825 58.1552 117.588 60.7892 122.218Z" fill="white"/>
|
||||
<path d="M65.8204 196.93C70.9767 198.303 76.2287 195.685 80.4592 192.428C84.1286 189.586 88.5975 187.967 93.236 187.799C97.8745 187.631 102.449 188.923 106.314 191.493C110.179 194.062 113.141 197.78 114.781 202.122C116.421 206.464 116.657 211.212 115.457 215.695C114.256 220.179 111.678 224.172 108.087 227.113C104.496 230.054 100.073 231.794 95.4404 232.087C90.8082 232.381 86.2006 231.214 82.2671 228.75C78.3335 226.286 75.2723 222.649 73.5149 218.353C71.4875 213.421 68.231 208.504 63.0906 207.179C57.9503 205.854 52.6662 208.424 48.4518 211.681C44.7804 214.528 40.308 216.151 35.6652 216.32C31.0224 216.49 26.4435 215.199 22.5738 212.628C18.7042 210.057 15.7391 206.336 14.0968 201.99C12.4544 197.644 12.2176 192.892 13.4197 188.404C14.6218 183.917 17.2021 179.919 20.7969 176.976C24.3917 174.033 28.8195 172.293 33.4562 172C38.0929 171.708 42.7045 172.878 46.6407 175.345C50.577 177.813 53.6394 181.454 55.3961 185.755C57.4235 190.656 60.6641 195.541 65.8204 196.93Z" fill="white"/>
|
||||
<path d="M122.205 246.21C126.818 243.544 128.686 237.956 129.373 232.672C129.96 228.068 131.978 223.763 135.142 220.366C138.306 216.969 142.456 214.651 147.008 213.738C151.559 212.825 156.283 213.364 160.512 215.278C164.741 217.192 168.263 220.385 170.58 224.408C172.898 228.43 173.895 233.078 173.43 237.697C172.966 242.316 171.064 246.673 167.991 250.153C164.919 253.633 160.832 256.061 156.306 257.095C151.781 258.129 147.045 257.717 142.766 255.916C137.833 253.856 132.07 252.723 127.457 255.357C122.843 257.991 120.96 263.594 120.289 268.894C119.7 273.498 117.681 277.8 114.517 281.196C111.353 284.591 107.204 286.908 102.653 287.821C98.1027 288.733 93.3808 288.194 89.1525 286.281C84.9243 284.367 81.4031 281.175 79.085 277.154C76.767 273.134 75.7689 268.487 76.2316 263.869C76.6942 259.251 78.5942 254.895 81.6638 251.414C84.7334 247.933 88.8179 245.503 93.3417 244.466C97.8655 243.429 102.601 243.838 106.88 245.635C111.828 247.694 117.591 248.876 122.205 246.21Z" fill="white"/>
|
||||
<path d="M196.915 241.18C198.304 236.024 195.686 230.756 192.414 226.542C189.567 222.87 187.944 218.398 187.774 213.755C187.604 209.112 188.896 204.533 191.467 200.664C194.038 196.794 197.759 193.829 202.104 192.187C206.45 190.544 211.202 190.307 215.69 191.509C220.178 192.712 224.175 195.292 227.118 198.887C230.061 202.481 231.802 206.909 232.094 211.546C232.387 216.183 231.217 220.794 228.749 224.731C226.281 228.667 222.64 231.729 218.339 233.486C213.406 235.513 208.505 238.77 207.164 243.91C205.823 249.051 208.393 254.335 211.666 258.549C214.513 262.22 216.136 266.693 216.306 271.335C216.476 275.978 215.184 280.557 212.613 284.427C210.042 288.297 206.321 291.262 201.975 292.904C197.629 294.546 192.877 294.783 188.39 293.581C183.902 292.379 179.905 289.799 176.962 286.204C174.019 282.609 172.278 278.181 171.985 273.545C171.693 268.908 172.863 264.296 175.331 260.36C177.799 256.424 181.44 253.361 185.741 251.605C190.658 249.577 195.543 246.337 196.915 241.18Z" fill="white"/>
|
||||
<path d="M246.195 184.797C243.529 180.184 237.957 178.316 232.673 177.629C228.069 177.045 223.764 175.03 220.365 171.869C216.967 168.708 214.646 164.56 213.729 160.01C212.813 155.46 213.348 150.737 215.258 146.507C217.168 142.277 220.357 138.753 224.376 136.431C228.395 134.11 233.041 133.108 237.66 133.567C242.279 134.026 246.637 135.923 250.12 138.991C253.604 142.058 256.037 146.141 257.077 150.664C258.117 155.188 257.711 159.923 255.917 164.204C253.857 169.137 252.724 174.9 255.342 179.513C257.96 184.127 263.595 186.01 268.879 186.681C273.484 187.267 277.789 189.283 281.186 192.446C284.584 195.608 286.904 199.757 287.819 204.308C288.733 208.859 288.197 213.582 286.285 217.811C284.372 222.041 281.181 225.564 277.16 227.884C273.14 230.203 268.492 231.203 263.874 230.741C259.255 230.279 254.897 228.379 251.416 225.31C247.934 222.24 245.503 218.155 244.466 213.63C243.429 209.106 243.838 204.37 245.636 200.09C247.695 195.173 248.861 189.411 246.195 184.797Z" fill="white"/>
|
||||
<path d="M241.18 110.07C236.024 108.697 230.756 111.315 226.542 114.588C222.87 117.435 218.398 119.058 213.755 119.228C209.112 119.398 204.533 118.106 200.664 115.535C196.794 112.964 193.829 109.243 192.187 104.897C190.544 100.551 190.307 95.7994 191.509 91.3117C192.712 86.824 195.292 82.8268 198.887 79.8837C202.481 76.9405 206.909 75.2 211.546 74.9073C216.183 74.6147 220.794 75.7849 224.731 78.2527C228.667 80.7206 231.729 84.3617 233.486 88.6627C235.513 93.5955 238.754 98.4964 243.91 99.8374C249.066 101.178 254.335 98.6082 258.549 95.3356C262.22 92.4959 266.69 90.8798 271.328 90.7151C275.967 90.5503 280.54 91.8452 284.403 94.4172C288.267 96.9892 291.226 100.709 292.863 105.052C294.501 109.394 294.734 114.142 293.531 118.624C292.327 123.107 289.748 127.099 286.155 130.037C282.563 132.976 278.139 134.714 273.507 135.005C268.875 135.296 264.268 134.126 260.336 131.661C256.403 129.195 253.344 125.557 251.589 121.261C249.577 116.36 246.321 111.459 241.18 110.07Z" fill="white"/>
|
||||
<path d="M153.491 191.533C174.501 191.533 191.533 174.501 191.533 153.491C191.533 132.482 174.501 115.45 153.491 115.45C132.481 115.45 115.449 132.482 115.449 153.491C115.449 174.501 132.481 191.533 153.491 191.533Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PreToolUse (matcher: Write|Edit)
|
||||
#
|
||||
# Blocks writes to MEMORY.md and auto-memory files, redirecting Claude
|
||||
# to use the mem0 MCP add_memory tool instead.
|
||||
#
|
||||
# Input: JSON on stdin with tool_name, tool_input
|
||||
# Output: stderr message (exit 2 = block)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = allow the tool call
|
||||
# 2 = block the tool call (stderr is shown to Claude as feedback)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$FILE_PATH" in
|
||||
*/MEMORY.md|*/memory/*.md|*/.claude/*/memory/*)
|
||||
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
@@ -1,239 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture session state via the Mem0 REST API.
|
||||
|
||||
Safety net for PreCompact and Stop hooks — reads the transcript JSONL,
|
||||
extracts structured session state, and stores it in Mem0 directly.
|
||||
|
||||
Used by:
|
||||
- PreCompact hook: Tags with "pre-compaction" (context about to be lost)
|
||||
- Stop hook: Tags with "session-end" (session ending, Claude can't respond)
|
||||
|
||||
Input: JSON on stdin with transcript_path, session_id, cwd
|
||||
Output: stderr logs only (exit 0 always — must not block)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
log = logging.getLogger("mem0-capture")
|
||||
log.setLevel(logging.DEBUG)
|
||||
_handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-capture] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_TAIL_LINES = 500
|
||||
MAX_USER_MESSAGES = 30
|
||||
MAX_BASH_COMMANDS = 20
|
||||
MAX_ASSISTANT_TEXT = 10000
|
||||
|
||||
|
||||
def tail_lines(filepath: str, n: int) -> list[str]:
|
||||
"""Read last n lines of a file efficiently."""
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
file_size = f.tell()
|
||||
if file_size == 0:
|
||||
return []
|
||||
chunk_size = min(file_size, n * 4096)
|
||||
f.seek(max(0, file_size - chunk_size))
|
||||
data = f.read().decode("utf-8", errors="replace")
|
||||
return data.splitlines()[-n:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def parse_transcript(lines: list[str]) -> dict:
|
||||
"""Parse transcript JSONL lines and extract session state."""
|
||||
user_messages: list[str] = []
|
||||
files_modified: set[str] = set()
|
||||
bash_commands: list[str] = []
|
||||
last_assistant_text = ""
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
entry_type = entry.get("type")
|
||||
if entry_type not in ("user", "assistant"):
|
||||
continue
|
||||
if entry.get("isSidechain"):
|
||||
continue
|
||||
|
||||
message = entry.get("message", {})
|
||||
content_blocks = message.get("content", [])
|
||||
|
||||
if entry_type == "user":
|
||||
parts = []
|
||||
if isinstance(content_blocks, str):
|
||||
parts.append(content_blocks)
|
||||
elif isinstance(content_blocks, list):
|
||||
for block in content_blocks:
|
||||
if isinstance(block, str):
|
||||
parts.append(block)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
text = "\n".join(parts).strip()
|
||||
if text and len(text) > 10 and not text.startswith("<"):
|
||||
user_messages.append(text)
|
||||
|
||||
elif entry_type == "assistant":
|
||||
for block in content_blocks:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
if block.get("type") == "text":
|
||||
text = block.get("text", "").strip()
|
||||
if text:
|
||||
last_assistant_text = text
|
||||
if block.get("type") == "tool_use":
|
||||
tool_name = block.get("name", "")
|
||||
tool_input = block.get("input", {})
|
||||
if tool_name in ("Write", "Edit"):
|
||||
fp = tool_input.get("file_path", "")
|
||||
if fp:
|
||||
files_modified.add(fp)
|
||||
elif tool_name == "Bash":
|
||||
cmd = tool_input.get("command", "")
|
||||
if cmd:
|
||||
bash_commands.append(cmd)
|
||||
|
||||
return {
|
||||
"user_messages": user_messages[-MAX_USER_MESSAGES:],
|
||||
"files_modified": sorted(files_modified),
|
||||
"bash_commands": bash_commands[-MAX_BASH_COMMANDS:],
|
||||
"last_assistant_text": last_assistant_text[:MAX_ASSISTANT_TEXT],
|
||||
}
|
||||
|
||||
|
||||
def build_content(state: dict, source: str) -> str:
|
||||
"""Build structured markdown from parsed state."""
|
||||
parts = [f"## Session State ({source})\n"]
|
||||
|
||||
if state["user_messages"]:
|
||||
parts.append("### What the user was working on")
|
||||
for msg in state["user_messages"]:
|
||||
truncated = msg[:5000] + "..." if len(msg) > 5000 else msg
|
||||
parts.append(f"- {truncated}")
|
||||
parts.append("")
|
||||
|
||||
if state["files_modified"]:
|
||||
parts.append("### Files modified this session")
|
||||
for fp in state["files_modified"]:
|
||||
parts.append(f"- `{fp}`")
|
||||
parts.append("")
|
||||
|
||||
if state["bash_commands"]:
|
||||
parts.append("### Recent commands")
|
||||
for cmd in state["bash_commands"]:
|
||||
truncated = cmd[:1000] + "..." if len(cmd) > 1000 else cmd
|
||||
parts.append(f"- `{truncated}`")
|
||||
parts.append("")
|
||||
|
||||
if state["last_assistant_text"]:
|
||||
parts.append("### Last context")
|
||||
parts.append(state["last_assistant_text"])
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
|
||||
"""Store session state as a memory via the Mem0 REST API."""
|
||||
body = {
|
||||
"messages": [
|
||||
{"role": "user", "content": content}
|
||||
],
|
||||
"user_id": user_id,
|
||||
"metadata": {
|
||||
"type": "session_state",
|
||||
"source": source,
|
||||
},
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v1/memories/",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status in (200, 201):
|
||||
log.info("Session state stored successfully")
|
||||
return True
|
||||
log.warning("API returned status %d", resp.status)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
source = "pre-compaction"
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith("--source="):
|
||||
source = arg.split("=", 1)[1]
|
||||
|
||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
||||
if not api_key:
|
||||
log.debug("MEM0_API_KEY not set, skipping capture")
|
||||
return
|
||||
|
||||
try:
|
||||
hook_input = json.loads(sys.stdin.read())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
log.debug("No valid JSON on stdin")
|
||||
return
|
||||
|
||||
transcript_path = hook_input.get("transcript_path", "")
|
||||
if not transcript_path:
|
||||
log.debug("No transcript_path provided")
|
||||
return
|
||||
|
||||
user_id = os.environ.get("MEM0_USER_ID", os.environ.get("USER", "default"))
|
||||
|
||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||
if not lines:
|
||||
log.debug("Transcript empty or unreadable: %s", transcript_path)
|
||||
return
|
||||
|
||||
state = parse_transcript(lines)
|
||||
if not state["user_messages"] and not state["files_modified"]:
|
||||
log.debug("No meaningful session state to capture")
|
||||
return
|
||||
|
||||
content = build_content(state, source)
|
||||
|
||||
log.info(
|
||||
"Capturing session state: %d user msgs, %d files, %d commands",
|
||||
len(state["user_messages"]),
|
||||
len(state["files_modified"]),
|
||||
len(state["bash_commands"]),
|
||||
)
|
||||
|
||||
store_memory(api_key, content, user_id, source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
log.error("Unexpected error: %s", e)
|
||||
sys.exit(0)
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PreCompact
|
||||
#
|
||||
# Fires BEFORE context compaction. This is the last chance to capture
|
||||
# the full context before it gets compressed.
|
||||
#
|
||||
# Output: Text instructions injected into Claude's context.
|
||||
# Claude still has the full conversation and can write an accurate summary.
|
||||
# A companion Python script (on_pre_compact.py) also runs to capture
|
||||
# transcript state directly via the Mem0 REST API as a safety net.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cat <<'EOF'
|
||||
## CRITICAL: Pre-Compaction Session Summary
|
||||
|
||||
Context compaction is about to happen. You are about to lose most of your conversation history. You MUST store a comprehensive session summary NOW using the mem0 `add_memory` tool.
|
||||
|
||||
### Step 1: Store session summary
|
||||
|
||||
Call `add_memory` with a thorough summary covering ALL of the following:
|
||||
|
||||
```
|
||||
## Session Summary (Pre-Compaction)
|
||||
|
||||
### User's Goal
|
||||
[What the user originally asked for and their intent]
|
||||
|
||||
### What Was Accomplished
|
||||
[Numbered list of tasks completed, features built, bugs fixed]
|
||||
|
||||
### Key Decisions Made
|
||||
[Architectural choices, design decisions, trade-offs discussed]
|
||||
|
||||
### Files Created or Modified
|
||||
[List of important file paths with what changed in each]
|
||||
|
||||
### Current State
|
||||
[What is in progress RIGHT NOW — the task you were in the middle of]
|
||||
[Any pending items, blockers, or next steps]
|
||||
|
||||
### Important Context
|
||||
[User preferences observed, coding patterns, anything that would help
|
||||
the post-compaction agent continue without asking redundant questions]
|
||||
```
|
||||
|
||||
Include metadata: `{"type": "session_state", "source": "pre-compaction"}`
|
||||
|
||||
### Step 2: Store any unstored learnings
|
||||
|
||||
If there are learnings from this session that you haven't stored yet, store them as separate memories:
|
||||
- Failed approaches -> metadata `{"type": "anti_pattern"}`
|
||||
- Successful strategies -> metadata `{"type": "task_learning"}`
|
||||
- Architecture decisions -> metadata `{"type": "decision"}`
|
||||
|
||||
### Step 3: Acknowledge
|
||||
|
||||
After storing, briefly tell the user that session state has been saved and you're ready for compaction.
|
||||
|
||||
Do this NOW. Do not skip any section. The quality of this summary directly determines whether you can continue the user's task after compaction.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: SessionStart (matcher: startup|resume|compact)
|
||||
#
|
||||
# Bootstraps mem0 context at the start of every session.
|
||||
# Output becomes part of Claude's context so it calls mem0 MCP tools.
|
||||
#
|
||||
# Input: JSON on stdin with session_id, source, transcript_path, model, cwd
|
||||
# Output: Text injected into Claude's context (exit 0)
|
||||
|
||||
# Intentionally omit -e so the script always outputs a bootstrap prompt
|
||||
# even if jq is missing or stdin is malformed.
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
|
||||
|
||||
if [ "$SOURCE" = "startup" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Bootstrap
|
||||
|
||||
You have access to persistent memory via the mem0 MCP tools. Before doing anything else:
|
||||
|
||||
1. Call `search_memories` with a query related to the current project or user request to load relevant context.
|
||||
2. Review the returned memories to understand what has been learned in prior sessions.
|
||||
3. If appropriate, call `get_memories` to browse all stored memories for this user.
|
||||
|
||||
IMPORTANT: Do NOT skip this step. Always bootstrap context first.
|
||||
EOF
|
||||
|
||||
elif [ "$SOURCE" = "resume" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Resumed
|
||||
|
||||
This is a resumed session. Your prior context is already loaded. Before continuing:
|
||||
|
||||
1. Call `search_memories` with a query related to the current task to refresh relevant memories.
|
||||
2. If significant time has passed, search for recent project-wide updates.
|
||||
|
||||
Continue where you left off.
|
||||
EOF
|
||||
|
||||
elif [ "$SOURCE" = "compact" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Post-Compaction Recovery
|
||||
|
||||
Context was just compacted. You may have lost important session context.
|
||||
|
||||
1. Call `search_memories` with queries related to what you were working on to reload relevant knowledge.
|
||||
2. Check for any session state memories that were saved before compaction.
|
||||
3. Continue working based on the recovered context.
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop
|
||||
#
|
||||
# Fires when Claude finishes responding.
|
||||
# Reminds Claude to store any unsaved learnings, then spawns a background
|
||||
# process to capture transcript state via the Mem0 REST API directly.
|
||||
#
|
||||
# Input: JSON on stdin with stop_hook_active, transcript_path, cwd
|
||||
# Output: Text that becomes Claude's context (exit 0), or nothing
|
||||
#
|
||||
# IMPORTANT: Check stop_hook_active to avoid infinite loops.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool:
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata `{"type": "decision"}`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata `{"type": "task_learning"}`
|
||||
3. Did any approach fail? -> Store with metadata `{"type": "anti_pattern"}`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata `{"type": "user_preference"}`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata `{"type": "environmental"}`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
|
||||
EOF
|
||||
|
||||
# Capture transcript state in the background via Mem0 REST API
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: TaskCompleted
|
||||
#
|
||||
# Fires when a task is marked as completed. Reminds Claude to extract
|
||||
# and store learnings via the mem0 MCP tools.
|
||||
#
|
||||
# Input: JSON on stdin with task_id, task_subject, task_description
|
||||
# Output: Text that becomes feedback to the model (exit 0)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
|
||||
|
||||
cat <<EOF
|
||||
Task completed: "$TASK_SUBJECT"
|
||||
|
||||
Extract key learnings from this completed task and store them using the mem0 \`add_memory\` tool:
|
||||
|
||||
1. What strategy worked well? -> Store with metadata \`{"type": "task_learning"}\`
|
||||
2. Were there failed approaches before finding the solution? -> Store with metadata \`{"type": "anti_pattern"}\`
|
||||
3. Were there architectural decisions? -> Store with metadata \`{"type": "decision"}\`
|
||||
4. Any new conventions or patterns established? -> Store with metadata \`{"type": "convention"}\`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
|
||||
Only store genuinely useful learnings — skip if the task was trivial.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: UserPromptSubmit
|
||||
#
|
||||
# Fires on every user message. Searches mem0 for relevant memories
|
||||
# and injects them into Claude's context before processing.
|
||||
#
|
||||
# Input: JSON on stdin with prompt, session_id, cwd, transcript_path
|
||||
# Output: Matching memories as context text (exit 0)
|
||||
#
|
||||
# Skips search for very short prompts (< 20 chars) and when
|
||||
# MEM0_API_KEY is not set. Uses a 3s timeout to minimize latency.
|
||||
|
||||
# Intentionally omit -e so the script always exits 0 even if
|
||||
# curl or jq fail — must never block the user's prompt.
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Skip trivial prompts — not worth a network call
|
||||
if [ ${#PROMPT} -lt 20 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
API_KEY="${MEM0_API_KEY:-}"
|
||||
if [ -z "$API_KEY" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
USER_ID="${MEM0_USER_ID:-${USER:-default}}"
|
||||
|
||||
# Build request body safely via jq to avoid injection
|
||||
BODY=$(jq -n --arg query "$PROMPT" --arg user_id "$USER_ID" \
|
||||
'{query: $query, filters: {user_id: $user_id}, top_k: 5}')
|
||||
|
||||
# Search mem0 for memories relevant to this prompt
|
||||
RESPONSE=$(curl -s --max-time 3 \
|
||||
-X POST "https://api.mem0.ai/v2/memories/search/" \
|
||||
-H "Authorization: Token $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RESPONSE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract memories from response (API returns a flat array)
|
||||
MEMORIES=$(echo "$RESPONSE" | jq -r '
|
||||
if type == "array" then . else .results // [] end |
|
||||
if length == 0 then empty else
|
||||
"## Relevant memories from mem0\n\n" +
|
||||
(map(select(.memory != null) | "- " + .memory) | join("\n"))
|
||||
end
|
||||
' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$MEMORIES" ]; then
|
||||
echo "$MEMORIES"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,189 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2024 Mem0.ai
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||