Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33d2bc495d | ||
|
|
c0cae68646 | ||
|
|
3b2f01796e | ||
|
|
9cd3d2cca8 | ||
|
|
c53f1f126d | ||
|
|
0b7615fa87 | ||
|
|
66d34fab3c | ||
|
|
868b63af63 | ||
|
|
6cc1c15320 | ||
|
|
7a20da59ee | ||
|
|
f89f7c7c81 | ||
|
|
5723136bed | ||
|
|
b5345f8498 | ||
|
|
6577ae7616 | ||
|
|
5e00d5c452 | ||
|
|
3b152a3e85 | ||
|
|
beca7cc873 | ||
|
|
30f242dc4c | ||
|
|
1bfaaf8750 | ||
|
|
c250ccfb5c | ||
|
|
e2b439c42a | ||
|
|
c788d771d3 | ||
|
|
2acf9571b3 | ||
|
|
713dba5d0a | ||
|
|
8ae7a06220 | ||
|
|
f94ea06588 | ||
|
|
215d8b5a71 | ||
|
|
82525dbf0f | ||
|
|
32c1ccba5b | ||
|
|
9bebcf45f7 | ||
|
|
93bd4e248c | ||
|
|
af19495f66 | ||
|
|
dbac83218f | ||
|
|
3618aeff22 | ||
|
|
25e25aaa2f | ||
|
|
5d30af9560 | ||
|
|
47969aaa6b | ||
|
|
7213e1b1a6 | ||
|
|
97291aa119 | ||
|
|
b8a5ca1b70 | ||
|
|
431cba20e9 | ||
|
|
4482228383 | ||
|
|
ae49edec44 | ||
|
|
4b7f51d194 | ||
|
|
41abb571a2 | ||
|
|
e280665578 | ||
|
|
27a6e7863e | ||
|
|
ae8e03c6b7 | ||
|
|
376be3b6d4 | ||
|
|
cd2dd7cc54 | ||
|
|
ece654811a | ||
|
|
13d42a99e9 | ||
|
|
3225e30859 | ||
|
|
88fd0e77d0 | ||
|
|
c2698f2b71 | ||
|
|
e87240d4d9 | ||
|
|
68cf4e118d | ||
|
|
12624555b4 | ||
|
|
e8004b93db | ||
|
|
4e414e4015 | ||
|
|
16455789d4 | ||
|
|
3ac4e047de | ||
|
|
a2ffca3266 | ||
|
|
3a9fcdbec2 | ||
|
|
515f87b6bd | ||
|
|
5edc0cc99f | ||
|
|
7fff26f374 | ||
|
|
1aecfadf45 | ||
|
|
7e06aeeada | ||
|
|
2a59c9fd99 | ||
|
|
669ed184e4 | ||
|
|
3c2683c1b5 | ||
|
|
f06e2d744d | ||
|
|
f9e30304d7 |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "mem0-plugins",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Mem0 Plugins"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./mem0-plugin"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "mem0-plugins",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mem0",
|
||||||
|
"email": "support@mem0.ai"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"description": "Official Mem0 plugins for Claude"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"source": "./mem0-plugin",
|
||||||
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "mem0-plugins",
|
||||||
|
"owner": {
|
||||||
|
"name": "Mem0",
|
||||||
|
"email": "support@mem0.ai"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"description": "Official Mem0 plugins for Cursor"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"source": "./mem0-plugin",
|
||||||
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||||
|
if: startsWith(github.event.release.tag_name, 'v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -38,7 +39,7 @@ jobs:
|
|||||||
# packages_dir: dist/
|
# packages_dir: dist/
|
||||||
|
|
||||||
- name: Publish distribution 📦 to PyPI
|
- name: Publish distribution 📦 to PyPI
|
||||||
if: startsWith(github.ref, 'refs/tags')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
packages_dir: dist/
|
packages_dir: dist/
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Publish @mem0/cli 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/cli 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: cli/node
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Upgrade npm for OIDC trusted publishing
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish --provenance --access public
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
name: CLI Node CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'cli/node/**'
|
||||||
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/node/**'
|
||||||
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run lint
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run typecheck
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [20, 22]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run test
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: cli/node
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish mem0-cli 📦 to PyPI
|
||||||
|
if: startsWith(github.event.release.tag_name, 'cli-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: cli/python
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install Hatch
|
||||||
|
run: pip install hatch
|
||||||
|
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: hatch build --clean
|
||||||
|
|
||||||
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
packages-dir: cli/python/dist/
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
name: CLI Python CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'cli/python/**'
|
||||||
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/python/**'
|
||||||
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install dev dependencies
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Lint with ruff
|
||||||
|
working-directory: cli/python
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
working-directory: cli/python
|
||||||
|
run: ruff format --check .
|
||||||
|
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- name: Install dev dependencies
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: cli/python
|
||||||
|
run: pytest
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install Hatch
|
||||||
|
run: pip install hatch
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: cli/python
|
||||||
|
run: hatch build --clean
|
||||||
|
|
||||||
|
- name: Verify dist output
|
||||||
|
run: |
|
||||||
|
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
|
||||||
|
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Publish @mem0/openclaw-mem0 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: openclaw
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Upgrade npm for OIDC trusted publishing
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish --provenance --access public
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Publish mem0ai 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish mem0ai 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: mem0-ts
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Upgrade npm for OIDC trusted publishing
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish --provenance --access public
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: vercel-ai-sdk
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Upgrade npm for OIDC trusted publishing
|
||||||
|
run: npm install -g npm@latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: npm publish --provenance --access public
|
||||||
@@ -61,3 +61,31 @@ make test # After activating a shell with hatch shell test_XX
|
|||||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||||
|
|
||||||
We look forward to your pull requests and can't wait to see your contributions!
|
We look forward to your pull requests and can't wait to see your contributions!
|
||||||
|
|
||||||
|
### 🚀 Releasing
|
||||||
|
|
||||||
|
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||||
|
|
||||||
|
#### Tag Prefixes
|
||||||
|
|
||||||
|
| Package | Registry | Tag Prefix | Example |
|
||||||
|
|---------|----------|------------|---------|
|
||||||
|
| `mem0ai` (Python SDK) | PyPI | `v*` | `v0.1.31` |
|
||||||
|
| `mem0-cli` (Python CLI) | PyPI | `cli-v*` | `cli-v0.2.1` |
|
||||||
|
| `mem0ai` (TypeScript SDK) | npm | `ts-v*` | `ts-v2.4.6` |
|
||||||
|
| `@mem0/cli` (Node CLI) | npm | `cli-node-v*` | `cli-node-v0.1.2` |
|
||||||
|
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||||
|
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||||
|
|
||||||
|
#### How to Release
|
||||||
|
|
||||||
|
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||||
|
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||||
|
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||||
|
|
||||||
|
#### Publishing Details
|
||||||
|
|
||||||
|
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||||
|
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||||
|
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||||
|
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ Install sdk via npm:
|
|||||||
npm install mem0ai
|
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
|
### Basic Usage
|
||||||
|
|
||||||
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
||||||
@@ -148,7 +162,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
|
|||||||
## 📚 Documentation & Support
|
## 📚 Documentation & Support
|
||||||
|
|
||||||
- Full docs: https://docs.mem0.ai
|
- Full docs: https://docs.mem0.ai
|
||||||
- Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
|
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
|
||||||
- Contact: founders@mem0.ai
|
- Contact: founders@mem0.ai
|
||||||
|
|
||||||
## Citation
|
## Citation
|
||||||
@@ -166,4 +180,4 @@ We now have a paper you can cite:
|
|||||||
|
|
||||||
## ⚖️ License
|
## ⚖️ License
|
||||||
|
|
||||||
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
|
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @mem0/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mem0-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Both packages install a `mem0` binary with identical behavior.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email (get a new API key)
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `mem0 init` | Setup wizard — login via email or configure API key manually |
|
||||||
|
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
|
||||||
|
| `mem0 search` | Search memories using natural language |
|
||||||
|
| `mem0 list` | List memories with optional filters and pagination |
|
||||||
|
| `mem0 get` | Retrieve a specific memory by ID |
|
||||||
|
| `mem0 update` | Update the text or metadata of a memory |
|
||||||
|
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
||||||
|
| `mem0 import` | Bulk import memories from a JSON file |
|
||||||
|
| `mem0 config` | View or modify CLI configuration |
|
||||||
|
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||||
|
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||||
|
| `mem0 status` | Verify API connection and display current project |
|
||||||
|
| `mem0 version` | Print the CLI version |
|
||||||
|
|
||||||
|
Run `mem0 <command> --help` for detailed usage on any command.
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
| Language | Directory | Package | Docs |
|
||||||
|
|----------|-----------|---------|------|
|
||||||
|
| TypeScript | [`node/`](./node/) | `@mem0/cli` | [README](./node/README.md) |
|
||||||
|
| Python | [`python/`](./python/) | `mem0-cli` | [README](./python/README.md) |
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
{
|
||||||
|
"specVersion": 1,
|
||||||
|
"cli": {
|
||||||
|
"name": "mem0",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "The Memory Layer for AI Agents",
|
||||||
|
"helpText": "\u25c6 mem0 CLI \u2014 The Memory Layer for AI Agents"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"logoMini": "\u25c6 mem0",
|
||||||
|
"tagline": "The Memory Layer for AI Agents",
|
||||||
|
"colors": {
|
||||||
|
"brand": "#8b5cf6",
|
||||||
|
"accent": "#a78bfa",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"error": "#ef4444",
|
||||||
|
"warning": "#f59e0b",
|
||||||
|
"dim": "#6b7280"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"success": "\u2713",
|
||||||
|
"error": "\u2717",
|
||||||
|
"warning": "\u26a0",
|
||||||
|
"info": "\u25c6",
|
||||||
|
"pending": "\u29d7",
|
||||||
|
"add": "+",
|
||||||
|
"update": "~",
|
||||||
|
"delete": "-",
|
||||||
|
"noop": "\u00b7",
|
||||||
|
"connected": "\u25cf",
|
||||||
|
"disconnected": "\u25cf"
|
||||||
|
},
|
||||||
|
"logo": "███\u2557 ███\u2557███████\u2557███\u2557 ███\u2557 ██████\u2557 ██████\u2557██\u2557 ██\u2557\n████\u2557 ████\u2551██\u2554\u2550\u2550\u2550\u2550\u255d████\u2557 ████\u2551██\u2554\u2550████\u2557 ██\u2554\u2550\u2550\u2550\u2550\u255d██\u2551 ██\u2551\n██\u2554████\u2554██\u2551█████\u2557 ██\u2554████\u2554██\u2551██\u2551██\u2554██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551\u255a██\u2554\u255d██\u2551██\u2554\u2550\u2550\u255d ██\u2551\u255a██\u2554\u255d██\u2551████\u2554\u255d██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551 \u255a\u2550\u255d ██\u2551███████\u2557██\u2551 \u255a\u2550\u255d ██\u2551\u255a██████\u2554\u255d \u255a██████\u2557███████\u2557██\u2551\n\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"configDir": "~/.mem0",
|
||||||
|
"configFile": "config.json",
|
||||||
|
"version": 1,
|
||||||
|
"defaultBaseUrl": "https://api.mem0.ai",
|
||||||
|
"sections": {
|
||||||
|
"platform": {
|
||||||
|
"fields": {
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_API_KEY",
|
||||||
|
"redact": true
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "https://api.mem0.ai",
|
||||||
|
"envVar": "MEM0_BASE_URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"fields": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_USER_ID"
|
||||||
|
},
|
||||||
|
"agent_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_AGENT_ID"
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_APP_ID"
|
||||||
|
},
|
||||||
|
"run_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_RUN_ID"
|
||||||
|
},
|
||||||
|
"enable_graph": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"envVar": "MEM0_ENABLE_GRAPH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"endpoints": {
|
||||||
|
"add": { "method": "POST", "path": "/v1/memories/" },
|
||||||
|
"search": { "method": "POST", "path": "/v2/memories/search/" },
|
||||||
|
"get": { "method": "GET", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"list": { "method": "POST", "path": "/v2/memories/" },
|
||||||
|
"update": { "method": "PUT", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"delete": { "method": "DELETE", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"deleteAll": { "method": "DELETE", "path": "/v1/memories/" },
|
||||||
|
"entities": { "method": "GET", "path": "/v1/entities/" },
|
||||||
|
"deleteEntities": { "method": "DELETE", "path": "/v1/entities/" }
|
||||||
|
},
|
||||||
|
"authHeader": "Authorization",
|
||||||
|
"authScheme": "Token",
|
||||||
|
"timeout": 30,
|
||||||
|
"entityTypeMap": {
|
||||||
|
"users": "user",
|
||||||
|
"agents": "agent",
|
||||||
|
"apps": "app",
|
||||||
|
"runs": "run"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"AuthError": {
|
||||||
|
"httpStatus": 401,
|
||||||
|
"message": "Authentication failed. Your API key may be invalid or expired."
|
||||||
|
},
|
||||||
|
"NotFoundError": {
|
||||||
|
"httpStatus": 404,
|
||||||
|
"messageTemplate": "Resource not found: {path}"
|
||||||
|
},
|
||||||
|
"APIError": {
|
||||||
|
"httpStatus": 400,
|
||||||
|
"messageTemplate": "Bad request to {path}: {detail}"
|
||||||
|
},
|
||||||
|
"noApiKey": {
|
||||||
|
"message": "No API key configured.",
|
||||||
|
"hint": "Run 'mem0 init' or set MEM0_API_KEY environment variable."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionGroups": {
|
||||||
|
"scope": {
|
||||||
|
"label": "Scope",
|
||||||
|
"options": ["user_id", "agent_id", "app_id", "run_id"]
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"label": "Search",
|
||||||
|
"options": ["top_k", "threshold", "rerank", "keyword", "filter_json", "fields", "graph", "no_graph"]
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"label": "Pagination",
|
||||||
|
"options": ["page", "page_size"]
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filters",
|
||||||
|
"options": ["category", "after", "before", "graph", "no_graph"]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"label": "Output",
|
||||||
|
"options": ["output"]
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"label": "Connection",
|
||||||
|
"options": ["api_key", "base_url"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globalOptions": [
|
||||||
|
{
|
||||||
|
"name": "api_key",
|
||||||
|
"flags": ["--api-key"],
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"envVar": "MEM0_API_KEY",
|
||||||
|
"help": "Override API key.",
|
||||||
|
"panel": "Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "base_url",
|
||||||
|
"flags": ["--base-url"],
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Override API base URL.",
|
||||||
|
"panel": "Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "version",
|
||||||
|
"flags": ["--version"],
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"help": "Show version and exit."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "add",
|
||||||
|
"description": "Add a memory from text, messages, file, or stdin.",
|
||||||
|
"usage": "mem0 add <text> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Text content to add as a memory."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||||
|
{ "name": "messages", "flags": ["--messages"], "type": "string", "help": "Conversation messages as JSON." },
|
||||||
|
{ "name": "file", "flags": ["--file", "-f"], "type": "path", "help": "Read messages from JSON file." },
|
||||||
|
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Custom metadata as JSON." },
|
||||||
|
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
|
||||||
|
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
|
||||||
|
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||||
|
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"description": "Search memories by semantic query.",
|
||||||
|
"usage": "mem0 search <query> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "table"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Search query."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||||
|
{ "name": "top_k", "flags": ["--top-k", "-k", "--limit"], "type": "integer", "default": 10, "help": "Number of results.", "panel": "Search" },
|
||||||
|
{ "name": "threshold", "flags": ["--threshold"], "type": "float", "default": 0.3, "help": "Minimum similarity score.", "panel": "Search" },
|
||||||
|
{ "name": "rerank", "flags": ["--rerank"], "type": "boolean", "default": false, "help": "Enable reranking (Platform only).", "panel": "Search" },
|
||||||
|
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
|
||||||
|
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
|
||||||
|
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get",
|
||||||
|
"description": "Get a specific memory by ID.",
|
||||||
|
"usage": "mem0 get <memory_id> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Memory ID to retrieve."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "get"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "List memories with optional filters.",
|
||||||
|
"usage": "mem0 list [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "table"],
|
||||||
|
"defaultOutput": "table",
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||||
|
{ "name": "page", "flags": ["--page"], "type": "integer", "default": 1, "help": "Page number.", "panel": "Pagination" },
|
||||||
|
{ "name": "page_size", "flags": ["--page-size"], "type": "integer", "default": 100, "help": "Results per page.", "panel": "Pagination" },
|
||||||
|
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
|
||||||
|
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
|
||||||
|
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"description": "Update a memory's text or metadata.",
|
||||||
|
"usage": "mem0 update <memory_id> [text] [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Memory ID to update."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "New memory text."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete",
|
||||||
|
"description": "Delete a memory, all memories matching a scope, or an entity.",
|
||||||
|
"usage": "mem0 delete [memory_id] [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": true,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Memory ID to delete (omit when using --all or --entity)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
|
||||||
|
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
|
||||||
|
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
|
||||||
|
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "delete",
|
||||||
|
"notes": "Mutually exclusive modes: (1) mem0 delete <id> -- single memory, (2) mem0 delete --all [scope] -- bulk delete, (3) mem0 delete --entity [scope] -- entity cascade delete. Cannot combine <memoryId> with --all or --entity, and cannot combine --all with --entity."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "import",
|
||||||
|
"description": "Import memories from a JSON file.",
|
||||||
|
"usage": "mem0 import <file_path> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "JSON file to import."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Override user ID.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Override agent ID.", "panel": "Scope" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "config",
|
||||||
|
"description": "Manage mem0 configuration.",
|
||||||
|
"isGroup": true,
|
||||||
|
"subcommands": [
|
||||||
|
{
|
||||||
|
"name": "show",
|
||||||
|
"description": "Display current configuration (secrets redacted).",
|
||||||
|
"usage": "mem0 config show",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get",
|
||||||
|
"description": "Get a configuration value.",
|
||||||
|
"usage": "mem0 config get <key>",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Config key (e.g. platform.api_key)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "set",
|
||||||
|
"description": "Set a configuration value.",
|
||||||
|
"usage": "mem0 config set <key> <value>",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Config key (e.g. platform.api_key)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Value to set."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entity",
|
||||||
|
"description": "Manage entities.",
|
||||||
|
"isGroup": true,
|
||||||
|
"subcommands": [
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "List all entities of a given type.",
|
||||||
|
"usage": "mem0 entity list <entity_type>",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["table", "json"],
|
||||||
|
"defaultOutput": "table",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "entity_type",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Entity type: users, agents, apps, runs.",
|
||||||
|
"choices": ["users", "agents", "apps", "runs"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: table, json.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "entities"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete",
|
||||||
|
"description": "Delete an entity and ALL its memories (cascade).",
|
||||||
|
"usage": "mem0 entity delete [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": true,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "User ID.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Agent ID.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "App ID.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Run ID.", "panel": "Scope" },
|
||||||
|
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "deleteEntities"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "init",
|
||||||
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
|
"usage": "mem0 init [OPTIONS]",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
|
||||||
|
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||||
|
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
||||||
|
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"description": "Check connectivity and authentication.",
|
||||||
|
"usage": "mem0 status [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "help",
|
||||||
|
"description": "Show help. Use --json for machine-readable output (for LLM agents).",
|
||||||
|
"usage": "mem0 help [OPTIONS]",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "json", "flags": ["--json"], "type": "boolean", "default": false, "help": "Output machine-readable JSON for LLM agents." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# mem0 CLI (Node.js)
|
||||||
|
|
||||||
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js **18+**
|
||||||
|
- pnpm (`npm install -g pnpm`) — for development only
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @mem0/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js **18+**
|
||||||
|
- pnpm (`npm install -g pnpm`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
From the `node/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the CLI
|
||||||
|
|
||||||
|
There are two ways to run the CLI during development:
|
||||||
|
|
||||||
|
### Option 1: Development mode (no build needed)
|
||||||
|
|
||||||
|
Uses `tsx` to run TypeScript directly. Pass CLI arguments after `pnpm dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev --help
|
||||||
|
pnpm dev version
|
||||||
|
pnpm dev add "test memory" --user-id alice
|
||||||
|
pnpm dev search "test" --user-id alice
|
||||||
|
pnpm dev config show
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Do NOT use `pnpm dev -- --help`. With pnpm, arguments pass through directly — adding `--` inserts a literal `--` that breaks the CLI parser.
|
||||||
|
|
||||||
|
### Option 2: Build and run compiled JS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build first
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Run the compiled CLI
|
||||||
|
node dist/index.js --help
|
||||||
|
node dist/index.js version
|
||||||
|
node dist/index.js add "test memory" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Link globally (makes `mem0` available system-wide)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
pnpm link --global
|
||||||
|
|
||||||
|
# Now use it like a normal CLI
|
||||||
|
mem0 --help
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled output is in `dist/`.
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
pnpm test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Auto-fix
|
||||||
|
pnpm lint:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
```
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@mem0/cli",
|
||||||
|
"version": "0.2.1",
|
||||||
|
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mem0": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "biome check src/",
|
||||||
|
"lint:fix": "biome check --write src/",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"author": "mem0.ai <founders@mem0.ai>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mem0ai/mem0",
|
||||||
|
"directory": "cli/node"
|
||||||
|
},
|
||||||
|
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.0.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-table3": "^0.6.4",
|
||||||
|
"ora": "^8.0.0",
|
||||||
|
"boxen": "^7.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"vitest": "^1.5.0",
|
||||||
|
"@biomejs/biome": "^1.7.0",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* 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>[]>;
|
||||||
|
|
||||||
|
listEvents(): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
|
getEvent(eventId: string): Promise<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthError extends Error {
|
||||||
|
constructor(
|
||||||
|
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
constructor(path: string) {
|
||||||
|
super(`Resource not found: ${path}`);
|
||||||
|
this.name = "NotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(path: string, detail: string) {
|
||||||
|
super(`Bad request to ${path}: ${detail}`);
|
||||||
|
this.name = "APIError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackend(config: Mem0Config): Backend {
|
||||||
|
return new PlatformBackend(config.platform);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Backend factory re-export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { getBackend } from "./base.js";
|
||||||
|
export type {
|
||||||
|
Backend,
|
||||||
|
AddOptions,
|
||||||
|
SearchOptions,
|
||||||
|
ListOptions,
|
||||||
|
DeleteOptions,
|
||||||
|
EntityIds,
|
||||||
|
} from "./base.js";
|
||||||
|
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
/**
|
||||||
|
* Platform (SaaS) backend — communicates with api.mem0.ai.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PlatformConfig } from "../config.js";
|
||||||
|
import {
|
||||||
|
APIError,
|
||||||
|
type AddOptions,
|
||||||
|
AuthError,
|
||||||
|
type Backend,
|
||||||
|
type DeleteOptions,
|
||||||
|
type EntityIds,
|
||||||
|
type ListOptions,
|
||||||
|
NotFoundError,
|
||||||
|
type SearchOptions,
|
||||||
|
} from "./base.js";
|
||||||
|
|
||||||
|
export class PlatformBackend implements Backend {
|
||||||
|
private baseUrl: string;
|
||||||
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(config: PlatformConfig) {
|
||||||
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||||
|
this.headers = {
|
||||||
|
Authorization: `Token ${config.apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
opts?: { json?: unknown; params?: Record<string, string> },
|
||||||
|
): Promise<unknown> {
|
||||||
|
let url = `${this.baseUrl}${path}`;
|
||||||
|
if (opts?.params) {
|
||||||
|
const qs = new URLSearchParams(opts.params).toString();
|
||||||
|
url += `?${qs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOpts: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: this.headers,
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
};
|
||||||
|
if (opts?.json) {
|
||||||
|
fetchOpts.body = JSON.stringify(opts.json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, fetchOpts);
|
||||||
|
|
||||||
|
if (resp.status === 401) {
|
||||||
|
throw new AuthError();
|
||||||
|
}
|
||||||
|
if (resp.status === 404) {
|
||||||
|
throw new NotFoundError(path);
|
||||||
|
}
|
||||||
|
if (resp.status === 400) {
|
||||||
|
let detail: string;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail =
|
||||||
|
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
|
||||||
|
resp.statusText;
|
||||||
|
} catch {
|
||||||
|
detail = resp.statusText;
|
||||||
|
}
|
||||||
|
throw new APIError(path, detail);
|
||||||
|
}
|
||||||
|
if (!resp.ok) {
|
||||||
|
let detail: string = resp.statusText;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
||||||
|
}
|
||||||
|
if (resp.status === 204) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(
|
||||||
|
content?: string,
|
||||||
|
messages?: Record<string, unknown>[],
|
||||||
|
opts: AddOptions = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (messages) {
|
||||||
|
payload.messages = messages;
|
||||||
|
} else if (content) {
|
||||||
|
payload.messages = [{ role: "user", content }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.userId) payload.user_id = opts.userId;
|
||||||
|
if (opts.agentId) payload.agent_id = opts.agentId;
|
||||||
|
if (opts.appId) payload.app_id = opts.appId;
|
||||||
|
if (opts.runId) payload.run_id = opts.runId;
|
||||||
|
if (opts.metadata) payload.metadata = opts.metadata;
|
||||||
|
if (opts.immutable) payload.immutable = true;
|
||||||
|
if (opts.infer === false) payload.infer = false;
|
||||||
|
if (opts.expires) payload.expiration_date = opts.expires;
|
||||||
|
if (opts.categories) payload.categories = opts.categories;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
|
return (await this._request("POST", "/v1/memories/", {
|
||||||
|
json: payload,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildFilters(opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
extraFilters?: Record<string, unknown>;
|
||||||
|
}): Record<string, unknown> | undefined {
|
||||||
|
// If caller passed a pre-built filter structure, use it directly
|
||||||
|
if (
|
||||||
|
opts.extraFilters &&
|
||||||
|
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
|
||||||
|
) {
|
||||||
|
return opts.extraFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const andConditions: Record<string, unknown>[] = [];
|
||||||
|
if (opts.userId) andConditions.push({ user_id: opts.userId });
|
||||||
|
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
|
||||||
|
if (opts.appId) andConditions.push({ app_id: opts.appId });
|
||||||
|
if (opts.runId) andConditions.push({ run_id: opts.runId });
|
||||||
|
|
||||||
|
if (opts.extraFilters) {
|
||||||
|
for (const [k, v] of Object.entries(opts.extraFilters)) {
|
||||||
|
andConditions.push({ [k]: v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (andConditions.length === 1) return andConditions[0];
|
||||||
|
if (andConditions.length > 1) return { AND: andConditions };
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
opts: SearchOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
query,
|
||||||
|
top_k: opts.topK ?? 10,
|
||||||
|
threshold: opts.threshold ?? 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiFilters = this._buildFilters({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
extraFilters: opts.filters,
|
||||||
|
});
|
||||||
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
|
if (opts.rerank) payload.rerank = true;
|
||||||
|
if (opts.keyword) payload.keyword_search = true;
|
||||||
|
if (opts.fields) payload.fields = opts.fields;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
|
const result = (await this._request("POST", "/v2/memories/search/", {
|
||||||
|
json: payload,
|
||||||
|
})) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
const obj = result as Record<string, unknown>;
|
||||||
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||||
|
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMemories(
|
||||||
|
opts: ListOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
page: String(opts.page ?? 1),
|
||||||
|
page_size: String(opts.pageSize ?? 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
const extra: Record<string, unknown> = {};
|
||||||
|
if (opts.category) {
|
||||||
|
extra.categories = { contains: opts.category };
|
||||||
|
}
|
||||||
|
if (opts.after) {
|
||||||
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
gte: opts.after,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.before) {
|
||||||
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
lte: opts.before,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiFilters = this._buildFilters({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
||||||
|
});
|
||||||
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
|
||||||
|
const result = (await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params,
|
||||||
|
})) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
const obj = result as Record<string, unknown>;
|
||||||
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
memoryId: string,
|
||||||
|
content?: string,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
if (content) payload.text = content;
|
||||||
|
if (metadata) payload.metadata = metadata;
|
||||||
|
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||||
|
json: payload,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts: DeleteOptions = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (opts.all) {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (opts.userId) params.user_id = opts.userId;
|
||||||
|
if (opts.agentId) params.agent_id = opts.agentId;
|
||||||
|
if (opts.appId) params.app_id = opts.appId;
|
||||||
|
if (opts.runId) params.run_id = opts.runId;
|
||||||
|
return (await this._request("DELETE", "/v1/memories/", {
|
||||||
|
params,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
if (memoryId) {
|
||||||
|
return (await this._request(
|
||||||
|
"DELETE",
|
||||||
|
`/v1/memories/${memoryId}/`,
|
||||||
|
)) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
throw new Error("Either memoryId or --all is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
|
||||||
|
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
|
const typeMap: [string, string | undefined][] = [
|
||||||
|
["user", opts.userId],
|
||||||
|
["agent", opts.agentId],
|
||||||
|
["app", opts.appId],
|
||||||
|
["run", opts.runId],
|
||||||
|
];
|
||||||
|
const entities = typeMap.filter(([, v]) => v) as [string, string][];
|
||||||
|
if (entities.length === 0) {
|
||||||
|
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||||
|
}
|
||||||
|
// Delete each provided entity via the v2 path-based endpoint
|
||||||
|
let result: Record<string, unknown> = {};
|
||||||
|
for (const [entityType, entityId] of entities) {
|
||||||
|
result = (await this._request(
|
||||||
|
"DELETE",
|
||||||
|
`/v2/entities/${entityType}/${entityId}/`,
|
||||||
|
)) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async status(
|
||||||
|
opts: { userId?: string; agentId?: string } = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
try {
|
||||||
|
await this._request("GET", "/v1/ping/");
|
||||||
|
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
backend: "platform",
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async entities(entityType: string): Promise<Record<string, unknown>[]> {
|
||||||
|
const result = (await this._request("GET", "/v1/entities/")) as unknown;
|
||||||
|
let items: Record<string, unknown>[];
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
items = result;
|
||||||
|
} else {
|
||||||
|
items = ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
users: "user",
|
||||||
|
agents: "agent",
|
||||||
|
apps: "app",
|
||||||
|
runs: "run",
|
||||||
|
};
|
||||||
|
const targetType = typeMap[entityType];
|
||||||
|
if (targetType) {
|
||||||
|
items = items.filter(
|
||||||
|
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listEvents(): Promise<Record<string, unknown>[]> {
|
||||||
|
const result = (await this._request("GET", "/v1/events/")) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
return ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||||
|
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* Branding and ASCII art for mem0 CLI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from "chalk";
|
||||||
|
import ora, { type Ora } from "ora";
|
||||||
|
import { getCurrentCommand, isAgentMode } from "./state.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
|
export const LOGO = `
|
||||||
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
|
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
|
||||||
|
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
|
||||||
|
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
|
||||||
|
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
|
||||||
|
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LOGO_MINI = "◆ mem0";
|
||||||
|
export const TAGLINE = "The Memory Layer for AI Agents";
|
||||||
|
|
||||||
|
export const BRAND_COLOR = "#8b5cf6";
|
||||||
|
export const ACCENT_COLOR = "#a78bfa";
|
||||||
|
export const SUCCESS_COLOR = "#22c55e";
|
||||||
|
export const ERROR_COLOR = "#ef4444";
|
||||||
|
export const WARNING_COLOR = "#f59e0b";
|
||||||
|
export const DIM_COLOR = "#6b7280";
|
||||||
|
|
||||||
|
const brand = chalk.hex(BRAND_COLOR);
|
||||||
|
const accent = chalk.hex(ACCENT_COLOR);
|
||||||
|
const success = chalk.hex(SUCCESS_COLOR);
|
||||||
|
const error = chalk.hex(ERROR_COLOR);
|
||||||
|
const warning = chalk.hex(WARNING_COLOR);
|
||||||
|
const dim = chalk.hex(DIM_COLOR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose a symbol based on TTY/NO_COLOR. Fancy for interactive terminals,
|
||||||
|
* plain-text for piped/non-TTY or NO_COLOR environments.
|
||||||
|
*/
|
||||||
|
export function sym(fancy: string, plain: string): string {
|
||||||
|
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
|
||||||
|
return fancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printBanner(): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||||
|
const logoLines = LOGO.trimEnd().split("\n");
|
||||||
|
const tagline = ` ${TAGLINE}`;
|
||||||
|
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||||
|
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||||
|
|
||||||
|
// Compute inner width from longest content line + padding both sides
|
||||||
|
const maxContent = Math.max(...contentLines.map((l) => l.length));
|
||||||
|
const innerWidth = maxContent + pad * 2;
|
||||||
|
const totalWidth = innerWidth + 2; // + 2 for │ borders
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printError(message: string, hint?: string): void {
|
||||||
|
if (isAgentMode()) {
|
||||||
|
const envelope = {
|
||||||
|
status: "error",
|
||||||
|
command: getCurrentCommand(),
|
||||||
|
error: message,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(envelope));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
|
||||||
|
const resolvedHint =
|
||||||
|
hint ??
|
||||||
|
(message.includes("Authentication failed")
|
||||||
|
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys`
|
||||||
|
: undefined);
|
||||||
|
if (resolvedHint) {
|
||||||
|
console.error(` ${dim(resolvedHint)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printWarning(message: string): void {
|
||||||
|
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printInfo(message: string): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printScope(ids: Record<string, string | undefined>): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [key, val] of Object.entries(ids)) {
|
||||||
|
if (val) {
|
||||||
|
parts.push(`${key}=${val}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parts.length > 0) {
|
||||||
|
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimedStatusContext {
|
||||||
|
successMsg: string;
|
||||||
|
errorMsg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> {
|
||||||
|
if (isAgentMode()) {
|
||||||
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
|
return fn(ctx);
|
||||||
|
}
|
||||||
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
|
const spinner = ora({
|
||||||
|
text: dim(message),
|
||||||
|
color: "yellow",
|
||||||
|
stream: process.stderr,
|
||||||
|
}).start();
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fn(ctx);
|
||||||
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
|
spinner.stop();
|
||||||
|
if (ctx.successMsg) {
|
||||||
|
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
|
spinner.stop();
|
||||||
|
if (ctx.errorMsg) {
|
||||||
|
printError(`${ctx.errorMsg} (${elapsed}s)`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format helpers using brand colors for external use. */
|
||||||
|
export const colors = { brand, accent, success, error, warning, dim };
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Config management commands: show, set, get.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import { colors, printError, printSuccess } from "../branding.js";
|
||||||
|
import {
|
||||||
|
getNestedValue,
|
||||||
|
loadConfig,
|
||||||
|
redactKey,
|
||||||
|
saveConfig,
|
||||||
|
setNestedValue,
|
||||||
|
} from "../config.js";
|
||||||
|
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||||
|
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, dim } = colors;
|
||||||
|
|
||||||
|
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||||
|
setCurrentCommand("config show");
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config show",
|
||||||
|
data: {
|
||||||
|
defaults: {
|
||||||
|
user_id: config.defaults.userId || null,
|
||||||
|
agent_id: config.defaults.agentId || null,
|
||||||
|
app_id: config.defaults.appId || null,
|
||||||
|
run_id: config.defaults.runId || null,
|
||||||
|
enable_graph: config.defaults.enableGraph,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
api_key: redactKey(config.platform.apiKey),
|
||||||
|
base_url: config.platform.baseUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(` ${brand("◆ mem0 Configuration")}\n`);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
setCurrentCommand("config get");
|
||||||
|
const config = loadConfig();
|
||||||
|
const value = getNestedValue(config, key);
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
printError(`Unknown config key: ${key}`);
|
||||||
|
} else {
|
||||||
|
// Redact secrets
|
||||||
|
const displayValue =
|
||||||
|
key.includes("api_key") || key.split(".").pop() === "key"
|
||||||
|
? redactKey(String(value))
|
||||||
|
: String(value);
|
||||||
|
if (isAgentMode()) {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config get",
|
||||||
|
data: { key, value: displayValue },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(displayValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdConfigSet(key: string, value: string): void {
|
||||||
|
setCurrentCommand("config set");
|
||||||
|
const config = loadConfig();
|
||||||
|
if (setNestedValue(config, key, value)) {
|
||||||
|
saveConfig(config);
|
||||||
|
const display = key.includes("key") ? redactKey(value) : value;
|
||||||
|
if (isAgentMode()) {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config set",
|
||||||
|
data: { key, value: display },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
printSuccess(`${key} = ${display}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printError(`Unknown config key: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Entity management commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import readline from "node:readline";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, dim } = colors;
|
||||||
|
|
||||||
|
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
|
||||||
|
|
||||||
|
export async function cmdEntitiesList(
|
||||||
|
backend: Backend,
|
||||||
|
entityType: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("entity list");
|
||||||
|
if (!VALID_TYPES.has(entityType)) {
|
||||||
|
printError(
|
||||||
|
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus(`Fetching ${entityType}...`, async () => {
|
||||||
|
return backend.entities(entityType);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
"This feature may require the mem0 Platform.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "entity list",
|
||||||
|
data: results,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
printInfo(`No ${entityType} found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
setCurrentCommand("entity delete");
|
||||||
|
const { isAgentMode } = await import("../state.js");
|
||||||
|
if (isAgentMode() && !opts.force) {
|
||||||
|
printError("Destructive operation requires --force in agent mode.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||||
|
printError(
|
||||||
|
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeParts: string[] = [];
|
||||||
|
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||||
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
|
const scope = scopeParts.join(", ");
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
printInfo(`Would delete entity ${scope} and all its memories.`);
|
||||||
|
printInfo("No changes made.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.force) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Deleting entity...", async () => {
|
||||||
|
return backend.deleteEntities({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "entity delete",
|
||||||
|
data: { deleted: true },
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Event commands: list and status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printInfo, timedStatus } from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, success, error: errorColor, warning, dim } = colors;
|
||||||
|
|
||||||
|
function statusStyled(status: string): string {
|
||||||
|
switch (status.toUpperCase()) {
|
||||||
|
case "SUCCEEDED":
|
||||||
|
return success("SUCCEEDED");
|
||||||
|
case "PENDING":
|
||||||
|
return accent("PENDING");
|
||||||
|
case "FAILED":
|
||||||
|
return errorColor("FAILED");
|
||||||
|
case "PROCESSING":
|
||||||
|
return warning("PROCESSING");
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdEventList(
|
||||||
|
backend: Backend,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("event list");
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Fetching events...", async () => {
|
||||||
|
return backend.listEvents();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "event list",
|
||||||
|
data: results,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
console.log();
|
||||||
|
printInfo("No events found.");
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: [
|
||||||
|
accent("Event ID"),
|
||||||
|
accent("Type"),
|
||||||
|
accent("Status"),
|
||||||
|
accent("Latency"),
|
||||||
|
accent("Created"),
|
||||||
|
],
|
||||||
|
colWidths: [12, 14, 14, 10, 22],
|
||||||
|
wordWrap: true,
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const ev of results) {
|
||||||
|
const evId = String(ev.id ?? "").slice(0, 8);
|
||||||
|
const evType = String(ev.event_type ?? "—");
|
||||||
|
const status = String(ev.status ?? "—");
|
||||||
|
const latency = ev.latency as number | undefined;
|
||||||
|
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||||
|
const created = String(ev.created_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log(
|
||||||
|
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdEventStatus(
|
||||||
|
backend: Backend,
|
||||||
|
eventId: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("event status");
|
||||||
|
const start = performance.now();
|
||||||
|
let ev: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
ev = await timedStatus("Fetching event...", async () => {
|
||||||
|
return backend.getEvent(eventId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "event status",
|
||||||
|
data: ev,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = String(ev.status ?? "—");
|
||||||
|
const evType = String(ev.event_type ?? "—");
|
||||||
|
const latency = ev.latency as number | undefined;
|
||||||
|
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||||
|
const created = String(ev.created_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
const updated = String(ev.updated_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(` ${dim("Event ID:")} ${eventId}`);
|
||||||
|
lines.push(` ${dim("Type:")} ${evType}`);
|
||||||
|
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
|
||||||
|
lines.push(` ${dim("Latency:")} ${latencyStr}`);
|
||||||
|
lines.push(` ${dim("Created:")} ${created}`);
|
||||||
|
lines.push(` ${dim("Updated:")} ${updated}`);
|
||||||
|
|
||||||
|
if (results && results.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(` ${dim(`Results (${results.length}):`)}`);
|
||||||
|
for (const r of results) {
|
||||||
|
const memId = String(r.id ?? "").slice(0, 8);
|
||||||
|
const data = r.data as Record<string, unknown> | undefined;
|
||||||
|
const memory = data?.memory ? String(data.memory) : "";
|
||||||
|
const evName = String(r.event ?? "");
|
||||||
|
const user = String(r.user_id ?? "");
|
||||||
|
let detail = `${evName} ${memory}`;
|
||||||
|
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
|
||||||
|
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Event Status"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
/**
|
||||||
|
* mem0 init — interactive setup wizard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import readline from "node:readline";
|
||||||
|
import { PlatformBackend } from "../backend/platform.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printBanner,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
} from "../branding.js";
|
||||||
|
import {
|
||||||
|
CONFIG_FILE,
|
||||||
|
DEFAULT_BASE_URL,
|
||||||
|
type Mem0Config,
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
redactKey,
|
||||||
|
saveConfig,
|
||||||
|
} from "../config.js";
|
||||||
|
|
||||||
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||||
|
|
||||||
|
function validateEmail(email: string): void {
|
||||||
|
if (!EMAIL_RE.test(email)) {
|
||||||
|
printError(`Invalid email address: ${JSON.stringify(email)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function emailLogin(
|
||||||
|
email: string,
|
||||||
|
code: string | undefined,
|
||||||
|
baseUrl: string,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const url = baseUrl.replace(/\/+$/, "");
|
||||||
|
let codeValue = code;
|
||||||
|
|
||||||
|
if (!codeValue) {
|
||||||
|
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
if (resp.status === 429) {
|
||||||
|
printError("Too many attempts. Try again in a few minutes.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!resp.ok) {
|
||||||
|
let detail: string;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail = (body.error ?? body.detail ?? resp.statusText) as string;
|
||||||
|
} catch {
|
||||||
|
detail = resp.statusText;
|
||||||
|
}
|
||||||
|
printError(`Failed to send code: ${detail}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
printSuccess("Verification code sent! Check your email.");
|
||||||
|
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
|
printError(
|
||||||
|
"No --code provided and terminal is non-interactive.",
|
||||||
|
"Run: mem0 init --email <email> --code <code>",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
const entered = await promptLine(` ${brand("Verification Code")}`);
|
||||||
|
if (!entered) {
|
||||||
|
printError("Code is required.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
codeValue = entered;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, code: codeValue.trim() }),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
});
|
||||||
|
if (verifyResp.status === 429) {
|
||||||
|
printError("Too many attempts. Try again in a few minutes.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!verifyResp.ok) {
|
||||||
|
let detail: string;
|
||||||
|
try {
|
||||||
|
const body = (await verifyResp.json()) as Record<string, unknown>;
|
||||||
|
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
|
||||||
|
} catch {
|
||||||
|
detail = verifyResp.statusText;
|
||||||
|
}
|
||||||
|
printError(`Verification failed: ${detail}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyResp.json() as Promise<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptSecret(label: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
process.stdout.write(label);
|
||||||
|
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
}
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.setEncoding("utf-8");
|
||||||
|
|
||||||
|
const chars: string[] = [];
|
||||||
|
|
||||||
|
const onData = (key: string) => {
|
||||||
|
for (const ch of key) {
|
||||||
|
if (ch === "\r" || ch === "\n") {
|
||||||
|
cleanup();
|
||||||
|
process.stdout.write("\n");
|
||||||
|
resolve(chars.join(""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ch === "\x03") {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("Interrupted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ch === "\x7f" || ch === "\x08") {
|
||||||
|
// backspace
|
||||||
|
if (chars.length > 0) {
|
||||||
|
chars.pop();
|
||||||
|
process.stdout.write("\b \b");
|
||||||
|
}
|
||||||
|
} else if (ch === "\x15") {
|
||||||
|
// Ctrl+U — clear line
|
||||||
|
process.stdout.write("\b \b".repeat(chars.length));
|
||||||
|
chars.length = 0;
|
||||||
|
} else if (ch >= " ") {
|
||||||
|
chars.push(ch);
|
||||||
|
process.stdout.write("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.removeListener("data", onData);
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(false);
|
||||||
|
}
|
||||||
|
process.stdin.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.stdin.on("data", onData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(prompt, (answer) => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer.trim() || defaultValue || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
process.stdout.write(` ${brand("API Key")}: `);
|
||||||
|
const apiKey = await promptSecret("");
|
||||||
|
if (!apiKey) {
|
||||||
|
printError("API key is required.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.platform.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||||
|
|
||||||
|
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
const userId = await promptLine(
|
||||||
|
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||||
|
_systemUser,
|
||||||
|
);
|
||||||
|
if (userId) config.defaults.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatePlatform(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
printInfo("Validating connection...");
|
||||||
|
try {
|
||||||
|
const backend = new PlatformBackend(config.platform);
|
||||||
|
const status = await backend.status({
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
});
|
||||||
|
if (status.connected) {
|
||||||
|
printSuccess("Connected to mem0 Platform!");
|
||||||
|
} else {
|
||||||
|
printError(
|
||||||
|
`Could not connect: ${status.error ?? "Unknown error"}`,
|
||||||
|
"Visit https://app.mem0.ai/dashboard/api-keys to get a new key, or run mem0 init again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runInit(
|
||||||
|
opts: {
|
||||||
|
apiKey?: string;
|
||||||
|
userId?: string;
|
||||||
|
email?: string;
|
||||||
|
code?: string;
|
||||||
|
force?: boolean;
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
const savedConfig = loadConfig();
|
||||||
|
const baseUrl =
|
||||||
|
process.env.MEM0_BASE_URL ||
|
||||||
|
savedConfig.platform.baseUrl ||
|
||||||
|
DEFAULT_BASE_URL;
|
||||||
|
|
||||||
|
// Guards
|
||||||
|
if (opts.code && !opts.email) {
|
||||||
|
printError("--code requires --email.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.email && opts.apiKey) {
|
||||||
|
printError("Cannot use both --api-key and --email.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if an existing config with an API key would be overwritten
|
||||||
|
if (
|
||||||
|
!opts.force &&
|
||||||
|
fs.existsSync(CONFIG_FILE) &&
|
||||||
|
savedConfig.platform.apiKey
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
|
||||||
|
);
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
" Overwrite existing config? This cannot be undone. [y/N] ",
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled. Use --force to skip this check.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printError(
|
||||||
|
"Existing config would be overwritten.",
|
||||||
|
"Use --force to overwrite.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Email login flow ──────────────────────────────────────────────────────
|
||||||
|
if (opts.email) {
|
||||||
|
const email = opts.email.trim().toLowerCase();
|
||||||
|
validateEmail(email);
|
||||||
|
|
||||||
|
printBanner();
|
||||||
|
console.log();
|
||||||
|
printInfo(`Logging in as ${email}...\n`);
|
||||||
|
|
||||||
|
const result = await emailLogin(email, opts.code, baseUrl);
|
||||||
|
|
||||||
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
|
if (!apiKeyVal) {
|
||||||
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.platform.apiKey = apiKeyVal;
|
||||||
|
config.platform.baseUrl = baseUrl;
|
||||||
|
config.defaults.userId =
|
||||||
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
|
||||||
|
saveConfig(config);
|
||||||
|
console.log();
|
||||||
|
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
|
||||||
|
console.log();
|
||||||
|
console.log(` ${dim("Get started:")}`);
|
||||||
|
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||||
|
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API key flow ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||||
|
if (!process.stdin.isTTY) {
|
||||||
|
if (!opts.apiKey) {
|
||||||
|
printError(
|
||||||
|
"Non-interactive terminal detected and --api-key is required.",
|
||||||
|
"Usage: mem0 init --api-key <key> [--user-id <id>]",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
opts.userId =
|
||||||
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-interactive: both flags provided
|
||||||
|
if (opts.apiKey && opts.userId) {
|
||||||
|
config.platform.apiKey = opts.apiKey;
|
||||||
|
config.defaults.userId = opts.userId;
|
||||||
|
await validatePlatform(config);
|
||||||
|
saveConfig(config);
|
||||||
|
printSuccess("Configuration saved to ~/.mem0/config.json");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printBanner();
|
||||||
|
console.log();
|
||||||
|
printInfo("Welcome! Let's set up your mem0 CLI.\n");
|
||||||
|
|
||||||
|
// Use provided API key or prompt
|
||||||
|
if (opts.apiKey) {
|
||||||
|
config.platform.apiKey = opts.apiKey;
|
||||||
|
} else {
|
||||||
|
console.log(` ${brand("How would you like to authenticate?")}`);
|
||||||
|
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
|
||||||
|
console.log(` ${dim("2.")} Enter API key manually`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
|
||||||
|
|
||||||
|
if (choice === "1") {
|
||||||
|
console.log();
|
||||||
|
const emailAddr = await promptLine(` ${brand("Email")}`);
|
||||||
|
if (!emailAddr) {
|
||||||
|
printError("Email is required.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = emailAddr.trim().toLowerCase();
|
||||||
|
validateEmail(email);
|
||||||
|
printInfo(`Logging in as ${email}...\n`);
|
||||||
|
|
||||||
|
const result = await emailLogin(email, undefined, baseUrl);
|
||||||
|
|
||||||
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
|
if (!apiKeyVal) {
|
||||||
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.platform.apiKey = apiKeyVal;
|
||||||
|
config.platform.baseUrl = baseUrl;
|
||||||
|
config.defaults.userId =
|
||||||
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
|
||||||
|
saveConfig(config);
|
||||||
|
console.log();
|
||||||
|
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
|
||||||
|
console.log();
|
||||||
|
console.log(` ${dim("Get started:")}`);
|
||||||
|
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||||
|
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// choice === "2": fall through to API key prompt
|
||||||
|
await setupPlatform(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use provided user ID or prompt
|
||||||
|
if (opts.userId) {
|
||||||
|
config.defaults.userId = opts.userId;
|
||||||
|
} else {
|
||||||
|
await setupDefaults(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
await validatePlatform(config);
|
||||||
|
|
||||||
|
saveConfig(config);
|
||||||
|
console.log();
|
||||||
|
printSuccess("Configuration saved to ~/.mem0/config.json");
|
||||||
|
console.log();
|
||||||
|
console.log(` ${dim("Get started:")}`);
|
||||||
|
if (config.defaults.userId) {
|
||||||
|
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||||
|
console.log(` ${dim(' mem0 search "preferences"')}`);
|
||||||
|
} else {
|
||||||
|
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
|
||||||
|
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
/**
|
||||||
|
* Memory CRUD commands: add, search, get, list, update, delete.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printScope,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
|
import {
|
||||||
|
formatAddResult,
|
||||||
|
formatAgentEnvelope,
|
||||||
|
formatJson,
|
||||||
|
formatJsonEnvelope,
|
||||||
|
formatMemoriesTable,
|
||||||
|
formatMemoriesText,
|
||||||
|
formatSingleMemory,
|
||||||
|
printResultSummary,
|
||||||
|
} from "../output.js";
|
||||||
|
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||||
|
function _stdinIsPiped(): boolean {
|
||||||
|
if (isAgentMode()) return false;
|
||||||
|
try {
|
||||||
|
const stat = fs.fstatSync(0);
|
||||||
|
return stat.isFIFO() || stat.isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdAdd(
|
||||||
|
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> {
|
||||||
|
setCurrentCommand("add");
|
||||||
|
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 only if stdin is an actual pipe or file redirect
|
||||||
|
else if (!content && _stdinIsPiped()) {
|
||||||
|
content = fs.readFileSync(0, "utf-8").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content !== undefined && content.trim() === "") {
|
||||||
|
printError("Content cannot be empty.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!content && !msgs) {
|
||||||
|
printError(
|
||||||
|
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate --expires
|
||||||
|
if (opts.expires) {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||||
|
printError(
|
||||||
|
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (new Date(opts.expires) <= new Date()) {
|
||||||
|
printError("--expires date must be in the future.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta: Record<string, unknown> | undefined;
|
||||||
|
if (opts.metadata) {
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(opts.metadata);
|
||||||
|
} catch {
|
||||||
|
printError("Invalid JSON in --metadata.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cats: string[] | undefined;
|
||||||
|
if (opts.categories) {
|
||||||
|
try {
|
||||||
|
cats = JSON.parse(opts.categories);
|
||||||
|
} catch {
|
||||||
|
cats = opts.categories.split(",").map((c) => c.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Adding memory...", async () => {
|
||||||
|
return backend.add(content ?? undefined, msgs, {
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
metadata: meta,
|
||||||
|
immutable: opts.immutable,
|
||||||
|
infer: !opts.noInfer,
|
||||||
|
expires: opts.expires,
|
||||||
|
categories: cats,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
// Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||||
|
const rawResults: Record<string, unknown>[] = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||||
|
const seenEvents = new Set<string>();
|
||||||
|
const deduped: Record<string, unknown>[] = [];
|
||||||
|
for (const r of rawResults) {
|
||||||
|
if (r.status === "PENDING") {
|
||||||
|
const eid = (r.event_id as string) ?? "";
|
||||||
|
if (eid && seenEvents.has(eid)) continue;
|
||||||
|
if (eid) seenEvents.add(eid);
|
||||||
|
}
|
||||||
|
deduped.push(r);
|
||||||
|
}
|
||||||
|
// Write back so downstream formatters see deduplicated data
|
||||||
|
const dedupedResult: Record<string, unknown> = Array.isArray(result)
|
||||||
|
? (deduped as unknown as Record<string, unknown>)
|
||||||
|
: { ...result, results: deduped };
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "add",
|
||||||
|
data: deduped,
|
||||||
|
scope,
|
||||||
|
count: deduped.length,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "json") {
|
||||||
|
formatAddResult(dedupedResult, opts.output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
printScope({
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
});
|
||||||
|
const count = deduped.length;
|
||||||
|
const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
|
||||||
|
if (allPending) {
|
||||||
|
printSuccess(
|
||||||
|
`Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printSuccess(
|
||||||
|
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
formatAddResult(dedupedResult, 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> {
|
||||||
|
setCurrentCommand("search");
|
||||||
|
if (!query) {
|
||||||
|
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filters: Record<string, unknown> | undefined;
|
||||||
|
if (opts.filterJson) {
|
||||||
|
try {
|
||||||
|
filters = JSON.parse(opts.filterJson);
|
||||||
|
} catch {
|
||||||
|
printError("Invalid JSON in --filter.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldList = opts.fields
|
||||||
|
? opts.fields.split(",").map((f) => f.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (opts.topK < 1) {
|
||||||
|
printError("--top-k must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.threshold < 0 || opts.threshold > 1) {
|
||||||
|
printError("--threshold must be between 0.0 and 1.0.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Searching memories...", async () => {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above
|
||||||
|
return backend.search(query!, {
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
topK: opts.topK,
|
||||||
|
threshold: opts.threshold,
|
||||||
|
rerank: opts.rerank,
|
||||||
|
keyword: opts.keyword,
|
||||||
|
filters,
|
||||||
|
fields: fieldList,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "search",
|
||||||
|
data: results,
|
||||||
|
scope,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "json") {
|
||||||
|
formatJson(results);
|
||||||
|
} else if (opts.output === "table") {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesTable(results, { showScore: true });
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found matching your query.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesText(results);
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found matching your query.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdGet(
|
||||||
|
backend: Backend,
|
||||||
|
memoryId: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("get");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({ command: "get", data: result });
|
||||||
|
} else {
|
||||||
|
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> {
|
||||||
|
setCurrentCommand("list");
|
||||||
|
if (opts.pageSize < 1) {
|
||||||
|
printError("--page-size must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.page < 1) {
|
||||||
|
printError("--page must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Listing memories...", async () => {
|
||||||
|
return backend.listMemories({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
page: opts.page,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
category: opts.category,
|
||||||
|
after: opts.after,
|
||||||
|
before: opts.before,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "list",
|
||||||
|
data: results,
|
||||||
|
scope,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} 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> {
|
||||||
|
setCurrentCommand("update");
|
||||||
|
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 === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "update",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else 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> {
|
||||||
|
setCurrentCommand("delete");
|
||||||
|
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 === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete",
|
||||||
|
data: { id: memoryId, deleted: true },
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else 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> {
|
||||||
|
setCurrentCommand("delete-all");
|
||||||
|
const { isAgentMode } = await import("../state.js");
|
||||||
|
if (isAgentMode() && !opts.force) {
|
||||||
|
printError("Destructive operation requires --force in agent mode.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.all) {
|
||||||
|
// Project-wide wipe using wildcard entity IDs
|
||||||
|
// Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||||
|
|
||||||
|
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 === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete-all",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else 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 === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete-all",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
if (result.message) {
|
||||||
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Utility commands: status, version, import.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import boxen from "boxen";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
|
|
||||||
|
const { brand, dim, success, error: errorColor } = colors;
|
||||||
|
|
||||||
|
export async function cmdStatus(
|
||||||
|
backend: Backend,
|
||||||
|
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("status");
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Checking connection...", async () => {
|
||||||
|
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
result = {
|
||||||
|
connected: false,
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "status",
|
||||||
|
data: {
|
||||||
|
connected: result.connected,
|
||||||
|
backend: result.backend ?? null,
|
||||||
|
base_url: result.base_url ?? null,
|
||||||
|
},
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (result.connected) {
|
||||||
|
lines.push(` ${success("\u25cf")} Connected`);
|
||||||
|
} else {
|
||||||
|
lines.push(` ${errorColor("\u25cf")} Disconnected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
|
||||||
|
if (result.base_url) {
|
||||||
|
lines.push(` ${dim("API URL:")} ${result.base_url}`);
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
||||||
|
if (String(result.error).includes("Authentication failed")) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(
|
||||||
|
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Connection Status"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdVersion(): void {
|
||||||
|
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdImport(
|
||||||
|
backend: Backend,
|
||||||
|
filePath: string,
|
||||||
|
opts: { userId?: string; agentId?: string; output?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("import");
|
||||||
|
let data: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
data = Array.isArray(parsed) ? parsed : [parsed];
|
||||||
|
} catch (e) {
|
||||||
|
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
let failed = 0;
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
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 === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "import",
|
||||||
|
data: {
|
||||||
|
added,
|
||||||
|
failed,
|
||||||
|
},
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
|
||||||
|
if (failed > 0) {
|
||||||
|
printError(`${failed} memories failed to import.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Configuration management for mem0 CLI.
|
||||||
|
*
|
||||||
|
* Config precedence (highest to lowest):
|
||||||
|
* 1. CLI flags (--api-key, --base-url, etc.)
|
||||||
|
* 2. Environment variables (MEM0_API_KEY, etc.)
|
||||||
|
* 3. Config file (~/.mem0/config.json)
|
||||||
|
* 4. Defaults
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const CONFIG_DIR = path.join(os.homedir(), ".mem0");
|
||||||
|
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||||
|
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
||||||
|
export const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
|
export interface PlatformConfig {
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultsConfig {
|
||||||
|
userId: string;
|
||||||
|
agentId: string;
|
||||||
|
appId: string;
|
||||||
|
runId: string;
|
||||||
|
enableGraph: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Mem0Config {
|
||||||
|
version: number;
|
||||||
|
defaults: DefaultsConfig;
|
||||||
|
platform: PlatformConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultConfig(): Mem0Config {
|
||||||
|
return {
|
||||||
|
version: CONFIG_VERSION,
|
||||||
|
defaults: {
|
||||||
|
userId: "",
|
||||||
|
agentId: "",
|
||||||
|
appId: "",
|
||||||
|
runId: "",
|
||||||
|
enableGraph: false,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
apiKey: "",
|
||||||
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureConfigDir(): string {
|
||||||
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
||||||
|
return CONFIG_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(): Mem0Config {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
config.version = data.version ?? CONFIG_VERSION;
|
||||||
|
|
||||||
|
const plat = data.platform ?? {};
|
||||||
|
config.platform.apiKey = plat.api_key ?? "";
|
||||||
|
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||||
|
|
||||||
|
const defaults = data.defaults ?? {};
|
||||||
|
config.defaults.userId = defaults.user_id ?? "";
|
||||||
|
config.defaults.agentId = defaults.agent_id ?? "";
|
||||||
|
config.defaults.appId = defaults.app_id ?? "";
|
||||||
|
config.defaults.runId = defaults.run_id ?? "";
|
||||||
|
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variable overrides
|
||||||
|
if (process.env.MEM0_API_KEY)
|
||||||
|
config.platform.apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (process.env.MEM0_BASE_URL)
|
||||||
|
config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
||||||
|
if (process.env.MEM0_USER_ID)
|
||||||
|
config.defaults.userId = process.env.MEM0_USER_ID;
|
||||||
|
if (process.env.MEM0_AGENT_ID)
|
||||||
|
config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
||||||
|
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
||||||
|
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
||||||
|
if (process.env.MEM0_ENABLE_GRAPH) {
|
||||||
|
config.defaults.enableGraph = ["true", "1", "yes"].includes(
|
||||||
|
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfig(config: Mem0Config): void {
|
||||||
|
ensureConfigDir();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
version: config.version,
|
||||||
|
defaults: {
|
||||||
|
user_id: config.defaults.userId,
|
||||||
|
agent_id: config.defaults.agentId,
|
||||||
|
app_id: config.defaults.appId,
|
||||||
|
run_id: config.defaults.runId,
|
||||||
|
enable_graph: config.defaults.enableGraph,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
api_key: config.platform.apiKey,
|
||||||
|
base_url: config.platform.baseUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||||
|
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactKey(key: string): string {
|
||||||
|
if (!key) return "(not set)";
|
||||||
|
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||||
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Key map from dotted config path to the config object fields. */
|
||||||
|
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||||
|
"platform.api_key": ["platform", "apiKey"],
|
||||||
|
"platform.base_url": ["platform", "baseUrl"],
|
||||||
|
"defaults.user_id": ["defaults", "userId"],
|
||||||
|
"defaults.agent_id": ["defaults", "agentId"],
|
||||||
|
"defaults.app_id": ["defaults", "appId"],
|
||||||
|
"defaults.run_id": ["defaults", "runId"],
|
||||||
|
"defaults.enable_graph": ["defaults", "enableGraph"],
|
||||||
|
// Short-form aliases
|
||||||
|
api_key: ["platform", "apiKey"],
|
||||||
|
base_url: ["platform", "baseUrl"],
|
||||||
|
user_id: ["defaults", "userId"],
|
||||||
|
agent_id: ["defaults", "agentId"],
|
||||||
|
app_id: ["defaults", "appId"],
|
||||||
|
run_id: ["defaults", "runId"],
|
||||||
|
enable_graph: ["defaults", "enableGraph"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
||||||
|
const mapping = KEY_MAP[dottedKey];
|
||||||
|
if (!mapping) return undefined;
|
||||||
|
const [section, field] = mapping;
|
||||||
|
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNestedValue(
|
||||||
|
config: Mem0Config,
|
||||||
|
dottedKey: string,
|
||||||
|
value: string,
|
||||||
|
): boolean {
|
||||||
|
const mapping = KEY_MAP[dottedKey];
|
||||||
|
if (!mapping) return false;
|
||||||
|
const [section, field] = mapping;
|
||||||
|
const obj = config[section] as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
|
const current = obj[field];
|
||||||
|
if (typeof current === "boolean") {
|
||||||
|
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||||
|
} else if (typeof current === "number") {
|
||||||
|
obj[field] = Number.parseInt(value, 10);
|
||||||
|
} else {
|
||||||
|
obj[field] = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
|
||||||
|
declare const __CLI_VERSION__: string | undefined;
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
/**
|
||||||
|
* Rich-style help formatter for Commander.js that matches the Python CLI's
|
||||||
|
* Typer + Rich output (rounded box panels, brand purple, grouped options).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from "chalk";
|
||||||
|
import type { Argument, Command, Help, Option } from "commander";
|
||||||
|
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||||
|
|
||||||
|
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||||
|
|
||||||
|
const cyanBold = chalk.cyan.bold; // option flags, command names
|
||||||
|
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
||||||
|
const yellowBold = chalk.yellow.bold; // metavar <value>
|
||||||
|
const yellow = chalk.yellow; // "Usage:" label
|
||||||
|
const bold = chalk.bold; // command name in usage
|
||||||
|
const dim = chalk.dim; // defaults, descriptions
|
||||||
|
const dimBorder = chalk.dim; // panel borders
|
||||||
|
|
||||||
|
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||||
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
|
function stripAnsi(str: string): number {
|
||||||
|
return str.replace(ANSI_RE, "").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command display order (matches Python CLI) ──────────────────────────
|
||||||
|
|
||||||
|
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
||||||
|
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||||
|
{
|
||||||
|
panel: "Memory",
|
||||||
|
commands: ["add", "search", "get", "list", "update", "delete"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
panel: "Management",
|
||||||
|
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Flat order derived from COMMAND_GROUPS. */
|
||||||
|
const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
|
||||||
|
|
||||||
|
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
||||||
|
|
||||||
|
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
||||||
|
add: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--top-k": "Search",
|
||||||
|
"--threshold": "Search",
|
||||||
|
"--rerank": "Search",
|
||||||
|
"--keyword": "Search",
|
||||||
|
"--filter": "Search",
|
||||||
|
"--fields": "Search",
|
||||||
|
"--graph": "Search",
|
||||||
|
"--no-graph": "Search",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--page": "Pagination",
|
||||||
|
"--page-size": "Pagination",
|
||||||
|
"--category": "Filters",
|
||||||
|
"--after": "Filters",
|
||||||
|
"--before": "Filters",
|
||||||
|
"--graph": "Filters",
|
||||||
|
"--no-graph": "Filters",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
import: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PANEL_ORDER: string[] = [
|
||||||
|
"Scope",
|
||||||
|
"Search",
|
||||||
|
"Pagination",
|
||||||
|
"Filters",
|
||||||
|
"Output",
|
||||||
|
"Connection",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Panel rendering ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a Rich-style ROUNDED box panel.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* ╭─ Title ────────────────────────╮
|
||||||
|
* │ row content padded │
|
||||||
|
* ╰────────────────────────────────╯
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function renderPanel(title: string, rows: string[], width: number): string {
|
||||||
|
if (rows.length === 0) return "";
|
||||||
|
|
||||||
|
// Inner width is total width minus the two border chars
|
||||||
|
const inner = width - 2;
|
||||||
|
|
||||||
|
// Top border: ╭─ Title ─...─╮
|
||||||
|
const titleStr = ` ${title} `;
|
||||||
|
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
||||||
|
const topLine =
|
||||||
|
dimBorder("╭─") +
|
||||||
|
dimBorder(titleStr) +
|
||||||
|
dimBorder("─".repeat(fillLen)) +
|
||||||
|
dimBorder("╮");
|
||||||
|
|
||||||
|
// Bottom border: ╰─...─╯
|
||||||
|
const bottomLine =
|
||||||
|
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||||
|
|
||||||
|
// Content rows
|
||||||
|
const contentLines = rows.map((row) => {
|
||||||
|
const visLen = stripAnsi(row);
|
||||||
|
const pad = Math.max(0, inner - 1 - visLen);
|
||||||
|
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format an option term (short + long) ────────────────────────────────
|
||||||
|
|
||||||
|
function formatOptionTerm(opt: Option): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (opt.short) parts.push(opt.short);
|
||||||
|
if (opt.long) parts.push(opt.long);
|
||||||
|
let term = parts.join(", ");
|
||||||
|
|
||||||
|
// Append value placeholder for non-boolean options
|
||||||
|
if (opt.flags) {
|
||||||
|
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||||
|
if (match) {
|
||||||
|
term += ` ${match[0]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return term;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Get the long flag name for panel lookup ─────────────────────────────
|
||||||
|
|
||||||
|
function getLongFlag(opt: Option): string {
|
||||||
|
if (opt.long) return opt.long;
|
||||||
|
return opt.short || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format a default value ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
function formatDefault(opt: Option): string {
|
||||||
|
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
||||||
|
return dim(` [default: ${opt.defaultValue}]`);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── The main help formatter ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function richFormatHelp(cmd: Command, helper: Help): string {
|
||||||
|
const width = process.stdout.columns || 80;
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
const isRoot = !cmd.parent;
|
||||||
|
|
||||||
|
// ── Usage line ──
|
||||||
|
const usage = helper.commandUsage(cmd);
|
||||||
|
lines.push("");
|
||||||
|
if (isRoot) {
|
||||||
|
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||||
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Subcommands: split into command path (bold) and args (yellow)
|
||||||
|
const usageParts = usage.split(" ");
|
||||||
|
const cmdPath: string[] = [];
|
||||||
|
const argParts: string[] = [];
|
||||||
|
let pastCmd = false;
|
||||||
|
for (const part of usageParts) {
|
||||||
|
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
||||||
|
cmdPath.push(part);
|
||||||
|
} else {
|
||||||
|
pastCmd = true;
|
||||||
|
argParts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
// ── Description ──
|
||||||
|
const desc = helper.commandDescription(cmd);
|
||||||
|
if (desc) {
|
||||||
|
// Split multi-line descriptions (e.g., title + tagline)
|
||||||
|
const descLines = desc.split("\n");
|
||||||
|
for (let i = 0; i < descLines.length; i++) {
|
||||||
|
const dLine = descLines[i];
|
||||||
|
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
||||||
|
if (i === 0 || dLine.trim() === "") {
|
||||||
|
lines.push(` ${dLine}`);
|
||||||
|
} else {
|
||||||
|
lines.push(` ${dim(dLine)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arguments panel (subcommands only) ──
|
||||||
|
if (!isRoot) {
|
||||||
|
const visibleArgs = helper.visibleArguments(cmd);
|
||||||
|
if (visibleArgs.length > 0) {
|
||||||
|
const maxLen = Math.max(
|
||||||
|
...visibleArgs.map((a: Argument) => a.name().length),
|
||||||
|
);
|
||||||
|
const argRows = visibleArgs.map((a: Argument) => {
|
||||||
|
const name = cyanBold(a.name().padEnd(maxLen));
|
||||||
|
const description = helper.argumentDescription(a);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel("Arguments", argRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collect options (grouped into panels for subcommands) ──
|
||||||
|
const visibleOpts = helper.visibleOptions(cmd);
|
||||||
|
const cmdName = cmd.name();
|
||||||
|
const panelMap =
|
||||||
|
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||||
|
|
||||||
|
const grouped: Record<string, Option[]> = { Options: [] };
|
||||||
|
for (const panelName of PANEL_ORDER) {
|
||||||
|
grouped[panelName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const opt of visibleOpts) {
|
||||||
|
const flag = getLongFlag(opt);
|
||||||
|
const panel = panelMap[flag];
|
||||||
|
if (panel && PANEL_ORDER.includes(panel)) {
|
||||||
|
grouped[panel].push(opt);
|
||||||
|
} else {
|
||||||
|
grouped.Options.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collect commands ──
|
||||||
|
const visibleCmds = helper.visibleCommands(cmd);
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||||
|
if (grouped.Options.length > 0) {
|
||||||
|
const optRows = formatOptionRows(grouped.Options);
|
||||||
|
const panel = renderPanel("Options", optRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
if (visibleCmds.length > 0) {
|
||||||
|
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
||||||
|
for (const group of COMMAND_GROUPS) {
|
||||||
|
const groupCmds = group.commands
|
||||||
|
.map((name) => cmdMap.get(name))
|
||||||
|
.filter((c): c is Command => c !== undefined);
|
||||||
|
if (groupCmds.length === 0) continue;
|
||||||
|
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
||||||
|
const cmdRows = groupCmds.map((c) => {
|
||||||
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
|
const description = helper.subcommandDescription(c);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel(group.panel, cmdRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
||||||
|
const panelSequence = ["Options", ...PANEL_ORDER];
|
||||||
|
for (const panelName of panelSequence) {
|
||||||
|
const opts = grouped[panelName];
|
||||||
|
if (opts && opts.length > 0) {
|
||||||
|
const optRows = formatOptionRows(opts);
|
||||||
|
const panel = renderPanel(panelName, optRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
||||||
|
if (visibleCmds.length > 0) {
|
||||||
|
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
||||||
|
const cmdRows = visibleCmds.map((c) => {
|
||||||
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
|
const description = helper.subcommandDescription(c);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel("Commands", cmdRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format option rows with aligned columns ─────────────────────────────
|
||||||
|
|
||||||
|
function formatOptionRows(opts: Option[]): string[] {
|
||||||
|
const terms = opts.map((o) => formatOptionTerm(o));
|
||||||
|
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
||||||
|
|
||||||
|
return opts.map((opt, i) => {
|
||||||
|
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
||||||
|
const desc = opt.description || "";
|
||||||
|
const def = formatDefault(opt);
|
||||||
|
return ` ${term} ${desc}${def}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
||||||
|
|
||||||
|
function sortCommands(cmds: Command[]): Command[] {
|
||||||
|
return [...cmds].sort((a, b) => {
|
||||||
|
const ai = COMMAND_ORDER.indexOf(a.name());
|
||||||
|
const bi = COMMAND_ORDER.indexOf(b.name());
|
||||||
|
// Unknown commands go to end, preserving original order
|
||||||
|
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
||||||
|
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
||||||
|
return aIdx - bIdx;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main CLI application — the entrypoint for `mem0`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import { type Backend, getBackend } from "./backend/index.js";
|
||||||
|
import { colors, printError } from "./branding.js";
|
||||||
|
import type { Mem0Config } from "./config.js";
|
||||||
|
import { loadConfig } from "./config.js";
|
||||||
|
import { richFormatHelp } from "./help.js";
|
||||||
|
import { setAgentMode } from "./state.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function getBackendAndConfig(
|
||||||
|
apiKey?: string,
|
||||||
|
baseUrl?: string,
|
||||||
|
): { backend: Backend; config: Mem0Config } {
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
if (apiKey) config.platform.apiKey = apiKey;
|
||||||
|
if (baseUrl) config.platform.baseUrl = baseUrl;
|
||||||
|
|
||||||
|
if (!config.platform.apiKey) {
|
||||||
|
printError(
|
||||||
|
"No API key configured.",
|
||||||
|
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { backend: getBackend(config), config };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackendOnly(apiKey?: string, baseUrl?: string): Backend {
|
||||||
|
return getBackendAndConfig(apiKey, baseUrl).backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAgentMode(): boolean {
|
||||||
|
const rootOpts = program.opts();
|
||||||
|
const isAgent = !!(rootOpts.json || rootOpts.agent);
|
||||||
|
if (isAgent) setAgentMode(true);
|
||||||
|
return isAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve entity IDs: CLI flag > config default > undefined.
|
||||||
|
*
|
||||||
|
* If any explicit ID is provided, only use explicit IDs (don't mix
|
||||||
|
* in defaults for other entity types which would over-filter).
|
||||||
|
* If no explicit IDs, fall back to all configured defaults.
|
||||||
|
*/
|
||||||
|
function resolveIds(
|
||||||
|
config: Mem0Config,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
},
|
||||||
|
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
|
||||||
|
const hasExplicit = !!(
|
||||||
|
opts.userId ||
|
||||||
|
opts.agentId ||
|
||||||
|
opts.appId ||
|
||||||
|
opts.runId
|
||||||
|
);
|
||||||
|
if (hasExplicit) {
|
||||||
|
return {
|
||||||
|
userId: opts.userId || undefined,
|
||||||
|
agentId: opts.agentId || undefined,
|
||||||
|
appId: opts.appId || undefined,
|
||||||
|
runId: opts.runId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
appId: config.defaults.appId || undefined,
|
||||||
|
runId: config.defaults.runId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve graph tri-state: --no-graph > --graph > config default.
|
||||||
|
*/
|
||||||
|
function resolveGraph(
|
||||||
|
config: Mem0Config,
|
||||||
|
opts: { graph?: boolean; noGraph?: boolean },
|
||||||
|
): boolean {
|
||||||
|
if (opts.noGraph) return false;
|
||||||
|
if (opts.graph) return true;
|
||||||
|
return config.defaults.enableGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main program ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("mem0")
|
||||||
|
.description(
|
||||||
|
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||||
|
)
|
||||||
|
.option("--version", "Show version and exit.")
|
||||||
|
.on("option:version", () => {
|
||||||
|
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||||
|
.option(
|
||||||
|
"--agent",
|
||||||
|
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||||
|
)
|
||||||
|
.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).")
|
||||||
|
.option("--email <email>", "Login via email verification code.")
|
||||||
|
.option(
|
||||||
|
"--code <code>",
|
||||||
|
"Verification code (use with --email for non-interactive login).",
|
||||||
|
)
|
||||||
|
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { runInit } = await import("./commands/init.js");
|
||||||
|
await runInit({
|
||||||
|
apiKey: opts.apiKey,
|
||||||
|
userId: opts.userId,
|
||||||
|
email: opts.email,
|
||||||
|
code: opts.code,
|
||||||
|
force: opts.force,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 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 isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph, output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: search ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("search [query]")
|
||||||
|
.description(
|
||||||
|
"Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||||
|
)
|
||||||
|
.option("-u, --user-id <id>", "Filter by user.")
|
||||||
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
|
.option("--app-id <id>", "Filter by app.")
|
||||||
|
.option("--run-id <id>", "Filter by run.")
|
||||||
|
.option(
|
||||||
|
"-k, --top-k <n>",
|
||||||
|
"Number of results.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--threshold <n>",
|
||||||
|
"Minimum similarity score.",
|
||||||
|
(v) => Number.parseFloat(v),
|
||||||
|
0.3,
|
||||||
|
)
|
||||||
|
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||||
|
.option("--keyword", "Use keyword search.", false)
|
||||||
|
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||||
|
.option("--fields <list>", "Specific fields to return (comma-separated).")
|
||||||
|
.option("--graph", "Enable graph in search.", false)
|
||||||
|
.option("--no-graph", "Disable graph in search.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
|
||||||
|
)
|
||||||
|
.action(async (query, opts) => {
|
||||||
|
let resolvedQuery = query;
|
||||||
|
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||||
|
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
|
||||||
|
}
|
||||||
|
if (!resolvedQuery) {
|
||||||
|
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { cmdSearch } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 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 isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdGet(backend, memoryId, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: list ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("list")
|
||||||
|
.description("List memories with optional filters.")
|
||||||
|
.option("-u, --user-id <id>", "Filter by user.")
|
||||||
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
|
.option("--app-id <id>", "Filter by app.")
|
||||||
|
.option("--run-id <id>", "Filter by run.")
|
||||||
|
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
|
||||||
|
.option(
|
||||||
|
"--page-size <n>",
|
||||||
|
"Results per page.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
.option("--category <name>", "Filter by category.")
|
||||||
|
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||||
|
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||||
|
.option("--graph", "Enable graph in listing.", false)
|
||||||
|
.option("--no-graph", "Disable graph in listing.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdList } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdList(backend, {
|
||||||
|
...ids,
|
||||||
|
page: opts.page,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
category: opts.category,
|
||||||
|
after: opts.after,
|
||||||
|
before: opts.before,
|
||||||
|
enableGraph,
|
||||||
|
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 isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||||
|
metadata: opts.metadata,
|
||||||
|
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) => {
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
// ── 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,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch: --entity ──
|
||||||
|
if (opts.entity) {
|
||||||
|
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||||
|
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");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
cmdConfigShow({ 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");
|
||||||
|
checkAgentMode();
|
||||||
|
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");
|
||||||
|
checkAgentMode();
|
||||||
|
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 isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEntitiesList(backend, entityType, { 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 isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Event subcommands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const eventCmd = program
|
||||||
|
.command("event")
|
||||||
|
.description("Inspect background processing events.")
|
||||||
|
.addHelpCommand(false)
|
||||||
|
.configureHelp({ formatHelp: richFormatHelp });
|
||||||
|
|
||||||
|
eventCmd
|
||||||
|
.command("list")
|
||||||
|
.description("List recent background processing events.")
|
||||||
|
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdEventList } = await import("./commands/events.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEventList(backend, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
eventCmd
|
||||||
|
.command("status <eventId>")
|
||||||
|
.description("Check the status of a specific background event.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
|
||||||
|
)
|
||||||
|
.action(async (eventId, opts) => {
|
||||||
|
const { cmdEventStatus } = await import("./commands/events.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEventStatus(backend, eventId, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 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 isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdStatus(backend, {
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
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 isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdImport(backend, filePath, {
|
||||||
|
userId: ids.userId,
|
||||||
|
agentId: ids.agentId,
|
||||||
|
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) => {
|
||||||
|
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||||
|
// program.opts().json is set when the root --json global flag was used first.
|
||||||
|
if (opts.json || program.opts().json) {
|
||||||
|
// Load spec from parent directory
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||||
|
if (fs.existsSync(specPath)) {
|
||||||
|
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
||||||
|
console.log(JSON.stringify(spec, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "mem0",
|
||||||
|
version: CLI_VERSION,
|
||||||
|
description: "The Memory Layer for AI Agents",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { brand: b } = colors;
|
||||||
|
console.log(
|
||||||
|
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
|
||||||
|
);
|
||||||
|
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(
|
||||||
|
" add Add a memory from text, messages, file, or stdin",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" search Query your memory store (semantic, keyword, hybrid)",
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
" event Inspect background events (list, status)",
|
||||||
|
);
|
||||||
|
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();
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
/**
|
||||||
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import { colors, sym } from "./branding.js";
|
||||||
|
|
||||||
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
|
|
||||||
|
function formatDate(dtStr?: string): string | undefined {
|
||||||
|
if (!dtStr) return undefined;
|
||||||
|
try {
|
||||||
|
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
||||||
|
return dt.toISOString().slice(0, 10);
|
||||||
|
} catch {
|
||||||
|
return dtStr?.slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMemoriesText(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
title = "memories",
|
||||||
|
): void {
|
||||||
|
const count = memories.length;
|
||||||
|
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
||||||
|
|
||||||
|
for (let i = 0; i < memories.length; i++) {
|
||||||
|
const mem = memories[i];
|
||||||
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
||||||
|
const score = mem.score as number | undefined;
|
||||||
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
|
let category: string | undefined;
|
||||||
|
const cats = mem.categories;
|
||||||
|
if (Array.isArray(cats)) {
|
||||||
|
category = cats[0] as string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ${i + 1}. ${memoryText}`);
|
||||||
|
|
||||||
|
const details: string[] = [];
|
||||||
|
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
||||||
|
if (memId) details.push(`ID: ${memId}`);
|
||||||
|
if (created) details.push(`Created: ${created}`);
|
||||||
|
if (category) details.push(`Category: ${category}`);
|
||||||
|
|
||||||
|
if (details.length > 0) {
|
||||||
|
console.log(` ${dim(details.join(" · "))}`);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMemoriesTable(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
opts: { showScore?: boolean } = {},
|
||||||
|
): void {
|
||||||
|
const head = opts.showScore
|
||||||
|
? [
|
||||||
|
accent("ID"),
|
||||||
|
accent("Score"),
|
||||||
|
accent("Memory"),
|
||||||
|
accent("Category"),
|
||||||
|
accent("Created"),
|
||||||
|
]
|
||||||
|
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
||||||
|
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
|
||||||
|
const table = new Table({
|
||||||
|
head,
|
||||||
|
colWidths,
|
||||||
|
wordWrap: true,
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mem of memories) {
|
||||||
|
const memId = (mem.id as string) ?? "";
|
||||||
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
if (memoryText.length > 60) {
|
||||||
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||||
|
}
|
||||||
|
const categories = mem.categories;
|
||||||
|
const cat =
|
||||||
|
Array.isArray(categories) && categories.length > 0
|
||||||
|
? categories.length > 1
|
||||||
|
? `${categories[0]} (+${categories.length - 1})`
|
||||||
|
: (categories[0] as string)
|
||||||
|
: "—";
|
||||||
|
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
||||||
|
if (opts.showScore) {
|
||||||
|
const score = mem.score as number | undefined;
|
||||||
|
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
|
||||||
|
table.push([dim(memId), scoreStr, memoryText, cat, created]);
|
||||||
|
} else {
|
||||||
|
table.push([dim(memId), memoryText, cat, created]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatJson(data: unknown): void {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSingleMemory(
|
||||||
|
mem: Record<string, unknown>,
|
||||||
|
output = "text",
|
||||||
|
): void {
|
||||||
|
if (output === "json") {
|
||||||
|
formatJson(mem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
const memId = (mem.id ?? "") as string;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(` ${memoryText}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
||||||
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
|
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
||||||
|
const updated = formatDate(mem.updated_at as string | undefined);
|
||||||
|
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
||||||
|
const meta = mem.metadata;
|
||||||
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||||
|
const categories = mem.categories;
|
||||||
|
if (categories) {
|
||||||
|
const catStr = Array.isArray(categories)
|
||||||
|
? categories.join(", ")
|
||||||
|
: String(categories);
|
||||||
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Memory"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAddResult(
|
||||||
|
result: Record<string, unknown> | Record<string, unknown>[],
|
||||||
|
output = "text",
|
||||||
|
): void {
|
||||||
|
if (output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (output === "quiet") return;
|
||||||
|
|
||||||
|
const results: Record<string, unknown>[] = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
console.log(` ${dim("No memories extracted.")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
const seenPendingEvents = new Set<string>();
|
||||||
|
for (const r of results) {
|
||||||
|
// Detect async PENDING response
|
||||||
|
if (r.status === "PENDING") {
|
||||||
|
const eventId = (r.event_id as string) ?? "";
|
||||||
|
// Deduplicate PENDING entries with the same event_id
|
||||||
|
if (eventId && seenPendingEvents.has(eventId)) continue;
|
||||||
|
if (eventId) seenPendingEvents.add(eventId);
|
||||||
|
const icon = accent(sym("⧗", "..."));
|
||||||
|
const parts = [
|
||||||
|
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||||
|
"Processing in background",
|
||||||
|
];
|
||||||
|
console.log(parts.join(" "));
|
||||||
|
if (eventId) {
|
||||||
|
console.log(` ${dim(` event_id: ${eventId}`)}`);
|
||||||
|
console.log(
|
||||||
|
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = (r.event ?? "ADD") as string;
|
||||||
|
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
||||||
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key in obj) result[key] = obj[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||||
|
if (data === null || data === undefined) return data;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "add": {
|
||||||
|
const items = Array.isArray(data) ? data : [data];
|
||||||
|
return items.map((item) => {
|
||||||
|
const r = item as Record<string, unknown>;
|
||||||
|
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
|
||||||
|
return pick(r, ["id", "memory", "event"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "get": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, [
|
||||||
|
"id",
|
||||||
|
"memory",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"categories",
|
||||||
|
"metadata",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
case "update": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, ["id", "memory"]);
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
case "delete-all":
|
||||||
|
case "entity delete":
|
||||||
|
return data;
|
||||||
|
case "entity list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) => ({
|
||||||
|
name: (r.name ?? r.id) as string,
|
||||||
|
...pick(r, ["type", "count"]),
|
||||||
|
}));
|
||||||
|
case "event list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
|
||||||
|
);
|
||||||
|
case "event status": {
|
||||||
|
const ev = data as Record<string, unknown>;
|
||||||
|
const rawResults =
|
||||||
|
(ev.results as Record<string, unknown>[] | undefined) ?? [];
|
||||||
|
const sanitizedResults = rawResults.map((r) => {
|
||||||
|
const nested = r.data as Record<string, unknown> | undefined;
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
event: r.event,
|
||||||
|
user_id: r.user_id,
|
||||||
|
memory: nested?.memory ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...pick(ev, [
|
||||||
|
"id",
|
||||||
|
"event_type",
|
||||||
|
"status",
|
||||||
|
"latency",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]),
|
||||||
|
results: sanitizedResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAgentEnvelope(opts: {
|
||||||
|
command: string;
|
||||||
|
data: unknown;
|
||||||
|
durationMs?: number;
|
||||||
|
scope?: Record<string, string | undefined>;
|
||||||
|
count?: number;
|
||||||
|
}): void {
|
||||||
|
const envelope: Record<string, unknown> = {
|
||||||
|
status: "success",
|
||||||
|
command: opts.command,
|
||||||
|
};
|
||||||
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||||
|
if (opts.scope) {
|
||||||
|
const filtered = Object.fromEntries(
|
||||||
|
Object.entries(opts.scope).filter(([, v]) => v),
|
||||||
|
);
|
||||||
|
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
|
||||||
|
}
|
||||||
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
|
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||||
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printResultSummary(opts: {
|
||||||
|
count: number;
|
||||||
|
durationSecs?: number;
|
||||||
|
page?: number;
|
||||||
|
scopeIds?: Record<string, string | undefined>;
|
||||||
|
}): 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}=${v}`);
|
||||||
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||||
|
}
|
||||||
|
if (opts.durationSecs !== undefined)
|
||||||
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||||
|
|
||||||
|
console.log(` ${dim(parts.join(" · "))}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Agent mode state — set by the root program option handler,
|
||||||
|
* read by commands and branding functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _agentMode = false;
|
||||||
|
let _currentCommand = "";
|
||||||
|
|
||||||
|
export function isAgentMode(): boolean {
|
||||||
|
return _agentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAgentMode(val: boolean): void {
|
||||||
|
_agentMode = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentCommand(): string {
|
||||||
|
return _currentCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentCommand(name: string): void {
|
||||||
|
_currentCommand = name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
|
||||||
|
// When running via tsx in dev/test mode, fall back to reading package.json.
|
||||||
|
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
|
||||||
|
export const CLI_VERSION: string =
|
||||||
|
typeof __CLI_VERSION__ !== "undefined"
|
||||||
|
? (__CLI_VERSION__ as string)
|
||||||
|
: (createRequire(import.meta.url)("../package.json") as { version: string })
|
||||||
|
.version;
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Tests for branding utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
BRAND_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
TAGLINE,
|
||||||
|
LOGO_MINI,
|
||||||
|
printSuccess,
|
||||||
|
printError,
|
||||||
|
printWarning,
|
||||||
|
printInfo,
|
||||||
|
printScope,
|
||||||
|
} from "../src/branding.js";
|
||||||
|
|
||||||
|
let output: string;
|
||||||
|
let errOutput: string;
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
output = "";
|
||||||
|
errOutput = "";
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
output += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
errOutput += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = originalLog;
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("branding constants", () => {
|
||||||
|
it("has correct brand color", () => {
|
||||||
|
expect(BRAND_COLOR).toBe("#8b5cf6");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has correct tagline", () => {
|
||||||
|
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has correct logo mini", () => {
|
||||||
|
expect(LOGO_MINI).toBe("◆ mem0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printSuccess", () => {
|
||||||
|
it("prints success message", () => {
|
||||||
|
printSuccess("Operation completed");
|
||||||
|
expect(output).toContain("Operation completed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printError", () => {
|
||||||
|
it("prints error message to stderr", () => {
|
||||||
|
printError("Something failed");
|
||||||
|
expect(errOutput).toContain("Something failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints hint when provided to stderr", () => {
|
||||||
|
printError("Failed", "Try again");
|
||||||
|
expect(errOutput).toContain("Try again");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printWarning", () => {
|
||||||
|
it("prints warning message to stderr", () => {
|
||||||
|
printWarning("Be careful");
|
||||||
|
expect(errOutput).toContain("Be careful");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printInfo", () => {
|
||||||
|
it("prints info message", () => {
|
||||||
|
printInfo("Important note");
|
||||||
|
expect(errOutput).toContain("Important note");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printScope", () => {
|
||||||
|
it("prints scope when IDs present", () => {
|
||||||
|
printScope({ user_id: "alice", agent_id: "bot" });
|
||||||
|
expect(errOutput).toContain("alice");
|
||||||
|
expect(errOutput).toContain("bot");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints nothing when no IDs", () => {
|
||||||
|
printScope({});
|
||||||
|
expect(errOutput).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* 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("help --json produces valid JSON", () => {
|
||||||
|
const result = run(["help", "--json"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const parsed = JSON.parse(result.stdout);
|
||||||
|
// spec may have cli.name or top-level name
|
||||||
|
const name = parsed.name ?? parsed.cli?.name;
|
||||||
|
expect(name).toBe("mem0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows add help", () => {
|
||||||
|
const result = run(["add", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("user-id");
|
||||||
|
expect(result.stdout).toContain("messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows search help", () => {
|
||||||
|
const result = run(["search", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("top-k");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows list help", () => {
|
||||||
|
const result = run(["list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("page-size");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows delete help with --all, --entity, --project", () => {
|
||||||
|
const result = run(["delete", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--all");
|
||||||
|
expect(result.stdout).toContain("--entity");
|
||||||
|
expect(result.stdout).toContain("--project");
|
||||||
|
expect(result.stdout).toContain("--force");
|
||||||
|
expect(result.stdout.toLowerCase()).toContain("memory");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delete with no args errors", () => {
|
||||||
|
const result = run(["delete"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
const combined = result.stdout + result.stderr;
|
||||||
|
expect(combined).toContain("--all");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows entity list help", () => {
|
||||||
|
const result = run(["entity", "list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout.toLowerCase()).toContain("entitytype");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows entity delete help", () => {
|
||||||
|
const result = run(["entity", "delete", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--user-id");
|
||||||
|
expect(result.stdout).toContain("--force");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows import help", () => {
|
||||||
|
const result = run(["import", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add help has --graph flag", () => {
|
||||||
|
const result = run(["add", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("search help has --graph flag", () => {
|
||||||
|
const result = run(["search", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list help has --graph flag", () => {
|
||||||
|
const result = run(["list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CLI Integration — isolated (clean home)", () => {
|
||||||
|
function cleanHome(): string {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("add without API key errors", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["add", "test", "--user-id", "alice"], { home });
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
const combined = result.stdout + result.stderr;
|
||||||
|
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("config show works with clean home", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["config", "show"], { home });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* 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";
|
||||||
|
import { setAgentMode } from "../src/state.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;
|
||||||
|
setAgentMode(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
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("cmdAdd deduplicates PENDING", () => {
|
||||||
|
const DUPLICATE_PENDING = {
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("text shows one pending block", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("json shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("agent shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
expect(data.count).toBe(1);
|
||||||
|
expect(data.data).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdSearch", () => {
|
||||||
|
it("searches and shows results in text mode", async () => {
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output).toContain("Found 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs json format", async () => {
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
expect(output).toContain("memory");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows no results message", async () => {
|
||||||
|
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "nonexistent", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(errOutput).toContain("No memories found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdGet", () => {
|
||||||
|
it("gets memory in text mode", async () => {
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets memory in json mode", async () => {
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
|
||||||
|
expect(output).toContain("memory");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdList", () => {
|
||||||
|
it("lists in table mode", async () => {
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "table",
|
||||||
|
});
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty message", async () => {
|
||||||
|
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(errOutput).toContain("No memories found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdUpdate", () => {
|
||||||
|
it("updates memory", async () => {
|
||||||
|
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||||
|
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
|
||||||
|
expect(output.toLowerCase()).toContain("updated");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdDelete", () => {
|
||||||
|
it("deletes memory", async () => {
|
||||||
|
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDelete(mockBackend, "abc-123", { output: "text" });
|
||||||
|
expect(output.toLowerCase()).toContain("deleted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdDeleteAll", () => {
|
||||||
|
it("deletes all with force", async () => {
|
||||||
|
const { cmdDeleteAll } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDeleteAll(mockBackend, {
|
||||||
|
force: true,
|
||||||
|
userId: "alice",
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output.toLowerCase()).toContain("deleted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdEventList", () => {
|
||||||
|
it("lists events in table mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect(output).toContain("evt-abc-");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists events in json mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("evt-def-456-ghi-789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty message when no events", async () => {
|
||||||
|
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect((output + errOutput).toLowerCase()).toContain("no events");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdEventStatus", () => {
|
||||||
|
it("shows event details in text mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows event details in json mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agent mode", () => {
|
||||||
|
it("cmdAdd outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test preference", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("add");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.scope).toMatchObject({ user_id: "alice" });
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdSearch outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("search");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
const keys = Object.keys(parsed.data[0]);
|
||||||
|
expect(keys).toContain("id");
|
||||||
|
expect(keys).toContain("memory");
|
||||||
|
expect(keys).toContain("score");
|
||||||
|
expect(keys).toContain("created_at");
|
||||||
|
expect(keys).toContain("categories");
|
||||||
|
expect(keys).not.toContain("user_id");
|
||||||
|
expect(keys).not.toContain("agent_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdGet outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("get");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
|
||||||
|
expect(Object.keys(parsed.data)).not.toContain("user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdUpdate outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||||
|
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("update");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdDelete outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("delete");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(
|
||||||
|
["created_at", "event_type", "id", "latency", "status"],
|
||||||
|
);
|
||||||
|
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventStatus outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event status");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
|
||||||
|
expect(parsed.data.results[0]).toHaveProperty("memory");
|
||||||
|
expect(parsed.data.results[0]).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Tests for configuration management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
redactKey,
|
||||||
|
getNestedValue,
|
||||||
|
setNestedValue,
|
||||||
|
CONFIG_DIR,
|
||||||
|
CONFIG_FILE,
|
||||||
|
} from "../src/config.js";
|
||||||
|
|
||||||
|
// Use a temp directory for config during tests
|
||||||
|
let origConfigDir: string;
|
||||||
|
let origConfigFile: string;
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||||
|
// Monkey-patch the module-level constants
|
||||||
|
// We'll use env vars and direct file manipulation instead
|
||||||
|
// Clear MEM0_ env vars
|
||||||
|
for (const key of Object.keys(process.env)) {
|
||||||
|
if (key.startsWith("MEM0_")) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("redactKey", () => {
|
||||||
|
it("returns '(not set)' for empty key", () => {
|
||||||
|
expect(redactKey("")).toBe("(not set)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts short key", () => {
|
||||||
|
expect(redactKey("abc")).toBe("ab***");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts normal key", () => {
|
||||||
|
const result = redactKey("m0-abcdefgh12345678");
|
||||||
|
expect(result).toBe("m0-a...5678");
|
||||||
|
expect(result).not.toContain("abcdefgh");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts exactly 8-char key as short", () => {
|
||||||
|
expect(redactKey("12345678")).toBe("12***");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createDefaultConfig", () => {
|
||||||
|
it("has correct defaults", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
|
||||||
|
expect(config.platform.apiKey).toBe("");
|
||||||
|
expect(config.defaults.userId).toBe("");
|
||||||
|
expect(config.defaults.enableGraph).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getNestedValue", () => {
|
||||||
|
it("gets platform.api_key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
config.platform.apiKey = "test-key";
|
||||||
|
expect(getNestedValue(config, "platform.api_key")).toBe("test-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for nonexistent key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(getNestedValue(config, "nonexistent.key")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets defaults.user_id", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
config.defaults.userId = "alice";
|
||||||
|
expect(getNestedValue(config, "defaults.user_id")).toBe("alice");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setNestedValue", () => {
|
||||||
|
it("sets platform.api_key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "platform.api_key", "new-key")).toBe(true);
|
||||||
|
expect(config.platform.apiKey).toBe("new-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for nonexistent key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "nonexistent.key", "val")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets defaults.user_id", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "defaults.user_id", "bob")).toBe(true);
|
||||||
|
expect(config.defaults.userId).toBe("bob");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("coerces boolean for enable_graph", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "defaults.enable_graph", "true")).toBe(true);
|
||||||
|
expect(config.defaults.enableGraph).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Tests for output formatting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
formatMemoriesText,
|
||||||
|
formatMemoriesTable,
|
||||||
|
formatJson,
|
||||||
|
formatSingleMemory,
|
||||||
|
formatAddResult,
|
||||||
|
printResultSummary,
|
||||||
|
sanitizeAgentData,
|
||||||
|
} 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");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deduplicates PENDING entries with same event_id", () => {
|
||||||
|
formatAddResult({
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Should show only one PENDING block despite two entries with same event_id
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printResultSummary", () => {
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeAgentData", () => {
|
||||||
|
it("projects add results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through PENDING add items", () => {
|
||||||
|
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects search results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("user_id");
|
||||||
|
expect(result[0]).toHaveProperty("score");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects list results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
|
||||||
|
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects get result", () => {
|
||||||
|
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
|
||||||
|
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
|
||||||
|
expect(result).not.toHaveProperty("user_id");
|
||||||
|
expect(result).toHaveProperty("metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects update result", () => {
|
||||||
|
const raw = { id: "abc", memory: "updated", extra: "noise" };
|
||||||
|
const result = sanitizeAgentData("update", raw);
|
||||||
|
expect(result).toEqual({ id: "abc", memory: "updated" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects event list results", () => {
|
||||||
|
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
|
||||||
|
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("updated_at");
|
||||||
|
expect(result[0]).not.toHaveProperty("graph_status");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flattens event status results", () => {
|
||||||
|
const raw = {
|
||||||
|
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
|
||||||
|
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
|
||||||
|
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
|
||||||
|
};
|
||||||
|
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
|
||||||
|
const firstResult = (result.results as Record<string, unknown>[])[0];
|
||||||
|
expect(firstResult).toHaveProperty("memory", "dark mode");
|
||||||
|
expect(firstResult).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through status/config/import commands unchanged", () => {
|
||||||
|
const data = { key: "value", other: "stuff" };
|
||||||
|
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
|
||||||
|
expect(sanitizeAgentData(cmd, data)).toEqual(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null data", () => {
|
||||||
|
expect(sanitizeAgentData("add", null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* 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 },
|
||||||
|
]),
|
||||||
|
listEvents: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: null,
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evt-def-456-ghi-789",
|
||||||
|
event_type: "SEARCH",
|
||||||
|
status: "PENDING",
|
||||||
|
graph_status: null,
|
||||||
|
latency: null,
|
||||||
|
created_at: "2026-04-01T10:01:00Z",
|
||||||
|
updated_at: "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
getEvent: vi.fn().mockResolvedValue({
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: "SUCCEEDED",
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: "mem-abc-123",
|
||||||
|
event: "ADD",
|
||||||
|
user_id: "alice",
|
||||||
|
data: { memory: "User prefers dark mode" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
const pkg = _require('./package.json');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
define: {
|
||||||
|
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
const pkg = _require("./package.json") as { version: string };
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
VENV := .venv
|
||||||
|
PYTHON := $(VENV)/bin/python
|
||||||
|
PIP := $(VENV)/bin/pip
|
||||||
|
|
||||||
|
.PHONY: install dev lint format test build clean publish publish-test shell
|
||||||
|
|
||||||
|
$(VENV)/bin/activate:
|
||||||
|
python3 -m venv $(VENV)
|
||||||
|
$(PIP) install -U pip
|
||||||
|
|
||||||
|
install: $(VENV)/bin/activate
|
||||||
|
$(PIP) install -e .
|
||||||
|
|
||||||
|
dev: $(VENV)/bin/activate
|
||||||
|
$(PIP) install -e ".[dev]"
|
||||||
|
|
||||||
|
lint: dev
|
||||||
|
$(VENV)/bin/ruff check .
|
||||||
|
$(VENV)/bin/ruff format --check .
|
||||||
|
|
||||||
|
format: dev
|
||||||
|
$(VENV)/bin/ruff check --fix .
|
||||||
|
$(VENV)/bin/ruff format .
|
||||||
|
|
||||||
|
test: dev
|
||||||
|
$(VENV)/bin/pytest
|
||||||
|
|
||||||
|
build: clean $(VENV)/bin/activate
|
||||||
|
$(PIP) install hatch
|
||||||
|
$(VENV)/bin/hatch build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist/
|
||||||
|
|
||||||
|
publish: build
|
||||||
|
$(VENV)/bin/hatch publish
|
||||||
|
|
||||||
|
publish-test: build
|
||||||
|
$(VENV)/bin/hatch publish --repo test
|
||||||
|
|
||||||
|
shell: $(VENV)/bin/activate
|
||||||
|
@echo "Spawning a new shell with the virtual environment activated..."
|
||||||
|
@VIRTUAL_ENV=$(CURDIR)/$(VENV) PATH=$(CURDIR)/$(VENV)/bin:$$PATH exec $(SHELL)
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# mem0 CLI (Python)
|
||||||
|
|
||||||
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python **3.10+**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 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
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/python
|
||||||
|
python -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Run during development
|
||||||
|
python -m mem0_cli --help
|
||||||
|
mem0 add "test memory" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
1. Update `version` in `pyproject.toml`
|
||||||
|
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
|
||||||
|
|
||||||
|
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python **3.10+**
|
||||||
|
- `make` (optional — you can use plain Python commands instead)
|
||||||
|
|
||||||
|
All commands below should be run from the `python/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd python
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Using Make (recommended)
|
||||||
|
|
||||||
|
All `make` targets automatically create a virtual environment (`.venv/`) and install the required dependencies — no manual setup needed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the CLI in editable mode
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Install with dev tools (tests + linting)
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Python directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install -U pip
|
||||||
|
|
||||||
|
# Install in editable mode
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# With dev tools
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make targets
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
| ------------------- | ------------------------------------------------ |
|
||||||
|
| `make install` | Create venv and install the CLI (editable mode) |
|
||||||
|
| `make dev` | Create venv and install CLI + dev dependencies |
|
||||||
|
| `make test` | Run all tests (installs dev deps if needed) |
|
||||||
|
| `make lint` | Run linter and format check |
|
||||||
|
| `make format` | Auto-fix lint issues and format code |
|
||||||
|
| `make build` | Build distribution packages |
|
||||||
|
| `make clean` | Remove `dist/` |
|
||||||
|
| `make publish` | Build and publish to PyPI |
|
||||||
|
| `make publish-test` | Build and publish to Test PyPI |
|
||||||
|
| `make shell` | Open a new shell with the venv activated |
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Using Python directly
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
pytest tests/test_cli_integration.py
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
pytest -k test_help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make — drop into an activated shell
|
||||||
|
make shell
|
||||||
|
mem0 --help
|
||||||
|
|
||||||
|
# Using Python directly (with venv activated)
|
||||||
|
source .venv/bin/activate
|
||||||
|
mem0 --help
|
||||||
|
mem0 version
|
||||||
|
|
||||||
|
# Or run without activating
|
||||||
|
.venv/bin/mem0 --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make
|
||||||
|
make lint # check only
|
||||||
|
make format # auto-fix
|
||||||
|
|
||||||
|
# Using Python directly (with venv activated)
|
||||||
|
ruff check .
|
||||||
|
ruff format .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional extras
|
||||||
|
|
||||||
|
### OSS integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e ".[oss]"
|
||||||
|
```
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mem0-cli"
|
||||||
|
version = "0.2.1"
|
||||||
|
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
authors = [
|
||||||
|
{ name = "mem0.ai", email = "founders@mem0.ai" },
|
||||||
|
]
|
||||||
|
keywords = ["mem0", "memory", "ai", "agents", "cli"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Software Development :: Libraries",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"typer>=0.9.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
"httpx>=0.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
oss = ["mem0ai>=0.1.0"]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-asyncio>=0.21",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mem0 = "mem0_cli.app:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/mem0_cli"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["src/mem0_cli"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort (import sorting)
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"UP", # pyupgrade (modern Python syntax)
|
||||||
|
"B", # flake8-bugbear (common bugs)
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"RUF", # ruff-specific rules
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long — handled by formatter
|
||||||
|
"B008", # function call in default arg — required by Typer's Option/Argument pattern
|
||||||
|
"SIM108", # ternary operator — sometimes less readable
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["mem0_cli"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
docstring-code-format = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||||
|
|
||||||
|
__version__ = "0.2.1"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Allow running with `python -m mem0_cli`."""
|
||||||
|
|
||||||
|
from mem0_cli.app import main
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Backend abstraction layer for mem0 CLI."""
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend, get_backend
|
||||||
|
|
||||||
|
__all__ = ["Backend", "get_backend"]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""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]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_events(self) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_event(self, event_id: str) -> dict: ...
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend(config: Mem0Config) -> Backend:
|
||||||
|
"""Return the Platform backend."""
|
||||||
|
from mem0_cli.backend.platform import PlatformBackend
|
||||||
|
|
||||||
|
return PlatformBackend(config.platform)
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""Platform (SaaS) backend — communicates with api.mem0.ai."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.config import PlatformConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformBackend(Backend):
|
||||||
|
"""Backend that talks to the mem0 Platform API."""
|
||||||
|
|
||||||
|
def __init__(self, config: PlatformConfig) -> None:
|
||||||
|
self.config = config
|
||||||
|
self.base_url = config.base_url.rstrip("/")
|
||||||
|
self._client = httpx.Client(
|
||||||
|
base_url=self.base_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Token {config.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||||
|
resp = self._client.request(method, path, **kwargs)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||||
|
if resp.status_code == 404:
|
||||||
|
raise NotFoundError(f"Resource not found: {path}")
|
||||||
|
if resp.status_code == 400:
|
||||||
|
# Extract API error detail when available
|
||||||
|
try:
|
||||||
|
detail = resp.json().get("detail", resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = resp.text
|
||||||
|
raise APIError(f"Bad request to {path}: {detail}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.status_code == 204:
|
||||||
|
return {}
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
content: str | None = None,
|
||||||
|
messages: list[dict] | None = None,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
metadata: dict | None = None,
|
||||||
|
immutable: bool = False,
|
||||||
|
infer: bool = True,
|
||||||
|
expires: str | None = None,
|
||||||
|
categories: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
payload["messages"] = messages
|
||||||
|
elif content:
|
||||||
|
payload["messages"] = [{"role": "user", "content": content}]
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
payload["user_id"] = user_id
|
||||||
|
if agent_id:
|
||||||
|
payload["agent_id"] = agent_id
|
||||||
|
if app_id:
|
||||||
|
payload["app_id"] = app_id
|
||||||
|
if run_id:
|
||||||
|
payload["run_id"] = run_id
|
||||||
|
if metadata:
|
||||||
|
payload["metadata"] = metadata
|
||||||
|
if immutable:
|
||||||
|
payload["immutable"] = True
|
||||||
|
if not infer:
|
||||||
|
payload["infer"] = False
|
||||||
|
if expires:
|
||||||
|
payload["expiration_date"] = expires
|
||||||
|
if categories:
|
||||||
|
payload["categories"] = categories
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
|
||||||
|
return self._request("POST", "/v1/memories/", json=payload)
|
||||||
|
|
||||||
|
def _build_filters(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
extra_filters: dict | None = None,
|
||||||
|
) -> dict | None:
|
||||||
|
"""Build a filters dict for v2 API endpoints.
|
||||||
|
|
||||||
|
Entity IDs are ANDed (all provided IDs must match).
|
||||||
|
Extra filters (date ranges, categories) are also ANDed.
|
||||||
|
"""
|
||||||
|
# If caller passed a pre-built filter structure (e.g. --filter from CLI), use it directly
|
||||||
|
if extra_filters and ("AND" in extra_filters or "OR" in extra_filters):
|
||||||
|
return extra_filters
|
||||||
|
|
||||||
|
# Build AND conditions for entity IDs
|
||||||
|
and_conditions: list[dict[str, Any]] = []
|
||||||
|
if user_id:
|
||||||
|
and_conditions.append({"user_id": user_id})
|
||||||
|
if agent_id:
|
||||||
|
and_conditions.append({"agent_id": agent_id})
|
||||||
|
if app_id:
|
||||||
|
and_conditions.append({"app_id": app_id})
|
||||||
|
if run_id:
|
||||||
|
and_conditions.append({"run_id": run_id})
|
||||||
|
|
||||||
|
# Append any extra filters (dates, categories)
|
||||||
|
if extra_filters:
|
||||||
|
for k, v in extra_filters.items():
|
||||||
|
and_conditions.append({k: v})
|
||||||
|
|
||||||
|
if len(and_conditions) == 1:
|
||||||
|
return and_conditions[0]
|
||||||
|
elif and_conditions:
|
||||||
|
return {"AND": and_conditions}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
top_k: int = 10,
|
||||||
|
threshold: float = 0.3,
|
||||||
|
rerank: bool = False,
|
||||||
|
keyword: bool = False,
|
||||||
|
filters: dict | None = None,
|
||||||
|
fields: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]:
|
||||||
|
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
||||||
|
|
||||||
|
api_filters = self._build_filters(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
extra_filters=filters,
|
||||||
|
)
|
||||||
|
if api_filters:
|
||||||
|
payload["filters"] = api_filters
|
||||||
|
if rerank:
|
||||||
|
payload["rerank"] = True
|
||||||
|
if keyword:
|
||||||
|
payload["keyword_search"] = True
|
||||||
|
if fields:
|
||||||
|
payload["fields"] = fields
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
|
||||||
|
result = self._request("POST", "/v2/memories/search/", json=payload)
|
||||||
|
return (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("results", result.get("memories", []))
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, memory_id: str) -> dict:
|
||||||
|
return self._request("GET", f"/v1/memories/{memory_id}/")
|
||||||
|
|
||||||
|
def list_memories(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
category: str | None = None,
|
||||||
|
after: str | None = None,
|
||||||
|
before: str | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
params = {"page": str(page), "page_size": str(page_size)}
|
||||||
|
|
||||||
|
# Build filters for v2 API — entity IDs and date filters go inside "filters"
|
||||||
|
extra: dict[str, Any] = {}
|
||||||
|
if category:
|
||||||
|
extra["categories"] = {"contains": category}
|
||||||
|
if after:
|
||||||
|
extra["created_at"] = {**(extra.get("created_at", {})), "gte": after}
|
||||||
|
if before:
|
||||||
|
extra["created_at"] = {**(extra.get("created_at", {})), "lte": before}
|
||||||
|
|
||||||
|
api_filters = self._build_filters(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
extra_filters=extra if extra else None,
|
||||||
|
)
|
||||||
|
if api_filters:
|
||||||
|
payload["filters"] = api_filters
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
|
||||||
|
result = self._request("POST", "/v2/memories/", json=payload, params=params)
|
||||||
|
return (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("results", result.get("memories", []))
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||||
|
) -> dict:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
if content:
|
||||||
|
payload["text"] = content
|
||||||
|
if metadata:
|
||||||
|
payload["metadata"] = metadata
|
||||||
|
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||||
|
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
memory_id: str | None = None,
|
||||||
|
*,
|
||||||
|
all: bool = False,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
if all:
|
||||||
|
params: dict[str, str] = {}
|
||||||
|
if user_id:
|
||||||
|
params["user_id"] = user_id
|
||||||
|
if agent_id:
|
||||||
|
params["agent_id"] = agent_id
|
||||||
|
if app_id:
|
||||||
|
params["app_id"] = app_id
|
||||||
|
if run_id:
|
||||||
|
params["run_id"] = run_id
|
||||||
|
return self._request("DELETE", "/v1/memories/", params=params)
|
||||||
|
elif memory_id:
|
||||||
|
return self._request("DELETE", f"/v1/memories/{memory_id}/")
|
||||||
|
else:
|
||||||
|
raise ValueError("Either memory_id or --all is required")
|
||||||
|
|
||||||
|
def delete_entities(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
|
type_map = {
|
||||||
|
"user": user_id,
|
||||||
|
"agent": agent_id,
|
||||||
|
"app": app_id,
|
||||||
|
"run": run_id,
|
||||||
|
}
|
||||||
|
entities = {t: v for t, v in type_map.items() if v}
|
||||||
|
if not entities:
|
||||||
|
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||||
|
# Delete each provided entity via the v2 path-based endpoint
|
||||||
|
result: dict = {}
|
||||||
|
for entity_type, entity_id in entities.items():
|
||||||
|
result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
|
||||||
|
return result
|
||||||
|
|
||||||
|
def status(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Check connectivity using the ping endpoint."""
|
||||||
|
try:
|
||||||
|
self._request("GET", "/v1/ping/")
|
||||||
|
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
|
||||||
|
|
||||||
|
def list_events(self) -> list[dict]:
|
||||||
|
result = self._request("GET", "/v1/events/")
|
||||||
|
return result if isinstance(result, list) else result.get("results", [])
|
||||||
|
|
||||||
|
def get_event(self, event_id: str) -> dict:
|
||||||
|
return self._request("GET", f"/v1/event/{event_id}/")
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""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."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
||||||
|
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
||||||
|
|
||||||
|
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:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("✓", "[ok]")
|
||||||
|
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
||||||
|
from mem0_cli.state import get_current_command, is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
envelope = {
|
||||||
|
"status": "error",
|
||||||
|
"command": get_current_command(),
|
||||||
|
"error": message,
|
||||||
|
"data": None,
|
||||||
|
}
|
||||||
|
print(_json.dumps(envelope))
|
||||||
|
return
|
||||||
|
sym = _sym("✗", "[error]")
|
||||||
|
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||||
|
if hint:
|
||||||
|
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
||||||
|
|
||||||
|
|
||||||
|
def print_warning(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("⚠", "[warn]")
|
||||||
|
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_info(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("◆", "*")
|
||||||
|
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
||||||
|
In agent mode the spinner is suppressed entirely.
|
||||||
|
"""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
class _Ctx:
|
||||||
|
def __init__(self):
|
||||||
|
self.success_msg = ""
|
||||||
|
self.error_msg = ""
|
||||||
|
|
||||||
|
ctx = _Ctx()
|
||||||
|
if is_agent_mode():
|
||||||
|
try:
|
||||||
|
yield ctx
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
return
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
||||||
|
yield ctx
|
||||||
|
except Exception:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
if ctx.error_msg:
|
||||||
|
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
||||||
|
if "Authentication failed" in ctx.error_msg:
|
||||||
|
_err.print(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
||||||
|
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
parts = []
|
||||||
|
for key, val in ids.items():
|
||||||
|
if val:
|
||||||
|
parts.append(f"{key}={val}")
|
||||||
|
if parts:
|
||||||
|
scope_str = ", ".join(parts)
|
||||||
|
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_version() -> str:
|
||||||
|
from mem0_cli import __version__
|
||||||
|
|
||||||
|
return __version__
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""CLI command modules."""
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""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_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config show")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
format_agent_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."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config get")
|
||||||
|
config = load_config()
|
||||||
|
value = get_nested_value(config, key)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
|
return
|
||||||
|
|
||||||
|
display_value = (
|
||||||
|
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config get", data={"key": key, "value": display_value}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print(display_value)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_set(key: str, value: str) -> None:
|
||||||
|
"""Set a config value."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config set")
|
||||||
|
config = load_config()
|
||||||
|
if set_nested_value(config, key, value):
|
||||||
|
save_config(config)
|
||||||
|
display = redact_key(value) if "key" in key else value
|
||||||
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config set", data={"key": key, "value": display}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(console, f"{key} = {display}")
|
||||||
|
else:
|
||||||
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""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_agent_envelope, 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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
valid_types = {"users", "agents", "apps", "runs"}
|
||||||
|
if entity_type not in valid_types:
|
||||||
|
print_error(
|
||||||
|
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
|
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 == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
return
|
||||||
|
|
||||||
|
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)."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if not any([user_id, agent_id, app_id, run_id]):
|
||||||
|
print_error(
|
||||||
|
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
scope_parts = []
|
||||||
|
if user_id:
|
||||||
|
scope_parts.append(f"user={user_id}")
|
||||||
|
if agent_id:
|
||||||
|
scope_parts.append(f"agent={agent_id}")
|
||||||
|
if app_id:
|
||||||
|
scope_parts.append(f"app={app_id}")
|
||||||
|
if run_id:
|
||||||
|
scope_parts.append(f"run={run_id}")
|
||||||
|
scope_str = ", ".join(scope_parts)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print_info(console, f"Would delete entity {scope_str} and all its memories.")
|
||||||
|
print_info(console, "No changes made (dry run).")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
confirm = typer.confirm(
|
||||||
|
f"\n \u26a0 Delete entity {scope_str} 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
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity delete",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Event commands: list and status."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
ACCENT_COLOR,
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
WARNING_COLOR,
|
||||||
|
print_info,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
from mem0_cli.output import format_agent_envelope, format_json
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
_STATUS_STYLE = {
|
||||||
|
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
|
||||||
|
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
|
||||||
|
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
|
||||||
|
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _status_styled(status: str) -> str:
|
||||||
|
return _STATUS_STYLE.get(status.upper(), status)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
|
||||||
|
"""List recent background events."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching events...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.list_events()
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No events found.")
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
header_style=f"bold {ACCENT_COLOR}",
|
||||||
|
row_styles=["", "dim"],
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
|
||||||
|
table.add_column("Type", max_width=14)
|
||||||
|
table.add_column("Status", max_width=12)
|
||||||
|
table.add_column("Latency", max_width=10, justify="right")
|
||||||
|
table.add_column("Created", max_width=20)
|
||||||
|
|
||||||
|
for ev in results:
|
||||||
|
ev_id = str(ev.get("id", ""))[:8]
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
|
||||||
|
"""Get the status of a specific background event."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching event...") as _ts:
|
||||||
|
try:
|
||||||
|
ev = backend.get_event(event_id)
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event status",
|
||||||
|
data=ev,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, ev)
|
||||||
|
return
|
||||||
|
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
|
||||||
|
results = ev.get("results")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||||
|
|
||||||
|
if results:
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
|
||||||
|
for r in results:
|
||||||
|
mem_id = str(r.get("id", ""))[:8]
|
||||||
|
data = r.get("data", {})
|
||||||
|
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
|
||||||
|
ev_name = str(r.get("event", ""))
|
||||||
|
user = str(r.get("user_id", ""))
|
||||||
|
detail = f"{ev_name} {memory}"
|
||||||
|
if user:
|
||||||
|
detail += f" [{DIM_COLOR}](user_id={user})[/]"
|
||||||
|
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Event Status[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
"""mem0 init — interactive setup wizard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
print_banner,
|
||||||
|
print_error,
|
||||||
|
print_info,
|
||||||
|
print_success,
|
||||||
|
)
|
||||||
|
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_secret(label: str) -> str:
|
||||||
|
"""Prompt for a secret value, echoing '*' for each character typed."""
|
||||||
|
sys.stdout.write(label)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
chars: list[str] = []
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ch = msvcrt.getwch()
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
if ch == "\x03":
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
if ch in ("\x08", "\x7f"): # backspace
|
||||||
|
if chars:
|
||||||
|
chars.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
chars.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
import termios
|
||||||
|
import tty
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
while True:
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\r\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
if ch == "\x03":
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
if ch in ("\x7f", "\x08"): # backspace/delete
|
||||||
|
if chars:
|
||||||
|
chars.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
sys.stdout.flush()
|
||||||
|
elif ch == "\x15": # Ctrl+U — clear line
|
||||||
|
sys.stdout.write("\b \b" * len(chars))
|
||||||
|
sys.stdout.flush()
|
||||||
|
chars = []
|
||||||
|
elif ch >= " ": # ignore other control characters
|
||||||
|
chars.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
|
return "".join(chars)
|
||||||
|
|
||||||
|
|
||||||
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_email(email: str) -> None:
|
||||||
|
"""Exit with an error if *email* doesn't look like a valid address."""
|
||||||
|
if not _EMAIL_RE.match(email):
|
||||||
|
print_error(err_console, f"Invalid email address: {email!r}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _email_login(
|
||||||
|
email: str,
|
||||||
|
code: str | None,
|
||||||
|
base_url: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Run the email verification code login flow.
|
||||||
|
|
||||||
|
Returns the parsed JSON response from the verify endpoint.
|
||||||
|
The caller expects at minimum an ``api_key`` field.
|
||||||
|
"""
|
||||||
|
url = base_url.rstrip("/")
|
||||||
|
|
||||||
|
with httpx.Client(timeout=30.0) as client:
|
||||||
|
# If code is already provided, skip sending — user already has a code
|
||||||
|
if not code:
|
||||||
|
# Step 1: Request verification code
|
||||||
|
resp = client.post(
|
||||||
|
f"{url}/api/v1/auth/email_code/",
|
||||||
|
json={"email": email},
|
||||||
|
)
|
||||||
|
if resp.status_code == 429:
|
||||||
|
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
try:
|
||||||
|
detail = resp.json().get("error", resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = resp.text
|
||||||
|
print_error(err_console, f"Failed to send code: {detail}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
print_success(console, "Verification code sent! Check your email.")
|
||||||
|
|
||||||
|
# Step 2: Get code from user
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"No --code provided and terminal is non-interactive.",
|
||||||
|
hint="Run: mem0 init --email <email> --code <code>",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
console.print()
|
||||||
|
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
|
||||||
|
if not code:
|
||||||
|
print_error(err_console, "Code is required.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Step 3: Verify code
|
||||||
|
resp = client.post(
|
||||||
|
f"{url}/api/v1/auth/email_code/verify/",
|
||||||
|
json={"email": email, "code": code.strip()},
|
||||||
|
)
|
||||||
|
if resp.status_code == 429:
|
||||||
|
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
try:
|
||||||
|
detail = resp.json().get("error", resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = resp.text
|
||||||
|
print_error(err_console, f"Verification failed: {detail}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
def run_init(
|
||||||
|
*,
|
||||||
|
api_key: str | None = None,
|
||||||
|
user_id: str | None = None,
|
||||||
|
email: str | None = None,
|
||||||
|
code: str | None = None,
|
||||||
|
force: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
|
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||||
|
(non-interactive mode). When running in a non-TTY without the required
|
||||||
|
flags, an error message is printed.
|
||||||
|
"""
|
||||||
|
config = Mem0Config()
|
||||||
|
|
||||||
|
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||||
|
|
||||||
|
if code and not email:
|
||||||
|
print_error(err_console, "--code requires --email.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Warn if an existing config with an API key would be overwritten
|
||||||
|
if not force and CONFIG_FILE.exists():
|
||||||
|
existing = load_config()
|
||||||
|
if existing.platform.api_key:
|
||||||
|
from mem0_cli.config import redact_key
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
|
||||||
|
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
|
||||||
|
)
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
|
||||||
|
if not confirm:
|
||||||
|
print_info(console, "Cancelled. Use --force to skip this check.")
|
||||||
|
raise typer.Exit(0)
|
||||||
|
else:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"Existing config would be overwritten.",
|
||||||
|
hint="Use --force to overwrite.",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# ── Email login flow ──────────────────────────────────────────────
|
||||||
|
if email:
|
||||||
|
if api_key:
|
||||||
|
print_error(err_console, "Cannot use both --api-key and --email.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
email = email.strip().lower()
|
||||||
|
_validate_email(email)
|
||||||
|
|
||||||
|
print_banner(console)
|
||||||
|
console.print()
|
||||||
|
print_info(console, f"Logging in as {email}...\n")
|
||||||
|
|
||||||
|
result = _email_login(email, code, base_url)
|
||||||
|
|
||||||
|
api_key_val = result.get("api_key")
|
||||||
|
if not api_key_val:
|
||||||
|
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
config.platform.api_key = api_key_val
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
config.defaults.user_id = (
|
||||||
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
|
||||||
|
console.print()
|
||||||
|
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── API key flow (existing) ───────────────────────────────────────
|
||||||
|
|
||||||
|
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||||
|
if not sys.stdin.isatty():
|
||||||
|
if not api_key:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"Non-interactive terminal detected and --api-key is required.",
|
||||||
|
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
|
||||||
|
# Fully non-interactive when both flags provided
|
||||||
|
if api_key and user_id:
|
||||||
|
config.platform.api_key = api_key
|
||||||
|
config.defaults.user_id = user_id
|
||||||
|
_validate_platform(config)
|
||||||
|
save_config(config)
|
||||||
|
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_banner(console)
|
||||||
|
console.print()
|
||||||
|
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||||
|
|
||||||
|
# If no flags at all, ask user how they want to authenticate
|
||||||
|
if not api_key:
|
||||||
|
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
|
||||||
|
console.print()
|
||||||
|
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
console.print()
|
||||||
|
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
|
||||||
|
if not email_addr:
|
||||||
|
print_error(err_console, "Email is required.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
email_addr = email_addr.strip().lower()
|
||||||
|
_validate_email(email_addr)
|
||||||
|
print_info(console, f"Logging in as {email_addr}...\n")
|
||||||
|
|
||||||
|
result = _email_login(email_addr, None, base_url)
|
||||||
|
|
||||||
|
api_key_val = result.get("api_key")
|
||||||
|
if not api_key_val:
|
||||||
|
print_error(
|
||||||
|
err_console, "Auth succeeded but no API key was returned. Contact support."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
config.platform.api_key = api_key_val
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
config.defaults.user_id = (
|
||||||
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
|
||||||
|
console.print()
|
||||||
|
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# API key flow
|
||||||
|
if api_key:
|
||||||
|
config.platform.api_key = api_key
|
||||||
|
else:
|
||||||
|
_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")
|
||||||
|
|
||||||
|
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
user_id = Prompt.ask(
|
||||||
|
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||||
|
default=_default_user,
|
||||||
|
)
|
||||||
|
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="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, f"Connection test failed: {e}")
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
"""Memory CRUD commands: add, search, get, list, update, delete."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat as _stat_mod
|
||||||
|
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_agent_envelope,
|
||||||
|
format_json,
|
||||||
|
format_memories_table,
|
||||||
|
format_memories_text,
|
||||||
|
format_single_memory,
|
||||||
|
print_result_summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _stdin_is_piped() -> bool:
|
||||||
|
"""Return True only when stdin is an actual pipe or file redirect."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||||
|
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(
|
||||||
|
backend: Backend,
|
||||||
|
text: str | None,
|
||||||
|
*,
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("add")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
msgs = None
|
||||||
|
content = text
|
||||||
|
|
||||||
|
# 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 only if stdin is an actual pipe or file redirect
|
||||||
|
elif not content and _stdin_is_piped():
|
||||||
|
content = sys.stdin.read().strip()
|
||||||
|
|
||||||
|
if not content and not msgs:
|
||||||
|
print_error(
|
||||||
|
err_console, "No content provided. Pass text, --messages, --file, or pipe via stdin."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
meta = None
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
meta = json.loads(metadata)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print_error(err_console, "Invalid JSON in --metadata.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
cats = None
|
||||||
|
if categories:
|
||||||
|
try:
|
||||||
|
cats = json.loads(categories)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
cats = [c.strip() for c in categories.split(",")]
|
||||||
|
|
||||||
|
# Validate --expires
|
||||||
|
if expires:
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
|
||||||
|
print_error(
|
||||||
|
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
if date.fromisoformat(expires) <= date.today():
|
||||||
|
print_error(err_console, "--expires date must be in the future.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
with timed_status(err_console, "Adding memory...") as ts:
|
||||||
|
try:
|
||||||
|
result = backend.add(
|
||||||
|
content=content,
|
||||||
|
messages=msgs,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
metadata=meta,
|
||||||
|
immutable=immutable,
|
||||||
|
infer=not no_infer,
|
||||||
|
expires=expires,
|
||||||
|
categories=cats,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||||
|
results_list = result if isinstance(result, list) else result.get("results", [result])
|
||||||
|
seen_events: set[str] = set()
|
||||||
|
deduped: list[dict] = []
|
||||||
|
for r in results_list:
|
||||||
|
if r.get("status") == "PENDING":
|
||||||
|
eid = r.get("event_id", "")
|
||||||
|
if eid and eid in seen_events:
|
||||||
|
continue
|
||||||
|
if eid:
|
||||||
|
seen_events.add(eid)
|
||||||
|
deduped.append(r)
|
||||||
|
# Write back so downstream formatters see deduplicated data
|
||||||
|
if isinstance(result, dict) and "results" in result:
|
||||||
|
result = {**result, "results": deduped}
|
||||||
|
else:
|
||||||
|
result = deduped
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="add",
|
||||||
|
data=deduped,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(deduped),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_add_result(console, result, output)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||||
|
count = len(deduped)
|
||||||
|
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
|
||||||
|
if all_pending:
|
||||||
|
print_success(
|
||||||
|
console,
|
||||||
|
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(
|
||||||
|
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||||
|
)
|
||||||
|
format_add_result(console, result, output)
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("search")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
filters = None
|
||||||
|
if filter_json:
|
||||||
|
try:
|
||||||
|
filters = json.loads(filter_json)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print_error(err_console, "Invalid JSON in --filter.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
field_list = None
|
||||||
|
if fields:
|
||||||
|
field_list = [f.strip() for f in fields.split(",")]
|
||||||
|
|
||||||
|
if top_k < 1:
|
||||||
|
print_error(err_console, "--top-k must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if not (0.0 <= threshold <= 1.0):
|
||||||
|
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Searching memories...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.search(
|
||||||
|
query,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
top_k=top_k,
|
||||||
|
threshold=threshold,
|
||||||
|
rerank=rerank,
|
||||||
|
keyword=keyword,
|
||||||
|
filters=filters,
|
||||||
|
fields=field_list,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="search",
|
||||||
|
data=results,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
elif output == "table":
|
||||||
|
if results:
|
||||||
|
format_memories_table(console, results, show_score=True)
|
||||||
|
print_result_summary(
|
||||||
|
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found matching your query.")
|
||||||
|
console.print()
|
||||||
|
else:
|
||||||
|
if results:
|
||||||
|
format_memories_text(console, results)
|
||||||
|
print_result_summary(
|
||||||
|
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found matching your query.")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||||
|
"""Get a specific memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("get")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.get(memory_id)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(console, command="get", data=result)
|
||||||
|
else:
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if page_size < 1:
|
||||||
|
print_error(err_console, "--page-size must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if page < 1:
|
||||||
|
print_error(err_console, "--page must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Listing memories...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.list_memories(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
category=category,
|
||||||
|
after=after,
|
||||||
|
before=before,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="list",
|
||||||
|
data=results,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "table":
|
||||||
|
if results:
|
||||||
|
format_memories_table(console, results)
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("update")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
meta = None
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
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 == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="update",
|
||||||
|
data=result,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete(
|
||||||
|
backend: Backend,
|
||||||
|
memory_id: str,
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
force: bool = False,
|
||||||
|
output: str,
|
||||||
|
) -> None:
|
||||||
|
"""Delete a single memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if dry_run:
|
||||||
|
# Fetch and display what would be deleted
|
||||||
|
try:
|
||||||
|
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 == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete",
|
||||||
|
data={"id": memory_id, "deleted": True},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete-all")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if all_:
|
||||||
|
# Project-wide wipe using wildcard entity IDs
|
||||||
|
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||||
|
|
||||||
|
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 == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True, "scope": "project"},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
if isinstance(result, dict) and "message" in result:
|
||||||
|
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
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
if isinstance(result, dict) and "message" in result:
|
||||||
|
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||||
|
else:
|
||||||
|
print_success(console, f"All matching memories deleted ({_elapsed:.2f}s)")
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""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_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Checking connection...") as _ts:
|
||||||
|
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="status",
|
||||||
|
data={
|
||||||
|
"connected": result.get("connected", False),
|
||||||
|
"backend": result.get("backend", "?"),
|
||||||
|
"base_url": result.get("base_url", ""),
|
||||||
|
},
|
||||||
|
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']}")
|
||||||
|
if "Authentication failed" in str(result["error"]):
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
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_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("import")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(Path(file_path).read_text())
|
||||||
|
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 in ("json", "agent"):
|
||||||
|
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="import",
|
||||||
|
data={"added": added, "failed": failed},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print_success(err_console, f"Imported {added} memories ({_elapsed:.2f}s)")
|
||||||
|
if failed:
|
||||||
|
print_error(err_console, f"{failed} memories failed to import.")
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
"""Configuration management for mem0 CLI.
|
||||||
|
|
||||||
|
Config precedence (highest to lowest):
|
||||||
|
1. CLI flags (--api-key, --base-url, etc.)
|
||||||
|
2. Environment variables (MEM0_API_KEY, etc.)
|
||||||
|
3. Config file (~/.mem0/config.json)
|
||||||
|
4. Defaults
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
CONFIG_DIR = Path.home() / ".mem0"
|
||||||
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||||
|
|
||||||
|
DEFAULT_BASE_URL = "https://api.mem0.ai"
|
||||||
|
CONFIG_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlatformConfig:
|
||||||
|
api_key: str = ""
|
||||||
|
base_url: str = DEFAULT_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DefaultsConfig:
|
||||||
|
user_id: str = ""
|
||||||
|
agent_id: str = ""
|
||||||
|
app_id: str = ""
|
||||||
|
run_id: str = ""
|
||||||
|
enable_graph: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mem0Config:
|
||||||
|
version: int = CONFIG_VERSION
|
||||||
|
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||||
|
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||||
|
|
||||||
|
|
||||||
|
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||||
|
"api_key": "platform.api_key",
|
||||||
|
"base_url": "platform.base_url",
|
||||||
|
"user_id": "defaults.user_id",
|
||||||
|
"agent_id": "defaults.agent_id",
|
||||||
|
"app_id": "defaults.app_id",
|
||||||
|
"run_id": "defaults.run_id",
|
||||||
|
"enable_graph": "defaults.enable_graph",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_config_dir() -> Path:
|
||||||
|
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700
|
||||||
|
return CONFIG_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> Mem0Config:
|
||||||
|
"""Load config from file, applying env var overrides."""
|
||||||
|
config = Mem0Config()
|
||||||
|
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
with open(CONFIG_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
config.version = data.get("version", CONFIG_VERSION)
|
||||||
|
|
||||||
|
plat = data.get("platform", {})
|
||||||
|
config.platform.api_key = plat.get("api_key", "")
|
||||||
|
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||||
|
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
config.defaults.user_id = defaults.get("user_id", "")
|
||||||
|
config.defaults.agent_id = defaults.get("agent_id", "")
|
||||||
|
config.defaults.app_id = defaults.get("app_id", "")
|
||||||
|
config.defaults.run_id = defaults.get("run_id", "")
|
||||||
|
config.defaults.enable_graph = defaults.get("enable_graph", False)
|
||||||
|
|
||||||
|
# Environment variable overrides
|
||||||
|
env_key = os.environ.get("MEM0_API_KEY")
|
||||||
|
if env_key:
|
||||||
|
config.platform.api_key = env_key
|
||||||
|
|
||||||
|
env_base = os.environ.get("MEM0_BASE_URL")
|
||||||
|
if env_base:
|
||||||
|
config.platform.base_url = env_base
|
||||||
|
|
||||||
|
env_user_id = os.environ.get("MEM0_USER_ID")
|
||||||
|
if env_user_id:
|
||||||
|
config.defaults.user_id = env_user_id
|
||||||
|
|
||||||
|
env_agent_id = os.environ.get("MEM0_AGENT_ID")
|
||||||
|
if env_agent_id:
|
||||||
|
config.defaults.agent_id = env_agent_id
|
||||||
|
|
||||||
|
env_app_id = os.environ.get("MEM0_APP_ID")
|
||||||
|
if env_app_id:
|
||||||
|
config.defaults.app_id = env_app_id
|
||||||
|
|
||||||
|
env_run_id = os.environ.get("MEM0_RUN_ID")
|
||||||
|
if env_run_id:
|
||||||
|
config.defaults.run_id = env_run_id
|
||||||
|
|
||||||
|
env_graph = os.environ.get("MEM0_ENABLE_GRAPH")
|
||||||
|
if env_graph:
|
||||||
|
config.defaults.enable_graph = env_graph.lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: Mem0Config) -> None:
|
||||||
|
"""Write config to disk with secure permissions."""
|
||||||
|
ensure_config_dir()
|
||||||
|
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"version": config.version,
|
||||||
|
"defaults": {
|
||||||
|
"user_id": config.defaults.user_id,
|
||||||
|
"agent_id": config.defaults.agent_id,
|
||||||
|
"app_id": config.defaults.app_id,
|
||||||
|
"run_id": config.defaults.run_id,
|
||||||
|
"enable_graph": config.defaults.enable_graph,
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"api_key": config.platform.api_key,
|
||||||
|
"base_url": config.platform.base_url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||||
|
|
||||||
|
|
||||||
|
def redact_key(key: str) -> str:
|
||||||
|
"""Redact an API key for display: m0-xxx...xxx"""
|
||||||
|
if not key:
|
||||||
|
return "(not set)"
|
||||||
|
if len(key) <= 8:
|
||||||
|
return key[:2] + "***"
|
||||||
|
return key[:4] + "..." + key[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||||
|
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
|
||||||
|
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
obj: Any = config
|
||||||
|
for part in parts:
|
||||||
|
if hasattr(obj, part):
|
||||||
|
obj = getattr(obj, part)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||||
|
"""Set a config value by dotted path. Returns True on success."""
|
||||||
|
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
obj: Any = config
|
||||||
|
for part in parts[:-1]:
|
||||||
|
if hasattr(obj, part):
|
||||||
|
obj = getattr(obj, part)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
final_key = parts[-1]
|
||||||
|
if not hasattr(obj, final_key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
current = getattr(obj, final_key)
|
||||||
|
# Type coercion
|
||||||
|
if isinstance(current, bool):
|
||||||
|
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||||
|
elif isinstance(current, int):
|
||||||
|
value = int(value) # type: ignore[assignment]
|
||||||
|
|
||||||
|
setattr(obj, final_key, value)
|
||||||
|
return True
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
"""Output formatting for mem0 CLI — text, JSON, table, quiet modes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, SUCCESS_COLOR, _sym
|
||||||
|
|
||||||
|
|
||||||
|
def format_memories_text(console: Console, memories: list[dict], title: str = "memories") -> None:
|
||||||
|
"""Render memories in human-friendly text mode."""
|
||||||
|
count = len(memories)
|
||||||
|
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
|
||||||
|
|
||||||
|
for i, mem in enumerate(memories, 1):
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
mem_id = mem.get("id", "")[:8]
|
||||||
|
score = mem.get("score")
|
||||||
|
created = _format_date(mem.get("created_at"))
|
||||||
|
category = mem.get("categories", [None])
|
||||||
|
if isinstance(category, list):
|
||||||
|
category = category[0] if category else None
|
||||||
|
|
||||||
|
line = Text()
|
||||||
|
line.append(f" {i}. ", style="bold")
|
||||||
|
line.append(memory_text, style="white")
|
||||||
|
console.print(line)
|
||||||
|
|
||||||
|
details = []
|
||||||
|
if score is not None:
|
||||||
|
details.append(f"Score: {score:.2f}")
|
||||||
|
if mem_id:
|
||||||
|
details.append(f"ID: {mem_id}")
|
||||||
|
if created:
|
||||||
|
details.append(f"Created: {created}")
|
||||||
|
if category:
|
||||||
|
details.append(f"Category: {category}")
|
||||||
|
|
||||||
|
if details:
|
||||||
|
detail_str = " · ".join(details)
|
||||||
|
console.print(f" [{DIM_COLOR}]{detail_str}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_memories_table(
|
||||||
|
console: Console, memories: list[dict], *, show_score: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Render memories in a rich table."""
|
||||||
|
table = Table(
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
header_style=f"bold {ACCENT_COLOR}",
|
||||||
|
row_styles=["", "dim"],
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
|
||||||
|
if show_score:
|
||||||
|
table.add_column("Score", max_width=7, justify="right")
|
||||||
|
table.add_column("Memory", max_width=50, no_wrap=False)
|
||||||
|
table.add_column("Category", max_width=14)
|
||||||
|
table.add_column("Created", max_width=12)
|
||||||
|
|
||||||
|
for mem in memories:
|
||||||
|
mem_id = mem.get("id", "")
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
if len(memory_text) > 60:
|
||||||
|
memory_text = memory_text[:57] + "..."
|
||||||
|
categories = mem.get("categories", [])
|
||||||
|
if isinstance(categories, list) and categories:
|
||||||
|
cat = (
|
||||||
|
categories[0]
|
||||||
|
if len(categories) == 1
|
||||||
|
else f"{categories[0]} (+{len(categories) - 1})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cat = "—"
|
||||||
|
created = _format_date(mem.get("created_at")) or "—"
|
||||||
|
if show_score:
|
||||||
|
score = mem.get("score")
|
||||||
|
score_str = f"{score:.2f}" if score is not None else "—"
|
||||||
|
table.add_row(mem_id, score_str, memory_text, cat, created)
|
||||||
|
else:
|
||||||
|
table.add_row(mem_id, memory_text, cat, created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_json(console: Console, data: Any) -> None:
|
||||||
|
"""Output data as pretty-printed JSON."""
|
||||||
|
console.print_json(json.dumps(data, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def format_single_memory(console: Console, mem: dict, output: str = "text") -> None:
|
||||||
|
"""Format a single memory for display."""
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, mem)
|
||||||
|
return
|
||||||
|
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
mem_id = mem.get("id", "")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" [white bold]{memory_text}[/]")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if mem_id:
|
||||||
|
lines.append(f" [{DIM_COLOR}]ID:[/] {mem_id}")
|
||||||
|
created = _format_date(mem.get("created_at"))
|
||||||
|
if created:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||||
|
updated = _format_date(mem.get("updated_at"))
|
||||||
|
if updated:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||||
|
meta = mem.get("metadata")
|
||||||
|
if meta:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Metadata:[/] {json.dumps(meta)}")
|
||||||
|
categories = mem.get("categories")
|
||||||
|
if categories:
|
||||||
|
cat_str = ", ".join(categories) if isinstance(categories, list) else categories
|
||||||
|
lines.append(f" [{DIM_COLOR}]Categories:[/] {cat_str}")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Memory[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_add_result(console: Console, result: dict | list, output: str = "text") -> None:
|
||||||
|
"""Format the result of an add operation."""
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
return
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
# result from API is typically {"results": [...]}
|
||||||
|
results = result if isinstance(result, list) else result.get("results", [result])
|
||||||
|
if not results:
|
||||||
|
console.print(f" [{DIM_COLOR}]No memories extracted.[/]")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
seen_pending_events: set[str] = set()
|
||||||
|
for r in results:
|
||||||
|
# Detect async PENDING response from Platform API
|
||||||
|
if r.get("status") == "PENDING":
|
||||||
|
event_id = r.get("event_id", "")
|
||||||
|
# Deduplicate PENDING entries with the same event_id
|
||||||
|
if event_id and event_id in seen_pending_events:
|
||||||
|
continue
|
||||||
|
if event_id:
|
||||||
|
seen_pending_events.add(event_id)
|
||||||
|
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
||||||
|
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
||||||
|
parts.append("[white]Processing in background[/]")
|
||||||
|
console.print(" ".join(parts))
|
||||||
|
if event_id:
|
||||||
|
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
|
||||||
|
continue
|
||||||
|
|
||||||
|
event = r.get("event", "ADD")
|
||||||
|
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 sanitize_agent_data(command: str, data: Any) -> Any:
|
||||||
|
"""Project API response data to minimal relevant fields for agent consumption."""
|
||||||
|
|
||||||
|
def pick(obj: dict, keys: list) -> dict:
|
||||||
|
return {k: obj[k] for k in keys if k in obj}
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "add":
|
||||||
|
items = data if isinstance(data, list) else [data]
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if item.get("status") == "PENDING":
|
||||||
|
result.append(pick(item, ["status", "event_id"]))
|
||||||
|
else:
|
||||||
|
result.append(pick(item, ["id", "memory", "event"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "search":
|
||||||
|
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "list":
|
||||||
|
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "get":
|
||||||
|
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||||
|
|
||||||
|
if command == "update":
|
||||||
|
return pick(data, ["id", "memory"])
|
||||||
|
|
||||||
|
if command in ("delete", "delete-all", "entity delete"):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "entity list":
|
||||||
|
result = []
|
||||||
|
for r in data:
|
||||||
|
item = pick(r, ["type", "count"])
|
||||||
|
item["name"] = r.get("name") or r.get("id", "")
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "event list":
|
||||||
|
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
|
||||||
|
|
||||||
|
if command == "event status":
|
||||||
|
ev = data
|
||||||
|
raw_results = ev.get("results") or []
|
||||||
|
sanitized_results = []
|
||||||
|
for r in raw_results:
|
||||||
|
nested = r.get("data") or {}
|
||||||
|
memory = nested.get("memory") if isinstance(nested, dict) else None
|
||||||
|
sanitized_results.append(
|
||||||
|
{
|
||||||
|
"id": r.get("id"),
|
||||||
|
"event": r.get("event"),
|
||||||
|
"user_id": r.get("user_id"),
|
||||||
|
"memory": memory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
|
||||||
|
result["results"] = sanitized_results
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Pass-through: status, import, config show/get/set
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def format_agent_envelope(
|
||||||
|
console: Console,
|
||||||
|
*,
|
||||||
|
command: str,
|
||||||
|
data: Any,
|
||||||
|
duration_ms: int | None = None,
|
||||||
|
scope: dict | None = None,
|
||||||
|
count: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
|
||||||
|
envelope: dict[str, Any] = {
|
||||||
|
"status": "success",
|
||||||
|
"command": command,
|
||||||
|
}
|
||||||
|
if duration_ms is not None:
|
||||||
|
envelope["duration_ms"] = duration_ms
|
||||||
|
if scope:
|
||||||
|
filtered = {k: v for k, v in scope.items() if v}
|
||||||
|
if filtered:
|
||||||
|
envelope["scope"] = filtered
|
||||||
|
if count is not None:
|
||||||
|
envelope["count"] = count
|
||||||
|
envelope["data"] = sanitize_agent_data(command, data)
|
||||||
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def print_result_summary(
|
||||||
|
console: Console,
|
||||||
|
count: int,
|
||||||
|
*,
|
||||||
|
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}={v}" for k, v in scope_ids.items() if v]
|
||||||
|
if scope_parts:
|
||||||
|
parts.append(", ".join(scope_parts))
|
||||||
|
if duration_secs is not None:
|
||||||
|
parts.append(f"{duration_secs:.2f}s")
|
||||||
|
|
||||||
|
summary = " · ".join(parts)
|
||||||
|
console.print(f" [{DIM_COLOR}]{summary}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(dt_str: str | None) -> str | None:
|
||||||
|
if not dt_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
||||||
|
return dt.strftime("%Y-%m-%d")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return str(dt_str)[:10] if dt_str else None
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Agent mode state — set by the root callback, read by commands and branding."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
_agent_mode: bool = False
|
||||||
|
_current_command: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_agent_mode() -> bool:
|
||||||
|
return _agent_mode
|
||||||
|
|
||||||
|
|
||||||
|
def set_agent_mode(val: bool) -> None:
|
||||||
|
global _agent_mode
|
||||||
|
_agent_mode = val
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_command() -> str:
|
||||||
|
return _current_command
|
||||||
|
|
||||||
|
|
||||||
|
def set_current_command(name: str) -> None:
|
||||||
|
global _current_command
|
||||||
|
_current_command = name
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""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},
|
||||||
|
]
|
||||||
|
backend.list_events.return_value = [
|
||||||
|
{
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "evt-def-456-ghi-789",
|
||||||
|
"event_type": "SEARCH",
|
||||||
|
"status": "PENDING",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": None,
|
||||||
|
"created_at": "2026-04-01T10:01:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
backend.get_event.return_value = {
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": "SUCCEEDED",
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "mem-abc-123",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"data": {"memory": "User prefers dark mode"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_config():
|
||||||
|
"""Return a sample config object."""
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "m0-test-key-12345678"
|
||||||
|
config.platform.base_url = "https://api.mem0.ai"
|
||||||
|
return config
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for branding and output helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from mem0_cli.branding import print_banner, print_error, print_info, print_success, print_warning
|
||||||
|
|
||||||
|
|
||||||
|
def _make_console() -> tuple[Console, StringIO]:
|
||||||
|
buf = StringIO()
|
||||||
|
return Console(file=buf, force_terminal=False, no_color=True, width=80), buf
|
||||||
|
|
||||||
|
|
||||||
|
class TestBranding:
|
||||||
|
def test_print_banner(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_banner(console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Banner contains the mem0 ASCII art and tagline
|
||||||
|
assert "Memory Layer" in output or "mem" in output.lower()
|
||||||
|
|
||||||
|
def test_print_success(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_success(console, "It worked!")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "It worked!" in output
|
||||||
|
|
||||||
|
def test_print_error(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_error(console, "Something failed", hint="Try this fix")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Something failed" in output
|
||||||
|
assert "Try this fix" in output
|
||||||
|
|
||||||
|
def test_print_error_no_hint(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_error(console, "Failed")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Failed" in output
|
||||||
|
|
||||||
|
def test_print_warning(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_warning(console, "Watch out")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Watch out" in output
|
||||||
|
|
||||||
|
def test_print_info(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_info(console, "FYI")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "FYI" in output
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
"""Integration tests — invoke CLI as subprocess to test end-to-end.
|
||||||
|
|
||||||
|
These tests launch the CLI as a real subprocess, so they must manage
|
||||||
|
environment isolation themselves (monkeypatch doesn't cross process
|
||||||
|
boundaries).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_ansi(text: str) -> str:
|
||||||
|
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
|
||||||
|
return _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(
|
||||||
|
args: list[str],
|
||||||
|
env_override: dict | None = None,
|
||||||
|
home_dir: str | None = None,
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""Run mem0 CLI command and capture output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: CLI arguments.
|
||||||
|
env_override: Extra env vars to set.
|
||||||
|
home_dir: If provided, set HOME to this path so the subprocess
|
||||||
|
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||||
|
of the user's real config. This is critical for tests that
|
||||||
|
depend on a clean (no API key) or custom config state.
|
||||||
|
|
||||||
|
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
|
||||||
|
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
|
||||||
|
fragment option names like --user-id into separately-styled ANSI segments,
|
||||||
|
making plain ``in`` checks fail. Stripping here is version-agnostic and
|
||||||
|
ensures all assertions see the same plain text regardless of terminal env.
|
||||||
|
"""
|
||||||
|
env = os.environ.copy()
|
||||||
|
# Strip all MEM0_ env vars so tests start clean
|
||||||
|
for key in list(env.keys()):
|
||||||
|
if key.startswith("MEM0_"):
|
||||||
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
|
if home_dir:
|
||||||
|
env["HOME"] = home_dir
|
||||||
|
if env_override:
|
||||||
|
env.update(env_override)
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clean_home(tmp_path):
|
||||||
|
"""Return a temp directory to use as HOME, ensuring no ~/.mem0 exists."""
|
||||||
|
return str(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIIntegration:
|
||||||
|
"""Tests that only inspect help text / version — no config needed."""
|
||||||
|
|
||||||
|
def test_help(self):
|
||||||
|
result = _run(["--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "mem0" in result.stdout
|
||||||
|
assert "add" in result.stdout
|
||||||
|
assert "search" in result.stdout
|
||||||
|
|
||||||
|
def test_add_help(self):
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "user-id" in result.stdout
|
||||||
|
assert "messages" in result.stdout
|
||||||
|
|
||||||
|
def test_add_help_has_scope_panel(self):
|
||||||
|
"""Verify rich_help_panel grouping shows in help output."""
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "Scope" in result.stdout
|
||||||
|
|
||||||
|
def test_search_help(self):
|
||||||
|
result = _run(["search", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "top-k" in result.stdout
|
||||||
|
|
||||||
|
def test_list_help(self):
|
||||||
|
result = _run(["list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "page-size" in result.stdout
|
||||||
|
|
||||||
|
def test_delete_help(self):
|
||||||
|
result = _run(["delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--all" in result.stdout
|
||||||
|
assert "--entity" in result.stdout
|
||||||
|
assert "--project" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
assert "--dry-run" in result.stdout
|
||||||
|
|
||||||
|
def test_entity_list_help(self):
|
||||||
|
result = _run(["entity", "list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "entity-type" in result.stdout.lower() or "entity_type" in result.stdout.lower()
|
||||||
|
|
||||||
|
def test_entity_delete_help(self):
|
||||||
|
result = _run(["entity", "delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--user-id" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
|
||||||
|
def test_import_help(self):
|
||||||
|
result = _run(["import", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
def test_no_args_shows_help(self):
|
||||||
|
"""no_args_is_help=True makes Typer print help and exit with code 2."""
|
||||||
|
result = _run([])
|
||||||
|
# Typer returns exit code 2 for "no command given" — this is standard
|
||||||
|
# Click/Typer behaviour and not an error.
|
||||||
|
assert result.returncode in (0, 2)
|
||||||
|
assert "Usage" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIIsolated:
|
||||||
|
"""Tests that need a clean HOME to avoid reading the user's real config."""
|
||||||
|
|
||||||
|
def test_add_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 add` must fail with a helpful message."""
|
||||||
|
result = _run(
|
||||||
|
["add", "test", "--user-id", "alice"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "api" in combined.lower() or "Error" in combined
|
||||||
|
|
||||||
|
def test_search_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 search` must fail."""
|
||||||
|
result = _run(
|
||||||
|
["search", "preferences", "--user-id", "alice"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "Error" in combined
|
||||||
|
|
||||||
|
def test_list_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 list` must fail."""
|
||||||
|
result = _run(["list"], home_dir=clean_home)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "Error" in combined
|
||||||
|
|
||||||
|
def test_delete_no_id_no_all_errors(self, clean_home):
|
||||||
|
"""Delete without memory_id, --all, or --entity must fail."""
|
||||||
|
result = _run(
|
||||||
|
["delete", "--api-key", "m0-fake-key"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert (
|
||||||
|
"memory ID" in combined.lower()
|
||||||
|
or "--all" in combined
|
||||||
|
or "--entity" in combined
|
||||||
|
or "Error" in combined
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_show_clean(self, clean_home):
|
||||||
|
"""config show with no config should still work."""
|
||||||
|
result = _run(["config", "show"], home_dir=clean_home)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "backend" in result.stdout.lower() or "platform" in result.stdout.lower()
|
||||||
|
|
||||||
|
def test_config_set_and_get_roundtrip(self, clean_home):
|
||||||
|
"""config set then config get should return the set value."""
|
||||||
|
_run(
|
||||||
|
["config", "set", "defaults.user_id", "integration-test-user"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
result = _run(
|
||||||
|
["config", "get", "defaults.user_id"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "integration-test-user" in result.stdout
|
||||||
|
|
||||||
|
def test_import_nonexistent_file(self, clean_home):
|
||||||
|
"""Importing a nonexistent file should fail gracefully."""
|
||||||
|
result = _run(
|
||||||
|
["import", "/nonexistent/file.json", "--api-key", "m0-fake"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "Failed" in combined or "Error" in combined or "error" in combined
|
||||||
|
|
||||||
|
def test_add_no_content_errors(self, clean_home):
|
||||||
|
"""add with no text/messages/file should fail."""
|
||||||
|
result = _run(
|
||||||
|
["add", "--user-id", "alice", "--api-key", "m0-fake"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "No content" in combined or "Error" in combined
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLINewFeatures:
|
||||||
|
"""Tests for MCP parity features: --graph, --limit, entities delete."""
|
||||||
|
|
||||||
|
def test_add_help_has_graph(self):
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
|
||||||
|
def test_search_help_has_graph_and_limit(self):
|
||||||
|
result = _run(["search", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
assert "--limit" in result.stdout
|
||||||
|
|
||||||
|
def test_list_help_has_graph(self):
|
||||||
|
result = _run(["list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
|
||||||
|
def test_delete_entity_via_delete_flag(self):
|
||||||
|
"""delete --entity should appear in help output."""
|
||||||
|
result = _run(["delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--entity" in result.stdout
|
||||||
|
|
||||||
|
def test_entity_delete_has_scope_options(self):
|
||||||
|
"""entity delete should expose scope options."""
|
||||||
|
result = _run(["entity", "delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--user-id" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
assert "--app-id" in result.stdout
|
||||||
|
assert "--run-id" in result.stdout
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
"""Tests for configuration management."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mem0_cli.config import (
|
||||||
|
Mem0Config,
|
||||||
|
get_nested_value,
|
||||||
|
load_config,
|
||||||
|
redact_key,
|
||||||
|
save_config,
|
||||||
|
set_nested_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedactKey:
|
||||||
|
def test_empty_key(self):
|
||||||
|
assert redact_key("") == "(not set)"
|
||||||
|
|
||||||
|
def test_short_key(self):
|
||||||
|
assert redact_key("abc") == "ab***"
|
||||||
|
|
||||||
|
def test_normal_key(self):
|
||||||
|
result = redact_key("m0-abcdefgh12345678")
|
||||||
|
assert result == "m0-a...5678"
|
||||||
|
assert "abcdefgh" not in result
|
||||||
|
|
||||||
|
def test_exact_8_chars(self):
|
||||||
|
# 8 chars is <= 8, so it gets the short redaction
|
||||||
|
assert redact_key("12345678") == "12***"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig:
|
||||||
|
def test_default_config(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert config.platform.base_url == "https://api.mem0.ai"
|
||||||
|
assert config.platform.api_key == ""
|
||||||
|
|
||||||
|
def test_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "m0-test-key"
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "m0-test-key"
|
||||||
|
|
||||||
|
def test_env_var_override(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "file-key"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
monkeypatch.setenv("MEM0_API_KEY", "env-key")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "env-key"
|
||||||
|
|
||||||
|
def test_load_nonexistent_config(self, isolate_config):
|
||||||
|
config = load_config()
|
||||||
|
assert config.platform.api_key == ""
|
||||||
|
|
||||||
|
def test_config_file_permissions(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "secret"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE
|
||||||
|
|
||||||
|
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||||
|
assert mode == 0o600
|
||||||
|
|
||||||
|
def test_defaults_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "alice"
|
||||||
|
config.defaults.agent_id = "support-bot"
|
||||||
|
config.defaults.app_id = "my-app"
|
||||||
|
config.defaults.run_id = "run-001"
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
loaded = load_config()
|
||||||
|
|
||||||
|
assert loaded.defaults.user_id == "alice"
|
||||||
|
assert loaded.defaults.agent_id == "support-bot"
|
||||||
|
assert loaded.defaults.app_id == "my-app"
|
||||||
|
assert loaded.defaults.run_id == "run-001"
|
||||||
|
|
||||||
|
def test_defaults_env_var_override(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "file-user"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
monkeypatch.setenv("MEM0_USER_ID", "env-user")
|
||||||
|
monkeypatch.setenv("MEM0_AGENT_ID", "env-agent")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.user_id == "env-user"
|
||||||
|
assert loaded.defaults.agent_id == "env-agent"
|
||||||
|
|
||||||
|
def test_backward_compat_no_defaults_key(self, isolate_config):
|
||||||
|
"""Old config files without 'defaults' key should load fine."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
|
||||||
|
|
||||||
|
ensure_config_dir()
|
||||||
|
# Write a config without the "defaults" key
|
||||||
|
data = {
|
||||||
|
"version": 1,
|
||||||
|
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
|
||||||
|
}
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "m0-test"
|
||||||
|
assert loaded.defaults.user_id == ""
|
||||||
|
assert loaded.defaults.agent_id == ""
|
||||||
|
|
||||||
|
def test_default_config_has_empty_defaults(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert config.defaults.user_id == ""
|
||||||
|
assert config.defaults.agent_id == ""
|
||||||
|
assert config.defaults.app_id == ""
|
||||||
|
assert config.defaults.run_id == ""
|
||||||
|
assert config.defaults.enable_graph is False
|
||||||
|
|
||||||
|
def test_enable_graph_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.enable_graph = True
|
||||||
|
save_config(config)
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is True
|
||||||
|
|
||||||
|
def test_enable_graph_env_var_true(self, isolate_config, monkeypatch):
|
||||||
|
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "true")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is True
|
||||||
|
|
||||||
|
def test_enable_graph_env_var_false(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.enable_graph = True
|
||||||
|
save_config(config)
|
||||||
|
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "false")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is False
|
||||||
|
|
||||||
|
def test_backward_compat_no_enable_graph_key(self, isolate_config):
|
||||||
|
"""Old config files without 'enable_graph' key should default to False."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
|
||||||
|
|
||||||
|
ensure_config_dir()
|
||||||
|
data = {
|
||||||
|
"version": 1,
|
||||||
|
"defaults": {"user_id": "alice"},
|
||||||
|
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
|
||||||
|
}
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is False
|
||||||
|
assert loaded.defaults.user_id == "alice"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNestedAccess:
|
||||||
|
def test_get_nested_value(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "test-key"
|
||||||
|
assert get_nested_value(config, "platform.api_key") == "test-key"
|
||||||
|
|
||||||
|
def test_get_nonexistent_key(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert get_nested_value(config, "nonexistent.key") is None
|
||||||
|
|
||||||
|
def test_set_nested_value(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "platform.api_key", "new-key")
|
||||||
|
assert config.platform.api_key == "new-key"
|
||||||
|
|
||||||
|
def test_set_nonexistent_key(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "nonexistent.key", "val") is False
|
||||||
|
|
||||||
|
def test_get_defaults_user_id(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "alice"
|
||||||
|
assert get_nested_value(config, "defaults.user_id") == "alice"
|
||||||
|
|
||||||
|
def test_set_defaults_user_id(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "defaults.user_id", "bob")
|
||||||
|
assert config.defaults.user_id == "bob"
|
||||||
|
|
||||||
|
def test_set_defaults_enable_graph(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "defaults.enable_graph", "true")
|
||||||
|
assert config.defaults.enable_graph is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveIds:
|
||||||
|
def test_cli_flag_overrides_default(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "default-user"
|
||||||
|
ids = _resolve_ids(
|
||||||
|
config,
|
||||||
|
user_id="cli-user",
|
||||||
|
agent_id=None,
|
||||||
|
)
|
||||||
|
assert ids["user_id"] == "cli-user"
|
||||||
|
|
||||||
|
def test_default_used_when_flag_is_none(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "default-user"
|
||||||
|
config.defaults.agent_id = "default-agent"
|
||||||
|
ids = _resolve_ids(config, user_id=None, agent_id=None)
|
||||||
|
assert ids["user_id"] == "default-user"
|
||||||
|
assert ids["agent_id"] == "default-agent"
|
||||||
|
|
||||||
|
def test_none_when_neither_set(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
ids = _resolve_ids(config, user_id=None, agent_id=None)
|
||||||
|
assert ids["user_id"] is None
|
||||||
|
assert ids["agent_id"] is None
|
||||||
|
assert ids["app_id"] is None
|
||||||
|
assert ids["run_id"] is None
|
||||||
|
|
||||||
|
def test_empty_string_treated_as_unset(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = ""
|
||||||
|
ids = _resolve_ids(config, user_id=None)
|
||||||
|
assert ids["user_id"] is None
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
"""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,
|
||||||
|
sanitize_agent_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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_deduplicates_pending_by_event_id(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Should show only one PENDING block despite two entries with same event_id
|
||||||
|
assert output.count("evt-dup") == 2 # event_id line + status hint line
|
||||||
|
assert output.count("Queued") == 1
|
||||||
|
|
||||||
|
def test_format_add_result_empty(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_add_result(console, {"results": []}, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "No memories extracted" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestSanitizeAgentData:
|
||||||
|
def test_add_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"event": "ADD",
|
||||||
|
"metadata": {"x": 1},
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
|
||||||
|
|
||||||
|
def test_add_pending_passthrough(self):
|
||||||
|
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
|
||||||
|
|
||||||
|
def test_search_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
"agent_id": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("search", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("list", raw)
|
||||||
|
assert result == [
|
||||||
|
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_get_projects_fields(self):
|
||||||
|
raw = {
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"categories": ["a"],
|
||||||
|
"metadata": {"k": "v"},
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("get", raw)
|
||||||
|
assert "user_id" not in result
|
||||||
|
assert "id" in result and "memory" in result
|
||||||
|
|
||||||
|
def test_update_projects_fields(self):
|
||||||
|
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
|
||||||
|
result = sanitize_agent_data("update", raw)
|
||||||
|
assert result == {"id": "abc", "memory": "updated"}
|
||||||
|
|
||||||
|
def test_event_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("event list", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert "updated_at" not in result[0]
|
||||||
|
assert "graph_status" not in result[0]
|
||||||
|
|
||||||
|
def test_event_status_flattens_results(self):
|
||||||
|
raw = {
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"results": [
|
||||||
|
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("event status", raw)
|
||||||
|
assert result["results"][0] == {
|
||||||
|
"id": "mem-1",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"memory": "dark mode",
|
||||||
|
}
|
||||||
|
assert "data" not in result["results"][0]
|
||||||
|
|
||||||
|
def test_passthrough_commands(self):
|
||||||
|
for cmd in ("status", "import", "config show", "config get", "config set"):
|
||||||
|
data = {"key": "value", "other": "stuff"}
|
||||||
|
assert sanitize_agent_data(cmd, data) == data
|
||||||
|
|
||||||
|
def test_none_data(self):
|
||||||
|
assert sanitize_agent_data("add", None) is None
|
||||||
@@ -10,5 +10,4 @@ List recent events for your organization and project.
|
|||||||
|
|
||||||
- **Dashboards**: Summarize adds/searches over time by paging through events.
|
- **Dashboards**: Summarize adds/searches over time by paging through events.
|
||||||
- **Alerting**: Poll for `FAILED` events and trigger follow-up workflows.
|
- **Alerting**: Poll for `FAILED` events and trigger follow-up workflows.
|
||||||
- **Audit**: Store the returned payload/metadata for compliance logs.
|
- **Audit**: Store the returned payload/metadata for compliance logs.
|
||||||
|
|
||||||
@@ -52,6 +52,10 @@ Provide at least one message or direct memory string. Most callers supply `messa
|
|||||||
|
|
||||||
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
|
> \* 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ new_project = client.project.create(
|
|||||||
|
|
||||||
### Update Project Settings
|
### Update Project Settings
|
||||||
|
|
||||||
Modify project configuration including custom instructions, categories, and graph settings:
|
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Update project with custom categories
|
# Update project with custom categories
|
||||||
@@ -101,6 +101,9 @@ client.project.update(
|
|||||||
# Enable graph memory for the project
|
# Enable graph memory for the project
|
||||||
client.project.update(enable_graph=True)
|
client.project.update(enable_graph=True)
|
||||||
|
|
||||||
|
# Use the input language for memory storage and retrieval
|
||||||
|
client.project.update(multilingual=True)
|
||||||
|
|
||||||
# Update multiple settings at once
|
# Update multiple settings at once
|
||||||
client.project.update(
|
client.project.update(
|
||||||
custom_instructions="...",
|
custom_instructions="...",
|
||||||
@@ -108,7 +111,8 @@ client.project.update(
|
|||||||
{"personal_info": "User personal information and preferences"},
|
{"personal_info": "User personal information and preferences"},
|
||||||
{"work_context": "Professional context and work-related information"}
|
{"work_context": "Professional context and work-related information"}
|
||||||
],
|
],
|
||||||
enable_graph=True
|
enable_graph=True,
|
||||||
|
multilingual=True
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -8,20 +8,89 @@ mode: "wide"
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
|
|
||||||
|
<Update label="2026-04-01" description="v1.0.10">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **LLMs:** Added MiniMax provider support for AWS Bedrock ([#4609](https://github.com/mem0ai/mem0/pull/4609))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Configs:** Migrated CassandraConfig and AzureMySQLConfig to pydantic v2 ConfigDict ([#4646](https://github.com/mem0ai/mem0/pull/4646))
|
||||||
|
- **LLMs:** Forward `response_format` to OpenAI-compatible API for DeepSeek ([#4635](https://github.com/mem0ai/mem0/pull/4635))
|
||||||
|
- **LLMs:** Forward `response_format` to OpenAI-compatible API for vLLM ([#4608](https://github.com/mem0ai/mem0/pull/4608))
|
||||||
|
- **Vector Stores:** Only list authorized collections when listing MongoDB collections ([#3888](https://github.com/mem0ai/mem0/pull/3888))
|
||||||
|
- **Core:** Reset graph database in `Memory.reset()` ([#4185](https://github.com/mem0ai/mem0/pull/4185))
|
||||||
|
- **Core:** Make `AsyncMemory.from_config` a regular classmethod ([#4183](https://github.com/mem0ai/mem0/pull/4183))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-28" description="v1.0.9">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **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">
|
<Update label="2026-03-19" description="v1.0.7">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **Core:** Fixed control characters in LLM JSON responses causing parse failures (#4420)
|
- **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)
|
- **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)
|
- **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)
|
- **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)
|
- **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)
|
- **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)
|
- **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)
|
- **Vector Stores:** Cast `vector_distance` to float in Redis search ([#4377](https://github.com/mem0ai/mem0/pull/4377))
|
||||||
|
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403)
|
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling ([#4403](https://github.com/mem0ai/mem0/pull/4403))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
@@ -763,6 +832,36 @@ mode: "wide"
|
|||||||
|
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
|
|
||||||
|
<Update label="2026-04-01" description="v2.4.5">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **OSS:** Replace `.single()` with `.maybeSingle()` in SupabaseDB.get() to handle missing rows ([#4599](https://github.com/mem0ai/mem0/pull/4599))
|
||||||
|
- **Embeddings:** Pass dimensions parameter to OpenAI embeddings API ([#4632](https://github.com/mem0ai/mem0/pull/4632))
|
||||||
|
- **OSS:** Extract JSON from chatty LLM responses in fact retrieval ([#4533](https://github.com/mem0ai/mem0/pull/4533))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-28" description="v2.4.4">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **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">
|
<Update label="2026-03-19" description="v2.4.2">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ config = {
|
|||||||
"embedder": {
|
"embedder": {
|
||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "models/text-embedding-004",
|
"model": "models/gemini-embedding-001",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ Here are the parameters available for configuring Gemini embedder:
|
|||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
| Parameter | Description | Default Value |
|
| Parameter | Description | Default Value |
|
||||||
| ---------------- | ------------------------------------ | ----------------------- |
|
| ---------------- | ------------------------------------ | ----------------------- |
|
||||||
| `model` | The name of the embedding model to use| `models/text-embedding-004` |
|
| `model` | The name of the embedding model to use| `models/gemini-embedding-001` |
|
||||||
| `embedding_dims` | Dimensions of the embedding model | `1536` |
|
| `embedding_dims` | Dimensions of the embedding model | `1536` |
|
||||||
| `api_key` | The Google API key | `None` |
|
| `api_key` | The Google API key | `None` |
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ config = {
|
|||||||
"embedder": {
|
"embedder": {
|
||||||
"provider": "vertexai",
|
"provider": "vertexai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "text-embedding-004",
|
"model": "gemini-embedding-001",
|
||||||
"memory_add_embedding_type": "RETRIEVAL_DOCUMENT",
|
"memory_add_embedding_type": "RETRIEVAL_DOCUMENT",
|
||||||
"memory_update_embedding_type": "RETRIEVAL_DOCUMENT",
|
"memory_update_embedding_type": "RETRIEVAL_DOCUMENT",
|
||||||
"memory_search_embedding_type": "RETRIEVAL_QUERY"
|
"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 |
|
| Parameter | Description | Default Value |
|
||||||
| ------------------------- | ------------------------------------------------ | -------------------- |
|
| ------------------------- | ------------------------------------------------ | -------------------- |
|
||||||
| `model` | The name of the Vertex AI embedding model to use | `text-embedding-004` |
|
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
|
||||||
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
||||||
| `embedding_dims` | Dimensions of the embedding model | `256` |
|
| `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` |
|
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ config = {
|
|||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gemini-2.0-flash-001",
|
"model": "gemini-2.0-flash-001",
|
||||||
|
"api_key": "your-gemini-api-key",
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
"top_p": 1.0
|
"top_p": 1.0
|
||||||
@@ -52,6 +53,7 @@ const config = {
|
|||||||
provider: "gemini",
|
provider: "gemini",
|
||||||
config: {
|
config: {
|
||||||
model: "gemini-2.0-flash-001",
|
model: "gemini-2.0-flash-001",
|
||||||
|
apiKey: process.env.GOOGLE_API_KEY || '',
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,58 +7,51 @@ When using LLM rerankers, you can customize the prompts used for ranking to bett
|
|||||||
|
|
||||||
## Default Prompt
|
## Default Prompt
|
||||||
|
|
||||||
The default LLM reranker prompt is designed to be general-purpose:
|
The default LLM reranker prompt scores each memory individually on a 0.0-1.0 scale:
|
||||||
|
|
||||||
```
|
```
|
||||||
Given a query and a list of memory entries, rank the memory entries based on their relevance to the query.
|
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||||
Rate each memory on a scale of 1-10 where 10 is most relevant.
|
|
||||||
|
|
||||||
Query: {query}
|
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||||
|
- 1.0 = Perfectly relevant and directly answers the query
|
||||||
|
- 0.8-0.9 = Highly relevant with good information
|
||||||
|
- 0.6-0.7 = Moderately relevant with some useful information
|
||||||
|
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||||
|
- 0.0-0.3 = Not relevant or no useful information
|
||||||
|
|
||||||
Memory entries:
|
Query: "{query}"
|
||||||
{memories}
|
Document: "{document}"
|
||||||
|
|
||||||
Provide your ranking as a JSON array with scores for each memory.
|
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Prompt Configuration
|
## Custom Prompt Configuration
|
||||||
|
|
||||||
You can provide a custom prompt template when configuring the LLM reranker:
|
You can provide a custom prompt template using the `scoring_prompt` parameter:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
|
||||||
custom_prompt = """
|
custom_prompt = """
|
||||||
You are an expert at ranking memories for a personal AI assistant.
|
You are an expert at evaluating memories for a personal AI assistant.
|
||||||
Given a user query and a list of memory entries, rank each memory based on:
|
Given a user query and a memory entry, score how relevant the memory is.
|
||||||
1. Direct relevance to the query
|
Consider direct relevance, temporal relevance, and actionability.
|
||||||
2. Temporal relevance (recent memories may be more important)
|
|
||||||
3. Emotional significance
|
|
||||||
4. Actionability
|
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
User Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memory entries:
|
Provide only a single numerical score between 0.0 and 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Rate each memory from 1-10 and provide reasoning.
|
|
||||||
Return as JSON: {{"rankings": [{{"index": 0, "score": 8, "reason": "..."}}]}}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-openai-key",
|
||||||
"model": "gpt-4.1-nano-2025-04-14",
|
"scoring_prompt": custom_prompt,
|
||||||
"api_key": "your-openai-key"
|
"top_k": 5
|
||||||
}
|
|
||||||
},
|
|
||||||
"custom_prompt": custom_prompt,
|
|
||||||
"top_n": 5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +63,14 @@ memory = Memory.from_config(config)
|
|||||||
|
|
||||||
Your custom prompt can use the following variables:
|
Your custom prompt can use the following variables:
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ---------------- | ------------------------------------- |
|
| ------------ | ----------------------------- |
|
||||||
| `{query}` | The search query |
|
| `{query}` | The search query |
|
||||||
| `{memories}` | The list of memory entries to rank |
|
| `{document}` | The memory entry being scored |
|
||||||
| `{user_id}` | The user ID (if available) |
|
|
||||||
| `{user_context}` | Additional user context (if provided) |
|
<Note>
|
||||||
|
Both `{query}` and `{document}` are required in your custom prompt. The LLM reranker scores each memory individually against the query, so the prompt is called once per candidate memory.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## Domain-Specific Examples
|
## Domain-Specific Examples
|
||||||
|
|
||||||
@@ -89,13 +84,10 @@ Prioritize memories that:
|
|||||||
- Show previous resolution patterns
|
- Show previous resolution patterns
|
||||||
- Indicate customer preferences or constraints
|
- Indicate customer preferences or constraints
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Customer Context: Previous interactions with this customer
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories:
|
Score relevance from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Rank each memory 1-10 based on support relevance.
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,19 +95,16 @@ Rank each memory 1-10 based on support relevance.
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
educational_prompt = """
|
educational_prompt = """
|
||||||
Rank these learning memories for a student query.
|
Score this learning memory for relevance to a student query.
|
||||||
Consider:
|
Consider:
|
||||||
- Prerequisite knowledge requirements
|
- Prerequisite knowledge requirements
|
||||||
- Learning progression and difficulty
|
- Learning progression and difficulty
|
||||||
- Relevance to current learning objectives
|
- Relevance to current learning objectives
|
||||||
|
|
||||||
Student Query: {query}
|
Student Query: "{query}"
|
||||||
Learning Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Available memories:
|
Score educational relevance from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Score each memory for educational value (1-10).
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -123,73 +112,64 @@ Score each memory for educational value (1-10).
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
personal_assistant_prompt = """
|
personal_assistant_prompt = """
|
||||||
Rank personal memories for relevance to the user's query.
|
Score this personal memory for relevance to the user's query.
|
||||||
Consider:
|
Consider:
|
||||||
- Recent vs. historical importance
|
- Recent vs. historical importance
|
||||||
- Personal preferences and habits
|
- Personal preferences and habits
|
||||||
- Contextual relationships between memories
|
- Contextual relationships
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Personal context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories to rank:
|
Provide relevance score from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Provide relevance scores (1-10) with brief explanations.
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Prompt Techniques
|
## Advanced Prompt Techniques
|
||||||
|
|
||||||
### Multi-Criteria Ranking
|
### Multi-Criteria Scoring
|
||||||
|
|
||||||
```python
|
```python
|
||||||
multi_criteria_prompt = """
|
multi_criteria_prompt = """
|
||||||
Evaluate memories using multiple criteria:
|
Evaluate this memory using multiple criteria:
|
||||||
|
|
||||||
1. RELEVANCE (40%): How directly related to the query
|
1. RELEVANCE (40%): How directly related to the query
|
||||||
2. RECENCY (20%): How recent the memory is
|
2. RECENCY (20%): How recent the memory appears to be
|
||||||
3. IMPORTANCE (25%): Personal or business significance
|
3. IMPORTANCE (25%): Personal or business significance
|
||||||
4. ACTIONABILITY (15%): How useful for next steps
|
4. ACTIONABILITY (15%): How useful for next steps
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories:
|
Compute a weighted score from 0.0 to 1.0 based on these criteria.
|
||||||
{memories}
|
Provide only the final numerical score.
|
||||||
|
|
||||||
For each memory, provide:
|
|
||||||
- Overall score (1-10)
|
|
||||||
- Breakdown by criteria
|
|
||||||
- Final ranking recommendation
|
|
||||||
|
|
||||||
Format: JSON with detailed scoring
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contextual Ranking
|
### Chain-of-Thought Scoring
|
||||||
|
|
||||||
```python
|
```python
|
||||||
contextual_prompt = """
|
reasoning_prompt = """
|
||||||
Consider the following context when ranking memories:
|
Evaluate this memory's relevance step by step:
|
||||||
- Current user situation: {user_context}
|
|
||||||
- Time of day: {current_time}
|
|
||||||
- Recent activities: {recent_activities}
|
|
||||||
|
|
||||||
Query: {query}
|
1. What is the main intent of the query?
|
||||||
|
2. What key information does the memory contain?
|
||||||
|
3. How directly does the memory address the query?
|
||||||
|
|
||||||
Rank these memories considering both direct relevance and contextual appropriateness:
|
Based on this analysis, provide a single relevance score from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Provide contextually-aware relevance scores (1-10).
|
Query: "{query}"
|
||||||
|
Memory: "{document}"
|
||||||
|
|
||||||
|
Score:
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
||||||
2. **Use Examples**: Include examples in your prompt for better model understanding
|
2. **Use 0.0-1.0 Scale**: The score extractor expects values between 0.0 and 1.0
|
||||||
3. **Structure Output**: Specify the exact JSON format you want returned
|
3. **Request Only the Score**: Ask for just the numerical score to improve extraction reliability
|
||||||
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
||||||
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
||||||
|
|
||||||
@@ -206,7 +186,7 @@ prompts = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
for i, prompt in enumerate(prompts):
|
for i, prompt in enumerate(prompts):
|
||||||
config["reranker"]["config"]["custom_prompt"] = prompt
|
config["reranker"]["config"]["scoring_prompt"] = prompt
|
||||||
memory = Memory.from_config(config)
|
memory = Memory.from_config(config)
|
||||||
|
|
||||||
results = memory.search("test query", user_id="test_user")
|
results = memory.search("test query", user_id="test_user")
|
||||||
@@ -216,6 +196,6 @@ for i, prompt in enumerate(prompts):
|
|||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
- **Too Long**: Keep prompts under token limits for your chosen LLM
|
- **Too Long**: Keep prompts under token limits for your chosen LLM
|
||||||
- **Too Vague**: Be specific about ranking criteria
|
- **Too Vague**: Be specific about scoring criteria
|
||||||
- **Inconsistent Format**: Ensure JSON output format is clearly specified
|
- **Wrong Scale**: Use 0.0-1.0 scale to match the default score extractor
|
||||||
- **Missing Context**: Include relevant variables for your use case
|
- **Extra Output**: Ask for only the numeric score — extra text can confuse score extraction
|
||||||
|
|||||||
@@ -18,13 +18,9 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-openai-api-key"
|
||||||
"model": "gpt-4",
|
|
||||||
"api_key": "your-openai-api-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,11 +32,14 @@ m = Memory.from_config(config)
|
|||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `llm` | dict | Required | LLM configuration object |
|
| `provider` | str | `"openai"` | LLM provider (openai, anthropic, etc.) |
|
||||||
| `top_k` | int | 10 | Number of results to rerank |
|
| `model` | str | `"gpt-4o-mini"` | LLM model to use for reranking |
|
||||||
|
| `api_key` | str | None | API key for the LLM provider |
|
||||||
|
| `top_k` | int | None | Number of top documents to return after reranking |
|
||||||
| `temperature` | float | 0.0 | LLM temperature for consistency |
|
| `temperature` | float | 0.0 | LLM temperature for consistency |
|
||||||
| `custom_prompt` | str | None | Custom reranking prompt |
|
| `max_tokens` | int | 100 | Maximum tokens for LLM response |
|
||||||
| `score_range` | tuple | (0, 10) | Score range for relevance |
|
| `scoring_prompt` | str | None | Custom prompt template for scoring documents |
|
||||||
|
| `llm` | dict | None | Optional nested LLM config for provider-specific fields (e.g., `ollama_base_url`, `azure_endpoint`). Overrides top-level `provider`/`model`/`api_key` when provided. |
|
||||||
|
|
||||||
### Advanced Configuration
|
### Advanced Configuration
|
||||||
|
|
||||||
@@ -49,20 +48,19 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "anthropic",
|
||||||
"provider": "anthropic",
|
"model": "claude-sonnet-4-20250514",
|
||||||
"config": {
|
"api_key": "your-anthropic-api-key",
|
||||||
"model": "claude-3-sonnet-20240229",
|
|
||||||
"api_key": "your-anthropic-api-key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"top_k": 15,
|
"top_k": 15,
|
||||||
"temperature": 0.0,
|
"temperature": 0.0,
|
||||||
"score_range": (1, 5),
|
"scoring_prompt": """
|
||||||
"custom_prompt": """
|
Rate the relevance of each memory to the query on a scale of 0.0-1.0.
|
||||||
Rate the relevance of each memory to the query on a scale of 1-5.
|
|
||||||
Consider semantic similarity, context, and practical utility.
|
Consider semantic similarity, context, and practical utility.
|
||||||
Only provide the numeric score.
|
Only provide the numeric score.
|
||||||
|
|
||||||
|
Query: "{query}"
|
||||||
|
Document: "{document}"
|
||||||
|
Score:
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,14 +76,10 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-openai-api-key",
|
||||||
"model": "gpt-4",
|
"temperature": 0.0
|
||||||
"api_key": "your-openai-api-key",
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +92,9 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "anthropic",
|
||||||
"provider": "anthropic",
|
"model": "claude-sonnet-4-20250514",
|
||||||
"config": {
|
"api_key": "your-anthropic-api-key"
|
||||||
"model": "claude-3-sonnet-20240229",
|
|
||||||
"api_key": "your-anthropic-api-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +107,12 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
|
"provider": "ollama",
|
||||||
|
"model": "llama3.2",
|
||||||
"llm": {
|
"llm": {
|
||||||
"provider": "ollama",
|
"provider": "ollama",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "llama2",
|
"model": "llama3.2",
|
||||||
"ollama_base_url": "http://localhost:11434"
|
"ollama_base_url": "http://localhost:11434"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +121,10 @@ config = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
For providers like Ollama that need extra fields (e.g., `ollama_base_url`), use the optional nested `llm` key to pass provider-specific configuration. The nested `llm` config overrides top-level `provider`/`model`/`api_key` when provided.
|
||||||
|
</Note>
|
||||||
|
|
||||||
### Azure OpenAI
|
### Azure OpenAI
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -136,13 +132,16 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
|
"provider": "azure_openai",
|
||||||
|
"model": "gpt-4o-mini",
|
||||||
|
"api_key": "your-azure-api-key",
|
||||||
"llm": {
|
"llm": {
|
||||||
"provider": "azure_openai",
|
"provider": "azure_openai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4",
|
"model": "gpt-4o-mini",
|
||||||
"api_key": "your-azure-api-key",
|
"api_key": "your-azure-api-key",
|
||||||
"azure_endpoint": "https://your-resource.openai.azure.com/",
|
"azure_endpoint": "https://your-resource.openai.azure.com/",
|
||||||
"azure_deployment": "gpt-4-deployment"
|
"azure_deployment": "gpt-4o-mini-deployment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,16 +153,22 @@ config = {
|
|||||||
|
|
||||||
### Default Prompt Behavior
|
### Default Prompt Behavior
|
||||||
|
|
||||||
The default prompt asks the LLM to score relevance on a 0-10 scale:
|
The default prompt asks the LLM to score relevance on a 0.0-1.0 scale:
|
||||||
|
|
||||||
```
|
```
|
||||||
Given a query and a memory, rate how relevant the memory is to answering the query.
|
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||||
Score from 0 (completely irrelevant) to 10 (perfectly relevant).
|
|
||||||
Only provide the numeric score.
|
|
||||||
|
|
||||||
Query: {query}
|
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||||
Memory: {memory}
|
- 1.0 = Perfectly relevant and directly answers the query
|
||||||
Score:
|
- 0.8-0.9 = Highly relevant with good information
|
||||||
|
- 0.6-0.7 = Moderately relevant with some useful information
|
||||||
|
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||||
|
- 0.0-0.3 = Not relevant or no useful information
|
||||||
|
|
||||||
|
Query: "{query}"
|
||||||
|
Document: "{document}"
|
||||||
|
|
||||||
|
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Prompt Examples
|
### Custom Prompt Examples
|
||||||
@@ -174,14 +179,14 @@ Score:
|
|||||||
custom_prompt = """
|
custom_prompt = """
|
||||||
You are a medical information specialist. Rate how relevant each memory is for answering the medical query.
|
You are a medical information specialist. Rate how relevant each memory is for answering the medical query.
|
||||||
Consider clinical accuracy, specificity, and practical applicability.
|
Consider clinical accuracy, specificity, and practical applicability.
|
||||||
Rate from 1-10 where:
|
Rate from 0.0 to 1.0 where:
|
||||||
- 1-3: Irrelevant or potentially harmful
|
- 0.0-0.3: Irrelevant or potentially harmful
|
||||||
- 4-6: Somewhat relevant but incomplete
|
- 0.4-0.6: Somewhat relevant but incomplete
|
||||||
- 7-8: Relevant and helpful
|
- 0.7-0.8: Relevant and helpful
|
||||||
- 9-10: Highly relevant and clinically useful
|
- 0.9-1.0: Highly relevant and clinically useful
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Memory: {memory}
|
Document: "{document}"
|
||||||
Score:
|
Score:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -189,14 +194,10 @@ config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-api-key",
|
||||||
"model": "gpt-4",
|
"scoring_prompt": custom_prompt
|
||||||
"api_key": "your-api-key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"custom_prompt": custom_prompt
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,15 +214,15 @@ Consider:
|
|||||||
- Recency and accuracy
|
- Recency and accuracy
|
||||||
- Practical usefulness
|
- Practical usefulness
|
||||||
|
|
||||||
Rate 1-5:
|
Rate 0.0-1.0:
|
||||||
1 = Not relevant
|
0.0 = Not relevant
|
||||||
2 = Slightly relevant
|
0.25 = Slightly relevant
|
||||||
3 = Moderately relevant
|
0.5 = Moderately relevant
|
||||||
4 = Very relevant
|
0.75 = Very relevant
|
||||||
5 = Perfectly answers the question
|
1.0 = Perfectly answers the question
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Memory: {memory}
|
Document: "{document}"
|
||||||
Score:
|
Score:
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
@@ -239,13 +240,17 @@ Consider:
|
|||||||
- Factual accuracy
|
- Factual accuracy
|
||||||
- Conversation flow
|
- Conversation flow
|
||||||
|
|
||||||
Rate 0-10:
|
Rate 0.0-1.0:
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Memory: {memory}
|
Document: "{document}"
|
||||||
Score:
|
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
|
## Usage Examples
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
@@ -295,9 +300,9 @@ results = safe_llm_rerank_search("What are my preferences?", "alice")
|
|||||||
|
|
||||||
| Model Type | Speed | Quality | Cost | Best For |
|
| Model Type | Speed | Quality | Cost | Best For |
|
||||||
|------------|-------|---------|------|----------|
|
|------------|-------|---------|------|----------|
|
||||||
| GPT-3.5 Turbo | Fast | Good | Low | High-volume applications |
|
| GPT-4o mini | Fast | Good | Low | High-volume applications |
|
||||||
| GPT-4 | Medium | Excellent | Medium | Quality-critical applications |
|
| GPT-4o | Medium | Excellent | Medium | Quality-critical applications |
|
||||||
| Claude 3 Sonnet | Medium | Excellent | Medium | Balanced performance |
|
| Claude Sonnet | Medium | Excellent | Medium | Balanced performance |
|
||||||
| Ollama Local | Variable | Good | Free | Privacy-sensitive applications |
|
| Ollama Local | Variable | Good | Free | Privacy-sensitive applications |
|
||||||
|
|
||||||
### Optimization Strategies
|
### Optimization Strategies
|
||||||
@@ -308,14 +313,10 @@ fast_config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-api-key",
|
||||||
"model": "gpt-3.5-turbo",
|
"top_k": 5,
|
||||||
"api_key": "your-api-key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"top_k": 5, # Limit candidates
|
|
||||||
"temperature": 0.0
|
"temperature": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,13 +327,9 @@ quality_config = {
|
|||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o",
|
||||||
"config": {
|
"api_key": "your-api-key",
|
||||||
"model": "gpt-4",
|
|
||||||
"api_key": "your-api-key"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"top_k": 15,
|
"top_k": 15,
|
||||||
"temperature": 0.0
|
"temperature": 0.0
|
||||||
}
|
}
|
||||||
@@ -353,10 +350,10 @@ Evaluate this memory's relevance using multi-step reasoning:
|
|||||||
3. How directly does the memory address the query?
|
3. How directly does the memory address the query?
|
||||||
4. What additional context might be needed?
|
4. What additional context might be needed?
|
||||||
|
|
||||||
Based on this analysis, rate relevance 1-10:
|
Based on this analysis, rate relevance 0.0-1.0:
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Memory: {memory}
|
Document: "{document}"
|
||||||
|
|
||||||
Analysis:
|
Analysis:
|
||||||
Step 1 (Intent):
|
Step 1 (Intent):
|
||||||
@@ -367,42 +364,6 @@ Final Score:
|
|||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
### Comparative Ranking
|
|
||||||
|
|
||||||
```python
|
|
||||||
comparative_prompt = """
|
|
||||||
You will see a query and multiple memories. Rank them in order of relevance.
|
|
||||||
Consider which memories best answer the question and would be most helpful.
|
|
||||||
|
|
||||||
Query: {query}
|
|
||||||
|
|
||||||
Memories to rank:
|
|
||||||
{memories}
|
|
||||||
|
|
||||||
Provide scores 1-10 for each memory, considering their relative usefulness.
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
### Emotional Intelligence
|
|
||||||
|
|
||||||
```python
|
|
||||||
emotional_prompt = """
|
|
||||||
Consider both factual relevance and emotional appropriateness.
|
|
||||||
Rate how suitable this memory is for responding to the user's query.
|
|
||||||
|
|
||||||
Factors to consider:
|
|
||||||
- Factual accuracy and relevance
|
|
||||||
- Emotional tone and sensitivity
|
|
||||||
- User's likely emotional state
|
|
||||||
- Appropriateness of response
|
|
||||||
|
|
||||||
Query: {query}
|
|
||||||
Memory: {memory}
|
|
||||||
Emotional Context: {context}
|
|
||||||
Score (1-10):
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling and Fallbacks
|
## Error Handling and Fallbacks
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -433,14 +394,22 @@ class RobustLLMReranker:
|
|||||||
primary_config = {
|
primary_config = {
|
||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-4"}}}
|
"config": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"api_key": "your-api-key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fallback_config = {
|
fallback_config = {
|
||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-3.5-turbo"}}}
|
"config": {
|
||||||
|
"provider": "openai",
|
||||||
|
"model": "gpt-4o-mini",
|
||||||
|
"api_key": "your-api-key"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,4 +455,4 @@ results = reranker.search("What are my preferences?", "alice")
|
|||||||
<Card title="Performance Optimization" icon="bolt" href="/components/rerankers/optimization">
|
<Card title="Performance Optimization" icon="bolt" href="/components/rerankers/optimization">
|
||||||
Optimize LLM reranker performance
|
Optimize LLM reranker performance
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Here are the parameters available for configuring FAISS:
|
|||||||
| Parameter | Description | Default Value |
|
| Parameter | Description | Default Value |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `collection_name` | The name of the collection | `mem0` |
|
| `collection_name` | The name of the collection | `mem0` |
|
||||||
| `path` | Path to store FAISS index and metadata | `~/.local/share/mem0/faiss/<collection_name>` |
|
| `path` | Path to store FAISS index and metadata | `/tmp/faiss/<collection_name>` |
|
||||||
| `distance_strategy` | Distance metric strategy to use (options: 'euclidean', 'inner_product', 'cosine') | `euclidean` |
|
| `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` |
|
| `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` |
|
| `client` | Custom client for qdrant | `None` |
|
||||||
| `host` | The host where the qdrant server is running | `None` |
|
| `host` | The host where the qdrant server is running | `None` |
|
||||||
| `port` | The port where the qdrant server is running | `None` |
|
| `port` | The port where the qdrant server is running | `None` |
|
||||||
| `path` | Path for the qdrant database | `~/.local/share/mem0/qdrant` |
|
| `path` | Path for the qdrant database | `/tmp/qdrant` |
|
||||||
| `url` | Full URL for the qdrant server | `None` |
|
| `url` | Full URL for the qdrant server | `None` |
|
||||||
| `api_key` | API key for the qdrant server | `None` |
|
| `api_key` | API key for the qdrant server | `None` |
|
||||||
| `on_disk` | For enabling persistent storage | `False` |
|
| `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` |
|
| `embeddingModelDims` | Dimensions of the embedding model | `1536` |
|
||||||
| `host` | The host where the Qdrant server is running | `None` |
|
| `host` | The host where the Qdrant server is running | `None` |
|
||||||
| `port` | The port where the Qdrant server is running | `None` |
|
| `port` | The port where the Qdrant server is running | `None` |
|
||||||
| `path` | Path for the Qdrant database | `~/.local/share/mem0/qdrant` |
|
| `path` | Path for the Qdrant database | `/tmp/qdrant` |
|
||||||
| `url` | Full URL for the Qdrant server | `None` |
|
| `url` | Full URL for the Qdrant server | `None` |
|
||||||
| `apiKey` | API key for the Qdrant server | `None` |
|
| `apiKey` | API key for the Qdrant server | `None` |
|
||||||
| `onDisk` | For enabling persistent storage | `False` |
|
| `onDisk` | For enabling persistent storage | `False` |
|
||||||
|
|||||||
@@ -10,6 +10,32 @@ Problem: LLMs are stateless. GPT doesn't remember conversations. You could stuff
|
|||||||
|
|
||||||
The solution: Mem0. It extracts and stores what matters from conversations, then retrieves it when needed. Your companion remembers user preferences, past events, and history.
|
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:
|
In this cookbook we'll build a **fitness companion** that:
|
||||||
|
|
||||||
- Remembers user goals across sessions
|
- Remembers user goals across sessions
|
||||||
@@ -25,6 +51,8 @@ By the end, you'll have a working fitness companion and know how to handle commo
|
|||||||
|
|
||||||
Max wants to train for a marathon. He starts chatting with Ray, an AI running coach.
|
Max wants to train for a marathon. He starts chatting with Ray, an AI running coach.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
from mem0 import MemoryClient
|
from mem0 import MemoryClient
|
||||||
@@ -53,8 +81,73 @@ def chat(user_input, user_id):
|
|||||||
], user_id=user_id)
|
], user_id=user_id)
|
||||||
|
|
||||||
return response
|
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:**
|
**Session 1:**
|
||||||
|
|
||||||
@@ -62,7 +155,6 @@ def chat(user_input, user_id):
|
|||||||
chat("I want to run a marathon in under 4 hours", user_id="max")
|
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?"
|
# Output: "That's a solid goal. What's your current weekly mileage?"
|
||||||
# Stored in Mem0: "Max wants to run sub-4 marathon"
|
# Stored in Mem0: "Max wants to run sub-4 marathon"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Session 2 (next day, app restarted):**
|
**Session 2 (next day, app restarted):**
|
||||||
@@ -70,7 +162,6 @@ chat("I want to run a marathon in under 4 hours", user_id="max")
|
|||||||
```python
|
```python
|
||||||
chat("What should I focus on today?", user_id="max")
|
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..."
|
# Output: "Based on your sub-4 marathon goal, let's work on building your aerobic base..."
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
@@ -87,6 +178,8 @@ Ray remembers. Restart the app, and the goal persists. From here on, we'll focus
|
|||||||
|
|
||||||
Max mentions his knee hurts. That's different from his marathon goal - one is temporary, the other is long-term.
|
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 vs Metadata:**
|
||||||
|
|
||||||
- **Categories**: AI-assigned by Mem0 based on content (you can't force them)
|
- **Categories**: AI-assigned by Mem0 based on content (you can't force them)
|
||||||
@@ -100,7 +193,6 @@ mem0_client.project.update(custom_categories=[
|
|||||||
{"constraints": "Injuries, limitations, recovery needs"},
|
{"constraints": "Injuries, limitations, recovery needs"},
|
||||||
{"preferences": "Training style, surfaces, schedules"}
|
{"preferences": "Training style, surfaces, schedules"}
|
||||||
])
|
])
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
@@ -121,7 +213,6 @@ mem0_client.add(
|
|||||||
[{"role": "user", "content": "My right knee flares up on downhills"}],
|
[{"role": "user", "content": "My right knee flares up on downhills"}],
|
||||||
user_id="max"
|
user_id="max"
|
||||||
)
|
)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Mem0 reads the content and intelligently picks which categories apply. You define the palette, it handles the tagging.
|
Mem0 reads the content and intelligently picks which categories apply. You define the palette, it handles the tagging.
|
||||||
@@ -135,13 +226,50 @@ mem0_client.add(
|
|||||||
user_id="max",
|
user_id="max",
|
||||||
metadata={"workout_type": "speed", "forced_tag": "custom_label"}
|
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
|
### Filtering by Category
|
||||||
|
|
||||||
Retrieve just constraints for workout planning:
|
Retrieve just constraints for workout planning:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
constraints = mem0_client.search(
|
constraints = mem0_client.search(
|
||||||
query="injury concerns",
|
query="injury concerns",
|
||||||
@@ -155,8 +283,21 @@ constraints = mem0_client.search(
|
|||||||
)
|
)
|
||||||
print([m["memory"] for m in constraints["results"]])
|
print([m["memory"] for m in constraints["results"]])
|
||||||
# Output: ["Max's right knee flares up on downhills"]
|
# 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.
|
Ray can plan workouts that avoid aggravating Max's knee, without pulling in race goals or other unrelated memories.
|
||||||
|
|
||||||
@@ -168,12 +309,22 @@ Ray can plan workouts that avoid aggravating Max's knee, without pulling in race
|
|||||||
|
|
||||||
Run the basic loop for a week and check what's stored:
|
Run the basic loop for a week and check what's stored:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
||||||
print([m["memory"] for m in memories["results"]])
|
print([m["memory"] for m in memories["results"]])
|
||||||
# Output: ["Max wants to run marathon under 4 hours", "hey", "lol ok", "cool thanks", "gtg bye"]
|
# 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>
|
<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.
|
Without filters, Mem0 stores everything—greetings, filler, and casual chat. This pollutes retrieval: instead of pulling "marathon goal," you get "lol ok." Set custom instructions to keep memory clean.
|
||||||
@@ -183,6 +334,8 @@ Noise. Greetings and filler clutter the memory.
|
|||||||
|
|
||||||
### Custom Instructions
|
### Custom Instructions
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
Tell Mem0 what matters:
|
Tell Mem0 what matters:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -198,11 +351,38 @@ Exclude:
|
|||||||
- Casual chatter
|
- Casual chatter
|
||||||
- Hypotheticals unless planning related
|
- 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:
|
Now chat again:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
chat("hey how's it going", user_id="max")
|
chat("hey how's it going", user_id="max")
|
||||||
chat("I prefer trail running over roads", user_id="max")
|
chat("I prefer trail running over roads", user_id="max")
|
||||||
@@ -210,8 +390,19 @@ chat("I prefer trail running over roads", user_id="max")
|
|||||||
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
||||||
print([m["memory"] for m in memories["results"]])
|
print([m["memory"] for m in memories["results"]])
|
||||||
# Output: ["Max wants to run marathon under 4 hours", "Max prefers trail running over roads"]
|
# 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>
|
<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.
|
**Expected output:** Only 2 memories stored—the marathon goal and trail preference. The greeting "hey how's it going" was filtered out automatically. Custom instructions are working.
|
||||||
@@ -221,8 +412,6 @@ Only meaningful facts. Filler gets dropped automatically.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Agent Memory for Personality
|
## Agent Memory for Personality
|
||||||
|
|
||||||
### Why Agents Need Memory Too
|
### Why Agents Need Memory Too
|
||||||
@@ -231,16 +420,30 @@ Max prefers direct feedback, not motivational fluff. Ray needs to remember how t
|
|||||||
|
|
||||||
Store agent personality:
|
Store agent personality:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
mem0_client.add(
|
mem0_client.add(
|
||||||
[{"role": "system", "content": "Max wants direct, data-driven feedback. Skip motivational language."}],
|
[{"role": "system", "content": "Max wants direct, data-driven feedback. Skip motivational language."}],
|
||||||
agent_id="ray_coach"
|
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:
|
Retrieve agent style alongside user memories:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
# Get coach personality
|
# Get coach personality
|
||||||
agent_memories = mem0_client.search("coaching style", agent_id="ray_coach")
|
agent_memories = mem0_client.search("coaching style", agent_id="ray_coach")
|
||||||
@@ -251,8 +454,26 @@ mem0_client.add([
|
|||||||
{"role": "user", "content": "How'd my run look today?"},
|
{"role": "user", "content": "How'd my run look today?"},
|
||||||
{"role": "assistant", "content": "Pace was 8:15/mile. Heart rate 152, zone 2."}
|
{"role": "assistant", "content": "Pace was 8:15/mile. Heart rate 152, zone 2."}
|
||||||
], user_id="max", agent_id="ray_coach")
|
], 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>
|
<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.
|
**Expected behavior:** Ray's responses are now data-driven and direct. The agent memory stored the coaching style preference, so future responses adapt automatically without Max having to repeat his preference.
|
||||||
@@ -268,6 +489,8 @@ No "Great job!" or "Keep it up!" - just data. Ray adapts to Max's preference.
|
|||||||
|
|
||||||
Don't send every single message to Mem0. Keep recent context in memory, let Mem0 handle the important long-term facts.
|
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
|
```python
|
||||||
# Store only meaningful exchanges in Mem0
|
# Store only meaningful exchanges in Mem0
|
||||||
mem0_client.add([
|
mem0_client.add([
|
||||||
@@ -280,8 +503,27 @@ mem0_client.add([
|
|||||||
# "cool thanks" → don't store
|
# "cool thanks" → don't store
|
||||||
|
|
||||||
# Or rely on custom_instructions to filter automatically
|
# 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.
|
Last 10 messages in your app's buffer. Important facts in Mem0. Faster, cheaper, still works.
|
||||||
|
|
||||||
@@ -293,6 +535,8 @@ Last 10 messages in your app's buffer. Important facts in Mem0. Faster, cheaper,
|
|||||||
|
|
||||||
Max tweaks his ankle. It'll heal in two weeks - the memory should expire too.
|
Max tweaks his ankle. It'll heal in two weeks - the memory should expire too.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
@@ -303,10 +547,26 @@ mem0_client.add(
|
|||||||
user_id="max",
|
user_id="max",
|
||||||
expiration_date=expiration
|
expiration_date=expiration
|
||||||
)
|
)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
In 14 days, this memory disappears automatically. Ray stops asking about the ankle.
|
In 14 days, this memory disappears automatically. Ray stops asking about the ankle.
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Open Source">
|
||||||
|
```python
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
memory.add(
|
||||||
|
[{"role": "user", "content": "Rolled my left ankle, needs rest"}],
|
||||||
|
user_id="max",
|
||||||
|
metadata={"memory_bucket": "constraints", "expires_on": expiration},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Store `expires_on` in metadata and prune expired memories in your app. Ray stops asking about the ankle once it's removed.
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -314,6 +574,8 @@ In 14 days, this memory disappears automatically. Ray stops asking about the ank
|
|||||||
|
|
||||||
Here's the Mem0 setup combining everything:
|
Here's the Mem0 setup combining everything:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
from mem0 import MemoryClient
|
from mem0 import MemoryClient
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -332,11 +594,55 @@ mem0_client.project.update(
|
|||||||
{"name": "preferences", "description": "Training style"}
|
{"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:**
|
**Week 1 - Store goals and preferences:**
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
mem0_client.add([
|
mem0_client.add([
|
||||||
{"role": "user", "content": "I want to run a sub-4 marathon"},
|
{"role": "user", "content": "I want to run a sub-4 marathon"},
|
||||||
@@ -346,11 +652,33 @@ mem0_client.add([
|
|||||||
mem0_client.add([
|
mem0_client.add([
|
||||||
{"role": "user", "content": "I prefer trail running over roads"}
|
{"role": "user", "content": "I prefer trail running over roads"}
|
||||||
], user_id="max", categories=["preferences"])
|
], 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:**
|
**Week 3 - Temporary injury with expiration:**
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
|
expiration = (datetime.now() + timedelta(days=14)).strftime("%Y-%m-%d")
|
||||||
mem0_client.add(
|
mem0_client.add(
|
||||||
@@ -359,16 +687,36 @@ mem0_client.add(
|
|||||||
categories=["constraints"],
|
categories=["constraints"],
|
||||||
expiration_date=expiration
|
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:**
|
**Retrieve for context:**
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
memories = mem0_client.search("training plan", user_id="max", limit=5)
|
memories = mem0_client.search("training plan", user_id="max", limit=5)
|
||||||
# Gets: marathon goal, trail preference, ankle injury (if still valid)
|
# 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.
|
Ray remembers goals, preferences, and personality. Handles temporary injuries. Works across sessions.
|
||||||
|
|
||||||
@@ -380,6 +728,8 @@ Ray remembers goals, preferences, and personality. Handles temporary injuries. W
|
|||||||
|
|
||||||
Training for Boston is different from training for New York. Separate the memory threads:
|
Training for Boston is different from training for New York. Separate the memory threads:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
mem0_client.add(messages, user_id="max", run_id="boston-2025")
|
mem0_client.add(messages, user_id="max", run_id="boston-2025")
|
||||||
mem0_client.add(messages, user_id="max", run_id="nyc-2025")
|
mem0_client.add(messages, user_id="max", run_id="nyc-2025")
|
||||||
@@ -390,8 +740,22 @@ boston_memories = mem0_client.search(
|
|||||||
user_id="max",
|
user_id="max",
|
||||||
run_id="boston-2025"
|
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.
|
Each race gets its own episodic boundary. No cross-contamination.
|
||||||
|
|
||||||
@@ -399,6 +763,8 @@ Each race gets its own episodic boundary. No cross-contamination.
|
|||||||
|
|
||||||
Max has 6 months of training logs to backfill:
|
Max has 6 months of training logs to backfill:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
old_logs = [
|
old_logs = [
|
||||||
[{"role": "user", "content": "Completed 20-mile long run"}],
|
[{"role": "user", "content": "Completed 20-mile long run"}],
|
||||||
@@ -407,13 +773,27 @@ old_logs = [
|
|||||||
|
|
||||||
for log in old_logs:
|
for log in old_logs:
|
||||||
mem0_client.add(log, user_id="max")
|
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
|
### Handling Contradictions
|
||||||
|
|
||||||
Max changes his goal from sub-4 to sub-3:45:
|
Max changes his goal from sub-4 to sub-3:45:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
# Find the old memory
|
# Find the old memory
|
||||||
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
memories = mem0_client.get_all(filters={"AND": [{"user_id": "max"}]})
|
||||||
@@ -421,8 +801,19 @@ goal_memory = [m for m in memories["results"] if "sub-4" in m["memory"]][0]
|
|||||||
|
|
||||||
# Update it
|
# Update it
|
||||||
mem0_client.update(goal_memory["id"], "Max wants to run sub-3:45 marathon")
|
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.
|
Update instead of creating duplicates.
|
||||||
|
|
||||||
@@ -430,11 +821,20 @@ Update instead of creating duplicates.
|
|||||||
|
|
||||||
Max works with Ray for running and Jordan for strength training:
|
Max works with Ray for running and Jordan for strength training:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
chat("easy run today", user_id="max", agent_id="ray")
|
chat("easy run today", user_id="max", agent_id="ray")
|
||||||
chat("leg day workout", user_id="max", agent_id="jordan")
|
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.
|
Each coach maintains separate personality memory while sharing user context.
|
||||||
|
|
||||||
@@ -442,19 +842,44 @@ Each coach maintains separate personality memory while sharing user context.
|
|||||||
|
|
||||||
Prioritize recent training over old data:
|
Prioritize recent training over old data:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
recent = mem0_client.search(
|
recent = mem0_client.search(
|
||||||
"training progress",
|
"training progress",
|
||||||
user_id="max",
|
user_id="max",
|
||||||
filters={"created_at": {"gte": "2025-10-01"}}
|
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
|
### Metadata Tagging
|
||||||
|
|
||||||
Tag workouts by type:
|
Tag workouts by type:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
mem0_client.add(
|
mem0_client.add(
|
||||||
[{"role": "user", "content": "10x400m intervals"}],
|
[{"role": "user", "content": "10x400m intervals"}],
|
||||||
@@ -468,20 +893,48 @@ speed_sessions = mem0_client.search(
|
|||||||
user_id="max",
|
user_id="max",
|
||||||
filters={"metadata": {"workout_type": "speed"}}
|
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
|
### Pruning Old Memories
|
||||||
|
|
||||||
Delete irrelevant memories:
|
Delete irrelevant memories:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
mem0_client.delete(memory_id="mem_xyz")
|
mem0_client.delete(memory_id="mem_xyz")
|
||||||
|
|
||||||
# Or clear an entire run_id
|
# Or clear an entire run_id
|
||||||
mem0_client.delete_all(user_id="max", run_id="old-training-cycle")
|
mem0_client.delete_all(user_id="max", run_id="old-training-cycle")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Open Source">
|
||||||
|
```python
|
||||||
|
memory.delete(memory_id="mem_xyz")
|
||||||
|
|
||||||
|
# Or clear an entire run_id
|
||||||
|
memory.delete_all(user_id="max", run_id="old-training-cycle")
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -515,7 +968,7 @@ Before launching:
|
|||||||
- Define 2-3 categories (goals, constraints, preferences)
|
- Define 2-3 categories (goals, constraints, preferences)
|
||||||
- Add expiration strategy for time-bound facts
|
- Add expiration strategy for time-bound facts
|
||||||
- Implement error handling for API calls
|
- Implement error handling for API calls
|
||||||
- Monitor memory quality in Mem0 dashboard
|
- Monitor memory quality (Mem0 dashboard or `get_all` / Qdrant when local)
|
||||||
- Clear test data from production project
|
- Clear test data from production project
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ Integrating Mem0 into your writing workflow helps you:
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
@@ -32,13 +34,81 @@ openai = OpenAI()
|
|||||||
USER_ID = "content_writer"
|
USER_ID = "content_writer"
|
||||||
RUN_ID = "smart_editing_session"
|
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
|
## Storing Your Writing Preferences in Mem0
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
def store_writing_preferences():
|
def store_writing_preferences():
|
||||||
"""Store your writing preferences in Mem0."""
|
"""Store your writing preferences in Mem0."""
|
||||||
|
|
||||||
preferences = """My writing preferences:
|
preferences = """My writing preferences:
|
||||||
1. Use headings and sub-headings for structure.
|
1. Use headings and sub-headings for structure.
|
||||||
2. Keep paragraphs concise (8–10 sentences max).
|
2. Keep paragraphs concise (8–10 sentences max).
|
||||||
@@ -61,9 +131,41 @@ def store_writing_preferences():
|
|||||||
|
|
||||||
return response
|
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
|
## Editing Content Using Stored Preferences
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
def apply_writing_style(original_content):
|
def apply_writing_style(original_content):
|
||||||
"""Use preferences stored in Mem0 to guide content rewriting."""
|
"""Use preferences stored in Mem0 to guide content rewriting."""
|
||||||
@@ -107,29 +209,73 @@ Preferences:
|
|||||||
|
|
||||||
return clean_response
|
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
|
## Complete Workflow: Content Editing
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def content_writing_workflow(content):
|
def content_writing_workflow(content):
|
||||||
"""Automated workflow for editing a document based on writing preferences."""
|
"""Automated workflow for editing a document based on writing preferences."""
|
||||||
|
|
||||||
# Store writing preferences (if not already stored)
|
# Store writing preferences (if not already stored)
|
||||||
store_writing_preferences() # Ideally done once, or with a conditional check
|
store_writing_preferences() # Ideally done once, or with a conditional check
|
||||||
|
|
||||||
# Edit the document with Mem0 preferences
|
# Edit the document with Mem0 preferences
|
||||||
edited_content = apply_writing_style(content)
|
edited_content = apply_writing_style(content)
|
||||||
|
|
||||||
if not edited_content:
|
if not edited_content:
|
||||||
return "Failed to edit document."
|
return "Failed to edit document."
|
||||||
|
|
||||||
# Display results
|
# Display results
|
||||||
print("\n=== ORIGINAL DOCUMENT ===\n")
|
print("\n=== ORIGINAL DOCUMENT ===\n")
|
||||||
print(content)
|
print(content)
|
||||||
|
|
||||||
print("\n=== EDITED DOCUMENT ===\n")
|
print("\n=== EDITED DOCUMENT ===\n")
|
||||||
print(edited_content)
|
print(edited_content)
|
||||||
|
|
||||||
return edited_content
|
return edited_content
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,8 +284,8 @@ def content_writing_workflow(content):
|
|||||||
```python
|
```python
|
||||||
# Define your document
|
# Define your document
|
||||||
original_content = """Project Proposal
|
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.
|
We believe this approach will significantly increase our market share.
|
||||||
|
|
||||||
Increase brand awareness
|
Increase brand awareness
|
||||||
@@ -160,8 +306,8 @@ Your document will be transformed into a structured, well-formatted version base
|
|||||||
### Original Document
|
### Original Document
|
||||||
```
|
```
|
||||||
Project Proposal
|
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.
|
We believe this approach will significantly increase our market share.
|
||||||
|
|
||||||
Increase brand awareness
|
Increase brand awareness
|
||||||
@@ -204,7 +350,7 @@ This proposal outlines our strategy for the Q3 marketing campaign. We aim to sig
|
|||||||
|
|
||||||
### Conclusion
|
### 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.
|
Mem0 enables a seamless, intelligent content-writing workflow, perfect for content creators, marketers, and technical writers looking to scale their personal tone and structure across work.
|
||||||
|
|||||||
@@ -18,11 +18,36 @@ Email overload is a common challenge for many professionals. By leveraging Mem0'
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
Before you begin, ensure you have the required dependencies installed:
|
Before you begin, ensure you have the required dependencies installed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install mem0ai openai
|
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
|
## Implementation
|
||||||
|
|
||||||
@@ -30,6 +55,8 @@ pip install mem0ai openai
|
|||||||
|
|
||||||
The following example shows how to create a basic email processing system with Mem0:
|
The following example shows how to create a basic email processing system with Mem0:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Platform">
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
from mem0 import MemoryClient
|
from mem0 import MemoryClient
|
||||||
@@ -45,11 +72,11 @@ class EmailProcessor:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the Email Processor with Mem0 memory client"""
|
"""Initialize the Email Processor with Mem0 memory client"""
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
def process_email(self, email_content, user_id):
|
def process_email(self, email_content, user_id):
|
||||||
"""
|
"""
|
||||||
Process an email and store it in Mem0 memory
|
Process an email and store it in Mem0 memory
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
email_content (str): Raw email content
|
email_content (str): Raw email content
|
||||||
user_id (str): User identifier for memory association
|
user_id (str): User identifier for memory association
|
||||||
@@ -57,20 +84,20 @@ class EmailProcessor:
|
|||||||
# Parse email
|
# Parse email
|
||||||
parser = Parser()
|
parser = Parser()
|
||||||
email = parser.parsestr(email_content)
|
email = parser.parsestr(email_content)
|
||||||
|
|
||||||
# Extract email details
|
# Extract email details
|
||||||
sender = email['from']
|
sender = email['from']
|
||||||
recipient = email['to']
|
recipient = email['to']
|
||||||
subject = email['subject']
|
subject = email['subject']
|
||||||
date = email['date']
|
date = email['date']
|
||||||
body = self._get_email_body(email)
|
body = self._get_email_body(email)
|
||||||
|
|
||||||
# Create message object for Mem0
|
# Create message object for Mem0
|
||||||
message = {
|
message = {
|
||||||
"role": "user",
|
"role": "user",
|
||||||
"content": f"Email from {sender}: {subject}\n\n{body}"
|
"content": f"Email from {sender}: {subject}\n\n{body}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create metadata for better retrieval
|
# Create metadata for better retrieval
|
||||||
metadata = {
|
metadata = {
|
||||||
"email_type": "incoming",
|
"email_type": "incoming",
|
||||||
@@ -79,18 +106,18 @@ class EmailProcessor:
|
|||||||
"subject": subject,
|
"subject": subject,
|
||||||
"date": date
|
"date": date
|
||||||
}
|
}
|
||||||
|
|
||||||
# Store in Mem0 with appropriate categories
|
# Store in Mem0 with appropriate categories
|
||||||
response = self.client.add(
|
response = self.client.add(
|
||||||
messages=[message],
|
messages=[message],
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
categories=["email", "correspondence"],
|
categories=["email", "correspondence"],
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _get_email_body(self, email):
|
def _get_email_body(self, email):
|
||||||
"""Extract the body content from an email"""
|
"""Extract the body content from an email"""
|
||||||
# Simplified extraction - in real-world, handle multipart emails
|
# Simplified extraction - in real-world, handle multipart emails
|
||||||
@@ -100,7 +127,7 @@ class EmailProcessor:
|
|||||||
return part.get_payload(decode=True).decode()
|
return part.get_payload(decode=True).decode()
|
||||||
else:
|
else:
|
||||||
return email.get_payload(decode=True).decode()
|
return email.get_payload(decode=True).decode()
|
||||||
|
|
||||||
def search_emails(self, query, user_id, sender=None):
|
def search_emails(self, query, user_id, sender=None):
|
||||||
"""
|
"""
|
||||||
Search through stored emails
|
Search through stored emails
|
||||||
@@ -132,7 +159,7 @@ class EmailProcessor:
|
|||||||
results = self.client.search(query=query, filters=filters)
|
results = self.client.search(query=query, filters=filters)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def get_email_thread(self, subject, user_id):
|
def get_email_thread(self, subject, user_id):
|
||||||
"""
|
"""
|
||||||
Retrieve all emails in a thread based on subject
|
Retrieve all emails in a thread based on subject
|
||||||
@@ -182,6 +209,208 @@ processor.process_email(sample_email, user_id)
|
|||||||
meeting_emails = processor.search_emails("meeting schedule", user_id)
|
meeting_emails = processor.search_emails("meeting schedule", user_id)
|
||||||
print(f"Found {len(meeting_emails['results'])} relevant emails")
|
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
|
## 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.",
|
"description": "Mem0 is a self-improving memory layer for LLM applications, enabling personalized AI experiences that save costs and delight users.",
|
||||||
"theme": "aspen",
|
"theme": "aspen",
|
||||||
"colors": {
|
"colors": {
|
||||||
"primary": "#9C58FA",
|
"primary": "#8F74E0",
|
||||||
"light": "#9C58FA",
|
"light": "#8F74E0",
|
||||||
"dark": "#9C58FA"
|
"dark": "#8F74E0"
|
||||||
},
|
},
|
||||||
"favicon": "/logo/favicon.png",
|
"favicon": "/logo/favicon.png",
|
||||||
"logo": {
|
"logo": {
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"platform/overview",
|
"platform/overview",
|
||||||
"vibecoding",
|
"vibecoding",
|
||||||
"platform/mem0-mcp",
|
"platform/mem0-mcp",
|
||||||
|
"platform/cli",
|
||||||
"platform/platform-vs-oss",
|
"platform/platform-vs-oss",
|
||||||
"platform/quickstart"
|
"platform/quickstart"
|
||||||
]
|
]
|
||||||
@@ -429,6 +430,20 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tab": "Agent Plugins",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Coding Agents",
|
||||||
|
"icon": "terminal",
|
||||||
|
"pages": [
|
||||||
|
"integrations/claude-code",
|
||||||
|
"integrations/cursor",
|
||||||
|
"integrations/codex"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tab": "API Reference",
|
"tab": "API Reference",
|
||||||
"groups": [
|
"groups": [
|
||||||
|
|||||||
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 282 KiB |
@@ -381,4 +381,25 @@ Here are the available integrations for Mem0:
|
|||||||
>
|
>
|
||||||
Build AI agents with persistent memory using Mastra's framework and tools.
|
Build AI agents with persistent memory using Mastra's framework and tools.
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Claude Code"
|
||||||
|
icon="terminal"
|
||||||
|
href="/integrations/claude-code"
|
||||||
|
>
|
||||||
|
Add persistent memory to Claude Code and Cowork with MCP server, lifecycle hooks, and SDK skill.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Cursor"
|
||||||
|
icon="i-cursor"
|
||||||
|
href="/integrations/cursor"
|
||||||
|
>
|
||||||
|
Add persistent memory to Cursor with MCP server, lifecycle hooks, and context-aware coding.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Codex"
|
||||||
|
icon="robot"
|
||||||
|
href="/integrations/codex"
|
||||||
|
>
|
||||||
|
Add persistent memory to OpenAI Codex with MCP server and skill-based memory protocol.
|
||||||
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
title: Claude Code
|
||||||
|
description: "Add persistent memory to Claude Code and Claude Cowork with the Mem0 plugin — MCP server, lifecycle hooks, and SDK skill."
|
||||||
|
---
|
||||||
|
|
||||||
|
Add persistent memory to [**Claude Code**](https://docs.anthropic.com/en/docs/claude-code) (CLI) and **Claude Cowork** (desktop app) with the Mem0 plugin. Your agent forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||||
|
2. **Lifecycle Hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
|
||||||
|
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||||
|
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before setting up Mem0 with Claude Code, ensure you have:
|
||||||
|
|
||||||
|
1. A Mem0 Platform account and API key:
|
||||||
|
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||||
|
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||||
|
|
||||||
|
2. Claude Code CLI or Claude Cowork desktop app installed
|
||||||
|
|
||||||
|
3. Your API key exported in your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MEM0_API_KEY="m0-your-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option A — Plugin Marketplace (Recommended)
|
||||||
|
|
||||||
|
Install the full plugin including MCP server, lifecycle hooks, and SDK skill:
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add mem0ai/mem0
|
||||||
|
/plugin install mem0@mem0-plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
**Claude Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
|
||||||
|
|
||||||
|
### Option B — MCP Only
|
||||||
|
|
||||||
|
Add the Mem0 MCP server directly with a single command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx mcp-add \
|
||||||
|
--name mem0-mcp \
|
||||||
|
--type http \
|
||||||
|
--url "https://mcp.mem0.ai/mcp" \
|
||||||
|
--clients "claude code"
|
||||||
|
```
|
||||||
|
|
||||||
|
This gives you the MCP tools but not the lifecycle hooks or SDK skill.
|
||||||
|
|
||||||
|
### Option C — Manual MCP Configuration
|
||||||
|
|
||||||
|
Add to your Claude Code MCP config (`.mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mem0": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.mem0.ai/mcp/",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Token ${MEM0_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Info icon="check">
|
||||||
|
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
| Component | Plugin Install | MCP Only |
|
||||||
|
|-----------|:--------------:|:--------:|
|
||||||
|
| MCP Server (9 memory tools) | Yes | Yes |
|
||||||
|
| Lifecycle Hooks | Yes | No |
|
||||||
|
| Mem0 SDK Skill | Yes | No |
|
||||||
|
|
||||||
|
## Available MCP Tools
|
||||||
|
|
||||||
|
Once installed, the following tools are available in every Claude Code session:
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_memory` | Save text or conversation history for a user/agent |
|
||||||
|
| `search_memories` | Semantic search across memories with filters |
|
||||||
|
| `get_memories` | List memories with filters and pagination |
|
||||||
|
| `get_memory` | Retrieve a specific memory by ID |
|
||||||
|
| `update_memory` | Overwrite a memory's text by ID |
|
||||||
|
| `delete_memory` | Delete a single memory by ID |
|
||||||
|
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||||
|
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||||
|
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||||
|
|
||||||
|
## Lifecycle Hooks
|
||||||
|
|
||||||
|
When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecycle to automatically manage memory:
|
||||||
|
|
||||||
|
### Session Start
|
||||||
|
On every new session, the plugin prompts Claude to call `search_memories` to load relevant context from prior sessions. On resumed or post-compaction sessions, it adjusts the prompt accordingly.
|
||||||
|
|
||||||
|
### User Prompt
|
||||||
|
Before processing each user message, the plugin searches Mem0 for memories relevant to the current prompt and injects them into context. Short prompts (< 20 characters) are skipped to minimize latency.
|
||||||
|
|
||||||
|
### Pre-Compaction
|
||||||
|
Before context compaction, the plugin prompts Claude to store a comprehensive session summary — including goals, accomplishments, decisions, modified files, and current state — so nothing is lost.
|
||||||
|
|
||||||
|
### Task Completed
|
||||||
|
After each task completion, the plugin prompts Claude to extract and store key learnings: successful strategies, failed approaches, architectural decisions, and new conventions.
|
||||||
|
|
||||||
|
### Session End
|
||||||
|
When Claude finishes responding, the plugin prompts for any unstored learnings and captures transcript state via the Mem0 REST API as a background safety net.
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Session 1: Working on a feature
|
||||||
|
You: Let's refactor the auth module to use JWT tokens instead of sessions.
|
||||||
|
|
||||||
|
# Claude searches memories, finds nothing relevant, proceeds with the work.
|
||||||
|
# After completing the task, Mem0 stores:
|
||||||
|
# - Decision: "Migrated auth from sessions to JWT tokens"
|
||||||
|
# - Files modified: auth/middleware.ts, auth/token.ts
|
||||||
|
# - User preference: "Prefers TypeScript, uses ESLint"
|
||||||
|
|
||||||
|
# Session 2 (days later): Related work
|
||||||
|
You: Add refresh token rotation to the auth system.
|
||||||
|
|
||||||
|
# Claude searches memories, retrieves the JWT migration context.
|
||||||
|
# Knows the file structure, decisions made, and user preferences.
|
||||||
|
# Continues seamlessly without re-explaining the codebase.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||||
|
- **No tools appearing** — Restart your Claude Code session after installation
|
||||||
|
- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations.
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||||
|
Detailed MCP configuration for all clients
|
||||||
|
</Card>
|
||||||
|
<Card title="Codex Integration" icon="robot" href="/integrations/codex">
|
||||||
|
Add Mem0 memory to OpenAI Codex workflows
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
title: Codex
|
||||||
|
description: "Add persistent memory to OpenAI Codex with the Mem0 plugin — MCP server, memory protocol skill, and plugin marketplace support."
|
||||||
|
---
|
||||||
|
|
||||||
|
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP and using a skill-based memory protocol to automatically retrieve context and store learnings.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||||
|
2. **Memory Protocol Skill** — Instructs the agent to retrieve memories at task start, store learnings on completion, and capture session state before context loss
|
||||||
|
3. **Plugin Marketplace** — Install via Codex's repo-level or personal plugin marketplace
|
||||||
|
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before setting up Mem0 with Codex, ensure you have:
|
||||||
|
|
||||||
|
1. A Mem0 Platform account and API key:
|
||||||
|
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||||
|
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||||
|
|
||||||
|
2. OpenAI Codex access
|
||||||
|
|
||||||
|
3. Your API key exported in your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MEM0_API_KEY="m0-your-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option A — Repo Marketplace (Recommended for Teams)
|
||||||
|
|
||||||
|
Add a `.agents/plugins/marketplace.json` to your repository root:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "mem0-plugins",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Mem0 Plugins"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "./plugins/mem0"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||||
|
|
||||||
|
### Option B — Personal Marketplace
|
||||||
|
|
||||||
|
Add to `~/.agents/plugins/marketplace.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "mem0-plugins",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Mem0 Plugins"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"source": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "/path/to/mem0-plugin"
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"installation": "AVAILABLE",
|
||||||
|
"authentication": "ON_INSTALL"
|
||||||
|
},
|
||||||
|
"category": "Productivity"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option C — Manual MCP Configuration
|
||||||
|
|
||||||
|
Add to your Codex MCP config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mem0": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "https://mcp.mem0.ai/mcp/",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Token ${MEM0_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<Info icon="check">
|
||||||
|
Start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
| Component | Plugin Install | MCP Only |
|
||||||
|
|-----------|:--------------:|:--------:|
|
||||||
|
| MCP Server (9 memory tools) | Yes | Yes |
|
||||||
|
| Memory Protocol Skill | Yes | No |
|
||||||
|
| Mem0 SDK Skill | Yes | No |
|
||||||
|
|
||||||
|
## Available MCP Tools
|
||||||
|
|
||||||
|
Once installed, the following tools are available in every Codex session:
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_memory` | Save text or conversation history for a user/agent |
|
||||||
|
| `search_memories` | Semantic search across memories with filters |
|
||||||
|
| `get_memories` | List memories with filters and pagination |
|
||||||
|
| `get_memory` | Retrieve a specific memory by ID |
|
||||||
|
| `update_memory` | Overwrite a memory's text by ID |
|
||||||
|
| `delete_memory` | Delete a single memory by ID |
|
||||||
|
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||||
|
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||||
|
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||||
|
|
||||||
|
## Memory Protocol Skill
|
||||||
|
|
||||||
|
Codex uses a skill-based approach instead of lifecycle hooks. When installed via the plugin marketplace, the memory protocol skill instructs the agent to:
|
||||||
|
|
||||||
|
### On Every New Task
|
||||||
|
1. Call `search_memories` with a query related to the current task to load relevant context
|
||||||
|
2. Review returned memories to understand what was learned in prior sessions
|
||||||
|
3. Optionally call `get_memories` to browse all stored memories
|
||||||
|
|
||||||
|
### After Completing Significant Work
|
||||||
|
Store key learnings using `add_memory` with structured metadata:
|
||||||
|
|
||||||
|
| What to store | Metadata type |
|
||||||
|
|--------------|---------------|
|
||||||
|
| Architectural decisions | `{"type": "decision"}` |
|
||||||
|
| Strategies that worked | `{"type": "task_learning"}` |
|
||||||
|
| Failed approaches | `{"type": "anti_pattern"}` |
|
||||||
|
| User preferences observed | `{"type": "user_preference"}` |
|
||||||
|
| Environment discoveries | `{"type": "environmental"}` |
|
||||||
|
| Conventions established | `{"type": "convention"}` |
|
||||||
|
|
||||||
|
### Before Losing Context
|
||||||
|
Store a comprehensive session summary including goals, accomplishments, decisions, files modified, and current state with metadata `{"type": "session_state"}`.
|
||||||
|
|
||||||
|
## Plugin Manifest
|
||||||
|
|
||||||
|
The Codex plugin manifest (`.codex-plugin/plugin.json`) follows the Codex plugin specification:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "mem0",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Mem0 memory layer for AI applications.",
|
||||||
|
"skills": "./skills/",
|
||||||
|
"mcpServers": "./.codex-mcp.json",
|
||||||
|
"interface": {
|
||||||
|
"displayName": "Mem0",
|
||||||
|
"shortDescription": "Persistent memory layer for AI coding workflows",
|
||||||
|
"category": "Productivity",
|
||||||
|
"capabilities": ["Read", "Write"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Task 1: Setting up a new service
|
||||||
|
You: Create a REST API for the notifications service using Express and TypeScript.
|
||||||
|
|
||||||
|
# Codex searches memories, finds user preferences from prior tasks.
|
||||||
|
# After completing the task, Mem0 stores:
|
||||||
|
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
|
||||||
|
# - Convention: "All API routes follow /api/v1/{resource} pattern"
|
||||||
|
# - Preference: "User prefers explicit error types over generic catch-all"
|
||||||
|
|
||||||
|
# Task 2 (days later): Extending the service
|
||||||
|
You: Add WebSocket support for real-time notification delivery.
|
||||||
|
|
||||||
|
# Codex searches memories, retrieves the architecture decisions and conventions.
|
||||||
|
# Follows the same patterns established in the first task.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||||
|
- **No tools appearing** — Restart your Codex session after plugin installation
|
||||||
|
- **Plugin not found** — Ensure `.agents/plugins/marketplace.json` is at the repository root and `source.path` points to the correct plugin directory
|
||||||
|
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||||
|
Detailed MCP configuration for all clients
|
||||||
|
</Card>
|
||||||
|
<Card title="Claude Code Integration" icon="terminal" href="/integrations/claude-code">
|
||||||
|
Add Mem0 memory to Claude Code workflows
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
---
|
||||||
|
title: Cursor
|
||||||
|
description: "Add persistent memory to Cursor with the Mem0 plugin — MCP server, lifecycle hooks, and SDK skill for context-aware coding."
|
||||||
|
---
|
||||||
|
|
||||||
|
Add persistent memory to [**Cursor**](https://cursor.com) with the Mem0 plugin. Your AI assistant forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||||
|
2. **Lifecycle Hooks** — Automatic memory capture at session start, compaction, and user prompts (Marketplace install)
|
||||||
|
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||||
|
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before setting up Mem0 with Cursor, ensure you have:
|
||||||
|
|
||||||
|
1. A Mem0 Platform account and API key:
|
||||||
|
- [Sign up at app.mem0.ai](https://app.mem0.ai)
|
||||||
|
- [Get your API key](https://app.mem0.ai/dashboard/api-keys) (starts with `m0-`)
|
||||||
|
|
||||||
|
2. Cursor installed ([cursor.com](https://cursor.com))
|
||||||
|
|
||||||
|
3. Your API key exported in your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MEM0_API_KEY="m0-your-api-key"
|
||||||
|
```
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Already have `mem0` configured as an MCP server in Cursor? Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option A — One-Click Deeplink (MCP Only)
|
||||||
|
|
||||||
|
The fastest way to get started. Click the link below to install the Mem0 MCP server directly in Cursor:
|
||||||
|
|
||||||
|
[Install Mem0 MCP in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=mem0&config=eyJtY3BTZXJ2ZXJzIjp7Im1lbTAiOnsidXJsIjoiaHR0cHM6Ly9tY3AubWVtMC5haS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiVG9rZW4gJHtlbnY6TUVNMF9BUElfS0VZfSJ9fX19)
|
||||||
|
|
||||||
|
### Option B — npx (MCP Only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx mcp-add \
|
||||||
|
--name mem0-mcp \
|
||||||
|
--type http \
|
||||||
|
--url "https://mcp.mem0.ai/mcp" \
|
||||||
|
--clients "cursor"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option C — Manual Configuration (MCP Only)
|
||||||
|
|
||||||
|
Add the following to your `.cursor/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"mem0": {
|
||||||
|
"url": "https://mcp.mem0.ai/mcp/",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option D — Cursor Marketplace (Full Plugin)
|
||||||
|
|
||||||
|
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks, the Mem0 SDK skill, and automatic memory capture.
|
||||||
|
|
||||||
|
<Info icon="check">
|
||||||
|
Start a new Cursor session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
## What's Included
|
||||||
|
|
||||||
|
| Component | Marketplace Install | Deeplink / Manual / npx |
|
||||||
|
|-----------|:-------------------:|:-----------------------:|
|
||||||
|
| MCP Server (9 memory tools) | Yes | Yes |
|
||||||
|
| Lifecycle Hooks | Yes | No |
|
||||||
|
| Mem0 SDK Skill | Yes | No |
|
||||||
|
|
||||||
|
## Available MCP Tools
|
||||||
|
|
||||||
|
Once installed, the following tools are available in every Cursor session:
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `add_memory` | Save text or conversation history for a user/agent |
|
||||||
|
| `search_memories` | Semantic search across memories with filters |
|
||||||
|
| `get_memories` | List memories with filters and pagination |
|
||||||
|
| `get_memory` | Retrieve a specific memory by ID |
|
||||||
|
| `update_memory` | Overwrite a memory's text by ID |
|
||||||
|
| `delete_memory` | Delete a single memory by ID |
|
||||||
|
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||||
|
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||||
|
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||||
|
|
||||||
|
## Lifecycle Hooks (Marketplace Install)
|
||||||
|
|
||||||
|
When installed via the Cursor Marketplace, Mem0 hooks into Cursor's lifecycle:
|
||||||
|
|
||||||
|
### Session Start
|
||||||
|
On every new session, the plugin prompts the agent to call `search_memories` to load relevant context from prior sessions.
|
||||||
|
|
||||||
|
### User Prompt
|
||||||
|
Before processing each user message, the plugin searches Mem0 for relevant memories and injects them into context. Short prompts are skipped to minimize latency.
|
||||||
|
|
||||||
|
### Pre-Compaction
|
||||||
|
Before context compaction, the plugin captures a comprehensive session summary so nothing is lost when the context window resets.
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Session 1: Debugging a performance issue
|
||||||
|
You: The API endpoint /users is taking 3 seconds. Help me optimize it.
|
||||||
|
|
||||||
|
# Cursor agent searches memories, proceeds with investigation.
|
||||||
|
# After completing the task, Mem0 stores:
|
||||||
|
# - Learning: "N+1 query in UserService.getAll() — fixed with eager loading"
|
||||||
|
# - Decision: "Added database index on users.email column"
|
||||||
|
# - Preference: "User prefers query-level fixes over caching"
|
||||||
|
|
||||||
|
# Session 2 (next week): Similar issue
|
||||||
|
You: The /orders endpoint is also slow, same pattern as before.
|
||||||
|
|
||||||
|
# Agent searches memories, retrieves the optimization learnings.
|
||||||
|
# Immediately checks for N+1 queries and missing indexes.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Connection failed"** — Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
|
||||||
|
- **Duplicate tools** — If you had a previous MCP config for `mem0`, remove it before installing the plugin
|
||||||
|
- **No tools appearing** — Go to Cursor Settings > MCP and verify the `mem0` server shows as connected
|
||||||
|
- **Hooks not running** — Hooks require the Marketplace install (Option D). Deeplink/manual installs only provide MCP tools.
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||||
|
Detailed MCP configuration for all clients
|
||||||
|
</Card>
|
||||||
|
<Card title="Claude Code Integration" icon="terminal" href="/integrations/claude-code">
|
||||||
|
Add Mem0 memory to Claude Code workflows
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
@@ -4,16 +4,16 @@ description: "Mem0 is a universal, self-improving memory layer for LLM applicati
|
|||||||
mode: "custom"
|
mode: "custom"
|
||||||
---
|
---
|
||||||
|
|
||||||
{/* debug: welcome-layout-v2 */}
|
{/* debug: welcome-layout-v3-grid */}
|
||||||
|
|
||||||
<div className="px-4 pt-16 pb-12 lg:pt-20 max-w-4xl mx-auto text-center space-y-6">
|
<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-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>
|
Build with <span className="text-primary">Mem0</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="max-w-2xl mx-auto text-base text-gray-600 dark:text-zinc-400 leading-relaxed">
|
<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.
|
Universal, Self-improving memory layer for LLM applications.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/platform/quickstart"
|
href="/platform/quickstart"
|
||||||
@@ -24,31 +24,25 @@ mode: "custom"
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="px-4 max-w-6xl mx-auto space-y-4">
|
<section className="px-4 pt-4 pb-6 max-w-6xl mx-auto">
|
||||||
<div className="text-center">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<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
|
<a
|
||||||
href="/platform/overview"
|
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"
|
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
|
<img
|
||||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/light/mem0_platform.png"
|
src="/images/docs thumbnails/light/mem0_platform.png"
|
||||||
alt="Mem0 Platform thumbnail"
|
alt="Mem0 Platform thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/dark/mem0_platform.png"
|
src="/images/docs thumbnails/dark/mem0_platform.png"
|
||||||
alt="Mem0 Platform thumbnail"
|
alt="Mem0 Platform thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||||
Mem0 Platform
|
Mem0 Platform
|
||||||
</h3>
|
</h3>
|
||||||
@@ -63,18 +57,18 @@ mode: "custom"
|
|||||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
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
|
<img
|
||||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/light/mem0_open_source.png"
|
src="/images/docs thumbnails/light/mem0_open_source.png"
|
||||||
alt="Mem0 Open Source thumbnail"
|
alt="Mem0 Open Source thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/dark/mem0_open_source.png"
|
src="/images/docs thumbnails/dark/mem0_open_source.png"
|
||||||
alt="Mem0 Open Source thumbnail"
|
alt="Mem0 Open Source thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||||
Mem0 Open Source
|
Mem0 Open Source
|
||||||
</h3>
|
</h3>
|
||||||
@@ -84,34 +78,23 @@ mode: "custom"
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</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
|
<a
|
||||||
href="/cookbooks/overview"
|
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"
|
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
|
<img
|
||||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/light/Cookbooks.png"
|
src="/images/docs thumbnails/light/Cookbooks.png"
|
||||||
alt="Cookbooks thumbnail"
|
alt="Cookbooks thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/dark/Cookbooks.png"
|
src="/images/docs thumbnails/dark/Cookbooks.png"
|
||||||
alt="Cookbooks thumbnail"
|
alt="Cookbooks thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||||
Cookbooks
|
Cookbooks
|
||||||
</h3>
|
</h3>
|
||||||
@@ -126,18 +109,18 @@ mode: "custom"
|
|||||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
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
|
<img
|
||||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/light/Integrations.png"
|
src="/images/docs thumbnails/light/Integrations.png"
|
||||||
alt="Integrations thumbnail"
|
alt="Integrations thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/dark/Integrations.png"
|
src="/images/docs thumbnails/dark/Integrations.png"
|
||||||
alt="Integrations thumbnail"
|
alt="Integrations thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||||
Integrations
|
Integrations
|
||||||
</h3>
|
</h3>
|
||||||
@@ -152,25 +135,51 @@ mode: "custom"
|
|||||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
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
|
<img
|
||||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
className="block dark:hidden aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/light/API.png"
|
src="/images/docs thumbnails/light/API.png"
|
||||||
alt="API reference thumbnail"
|
alt="API reference thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
className="hidden dark:block aspect-[2/1] w-full object-cover"
|
||||||
src="/images/docs thumbnails/dark/API.png"
|
src="/images/docs thumbnails/dark/API.png"
|
||||||
alt="API reference thumbnail"
|
alt="API reference thumbnail"
|
||||||
style={{pointerEvents: "none"}}
|
style={{pointerEvents: "none"}}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-3 text-left">
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||||
API reference
|
API Reference
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600 dark:text-zinc-400">
|
<p className="text-sm text-gray-600 dark:text-zinc-400">
|
||||||
Explore every REST endpoint with payload examples and usage guidance.
|
Explore every REST endpoint with payload examples and usage guidance.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ Key differentiators:
|
|||||||
## Getting Started
|
## 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
|
- [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
|
- [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 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
|
- [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
|
## Core Concepts
|
||||||
|
|
||||||
@@ -28,20 +31,20 @@ Key differentiators:
|
|||||||
- [Memory Operations - Update](https://docs.mem0.ai/core-concepts/memory-operations/update): Modifying existing memories when new information conflicts or supplements stored data
|
- [Memory Operations - 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
|
- [Memory Operations - Delete](https://docs.mem0.ai/core-concepts/memory-operations/delete): Removing outdated or irrelevant memories to maintain memory quality
|
||||||
|
|
||||||
## Platform (Managed Solution)
|
## Platform Features
|
||||||
|
|
||||||
- [Platform Quickstart](https://docs.mem0.ai/platform/quickstart): Complete guide to using Mem0 Platform with Python, JavaScript, and cURL examples
|
- [Platform Features Overview](https://docs.mem0.ai/platform/features/platform-overview): High-level overview of all Mem0 Platform capabilities
|
||||||
- [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
|
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations): Sophisticated memory management techniques for complex applications
|
||||||
|
|
||||||
### Essential Platform Features
|
### Essential Features
|
||||||
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters): Advanced filtering and querying capabilities
|
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters): Advanced filtering and querying capabilities for memories
|
||||||
|
- [Entity-Scoped Memory](https://docs.mem0.ai/platform/features/entity-scoped-memory): Organize memories by user, agent, app, and session identifiers
|
||||||
- [Async Client](https://docs.mem0.ai/platform/features/async-client): Non-blocking operations for high-concurrency applications
|
- [Async 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
|
- [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
|
- [Custom Categories](https://docs.mem0.ai/platform/features/custom-categories): Define domain-specific categories to improve memory organization
|
||||||
- [Async Mode Default Changes](https://docs.mem0.ai/platform/features/async-mode-default-change): Understanding new async behavior defaults
|
|
||||||
|
|
||||||
### Advanced Platform Features
|
### Advanced Features
|
||||||
- [Graph Memory](https://docs.mem0.ai/platform/features/graph-memory): Build and query relationships between entities for contextually relevant retrieval
|
- [Graph 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
|
- [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
|
- [Advanced Retrieval](https://docs.mem0.ai/platform/features/advanced-retrieval): Enhanced search with keyword search, reranking, and filtering capabilities
|
||||||
@@ -59,10 +62,15 @@ Key differentiators:
|
|||||||
- [Webhooks](https://docs.mem0.ai/platform/features/webhooks): Real-time notifications for memory events
|
- [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
|
- [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
|
- [Group Chat Support](https://docs.mem0.ai/platform/features/group-chat): Multi-conversation memory management
|
||||||
|
- [MCP Integration](https://docs.mem0.ai/platform/features/mcp-integration): Model Context Protocol integration for AI coding tools
|
||||||
|
|
||||||
### Platform Support
|
### Support & Migration
|
||||||
- [FAQs](https://docs.mem0.ai/platform/faqs): Frequently asked questions about Mem0 Platform
|
- [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
|
- [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
|
## Open Source
|
||||||
|
|
||||||
@@ -72,93 +80,130 @@ Key differentiators:
|
|||||||
- [Configuration Guide](https://docs.mem0.ai/open-source/configuration): Complete configuration options for self-hosted deployment
|
- [Configuration Guide](https://docs.mem0.ai/open-source/configuration): Complete configuration options for self-hosted deployment
|
||||||
|
|
||||||
### Open Source Features
|
### Open Source Features
|
||||||
- [OpenAI Compatibility](https://docs.mem0.ai/open-source/features/openai_compatibility): Seamless integration with OpenAI-compatible APIs
|
- [Features Overview](https://docs.mem0.ai/open-source/features/overview): Overview of all open-source features
|
||||||
- [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
|
- [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
|
- [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
|
- [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
|
- [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
|
- [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 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
|
- [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
|
## Components
|
||||||
|
|
||||||
|
### LLMs
|
||||||
- [LLM Overview](https://docs.mem0.ai/components/llms/overview): Comprehensive guide to Large Language Model integration and configuration options
|
- [LLM Overview](https://docs.mem0.ai/components/llms/overview): Comprehensive guide to Large Language Model integration and configuration options
|
||||||
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview): Guide to supported vector databases for semantic memory storage
|
- [LLM Configuration](https://docs.mem0.ai/components/llms/config): Configuration reference for LLM providers
|
||||||
- [Embeddings Overview](https://docs.mem0.ai/components/embedders/overview): Embedding model configuration for semantic understanding
|
- [OpenAI](https://docs.mem0.ai/components/llms/models/openai): Integration with OpenAI models including GPT-4
|
||||||
|
|
||||||
### 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
|
- [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
|
- [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
|
- [Ollama](https://docs.mem0.ai/components/llms/models/ollama): Local model deployment for privacy-focused applications
|
||||||
- [vLLM](https://docs.mem0.ai/components/llms/models/vllm): High-performance inference framework
|
|
||||||
- [LM Studio](https://docs.mem0.ai/components/llms/models/lmstudio): Local model management and deployment
|
|
||||||
- [Together](https://docs.mem0.ai/components/llms/models/together): Open-source model inference platform
|
- [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
|
- [Groq](https://docs.mem0.ai/components/llms/models/groq): High-performance LPU optimized models for fast inference
|
||||||
- [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
|
- [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
|
- [Mistral AI](https://docs.mem0.ai/components/llms/models/mistral_AI): Mistral model integration
|
||||||
- [OpenAI Structured](https://docs.mem0.ai/components/llms/models/openai_structured): OpenAI with structured output support
|
- [Google AI](https://docs.mem0.ai/components/llms/models/google_AI): Gemini model integration for multimodal applications
|
||||||
- [Azure OpenAI Structured](https://docs.mem0.ai/components/llms/models/azure_openai_structured): Azure OpenAI with structured outputs
|
- [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
|
||||||
### Supported Vector Databases
|
- [MiniMax](https://docs.mem0.ai/components/llms/models/minimax): MiniMax model integration
|
||||||
|
- [xAI](https://docs.mem0.ai/components/llms/models/xAI): xAI Grok models integration
|
||||||
|
- [Sarvam](https://docs.mem0.ai/components/llms/models/sarvam): Indian language models
|
||||||
|
- [LM Studio](https://docs.mem0.ai/components/llms/models/lmstudio): Local model management and deployment
|
||||||
|
- [LangChain LLM](https://docs.mem0.ai/components/llms/models/langchain): LangChain LLM integration
|
||||||
|
- [vLLM](https://docs.mem0.ai/components/llms/models/vllm): High-performance inference framework
|
||||||
|
|
||||||
|
### Vector Databases
|
||||||
|
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview): Guide to supported vector databases for semantic memory storage
|
||||||
|
- [Vector Database Configuration](https://docs.mem0.ai/components/vectordbs/config): Configuration reference for vector database providers
|
||||||
- [Qdrant](https://docs.mem0.ai/components/vectordbs/dbs/qdrant): High-performance vector similarity search engine
|
- [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
|
- [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
|
- [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
|
- [Milvus](https://docs.mem0.ai/components/vectordbs/dbs/milvus): Open-source vector database for AI applications at scale
|
||||||
- [Redis](https://docs.mem0.ai/components/vectordbs/dbs/redis): Real-time vector storage and search with Redis Stack
|
- [Pinecone](https://docs.mem0.ai/components/vectordbs/dbs/pinecone): Managed vector database with serverless and pod deployment options
|
||||||
- [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase): Open-source Firebase alternative with vector support
|
|
||||||
- [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector): Serverless vector database
|
|
||||||
- [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch): Distributed search and analytics engine
|
|
||||||
- [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch): Open-source search and analytics platform
|
|
||||||
- [FAISS](https://docs.mem0.ai/components/vectordbs/dbs/faiss): Facebook AI Similarity Search library
|
|
||||||
- [MongoDB](https://docs.mem0.ai/components/vectordbs/dbs/mongodb): Document database with vector search capabilities
|
- [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 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
|
- [Vertex AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/vertex_ai): Google Cloud's vector search service
|
||||||
- [Databricks](https://docs.mem0.ai/components/vectordbs/dbs/databricks): Delta Lake integration for vector search
|
- [Weaviate](https://docs.mem0.ai/components/vectordbs/dbs/weaviate): Open-source vector search engine with built-in ML capabilities
|
||||||
|
- [FAISS](https://docs.mem0.ai/components/vectordbs/dbs/faiss): Facebook AI Similarity Search library
|
||||||
|
- [LangChain Vector Store](https://docs.mem0.ai/components/vectordbs/dbs/langchain): LangChain vector store integration
|
||||||
- [Baidu](https://docs.mem0.ai/components/vectordbs/dbs/baidu): Baidu vector database integration
|
- [Baidu](https://docs.mem0.ai/components/vectordbs/dbs/baidu): Baidu vector database integration
|
||||||
- [LangChain](https://docs.mem0.ai/components/vectordbs/dbs/langchain): LangChain vector store integration
|
- [Cassandra](https://docs.mem0.ai/components/vectordbs/dbs/cassandra): Apache Cassandra with vector search capabilities
|
||||||
- [S3 Vectors](https://docs.mem0.ai/components/vectordbs/dbs/s3_vectors): Amazon S3 Vectors integration
|
- [S3 Vectors](https://docs.mem0.ai/components/vectordbs/dbs/s3_vectors): Amazon S3 Vectors integration
|
||||||
|
- [Databricks](https://docs.mem0.ai/components/vectordbs/dbs/databricks): Delta Lake integration for vector search
|
||||||
|
- [Neptune Analytics](https://docs.mem0.ai/components/vectordbs/dbs/neptune_analytics): AWS Neptune Analytics for graph and vector search
|
||||||
|
- [Turbopuffer](https://docs.mem0.ai/components/vectordbs/dbs/turbopuffer): High-performance serverless vector database
|
||||||
|
|
||||||
### Supported Embeddings
|
### Embedding Models
|
||||||
|
- [Embeddings Overview](https://docs.mem0.ai/components/embedders/overview): Embedding model configuration for semantic understanding
|
||||||
|
- [Embeddings Configuration](https://docs.mem0.ai/components/embedders/config): Configuration reference for embedding providers
|
||||||
- [OpenAI Embeddings](https://docs.mem0.ai/components/embedders/models/openai): High-quality text embeddings with customizable dimensions
|
- [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
|
- [Azure OpenAI Embeddings](https://docs.mem0.ai/components/embedders/models/azure_openai): Enterprise Azure-hosted embedding models
|
||||||
- [Google AI](https://docs.mem0.ai/components/embedders/models/google_AI): Gemini embedding models
|
- [Ollama Embeddings](https://docs.mem0.ai/components/embedders/models/ollama): Local embedding models for privacy-focused applications
|
||||||
- [AWS Bedrock](https://docs.mem0.ai/components/embedders/models/aws_bedrock): Amazon embedding models through Bedrock
|
- [Hugging Face Embeddings](https://docs.mem0.ai/components/embedders/models/huggingface): Open-source embedding models for local deployment
|
||||||
- [Hugging Face](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
|
||||||
- [Vertex AI](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
|
||||||
- [Ollama](https://docs.mem0.ai/components/embedders/models/ollama): Local embedding models for privacy-focused applications
|
- [LM Studio Embeddings](https://docs.mem0.ai/components/embedders/models/lmstudio): Local model embeddings
|
||||||
- [Together](https://docs.mem0.ai/components/embedders/models/together): Open-source model embeddings
|
- [Together Embeddings](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 Embeddings](https://docs.mem0.ai/components/embedders/models/langchain): LangChain embedder integration
|
||||||
- [LangChain](https://docs.mem0.ai/components/embedders/models/langchain): LangChain embedder integration
|
- [AWS Bedrock Embeddings](https://docs.mem0.ai/components/embedders/models/aws_bedrock): Amazon embedding models through Bedrock
|
||||||
|
|
||||||
|
### Rerankers
|
||||||
|
- [Reranker Overview](https://docs.mem0.ai/components/rerankers/overview): Guide to reranking models for improving search result quality
|
||||||
|
- [Reranker Configuration](https://docs.mem0.ai/components/rerankers/config): Configuration reference for reranker providers
|
||||||
|
- [Reranker Optimization](https://docs.mem0.ai/components/rerankers/optimization): Performance tuning and optimization strategies for rerankers
|
||||||
|
- [Custom Reranker Prompts](https://docs.mem0.ai/components/rerankers/custom-prompts): Customize reranker behavior with custom prompts
|
||||||
|
- [Cohere Reranker](https://docs.mem0.ai/components/rerankers/models/cohere): Cohere reranking model integration
|
||||||
|
- [Sentence Transformer Reranker](https://docs.mem0.ai/components/rerankers/models/sentence_transformer): Cross-encoder reranking with sentence transformers
|
||||||
|
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface): Hugging Face reranking models
|
||||||
|
- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker): Use LLMs as rerankers for flexible relevance scoring
|
||||||
|
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy): Zero Entropy reranking model
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
|
- [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
|
- [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
|
- [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
|
- [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
|
- [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
|
- [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
|
- [Vercel AI SDK](https://docs.mem0.ai/integrations/vercel-ai-sdk): Build AI-powered web applications with persistent memory
|
||||||
- [Flowise](https://docs.mem0.ai/integrations/flowise): No-code LLM workflow builder with memory capabilities
|
|
||||||
|
### Voice & Real-time
|
||||||
|
- [LiveKit](https://docs.mem0.ai/integrations/livekit): Real-time voice and video AI with persistent memory
|
||||||
|
- [Pipecat](https://docs.mem0.ai/integrations/pipecat): Voice AI pipeline framework with memory capabilities
|
||||||
|
- [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs): Voice synthesis integration with conversational memory
|
||||||
|
|
||||||
|
### Cloud & Infrastructure
|
||||||
|
- [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock): Enterprise AWS integration for managed AI services
|
||||||
|
|
||||||
|
### Developer Tools
|
||||||
- [Dify](https://docs.mem0.ai/integrations/dify): LLMOps platform integration for production AI applications
|
- [Dify](https://docs.mem0.ai/integrations/dify): LLMOps platform integration for production AI applications
|
||||||
|
- [Flowise](https://docs.mem0.ai/integrations/flowise): No-code LLM workflow builder with memory capabilities
|
||||||
|
- [LangChain Tools](https://docs.mem0.ai/integrations/langchain-tools): Use Mem0 as a LangChain tool for agents
|
||||||
|
- [AgentOps](https://docs.mem0.ai/integrations/agentops): Agent observability and monitoring with memory tracking
|
||||||
|
- [Keywords AI](https://docs.mem0.ai/integrations/keywords): Keywords AI integration for LLM monitoring
|
||||||
|
- [Raycast](https://docs.mem0.ai/integrations/raycast): Raycast extension for quick memory access
|
||||||
|
|
||||||
## Cookbooks and Examples
|
## Cookbooks and Examples
|
||||||
|
|
||||||
### Cookbooks Overview
|
|
||||||
- [Cookbooks Overview](https://docs.mem0.ai/cookbooks/overview): Complete guide to Mem0 examples and implementation patterns
|
- [Cookbooks Overview](https://docs.mem0.ai/cookbooks/overview): Complete guide to Mem0 examples and implementation patterns
|
||||||
|
|
||||||
### Essential Guides
|
### Essential Guides
|
||||||
@@ -171,12 +216,13 @@ Key differentiators:
|
|||||||
- [Choosing Memory Architecture](https://docs.mem0.ai/cookbooks/essentials/choosing-memory-architecture-vector-vs-graph): Vector vs Graph memory architectures comparison
|
- [Choosing Memory Architecture](https://docs.mem0.ai/cookbooks/essentials/choosing-memory-architecture-vector-vs-graph): Vector vs Graph memory architectures comparison
|
||||||
|
|
||||||
### AI Companion Examples
|
### 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
|
- [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
|
- [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
|
- [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
|
- [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
|
### Operations & Automation
|
||||||
- [Support Inbox](https://docs.mem0.ai/cookbooks/operations/support-inbox): Customer service agents with conversation history
|
- [Support Inbox](https://docs.mem0.ai/cookbooks/operations/support-inbox): Customer service agents with conversation history
|
||||||
@@ -186,30 +232,77 @@ Key differentiators:
|
|||||||
- [Team Task Agent](https://docs.mem0.ai/cookbooks/operations/team-task-agent): Collaborative AI agents with shared project memory
|
- [Team Task Agent](https://docs.mem0.ai/cookbooks/operations/team-task-agent): Collaborative AI agents with shared project memory
|
||||||
|
|
||||||
### Integration Examples
|
### 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
|
- [OpenAI Tool Calls](https://docs.mem0.ai/cookbooks/integrations/openai-tool-calls): Mem0 integrated with OpenAI function calling
|
||||||
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock): Enterprise memory with AWS managed services
|
|
||||||
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search): Web search with persistent memory of results
|
|
||||||
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk): Medical AI applications with memory
|
|
||||||
- [Mastra Agent](https://docs.mem0.ai/cookbooks/integrations/mastra-agent): Mastra framework integration with memory
|
- [Mastra Agent](https://docs.mem0.ai/cookbooks/integrations/mastra-agent): Mastra framework integration with memory
|
||||||
|
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk): Medical AI applications with memory
|
||||||
|
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock): Enterprise memory with AWS managed services
|
||||||
|
- [Neptune Analytics](https://docs.mem0.ai/cookbooks/integrations/neptune-analytics): Graph and vector search with AWS Neptune
|
||||||
|
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search): Web search with persistent memory of results
|
||||||
|
|
||||||
### Framework Examples
|
### Framework Examples
|
||||||
- [LlamaIndex React](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-react): React applications with LlamaIndex and memory
|
- [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
|
- [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
|
- [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
|
- [Chrome Extension](https://docs.mem0.ai/cookbooks/frameworks/chrome-extension): Browser extensions that remember user interactions
|
||||||
- [Multimodal Retrieval](https://docs.mem0.ai/cookbooks/frameworks/multimodal-retrieval): Memory systems handling text, images, and documents
|
- [Gemini with Mem0 MCP](https://docs.mem0.ai/cookbooks/frameworks/gemini-3-with-mem0-mcp): Google Gemini integration using MCP server
|
||||||
|
- [Mirofish Swarm Memory](https://docs.mem0.ai/cookbooks/frameworks/mirofish-swarm-memory): Swarm-based multi-agent memory patterns
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
- [Memory APIs](https://docs.mem0.ai/api-reference/memory/add-memories): Comprehensive API documentation for memory operations
|
- [API Reference Overview](https://docs.mem0.ai/api-reference): REST API overview with authentication and quick start guide
|
||||||
|
- [Organizations & Projects](https://docs.mem0.ai/api-reference/organizations-projects): Managing organizations and projects for multi-tenant setups
|
||||||
|
|
||||||
|
### Core Memory APIs
|
||||||
- [Add Memories](https://docs.mem0.ai/api-reference/memory/add-memories): REST API for storing new memories with detailed request/response formats
|
- [Add Memories](https://docs.mem0.ai/api-reference/memory/add-memories): REST API for storing new memories with detailed request/response formats
|
||||||
- [Search Memories](https://docs.mem0.ai/api-reference/memory/search-memories): Advanced search API with filtering and ranking capabilities
|
|
||||||
- [Get All Memories](https://docs.mem0.ai/api-reference/memory/get-memories): Retrieve all memories with pagination and filtering options
|
- [Get All Memories](https://docs.mem0.ai/api-reference/memory/get-memories): Retrieve all memories with pagination and filtering options
|
||||||
|
- [Search Memories](https://docs.mem0.ai/api-reference/memory/search-memories): Advanced search API with filtering and ranking capabilities
|
||||||
- [Update Memory](https://docs.mem0.ai/api-reference/memory/update-memory): Modify existing memories with conflict resolution
|
- [Update Memory](https://docs.mem0.ai/api-reference/memory/update-memory): Modify existing memories with conflict resolution
|
||||||
- [Delete Memory](https://docs.mem0.ai/api-reference/memory/delete-memory): Remove memories individually or in batches
|
- [Delete Memory](https://docs.mem0.ai/api-reference/memory/delete-memory): Remove a specific memory by ID
|
||||||
|
|
||||||
## Optional
|
### Additional Memory APIs
|
||||||
|
- [Create Memory Export](https://docs.mem0.ai/api-reference/memory/create-memory-export): Export memories in bulk
|
||||||
|
- [Feedback](https://docs.mem0.ai/api-reference/memory/feedback): Submit feedback on memory quality
|
||||||
|
- [Get Memory](https://docs.mem0.ai/api-reference/memory/get-memory): Retrieve a single memory by ID
|
||||||
|
- [Memory History](https://docs.mem0.ai/api-reference/memory/history-memory): View the history of changes to a memory
|
||||||
|
- [Get Memory Export](https://docs.mem0.ai/api-reference/memory/get-memory-export): Retrieve a previously created memory export
|
||||||
|
- [Batch Update](https://docs.mem0.ai/api-reference/memory/batch-update): Update multiple memories in a single request
|
||||||
|
- [Batch Delete](https://docs.mem0.ai/api-reference/memory/batch-delete): Delete multiple memories in a single request
|
||||||
|
- [Delete All Memories](https://docs.mem0.ai/api-reference/memory/delete-memories): Remove all memories matching criteria
|
||||||
|
|
||||||
- [FAQs](https://docs.mem0.ai/platform/faqs): Frequently asked questions about Mem0's Platform capabilities and implementation details
|
### Events APIs
|
||||||
- [Changelog](https://docs.mem0.ai/changelog): Detailed product updates and version history for tracking new features and improvements
|
- [Get Events](https://docs.mem0.ai/api-reference/events/get-events): List asynchronous memory operation events
|
||||||
- [Contributing Guide](https://docs.mem0.ai/contributing/development): Guidelines for contributing to Mem0's open-source development
|
- [Get Event](https://docs.mem0.ai/api-reference/events/get-event): Retrieve details of a specific event
|
||||||
|
|
||||||
|
### Entities APIs
|
||||||
|
- [Get Users](https://docs.mem0.ai/api-reference/entities/get-users): List all entities (users, agents, apps)
|
||||||
|
- [Delete User](https://docs.mem0.ai/api-reference/entities/delete-user): Remove an entity and all associated memories
|
||||||
|
|
||||||
|
### Organizations APIs
|
||||||
|
- [Create Organization](https://docs.mem0.ai/api-reference/organization/create-org): Create a new organization
|
||||||
|
- [Get Organizations](https://docs.mem0.ai/api-reference/organization/get-orgs): List all organizations
|
||||||
|
- [Get Organization](https://docs.mem0.ai/api-reference/organization/get-org): Retrieve organization details
|
||||||
|
- [Get Organization Members](https://docs.mem0.ai/api-reference/organization/get-org-members): List organization members
|
||||||
|
- [Add Organization Member](https://docs.mem0.ai/api-reference/organization/add-org-member): Add a member to an organization
|
||||||
|
- [Delete Organization](https://docs.mem0.ai/api-reference/organization/delete-org): Remove an organization
|
||||||
|
|
||||||
|
### Project APIs
|
||||||
|
- [Create Project](https://docs.mem0.ai/api-reference/project/create-project): Create a new project within an organization
|
||||||
|
- [Get Projects](https://docs.mem0.ai/api-reference/project/get-projects): List all projects
|
||||||
|
- [Get Project](https://docs.mem0.ai/api-reference/project/get-project): Retrieve project details
|
||||||
|
- [Get Project Members](https://docs.mem0.ai/api-reference/project/get-project-members): List project members
|
||||||
|
- [Add Project Member](https://docs.mem0.ai/api-reference/project/add-project-member): Add a member to a project
|
||||||
|
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project): Remove a project
|
||||||
|
|
||||||
|
### Webhook APIs
|
||||||
|
- [Create Webhook](https://docs.mem0.ai/api-reference/webhook/create-webhook): Register a new webhook endpoint
|
||||||
|
- [Get Webhook](https://docs.mem0.ai/api-reference/webhook/get-webhook): Retrieve webhook configuration
|
||||||
|
- [Update Webhook](https://docs.mem0.ai/api-reference/webhook/update-webhook): Modify webhook settings
|
||||||
|
- [Delete Webhook](https://docs.mem0.ai/api-reference/webhook/delete-webhook): Remove a webhook
|
||||||
|
|
||||||
|
## Community & Support
|
||||||
|
|
||||||
|
- [Contributing - Development](https://docs.mem0.ai/contributing/development): Guidelines for contributing to Mem0's open-source development
|
||||||
|
- [Contributing - Documentation](https://docs.mem0.ai/contributing/documentation): Guidelines for contributing to Mem0's documentation
|
||||||
|
- [Changelog](https://docs.mem0.ai/changelog): Detailed product updates and version history
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |