Compare commits
80
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62dca096f9 | ||
|
|
5d40592e42 | ||
|
|
a488e19044 | ||
|
|
57f944e18a | ||
|
|
fe3f7ae618 | ||
|
|
4a7e166f9a | ||
|
|
85768e78e7 | ||
|
|
7b395f3bf7 | ||
|
|
4180409b09 | ||
|
|
649e719ce6 | ||
|
|
1a53852d93 | ||
|
|
ac9cdd4840 | ||
|
|
cf530c4bec | ||
|
|
92b958c1cc | ||
|
|
c239d8a483 | ||
|
|
9d6b79a14e | ||
|
|
e44b46ef2e | ||
|
|
3882af7450 | ||
|
|
d39ebad09f | ||
|
|
9d82e2329d | ||
|
|
789cc9d607 | ||
|
|
e59e3d5f0c | ||
|
|
d926f3697c | ||
|
|
c996b0e7fa | ||
|
|
78ca85a260 | ||
|
|
88f696a60a | ||
|
|
081eca6d8f | ||
|
|
2434b9d550 | ||
|
|
3ffea554bc | ||
|
|
1ad8a59b0c | ||
|
|
a670333d67 | ||
|
|
4c2db3e68b | ||
|
|
07f0d4f1e0 | ||
|
|
3565404eef | ||
|
|
144627c4ce | ||
|
|
6984958138 | ||
|
|
b13748c446 | ||
|
|
4642a1d6e3 | ||
|
|
686d5e987d | ||
|
|
c55447c1e4 | ||
|
|
ee67602c58 | ||
|
|
0daa5d7d03 | ||
|
|
cfb3f58e4a | ||
|
|
66230b3f1f | ||
|
|
1941cae031 | ||
|
|
fcbb70ab3b | ||
|
|
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 |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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,46 @@
|
|||||||
|
name: Publish @mem0/cli 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/cli 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: cli/node
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: cli/node/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
|
else
|
||||||
|
npx npm@latest publish --provenance --access public
|
||||||
|
fi
|
||||||
@@ -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,46 @@
|
|||||||
|
name: Publish @mem0/openclaw-mem0 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: openclaw
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
|
else
|
||||||
|
npx npm@latest publish --provenance --access public
|
||||||
|
fi
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Publish mem0ai 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish mem0ai 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: mem0-ts
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: mem0-ts/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
|
else
|
||||||
|
npx npm@latest publish --provenance --access public
|
||||||
|
fi
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-n-publish:
|
||||||
|
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||||
|
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: vercel-ai-sdk
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
cache: 'pnpm'
|
||||||
|
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
|
else
|
||||||
|
npx npm@latest publish --provenance --access public
|
||||||
|
fi
|
||||||
@@ -0,0 +1,580 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
|
||||||
|
|
||||||
|
- **Repository**: https://github.com/mem0ai/mem0
|
||||||
|
- **Documentation**: https://docs.mem0.ai
|
||||||
|
- **License**: Apache-2.0
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
|
||||||
|
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
|
||||||
|
|
||||||
|
### Key Directories
|
||||||
|
|
||||||
|
| Directory | Description |
|
||||||
|
|-----------|-------------|
|
||||||
|
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
|
||||||
|
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||||
|
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
||||||
|
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||||
|
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||||
|
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||||
|
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||||
|
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||||
|
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
|
||||||
|
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
|
||||||
|
| `docs/` | Documentation site (Mintlify) |
|
||||||
|
| `tests/` | Python SDK tests (pytest) |
|
||||||
|
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
|
||||||
|
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
|
||||||
|
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
|
||||||
|
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
|
||||||
|
| `pr-reviews/` | Pull request review materials |
|
||||||
|
|
||||||
|
### Core Package Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||||
|
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
|
||||||
|
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
|
||||||
|
├── mem0/embeddings/ ├── src/llms/
|
||||||
|
├── mem0/vector_stores/ ├── src/embeddings/
|
||||||
|
├── mem0/graphs/ ├── src/vector_stores/
|
||||||
|
└── mem0/reranker/ └── src/graphs/
|
||||||
|
|
||||||
|
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||||
|
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||||
|
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||||
|
openclaw/ ──▶ mem0ai (npm)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- **Python**: 3.9+ (3.10+ for CLI)
|
||||||
|
- **Node.js**: v18+ (v20 or v22 recommended)
|
||||||
|
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
|
||||||
|
- **Hatch**: Python build/environment tool (`pip install hatch`)
|
||||||
|
- **Docker**: Required for `server/` and `openmemory/` development
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python SDK
|
||||||
|
hatch shell dev_py_3_11 # creates environment with all deps
|
||||||
|
pre-commit install # install git hooks
|
||||||
|
|
||||||
|
# TypeScript packages
|
||||||
|
cd mem0-ts && pnpm install # TS SDK
|
||||||
|
cd cli/node && pnpm install # Node CLI
|
||||||
|
cd vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||||
|
cd openclaw && pnpm install # OpenClaw plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build, Lint, and Test Commands
|
||||||
|
|
||||||
|
### Python SDK (`mem0/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Environment setup (uses Hatch)
|
||||||
|
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
|
||||||
|
|
||||||
|
# Linting and formatting
|
||||||
|
make lint # ruff check
|
||||||
|
make format # ruff format
|
||||||
|
make sort # isort mem0/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
make test # pytest tests/
|
||||||
|
make test-py-3.9 # test specific Python version (3.9–3.12)
|
||||||
|
|
||||||
|
# Build and publish
|
||||||
|
make build # hatch build
|
||||||
|
make publish # hatch publish
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Python:** 3.9, 3.10, 3.11, 3.12
|
||||||
|
- **Linter/formatter:** Ruff (line length **120**)
|
||||||
|
- **Import sorting:** isort (`profile = "black"`)
|
||||||
|
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
|
||||||
|
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
|
||||||
|
|
||||||
|
### TypeScript SDK (`mem0-ts/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mem0-ts
|
||||||
|
pnpm install
|
||||||
|
pnpm run build # tsup
|
||||||
|
pnpm run test # jest (all tests)
|
||||||
|
pnpm run test:unit # jest --coverage (unit tests only)
|
||||||
|
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
|
||||||
|
pnpm run test:ci # jest --coverage --ci (CI mode)
|
||||||
|
pnpm run test:watch # jest watch mode
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Node:** 20, 22 (CI-tested)
|
||||||
|
- **Build:** tsup (CJS + ESM)
|
||||||
|
- **Test:** jest
|
||||||
|
- **Formatter:** prettier
|
||||||
|
|
||||||
|
### Python CLI (`cli/python/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/python
|
||||||
|
pip install -e ".[dev]" # dev install with ruff + pytest
|
||||||
|
ruff check . # lint
|
||||||
|
ruff format . # format
|
||||||
|
pytest # test
|
||||||
|
hatch build # build
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Python:** 3.10+ (not 3.9)
|
||||||
|
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
|
||||||
|
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
|
||||||
|
- **Framework:** Typer + Rich + httpx
|
||||||
|
- **Entry point:** `mem0 = "mem0_cli.app:main"`
|
||||||
|
- **Source layout:** `src/mem0_cli/`
|
||||||
|
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
|
||||||
|
|
||||||
|
### Node CLI (`cli/node/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/node
|
||||||
|
pnpm install
|
||||||
|
pnpm run build # tsup
|
||||||
|
pnpm run lint # biome check src/
|
||||||
|
pnpm run lint:fix # biome check --write src/
|
||||||
|
pnpm run typecheck # tsc --noEmit
|
||||||
|
pnpm run test # vitest run
|
||||||
|
pnpm run test:watch # vitest (watch mode)
|
||||||
|
pnpm run dev # tsx src/index.ts (development)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Node:** 18+ required
|
||||||
|
- **Build:** tsup (ESM)
|
||||||
|
- **Linter:** Biome (not ESLint, not Ruff)
|
||||||
|
- **Test:** vitest (not jest)
|
||||||
|
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||||
|
|
||||||
|
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd vercel-ai-sdk
|
||||||
|
pnpm install
|
||||||
|
pnpm run build # tsup
|
||||||
|
pnpm run lint # eslint
|
||||||
|
pnpm run type-check # tsc --noEmit
|
||||||
|
pnpm run prettier-check # prettier --check
|
||||||
|
pnpm run test # jest
|
||||||
|
pnpm run test:edge # vitest (edge runtime)
|
||||||
|
pnpm run test:node # vitest (node runtime)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Build:** tsup (CJS + ESM)
|
||||||
|
- **Lint:** ESLint + Prettier
|
||||||
|
- **Test:** jest + vitest (edge/node configs)
|
||||||
|
|
||||||
|
### OpenClaw Plugin (`openclaw/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd openclaw
|
||||||
|
pnpm install
|
||||||
|
pnpm run build # tsup
|
||||||
|
pnpm run test # vitest run
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Build:** tsup (ESM)
|
||||||
|
- **Test:** vitest (with Codecov in CI)
|
||||||
|
- **Plugin manifest:** `openclaw.plugin.json`
|
||||||
|
|
||||||
|
### Server (`server/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker production build
|
||||||
|
cd server
|
||||||
|
make build # docker build -t mem0-api-server .
|
||||||
|
make run_local # docker run -p 8000:8000 with .env
|
||||||
|
|
||||||
|
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
|
||||||
|
cd server
|
||||||
|
docker-compose up # starts all 3 services
|
||||||
|
# mem0 API: localhost:8888
|
||||||
|
# PostgreSQL: localhost:8432
|
||||||
|
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
|
||||||
|
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
|
||||||
|
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
|
||||||
|
|
||||||
|
### OpenMemory (`openmemory/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full stack via Docker Compose
|
||||||
|
cd openmemory
|
||||||
|
docker-compose up
|
||||||
|
# Qdrant: localhost:6333
|
||||||
|
# API (MCP): localhost:8765
|
||||||
|
# UI: localhost:3000
|
||||||
|
|
||||||
|
# Individual development
|
||||||
|
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
|
||||||
|
cd openmemory/ui && npm run dev # Next.js frontend
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
|
||||||
|
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
|
||||||
|
- **Vector store:** Qdrant
|
||||||
|
|
||||||
|
### Documentation (`docs/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docs # or: cd docs && mintlify dev
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Framework:** Mintlify
|
||||||
|
- **API spec:** `docs/openapi.json`
|
||||||
|
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||||
|
|
||||||
|
### Evaluation (`evaluation/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd evaluation
|
||||||
|
make run-mem0-add # Run mem0 add experiments
|
||||||
|
make run-mem0-search # Run mem0 search experiments
|
||||||
|
make run-mem0-plus-add # With graph memory
|
||||||
|
make run-mem0-plus-search # With graph memory
|
||||||
|
make run-rag # RAG baseline
|
||||||
|
make run-full-context # Full context baseline
|
||||||
|
make run-langmem # LangMem comparison
|
||||||
|
make run-openai # OpenAI comparison
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core APIs
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
| Function / Class | Purpose | Import |
|
||||||
|
|-----------------|---------|--------|
|
||||||
|
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
|
||||||
|
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
|
||||||
|
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
|
||||||
|
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
|
||||||
|
|
||||||
|
**Key `Memory` / `MemoryClient` methods:**
|
||||||
|
|
||||||
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
|
||||||
|
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
|
||||||
|
| `get(memory_id)` | Retrieve a single memory by ID |
|
||||||
|
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
|
||||||
|
| `update(memory_id, data)` | Update a memory |
|
||||||
|
| `delete(memory_id)` | Delete a memory |
|
||||||
|
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
|
||||||
|
| `history(memory_id)` | Get change history for a memory |
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
| Export | Purpose | Import |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
|
||||||
|
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||||
|
|
||||||
|
## Import Patterns
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
| What | Import |
|
||||||
|
|------|--------|
|
||||||
|
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
|
||||||
|
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
|
||||||
|
| Configuration | `from mem0.configs.base import MemoryConfig` |
|
||||||
|
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
|
||||||
|
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
|
||||||
|
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
| What | Import |
|
||||||
|
|------|--------|
|
||||||
|
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
|
||||||
|
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
|
||||||
|
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### File Naming Conventions
|
||||||
|
|
||||||
|
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
|
||||||
|
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
|
||||||
|
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
|
||||||
|
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
|
||||||
|
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
|
||||||
|
|
||||||
|
### Python Conventions
|
||||||
|
|
||||||
|
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
|
||||||
|
- **Pydantic v2** for all data models and configuration.
|
||||||
|
- **Ruff** is the single linting and formatting tool — no black, no flake8.
|
||||||
|
- Root SDK: line length **120**
|
||||||
|
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||||
|
- **isort** with `profile = "black"` for import sorting.
|
||||||
|
- Ruff excludes `embedchain/` and `openmemory/` from root config.
|
||||||
|
|
||||||
|
### TypeScript Conventions
|
||||||
|
|
||||||
|
- **Build:** tsup across all packages.
|
||||||
|
- **Package manager:** pnpm everywhere (no npm, no yarn).
|
||||||
|
- **TypeScript strict mode** across all packages.
|
||||||
|
- **Linting varies by package:**
|
||||||
|
|
||||||
|
| Package | Linter | Formatter | Test Framework |
|
||||||
|
|---------|--------|-----------|---------------|
|
||||||
|
| `mem0-ts/` | — | Prettier | jest |
|
||||||
|
| `cli/node/` | Biome | Biome | vitest |
|
||||||
|
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||||
|
| `openclaw/` | — | — | vitest |
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
|
||||||
|
Always run type checking after modifying TypeScript code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd <package> && pnpm run typecheck # or: tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Provider Pattern
|
||||||
|
|
||||||
|
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
|
||||||
|
|
||||||
|
| Category | Count | Examples |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
|
||||||
|
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
|
||||||
|
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
|
||||||
|
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
|
||||||
|
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
|
||||||
|
|
||||||
|
### Two Usage Modes
|
||||||
|
|
||||||
|
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
|
||||||
|
|
||||||
|
### Graph Memory
|
||||||
|
|
||||||
|
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
|
||||||
|
|
||||||
|
### MCP Integration
|
||||||
|
|
||||||
|
Model Context Protocol support in multiple places:
|
||||||
|
|
||||||
|
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||||
|
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||||
|
- **Plugin:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||||
|
|
||||||
|
### Plugin & Skills System
|
||||||
|
|
||||||
|
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||||
|
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
|
||||||
|
|
||||||
|
### Adding a New Provider
|
||||||
|
|
||||||
|
To add a new LLM, embedding, vector store, or reranker provider:
|
||||||
|
|
||||||
|
1. Create `mem0/<category>/<provider_name>.py`
|
||||||
|
2. Inherit from the abstract base class in `mem0/<category>/base.py`
|
||||||
|
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
|
||||||
|
4. Register the provider in `mem0/<category>/__init__.py`
|
||||||
|
5. Add tests in `tests/<category>/<provider_name>/`
|
||||||
|
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
|
||||||
|
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
### CI Workflows (automated testing)
|
||||||
|
|
||||||
|
| Workflow | File | Triggers | Tests |
|
||||||
|
|----------|------|----------|-------|
|
||||||
|
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||||
|
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
|
||||||
|
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||||
|
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||||
|
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||||
|
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
|
||||||
|
|
||||||
|
### CD Workflows (automated publishing)
|
||||||
|
|
||||||
|
| Workflow | File | Tag Prefix | Target |
|
||||||
|
|----------|------|------------|--------|
|
||||||
|
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||||
|
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||||
|
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||||
|
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
|
||||||
|
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||||
|
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||||
|
|
||||||
|
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
|
||||||
|
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
|
||||||
|
|
||||||
|
### Utility Workflows
|
||||||
|
|
||||||
|
| Workflow | File | Purpose |
|
||||||
|
|----------|------|---------|
|
||||||
|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
|
||||||
|
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||||
|
|
||||||
|
## Task Completion Guidelines
|
||||||
|
|
||||||
|
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
|
||||||
|
2. **Implementation**: Fix the bug
|
||||||
|
3. **Manual verification**: Run the relevant test suite to confirm the fix
|
||||||
|
4. **Lint**: Run the appropriate linter for the package you modified
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
1. **Implementation**: Build the feature following existing patterns
|
||||||
|
2. **Unit tests**: Comprehensive test coverage for new functionality
|
||||||
|
3. **Documentation**: Update relevant docs in `docs/` for public APIs
|
||||||
|
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
|
||||||
|
|
||||||
|
### New Provider (LLM / Embedding / Vector Store / Reranker)
|
||||||
|
|
||||||
|
1. **Implementation**: Follow the "Adding a New Provider" steps above
|
||||||
|
2. **Tests**: Add unit tests matching the pattern of existing providers
|
||||||
|
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
|
||||||
|
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
|
||||||
|
5. **Documentation**: Add an integration guide in `docs/integrations/`
|
||||||
|
|
||||||
|
### Refactoring / Internal Changes
|
||||||
|
|
||||||
|
- Unit tests for any changed behavior
|
||||||
|
- No documentation needed for internal-only changes
|
||||||
|
- Ensure all existing tests still pass
|
||||||
|
|
||||||
|
### When to Deviate
|
||||||
|
|
||||||
|
These are guidelines, not rigid rules. Adjust based on:
|
||||||
|
|
||||||
|
- **Scope**: Trivial fixes (typos, comments) may not need tests
|
||||||
|
- **Visibility**: Internal changes may not need documentation
|
||||||
|
- **Context**: Some changes span multiple categories — use judgment
|
||||||
|
|
||||||
|
When uncertain about expected artifacts, ask for clarification.
|
||||||
|
|
||||||
|
## Contributing Guidelines
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. Fork and clone the repository.
|
||||||
|
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
|
||||||
|
3. Make your changes — add tests, docs, and examples as appropriate.
|
||||||
|
4. Run linting and tests for every package you modified (see commands above).
|
||||||
|
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
|
||||||
|
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
|
||||||
|
7. Push and open a Pull Request against `main`.
|
||||||
|
|
||||||
|
### Pull Request Requirements
|
||||||
|
|
||||||
|
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
|
||||||
|
|
||||||
|
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
|
||||||
|
2. **Description** — Explain what the PR does and why it's needed.
|
||||||
|
3. **Type of Change** — Check the appropriate box:
|
||||||
|
- Bug fix / New feature / Breaking change / Refactor / Documentation update
|
||||||
|
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
|
||||||
|
5. **Test Coverage** — Check what applies:
|
||||||
|
- Added/updated unit tests
|
||||||
|
- Added/updated integration tests
|
||||||
|
- Tested manually (describe how)
|
||||||
|
- No tests needed (explain why)
|
||||||
|
6. **Checklist** — All must be checked before merge:
|
||||||
|
- [ ] Code follows the project's style guidelines
|
||||||
|
- [ ] Self-review performed
|
||||||
|
- [ ] Tests added that prove the fix/feature works
|
||||||
|
- [ ] New and existing tests pass locally
|
||||||
|
- [ ] Documentation updated if needed
|
||||||
|
|
||||||
|
### PR Description Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Linked Issue
|
||||||
|
|
||||||
|
Closes #<!-- issue number -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- What does this PR do? Why is it needed? -->
|
||||||
|
|
||||||
|
## Type of Change
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change that adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||||
|
- [ ] Refactor (no functional changes)
|
||||||
|
- [ ] Documentation update
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
N/A
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
- [ ] I added/updated unit tests
|
||||||
|
- [ ] I added/updated integration tests
|
||||||
|
- [ ] I tested manually (describe below)
|
||||||
|
- [ ] No tests needed (explain why)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] My code follows the project's style guidelines
|
||||||
|
- [ ] I have performed a self-review of my code
|
||||||
|
- [ ] I have added tests that prove my fix/feature works
|
||||||
|
- [ ] New and existing tests pass locally
|
||||||
|
- [ ] I have updated documentation if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### General Rules
|
||||||
|
|
||||||
|
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
|
||||||
|
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
|
||||||
|
- For `server/` and `openmemory/` work, use Docker Compose for local development.
|
||||||
|
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
|
||||||
|
|
||||||
|
### Contributing Guides
|
||||||
|
|
||||||
|
| Task | Guide |
|
||||||
|
|------|-------|
|
||||||
|
| Code contributions | `docs/contributing/development.mdx` |
|
||||||
|
| Documentation contributions | `docs/contributing/documentation.mdx` |
|
||||||
|
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
|
||||||
|
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
|
||||||
|
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
|
||||||
|
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
|
||||||
|
|
||||||
|
## Do NOT
|
||||||
|
|
||||||
|
- Modify CI/CD workflows without explicit approval.
|
||||||
|
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
|
||||||
|
- Commit `.env` files, API keys, or credentials.
|
||||||
|
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
|
||||||
|
- Skip pre-commit hooks.
|
||||||
|
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||||
|
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||||
|
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
|
||||||
|
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
|
||||||
|
- Change public APIs without updating documentation in `docs/`.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ config = MemoryConfig(
|
|||||||
graph_store=GraphStoreConfig(provider="neo4j", config={...}), # optional
|
graph_store=GraphStoreConfig(provider="neo4j", config={...}), # optional
|
||||||
history_db_path="~/.mem0/history.db",
|
history_db_path="~/.mem0/history.db",
|
||||||
version="v1.1",
|
version="v1.1",
|
||||||
custom_fact_extraction_prompt="Custom prompt...",
|
custom_instructions="Custom prompt...",
|
||||||
custom_update_memory_prompt="Custom prompt..."
|
custom_update_memory_prompt="Custom prompt..."
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -684,7 +684,7 @@ Conversation: {messages}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
config = MemoryConfig(
|
config = MemoryConfig(
|
||||||
custom_fact_extraction_prompt=custom_extraction_prompt
|
custom_instructions=custom_extraction_prompt
|
||||||
)
|
)
|
||||||
memory = Memory(config)
|
memory = Memory(config)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ Install the sdk via pip:
|
|||||||
pip install mem0ai
|
pip install mem0ai
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For enhanced hybrid search with BM25 keyword matching and entity extraction, install with NLP support:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mem0ai[nlp]
|
||||||
|
python -m spacy download en_core_web_sm
|
||||||
|
```
|
||||||
|
|
||||||
Install sdk via npm:
|
Install sdk via npm:
|
||||||
```bash
|
```bash
|
||||||
npm install mem0ai
|
npm install mem0ai
|
||||||
@@ -109,7 +116,9 @@ See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full comm
|
|||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
||||||
|
|
||||||
|
Mem0 uses `text-embedding-3-small` from OpenAI as the default embedding model. For best results with hybrid search (semantic + keyword + entity boosting), we recommend using at least [Qwen 600M](https://huggingface.co/Alibaba-NLP/gte-Qwen2-1.5B-instruct) or a comparable embedding model. See [Supported Embeddings](https://docs.mem0.ai/components/embedders/overview) for configuration details.
|
||||||
|
|
||||||
First step is to instantiate the memory:
|
First step is to instantiate the memory:
|
||||||
|
|
||||||
@@ -162,7 +171,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
|
||||||
|
|||||||
+188
-15
@@ -28,7 +28,7 @@ mem0 CLI is the official command-line interface for [mem0](https://mem0.ai) -- t
|
|||||||
### Who is it for?
|
### Who is it for?
|
||||||
|
|
||||||
- Developers integrating mem0 into their workflows
|
- Developers integrating mem0 into their workflows
|
||||||
- AI agents that need persistent memory (the CLI is designed with `--output json` and `help --json` specifically for machine consumption)
|
- AI agents that need persistent memory (the CLI is designed with `--json`/`--agent` global flags and `help --json` specifically for machine consumption)
|
||||||
- DevOps/CI pipelines that need to manage memories programmatically
|
- DevOps/CI pipelines that need to manage memories programmatically
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
@@ -79,6 +79,7 @@ Apache-2.0
|
|||||||
│ ├── init_cmd.py # run_init (interactive wizard)
|
│ ├── init_cmd.py # run_init (interactive wizard)
|
||||||
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
|
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
|
||||||
│ ├── entities.py # cmd_entities_list, cmd_entities_delete
|
│ ├── entities.py # cmd_entities_list, cmd_entities_delete
|
||||||
|
│ ├── events_cmd.py # cmd_event_list, cmd_event_status
|
||||||
│ └── utils.py # cmd_status, cmd_version, cmd_import
|
│ └── utils.py # cmd_status, cmd_version, cmd_import
|
||||||
└── node/
|
└── node/
|
||||||
├── package.json # Node package config (tsup build)
|
├── package.json # Node package config (tsup build)
|
||||||
@@ -88,6 +89,7 @@ Apache-2.0
|
|||||||
├── config.ts # Config loading/saving, env var overrides
|
├── config.ts # Config loading/saving, env var overrides
|
||||||
├── branding.ts # Colors, icons, banner, timedStatus, print helpers
|
├── branding.ts # Colors, icons, banner, timedStatus, print helpers
|
||||||
├── output.ts # Output formatting (text, json, table, quiet)
|
├── output.ts # Output formatting (text, json, table, quiet)
|
||||||
|
├── state.ts # Agent mode flag (setAgentMode, isAgentMode)
|
||||||
├── help.ts # Rich-style help formatter (panels, command ordering)
|
├── help.ts # Rich-style help formatter (panels, command ordering)
|
||||||
├── backend/
|
├── backend/
|
||||||
│ ├── index.ts # Re-exports
|
│ ├── index.ts # Re-exports
|
||||||
@@ -98,6 +100,7 @@ Apache-2.0
|
|||||||
├── init.ts # runInit (interactive wizard)
|
├── init.ts # runInit (interactive wizard)
|
||||||
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
|
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
|
||||||
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
|
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
|
||||||
|
├── events.ts # cmdEventList, cmdEventStatus
|
||||||
└── utils.ts # cmdStatus, cmdVersion, cmdImport
|
└── utils.ts # cmdStatus, cmdVersion, cmdImport
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -153,8 +156,25 @@ Interactive setup wizard for mem0 CLI.
|
|||||||
|-----------------|--------|----------|---------|------|
|
|-----------------|--------|----------|---------|------|
|
||||||
| `--api-key` | string | No | - | API key (skip prompt). |
|
| `--api-key` | string | No | - | API key (skip prompt). |
|
||||||
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
|
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
|
||||||
|
| `--email` | string | No | - | Login via email verification code. |
|
||||||
|
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
|
||||||
|
| `--force` | bool | No | false | Overwrite existing config without confirmation. |
|
||||||
|
|
||||||
**Behavior:**
|
**Behavior:**
|
||||||
|
|
||||||
|
*Existing config protection:*
|
||||||
|
- If `~/.mem0/config.json` exists with an API key, the CLI warns and asks for confirmation before overwriting.
|
||||||
|
- In non-TTY mode, this is a hard error unless `--force` is passed.
|
||||||
|
- `--force` skips the confirmation in both TTY and non-TTY modes.
|
||||||
|
|
||||||
|
*Email login flow (when `--email` is provided):*
|
||||||
|
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
|
||||||
|
- If `--code` is also provided, verifies immediately (fully non-interactive).
|
||||||
|
- If `--code` is not provided, prompts for the code interactively.
|
||||||
|
- On success: receives API key, org_id, project_id. Saves to config. Creates account if email is new.
|
||||||
|
- Cannot be combined with `--api-key`.
|
||||||
|
|
||||||
|
*API key flow (existing behavior):*
|
||||||
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
|
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
|
||||||
- If running in a non-TTY without both flags, prints an error with usage hint and exits.
|
- If running in a non-TTY without both flags, prints an error with usage hint and exits.
|
||||||
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
|
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
|
||||||
@@ -164,6 +184,9 @@ Interactive setup wizard for mem0 CLI.
|
|||||||
```bash
|
```bash
|
||||||
mem0 init
|
mem0 init
|
||||||
mem0 init --api-key m0-xxx --user-id alice
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
mem0 init --email alice@company.com --code 482901
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -687,7 +710,114 @@ mem0 entity delete --user-id alice --dry-run
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.14 `status`
|
### 3.14 `event list`
|
||||||
|
|
||||||
|
List recent background processing events.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|-------|
|
||||||
|
| Usage | `mem0 event list [OPTIONS]` |
|
||||||
|
| needsBackend | Yes |
|
||||||
|
| needsConfig | Yes |
|
||||||
|
| resolveIds | No |
|
||||||
|
| resolveGraph | No |
|
||||||
|
| confirmDangerous | No |
|
||||||
|
| Output formats | text (table), json |
|
||||||
|
| Default output | table |
|
||||||
|
| API endpoint | `GET /v1/events/` |
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Flag | Type | Default | Panel | Help |
|
||||||
|
|----------------|--------|---------|------------|------|
|
||||||
|
| `-o, --output` | string | "table" | Output | Output: text, json. |
|
||||||
|
| `--api-key` | string | - | Connection | Override API key. |
|
||||||
|
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||||
|
|
||||||
|
**Behavior:** Fetches all background events for the project. Displays as a table with columns: Event ID (first 8 chars), Type, Status (color-coded), Latency, Created. Status values: `PENDING` (accent), `SUCCEEDED` (green), `FAILED` (red), `PROCESSING` (yellow).
|
||||||
|
|
||||||
|
**JSON output envelope:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "event list",
|
||||||
|
"count": 3,
|
||||||
|
"duration_ms": 87,
|
||||||
|
"data": [
|
||||||
|
{ "id": "evt-abc", "event_type": "ADD", "status": "SUCCEEDED", "latency": 412.0, "created_at": "2026-01-01T10:00:00Z" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
mem0 event list
|
||||||
|
mem0 event list --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.15 `event status`
|
||||||
|
|
||||||
|
Get the status and results of a specific background event.
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|------------------|-------|
|
||||||
|
| Usage | `mem0 event status <event_id> [OPTIONS]` |
|
||||||
|
| needsBackend | Yes |
|
||||||
|
| needsConfig | Yes |
|
||||||
|
| resolveIds | No |
|
||||||
|
| resolveGraph | No |
|
||||||
|
| confirmDangerous | No |
|
||||||
|
| Output formats | text, json |
|
||||||
|
| Default output | text |
|
||||||
|
| API endpoint | `GET /v1/events/{event_id}/` |
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
|
| Name | Type | Required | Help |
|
||||||
|
|------------|--------|----------|------|
|
||||||
|
| `event_id` | string | Yes | Event ID to inspect. |
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
| Flag | Type | Default | Panel | Help |
|
||||||
|
|----------------|--------|---------|------------|------|
|
||||||
|
| `-o, --output` | string | "text" | Output | Output: text, json. |
|
||||||
|
| `--api-key` | string | - | Connection | Override API key. |
|
||||||
|
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||||
|
|
||||||
|
**Behavior:** Fetches the event by ID and displays: Event ID, Type, Status (color-coded), Latency, Created, Updated, and a numbered list of result memories (event type, memory text, user_id, truncated memory ID). Displayed in a boxed panel (text) or JSON envelope.
|
||||||
|
|
||||||
|
**JSON output envelope:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "event status",
|
||||||
|
"duration_ms": 65,
|
||||||
|
"data": {
|
||||||
|
"id": "evt-abc",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 412.0,
|
||||||
|
"created_at": "2026-01-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-01-01T10:00:01Z",
|
||||||
|
"results": [
|
||||||
|
{ "id": "mem-xyz", "event": "ADD", "user_id": "alice", "memory": "User prefers dark mode" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
mem0 event status evt-abc-123
|
||||||
|
mem0 event status evt-abc-123 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.16 `status`
|
||||||
|
|
||||||
Check connectivity and authentication.
|
Check connectivity and authentication.
|
||||||
|
|
||||||
@@ -708,20 +838,19 @@ Check connectivity and authentication.
|
|||||||
| `--api-key` | string | - | Connection | Override API key. |
|
| `--api-key` | string | - | Connection | Override API key. |
|
||||||
| `--base-url` | string | - | Connection | Override API base URL. |
|
| `--base-url` | string | - | Connection | Override API base URL. |
|
||||||
|
|
||||||
**Behavior:** If config has a default `user_id` or `agent_id`, validates by making a minimal `POST /v2/memories/` with `page=1&page_size=1`. Otherwise validates via `GET /v1/entities/`. Displays connection status in a boxed panel (text) or JSON envelope.
|
**Behavior:** Validates connectivity by calling `GET /v1/ping/`. Displays connection status in a boxed panel (text) or JSON envelope. The ping endpoint is lightweight and does not require any entity scope.
|
||||||
|
|
||||||
**JSON output:**
|
**JSON output:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"command": "status",
|
"command": "status",
|
||||||
|
"duration_ms": 112,
|
||||||
"data": {
|
"data": {
|
||||||
"connected": true,
|
"connected": true,
|
||||||
"backend": "platform",
|
"backend": "platform",
|
||||||
"base_url": "https://api.mem0.ai",
|
"base_url": "https://api.mem0.ai"
|
||||||
"latency_ms": 245
|
}
|
||||||
},
|
|
||||||
"duration_ms": 245
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -733,7 +862,7 @@ mem0 status -o json
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.15 `help`
|
### 3.17 `help`
|
||||||
|
|
||||||
Show help. Use `--json` for machine-readable output (for LLM agents).
|
Show help. Use `--json` for machine-readable output (for LLM agents).
|
||||||
|
|
||||||
@@ -793,6 +922,9 @@ The auth header name is `Authorization` and the scheme is `Token` (not Bearer).
|
|||||||
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
|
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
|
||||||
| List entities | `GET` | `/v1/entities/` | - | - |
|
| List entities | `GET` | `/v1/entities/` | - | - |
|
||||||
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params |
|
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params |
|
||||||
|
| List events | `GET` | `/v1/events/` | - | - |
|
||||||
|
| Get event | `GET` | `/v1/events/{event_id}/` | - | - |
|
||||||
|
| Ping (status) | `GET` | `/v1/ping/` | - | - |
|
||||||
|
|
||||||
### How Filters Are Built (`_buildFilters` / `_build_filters`)
|
### How Filters Are Built (`_buildFilters` / `_build_filters`)
|
||||||
|
|
||||||
@@ -1095,6 +1227,8 @@ For `PENDING` events, displays "Processing in background" with the event ID.
|
|||||||
|
|
||||||
### 7.1 Supported Modes Per Command
|
### 7.1 Supported Modes Per Command
|
||||||
|
|
||||||
|
All commands also support `agent` mode via the global `--json`/`--agent` flag, which wraps output in a structured JSON envelope with sanitized fields.
|
||||||
|
|
||||||
| Command | text | json | table | quiet |
|
| Command | text | json | table | quiet |
|
||||||
|----------------|------|------|-------|-------|
|
|----------------|------|------|-------|-------|
|
||||||
| add | Y | Y | - | Y |
|
| add | Y | Y | - | Y |
|
||||||
@@ -1109,12 +1243,16 @@ For `PENDING` events, displays "Processing in background" with the event ID.
|
|||||||
| config set | (success msg) | - | - | - |
|
| config set | (success msg) | - | - | - |
|
||||||
| entity list | - | Y | Y (default) | - |
|
| entity list | - | Y | Y (default) | - |
|
||||||
| entity delete | Y | Y | - | Y |
|
| entity delete | Y | Y | - | Y |
|
||||||
|
| event list | Y (table) | Y | - | - |
|
||||||
|
| event status | Y | Y | - | - |
|
||||||
| status | Y | Y | - | - |
|
| status | Y | Y | - | - |
|
||||||
| help | Y | Y (--json) | - | - |
|
| help | Y | Y (--json) | - | - |
|
||||||
|
|
||||||
### 7.2 JSON Envelope Format (`formatJsonEnvelope`)
|
### 7.2 JSON Envelope Format
|
||||||
|
|
||||||
Used by `config show`, `status`, and `import` for structured JSON output:
|
There are two related envelope formats:
|
||||||
|
|
||||||
|
**`formatJsonEnvelope`** — used by `config show`, `status`, and `import` for `--output json`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -1128,14 +1266,39 @@ Used by `config show`, `status`, and `import` for structured JSON output:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`formatAgentEnvelope`** — used by all commands in agent mode (`--json`/`--agent`). Same structure, but `data` is passed through `sanitizeAgentData(command, data)` to project only the most relevant fields:
|
||||||
|
|
||||||
|
| Command | Fields in `data` |
|
||||||
|
|---------------|-----------------|
|
||||||
|
| add | `[{id, memory, event}]` or `[{status, event_id}]` for PENDING |
|
||||||
|
| search | `[{id, memory, score, created_at, categories}]` |
|
||||||
|
| list | `[{id, memory, created_at, categories}]` |
|
||||||
|
| get | `{id, memory, created_at, updated_at, categories, metadata}` |
|
||||||
|
| update | `{id, memory}` |
|
||||||
|
| delete | (raw API response) |
|
||||||
|
| entity list | `[{name, type, count}]` |
|
||||||
|
| event list | `[{id, event_type, status, latency, created_at}]` |
|
||||||
|
| event status | `{id, event_type, status, latency, created_at, updated_at, results: [{id, event, user_id, memory}]}` |
|
||||||
|
| status/config/import | (pass-through) |
|
||||||
|
|
||||||
|
Error envelopes (on non-zero exit):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"command": "<command_name>",
|
||||||
|
"error": "Authentication failed. Your API key may be invalid or expired.",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
Fields:
|
Fields:
|
||||||
- `status`: Always `"success"` (errors go to stderr before exit).
|
- `status`: `"success"` or `"error"`.
|
||||||
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`).
|
- `command`: The command name.
|
||||||
- `duration_ms`: Optional, elapsed time in milliseconds.
|
- `duration_ms`: Optional, elapsed time in milliseconds.
|
||||||
- `scope`: Optional, active entity scope.
|
- `scope`: Optional, active entity scope (omitted if empty).
|
||||||
- `count`: Optional, result count.
|
- `count`: Optional, result count.
|
||||||
- `error`: Optional, error message string.
|
- `error`: Only present when `status` is `"error"`.
|
||||||
- `data`: The primary payload.
|
- `data`: The primary payload (sanitized in agent mode).
|
||||||
|
|
||||||
### 7.3 Text Output
|
### 7.3 Text Output
|
||||||
|
|
||||||
@@ -1206,6 +1369,16 @@ Destructive commands (`delete --all`, `delete --entity`, `entity delete`) requir
|
|||||||
- CI/CD pipelines
|
- CI/CD pipelines
|
||||||
- Scripting
|
- Scripting
|
||||||
|
|
||||||
|
### Why `--json`/`--agent` global flags exist
|
||||||
|
|
||||||
|
The `--json` and `--agent` flags (aliases of each other) activate agent mode globally. When set:
|
||||||
|
1. All output becomes a structured JSON envelope (`{status, command, duration_ms, scope, count, data}`).
|
||||||
|
2. The `data` field is sanitized via `sanitizeAgentData` — only the most relevant fields are included per command, reducing noise for agents parsing the output.
|
||||||
|
3. All human-readable output (spinners, colors, banners, timing lines) is suppressed.
|
||||||
|
4. Errors are emitted as JSON to stdout with a non-zero exit code, not to stderr as text.
|
||||||
|
|
||||||
|
This is distinct from `--output json`, which returns the raw API response without sanitization.
|
||||||
|
|
||||||
### Why `--output json` is on every command
|
### Why `--output json` is on every command
|
||||||
|
|
||||||
Every data-returning command supports `--output json` (or `--json` for `help`). This enables machine consumption by AI agents and scripts. JSON output goes to stdout while human-readable spinners/timing go to stderr, so piping `mem0 list -o json | jq .` works cleanly.
|
Every data-returning command supports `--output json` (or `--json` for `help`). This enables machine consumption by AI agents and scripts. JSON output goes to stdout while human-readable spinners/timing go to stderr, so piping `mem0 list -o json | jq .` works cleanly.
|
||||||
|
|||||||
+46
-8
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
|
||||||
|
|
||||||
|
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -17,9 +19,15 @@ Both packages install a `mem0` binary with identical behavior.
|
|||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Authenticate and save config
|
# Interactive setup wizard
|
||||||
mem0 init
|
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
|
# Add a memory
|
||||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
@@ -40,7 +48,7 @@ mem0 delete <memory-id>
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `mem0 init` | Interactive setup wizard — configures API key and default user ID |
|
| `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 add` | Add a memory from text, JSON messages, a file, or stdin |
|
||||||
| `mem0 search` | Search memories using natural language |
|
| `mem0 search` | Search memories using natural language |
|
||||||
| `mem0 list` | List memories with optional filters and pagination |
|
| `mem0 list` | List memories with optional filters and pagination |
|
||||||
@@ -49,12 +57,45 @@ mem0 delete <memory-id>
|
|||||||
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
||||||
| `mem0 import` | Bulk import memories from a JSON file |
|
| `mem0 import` | Bulk import memories from a JSON file |
|
||||||
| `mem0 config` | View or modify CLI configuration |
|
| `mem0 config` | View or modify CLI configuration |
|
||||||
| `mem0 entities` | List or delete entities (users, agents, apps) |
|
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||||
|
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||||
| `mem0 status` | Verify API connection and display current project |
|
| `mem0 status` | Verify API connection and display current project |
|
||||||
| `mem0 version` | Print the CLI version |
|
| `mem0 version` | Print the CLI version |
|
||||||
|
|
||||||
Run `mem0 <command> --help` for detailed usage on any command.
|
Run `mem0 <command> --help` for detailed usage on any command.
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
## Output formats
|
## Output formats
|
||||||
|
|
||||||
Control how results are displayed with `--output`:
|
Control how results are displayed with `--output`:
|
||||||
@@ -62,13 +103,10 @@ Control how results are displayed with `--output`:
|
|||||||
| Format | Description |
|
| Format | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `text` | Human-readable with colors and formatting (default) |
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
| `json` | Structured JSON for piping to `jq` or agent consumption |
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
| `table` | Tabular format (default for `list`) |
|
| `table` | Tabular format (default for `list`) |
|
||||||
| `quiet` | Minimal — just IDs or status codes |
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
```bash
|
|
||||||
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -503,15 +503,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "init",
|
"name": "init",
|
||||||
"description": "Interactive setup wizard for mem0 CLI.",
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
"usage": "mem0 init",
|
"usage": "mem0 init [OPTIONS]",
|
||||||
"needsBackend": false,
|
"needsBackend": false,
|
||||||
"needsConfig": false,
|
"needsConfig": false,
|
||||||
"resolveIds": false,
|
"resolveIds": false,
|
||||||
"resolveGraph": false,
|
"resolveGraph": false,
|
||||||
"confirmDangerous": false,
|
"confirmDangerous": false,
|
||||||
"arguments": [],
|
"arguments": [],
|
||||||
"options": []
|
"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",
|
"name": "status",
|
||||||
|
|||||||
+299
-37
@@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Node.js **18+**
|
- Node.js **18+**
|
||||||
- pnpm (`npm install -g pnpm`)
|
- pnpm (`npm install -g pnpm`) — for development only
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -13,22 +15,307 @@ The official command-line interface for [mem0](https://mem0.ai) — the memory l
|
|||||||
npm install -g @mem0/cli
|
npm install -g @mem0/cli
|
||||||
```
|
```
|
||||||
|
|
||||||
Or from source:
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd node
|
# Interactive setup wizard
|
||||||
pnpm install
|
mem0 init
|
||||||
pnpm build
|
|
||||||
pnpm link --global
|
|
||||||
|
|
||||||
# Now use it like a normal CLI
|
# Or login via email
|
||||||
mem0 --help
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running during development
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd node
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/node
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# Development mode (runs TypeScript directly, no build needed)
|
# Development mode (runs TypeScript directly, no build needed)
|
||||||
@@ -39,36 +326,11 @@ pnpm dev search "test" --user-id alice
|
|||||||
# Or build first, then run the compiled JS
|
# Or build first, then run the compiled JS
|
||||||
pnpm build
|
pnpm build
|
||||||
node dist/index.js --help
|
node dist/index.js --help
|
||||||
node dist/index.js add "test memory" --user-id alice
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Documentation
|
||||||
|
|
||||||
```bash
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
# Set up your configuration
|
|
||||||
mem0 init
|
|
||||||
|
|
||||||
# Add a memory
|
|
||||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
|
||||||
|
|
||||||
# Search memories
|
|
||||||
mem0 search "What are Alice's preferences?" --user-id alice
|
|
||||||
|
|
||||||
# List all memories
|
|
||||||
mem0 list --user-id alice
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `MEM0_API_KEY` | API key (overrides config file) |
|
|
||||||
| `MEM0_BASE_URL` | API base URL |
|
|
||||||
| `MEM0_USER_ID` | Default user ID |
|
|
||||||
| `MEM0_AGENT_ID` | Default agent ID |
|
|
||||||
| `MEM0_APP_ID` | Default app ID |
|
|
||||||
| `MEM0_RUN_ID` | Default run ID |
|
|
||||||
| `MEM0_ENABLE_GRAPH` | Enable graph memory (true/false) |
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/cli",
|
"name": "@mem0/cli",
|
||||||
"version": "0.1.0",
|
"version": "0.2.3",
|
||||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"mem0": "./dist/index.js"
|
"mem0": "./dist/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --format esm --dts --clean",
|
"build": "tsup",
|
||||||
"dev": "tsx src/index.ts",
|
"dev": "tsx src/index.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"author": "mem0.ai <founders@mem0.ai>",
|
"author": "mem0.ai <founders@mem0.ai>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mem0ai/mem0",
|
||||||
|
"directory": "cli/node"
|
||||||
|
},
|
||||||
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@@ -6,110 +6,125 @@ import type { Mem0Config } from "../config.js";
|
|||||||
import { PlatformBackend } from "./platform.js";
|
import { PlatformBackend } from "./platform.js";
|
||||||
|
|
||||||
export interface AddOptions {
|
export interface AddOptions {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
immutable?: boolean;
|
immutable?: boolean;
|
||||||
infer?: boolean;
|
infer?: boolean;
|
||||||
expires?: string;
|
expires?: string;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
enableGraph?: boolean;
|
enableGraph?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchOptions {
|
export interface SearchOptions {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
topK?: number;
|
topK?: number;
|
||||||
threshold?: number;
|
threshold?: number;
|
||||||
rerank?: boolean;
|
rerank?: boolean;
|
||||||
keyword?: boolean;
|
keyword?: boolean;
|
||||||
filters?: Record<string, unknown>;
|
filters?: Record<string, unknown>;
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
enableGraph?: boolean;
|
enableGraph?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListOptions {
|
export interface ListOptions {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
category?: string;
|
category?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
enableGraph?: boolean;
|
enableGraph?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteOptions {
|
export interface DeleteOptions {
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityIds {
|
export interface EntityIds {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Backend {
|
export interface Backend {
|
||||||
add(
|
add(
|
||||||
content?: string,
|
content?: string,
|
||||||
messages?: Record<string, unknown>[],
|
messages?: Record<string, unknown>[],
|
||||||
opts?: AddOptions,
|
opts?: AddOptions,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
|
search(
|
||||||
|
query: string,
|
||||||
|
opts?: SearchOptions,
|
||||||
|
): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
get(memoryId: string): Promise<Record<string, unknown>>;
|
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
|
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
update(
|
update(
|
||||||
memoryId: string,
|
memoryId: string,
|
||||||
content?: string,
|
content?: string,
|
||||||
metadata?: Record<string, unknown>,
|
metadata?: Record<string, unknown>,
|
||||||
): Promise<Record<string, unknown>>;
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
|
delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts?: DeleteOptions,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
|
ping(): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
entities(entityType: string): 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 {
|
export class AuthError extends Error {
|
||||||
constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
|
constructor(
|
||||||
super(message);
|
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||||
this.name = "AuthError";
|
) {
|
||||||
}
|
super(message);
|
||||||
|
this.name = "AuthError";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotFoundError extends Error {
|
export class NotFoundError extends Error {
|
||||||
constructor(path: string) {
|
constructor(path: string) {
|
||||||
super(`Resource not found: ${path}`);
|
super(`Resource not found: ${path}`);
|
||||||
this.name = "NotFoundError";
|
this.name = "NotFoundError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class APIError extends Error {
|
export class APIError extends Error {
|
||||||
constructor(path: string, detail: string) {
|
constructor(path: string, detail: string) {
|
||||||
super(`Bad request to ${path}: ${detail}`);
|
super(`Bad request to ${path}: ${detail}`);
|
||||||
this.name = "APIError";
|
this.name = "APIError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBackend(config: Mem0Config): Backend {
|
export function getBackend(config: Mem0Config): Backend {
|
||||||
return new PlatformBackend(config.platform);
|
return new PlatformBackend(config.platform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export { getBackend } from "./base.js";
|
export { getBackend } from "./base.js";
|
||||||
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
|
export type {
|
||||||
|
Backend,
|
||||||
|
AddOptions,
|
||||||
|
SearchOptions,
|
||||||
|
ListOptions,
|
||||||
|
DeleteOptions,
|
||||||
|
EntityIds,
|
||||||
|
} from "./base.js";
|
||||||
export { AuthError, NotFoundError, APIError } from "./base.js";
|
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||||
|
|||||||
+326
-266
@@ -3,301 +3,361 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { PlatformConfig } from "../config.js";
|
import type { PlatformConfig } from "../config.js";
|
||||||
|
import { isAgentMode } from "../state.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
import {
|
import {
|
||||||
type AddOptions,
|
APIError,
|
||||||
APIError,
|
type AddOptions,
|
||||||
AuthError,
|
AuthError,
|
||||||
type Backend,
|
type Backend,
|
||||||
type DeleteOptions,
|
type DeleteOptions,
|
||||||
type EntityIds,
|
type EntityIds,
|
||||||
type ListOptions,
|
type ListOptions,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
type SearchOptions,
|
type SearchOptions,
|
||||||
} from "./base.js";
|
} from "./base.js";
|
||||||
|
|
||||||
export class PlatformBackend implements Backend {
|
export class PlatformBackend implements Backend {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private headers: Record<string, string>;
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
constructor(config: PlatformConfig) {
|
constructor(config: PlatformConfig) {
|
||||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||||
this.headers = {
|
this.headers = {
|
||||||
Authorization: `Token ${config.apiKey}`,
|
Authorization: `Token ${config.apiKey}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
"X-Mem0-Source": "cli",
|
||||||
}
|
"X-Mem0-Client-Language": "node",
|
||||||
|
"X-Mem0-Client-Version": CLI_VERSION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async _request(
|
private async _request(
|
||||||
method: string,
|
method: string,
|
||||||
path: string,
|
path: string,
|
||||||
opts?: { json?: unknown; params?: Record<string, string> },
|
opts?: { json?: unknown; params?: Record<string, string> },
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
let url = `${this.baseUrl}${path}`;
|
let url = `${this.baseUrl}${path}`;
|
||||||
if (opts?.params) {
|
if (opts?.params) {
|
||||||
const qs = new URLSearchParams(opts.params).toString();
|
const qs = new URLSearchParams(opts.params).toString();
|
||||||
url += `?${qs}`;
|
url += `?${qs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchOpts: RequestInit = {
|
const headers = {
|
||||||
method,
|
...this.headers,
|
||||||
headers: this.headers,
|
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
|
||||||
signal: AbortSignal.timeout(30_000),
|
};
|
||||||
};
|
|
||||||
if (opts?.json) {
|
|
||||||
fetchOpts.body = JSON.stringify(opts.json);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resp = await fetch(url, fetchOpts);
|
const fetchOpts: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
};
|
||||||
|
if (opts?.json) {
|
||||||
|
fetchOpts.body = JSON.stringify(opts.json);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.status === 401) {
|
const resp = await fetch(url, fetchOpts);
|
||||||
throw new AuthError();
|
|
||||||
}
|
|
||||||
if (resp.status === 404) {
|
|
||||||
throw new NotFoundError(path);
|
|
||||||
}
|
|
||||||
if (resp.status === 400) {
|
|
||||||
let detail: string;
|
|
||||||
try {
|
|
||||||
const body = await resp.json();
|
|
||||||
detail = (body as Record<string, string>).detail ?? resp.statusText;
|
|
||||||
} catch {
|
|
||||||
detail = resp.statusText;
|
|
||||||
}
|
|
||||||
throw new APIError(path, detail);
|
|
||||||
}
|
|
||||||
if (!resp.ok) {
|
|
||||||
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
||||||
}
|
|
||||||
if (resp.status === 204) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return resp.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async add(
|
if (resp.status === 401) {
|
||||||
content?: string,
|
throw new AuthError();
|
||||||
messages?: Record<string, unknown>[],
|
}
|
||||||
opts: AddOptions = {},
|
if (resp.status === 404) {
|
||||||
): Promise<Record<string, unknown>> {
|
throw new NotFoundError(path);
|
||||||
const payload: Record<string, unknown> = {};
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
if (messages) {
|
async add(
|
||||||
payload.messages = messages;
|
content?: string,
|
||||||
} else if (content) {
|
messages?: Record<string, unknown>[],
|
||||||
payload.messages = [{ role: "user", content }];
|
opts: AddOptions = {},
|
||||||
}
|
): Promise<Record<string, unknown>> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
|
||||||
if (opts.userId) payload.user_id = opts.userId;
|
if (messages) {
|
||||||
if (opts.agentId) payload.agent_id = opts.agentId;
|
payload.messages = messages;
|
||||||
if (opts.appId) payload.app_id = opts.appId;
|
} else if (content) {
|
||||||
if (opts.runId) payload.run_id = opts.runId;
|
payload.messages = [{ role: "user", content }];
|
||||||
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<
|
if (opts.userId) payload.user_id = opts.userId;
|
||||||
string,
|
if (opts.agentId) payload.agent_id = opts.agentId;
|
||||||
unknown
|
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;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
private _buildFilters(opts: {
|
return (await this._request("POST", "/v1/memories/", {
|
||||||
userId?: string;
|
json: payload,
|
||||||
agentId?: string;
|
})) as Record<string, unknown>;
|
||||||
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>[] = [];
|
private _buildFilters(opts: {
|
||||||
if (opts.userId) andConditions.push({ user_id: opts.userId });
|
userId?: string;
|
||||||
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
|
agentId?: string;
|
||||||
if (opts.appId) andConditions.push({ app_id: opts.appId });
|
appId?: string;
|
||||||
if (opts.runId) andConditions.push({ run_id: opts.runId });
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.extraFilters) {
|
const andConditions: Record<string, unknown>[] = [];
|
||||||
for (const [k, v] of Object.entries(opts.extraFilters)) {
|
if (opts.userId) andConditions.push({ user_id: opts.userId });
|
||||||
andConditions.push({ [k]: v });
|
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 (andConditions.length === 1) return andConditions[0];
|
if (opts.extraFilters) {
|
||||||
if (andConditions.length > 1) return { AND: andConditions };
|
for (const [k, v] of Object.entries(opts.extraFilters)) {
|
||||||
return undefined;
|
andConditions.push({ [k]: v });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
|
if (andConditions.length === 1) return andConditions[0];
|
||||||
const payload: Record<string, unknown> = {
|
if (andConditions.length > 1) return { AND: andConditions };
|
||||||
query,
|
return undefined;
|
||||||
top_k: opts.topK ?? 10,
|
}
|
||||||
threshold: opts.threshold ?? 0.3,
|
|
||||||
};
|
|
||||||
|
|
||||||
const apiFilters = this._buildFilters({
|
async search(
|
||||||
userId: opts.userId,
|
query: string,
|
||||||
agentId: opts.agentId,
|
opts: SearchOptions = {},
|
||||||
appId: opts.appId,
|
): Promise<Record<string, unknown>[]> {
|
||||||
runId: opts.runId,
|
const payload: Record<string, unknown> = {
|
||||||
extraFilters: opts.filters,
|
query,
|
||||||
});
|
top_k: opts.topK ?? 10,
|
||||||
if (apiFilters) payload.filters = apiFilters;
|
threshold: opts.threshold ?? 0.3,
|
||||||
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/", {
|
const apiFilters = this._buildFilters({
|
||||||
json: payload,
|
userId: opts.userId,
|
||||||
})) as unknown;
|
agentId: opts.agentId,
|
||||||
if (Array.isArray(result)) return result;
|
appId: opts.appId,
|
||||||
const obj = result as Record<string, unknown>;
|
runId: opts.runId,
|
||||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
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;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
const result = (await this._request("POST", "/v2/memories/search/", {
|
||||||
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
|
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 listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
|
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||||
const payload: Record<string, unknown> = {};
|
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||||
const params: Record<string, string> = {
|
params: { source: "CLI" },
|
||||||
page: String(opts.page ?? 1),
|
})) as Record<string, unknown>;
|
||||||
page_size: String(opts.pageSize ?? 100),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const extra: Record<string, unknown> = {};
|
async listMemories(
|
||||||
if (opts.category) {
|
opts: ListOptions = {},
|
||||||
extra.categories = { contains: opts.category };
|
): Promise<Record<string, unknown>[]> {
|
||||||
}
|
const payload: Record<string, unknown> = {};
|
||||||
if (opts.after) {
|
const params: Record<string, string> = {
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
|
page: String(opts.page ?? 1),
|
||||||
}
|
page_size: String(opts.pageSize ?? 100),
|
||||||
if (opts.before) {
|
};
|
||||||
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiFilters = this._buildFilters({
|
const extra: Record<string, unknown> = {};
|
||||||
userId: opts.userId,
|
if (opts.category) {
|
||||||
agentId: opts.agentId,
|
extra.categories = { contains: opts.category };
|
||||||
appId: opts.appId,
|
}
|
||||||
runId: opts.runId,
|
if (opts.after) {
|
||||||
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
extra.created_at = {
|
||||||
});
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
if (apiFilters) payload.filters = apiFilters;
|
gte: opts.after,
|
||||||
if (opts.enableGraph) payload.enable_graph = true;
|
};
|
||||||
|
}
|
||||||
|
if (opts.before) {
|
||||||
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
lte: opts.before,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
|
const apiFilters = this._buildFilters({
|
||||||
if (Array.isArray(result)) return result;
|
userId: opts.userId,
|
||||||
const obj = result as Record<string, unknown>;
|
agentId: opts.agentId,
|
||||||
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
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;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
async update(
|
const result = (await this._request("POST", "/v2/memories/", {
|
||||||
memoryId: string,
|
json: payload,
|
||||||
content?: string,
|
params,
|
||||||
metadata?: Record<string, unknown>,
|
})) as unknown;
|
||||||
): Promise<Record<string, unknown>> {
|
if (Array.isArray(result)) return result;
|
||||||
const payload: Record<string, unknown> = {};
|
const obj = result as Record<string, unknown>;
|
||||||
if (content) payload.text = content;
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
if (metadata) payload.metadata = metadata;
|
}
|
||||||
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
|
||||||
json: payload,
|
|
||||||
})) as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(
|
async update(
|
||||||
memoryId?: string,
|
memoryId: string,
|
||||||
opts: DeleteOptions = {},
|
content?: string,
|
||||||
): Promise<Record<string, unknown>> {
|
metadata?: Record<string, unknown>,
|
||||||
if (opts.all) {
|
): Promise<Record<string, unknown>> {
|
||||||
const params: Record<string, string> = {};
|
const payload: Record<string, unknown> = {};
|
||||||
if (opts.userId) params.user_id = opts.userId;
|
if (content) payload.text = content;
|
||||||
if (opts.agentId) params.agent_id = opts.agentId;
|
if (metadata) payload.metadata = metadata;
|
||||||
if (opts.appId) params.app_id = opts.appId;
|
payload.source = "CLI";
|
||||||
if (opts.runId) params.run_id = opts.runId;
|
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||||
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
|
json: payload,
|
||||||
string,
|
})) as Record<string, unknown>;
|
||||||
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>> {
|
async delete(
|
||||||
const params: Record<string, string> = {};
|
memoryId?: string,
|
||||||
if (opts.userId) params.user_id = opts.userId;
|
opts: DeleteOptions = {},
|
||||||
if (opts.agentId) params.agent_id = opts.agentId;
|
): Promise<Record<string, unknown>> {
|
||||||
if (opts.appId) params.app_id = opts.appId;
|
if (opts.all) {
|
||||||
if (opts.runId) params.run_id = opts.runId;
|
const params: Record<string, string> = { source: "CLI" };
|
||||||
if (Object.keys(params).length === 0) {
|
if (opts.userId) params.user_id = opts.userId;
|
||||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
if (opts.agentId) params.agent_id = opts.agentId;
|
||||||
}
|
if (opts.appId) params.app_id = opts.appId;
|
||||||
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
|
if (opts.runId) params.run_id = opts.runId;
|
||||||
string,
|
return (await this._request("DELETE", "/v1/memories/", {
|
||||||
unknown
|
params,
|
||||||
>;
|
})) as Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
if (memoryId) {
|
||||||
|
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||||
|
params: { source: "CLI" },
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
throw new Error("Either memoryId or --all is required");
|
||||||
|
}
|
||||||
|
|
||||||
async status(
|
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
|
||||||
opts: { userId?: string; agentId?: string } = {},
|
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
): Promise<Record<string, unknown>> {
|
const typeMap: [string, string | undefined][] = [
|
||||||
try {
|
["user", opts.userId],
|
||||||
if (opts.userId || opts.agentId) {
|
["agent", opts.agentId],
|
||||||
const payload: Record<string, unknown> = {};
|
["app", opts.appId],
|
||||||
const statusParams: Record<string, string> = { page: "1", page_size: "1" };
|
["run", opts.runId],
|
||||||
const apiFilters = this._buildFilters({
|
];
|
||||||
userId: opts.userId,
|
const entities = typeMap.filter(([, v]) => v) as [string, string][];
|
||||||
agentId: opts.agentId,
|
if (entities.length === 0) {
|
||||||
});
|
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||||
if (apiFilters) payload.filters = apiFilters;
|
}
|
||||||
await this._request("POST", "/v2/memories/", { json: payload, params: statusParams });
|
// Delete each provided entity via the v2 path-based endpoint
|
||||||
} else {
|
let result: Record<string, unknown> = {};
|
||||||
await this._request("GET", "/v1/entities/");
|
for (const [entityType, entityId] of entities) {
|
||||||
}
|
result = (await this._request(
|
||||||
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
"DELETE",
|
||||||
} catch (e) {
|
`/v2/entities/${entityType}/${entityId}/`,
|
||||||
return {
|
{ params: { source: "CLI" } },
|
||||||
connected: false,
|
)) as Record<string, unknown>;
|
||||||
backend: "platform",
|
}
|
||||||
error: e instanceof Error ? e.message : String(e),
|
return result;
|
||||||
};
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async entities(entityType: string): Promise<Record<string, unknown>[]> {
|
async ping(): Promise<Record<string, unknown>> {
|
||||||
const result = (await this._request("GET", "/v1/entities/")) as unknown;
|
return (await this._request("GET", "/v1/ping/")) as Record<string, 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> = {
|
async status(
|
||||||
users: "user",
|
opts: { userId?: string; agentId?: string } = {},
|
||||||
agents: "agent",
|
): Promise<Record<string, unknown>> {
|
||||||
apps: "app",
|
try {
|
||||||
runs: "run",
|
await this.ping();
|
||||||
};
|
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
||||||
const targetType = typeMap[entityType];
|
} catch (e) {
|
||||||
if (targetType) {
|
return {
|
||||||
items = items.filter(
|
connected: false,
|
||||||
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
|
backend: "platform",
|
||||||
);
|
error: e instanceof Error ? e.message : String(e),
|
||||||
}
|
};
|
||||||
return items;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-69
@@ -4,10 +4,8 @@
|
|||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import ora, { type Ora } from "ora";
|
import ora, { type Ora } from "ora";
|
||||||
import { createRequire } from "node:module";
|
import { getCurrentCommand, isAgentMode } from "./state.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
const _require = createRequire(import.meta.url);
|
|
||||||
const PKG_VERSION: string = _require("../package.json").version;
|
|
||||||
|
|
||||||
export const LOGO = `
|
export const LOGO = `
|
||||||
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
@@ -40,75 +38,96 @@ const dim = chalk.hex(DIM_COLOR);
|
|||||||
* plain-text for piped/non-TTY or NO_COLOR environments.
|
* plain-text for piped/non-TTY or NO_COLOR environments.
|
||||||
*/
|
*/
|
||||||
export function sym(fancy: string, plain: string): string {
|
export function sym(fancy: string, plain: string): string {
|
||||||
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
|
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
|
||||||
return fancy;
|
return fancy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printBanner(): void {
|
export function printBanner(): void {
|
||||||
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
if (isAgentMode()) return;
|
||||||
const logoLines = LOGO.trimEnd().split("\n");
|
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||||
const tagline = ` ${TAGLINE}`;
|
const logoLines = LOGO.trimEnd().split("\n");
|
||||||
const subtitle = `Node.js SDK · v${PKG_VERSION}`;
|
const tagline = ` ${TAGLINE}`;
|
||||||
const contentLines = ["", ...logoLines, "", tagline, ""];
|
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||||
|
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||||
|
|
||||||
// Compute inner width from longest content line + padding both sides
|
// Compute inner width from longest content line + padding both sides
|
||||||
const maxContent = Math.max(...contentLines.map((l) => l.length));
|
const maxContent = Math.max(...contentLines.map((l) => l.length));
|
||||||
const innerWidth = maxContent + pad * 2;
|
const innerWidth = maxContent + pad * 2;
|
||||||
const totalWidth = innerWidth + 2; // + 2 for │ borders
|
const totalWidth = innerWidth + 2; // + 2 for │ borders
|
||||||
|
|
||||||
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
||||||
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
||||||
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
|
const bottomBorder = brand(
|
||||||
|
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
|
||||||
|
);
|
||||||
|
|
||||||
const body = contentLines.map((line) => {
|
const body = contentLines.map((line) => {
|
||||||
const rightPad = innerWidth - pad - line.length;
|
const rightPad = innerWidth - pad - line.length;
|
||||||
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
|
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
|
||||||
});
|
});
|
||||||
// Re-color tagline line with accent instead of brand.bold
|
// Re-color tagline line with accent instead of brand.bold
|
||||||
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
||||||
const taglineRightPad = innerWidth - pad - tagline.length;
|
const taglineRightPad = innerWidth - pad - tagline.length;
|
||||||
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
body[taglineIdx] =
|
||||||
|
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
||||||
|
|
||||||
console.log(topBorder);
|
console.log(topBorder);
|
||||||
for (const line of body) console.log(line);
|
for (const line of body) console.log(line);
|
||||||
console.log(bottomBorder);
|
console.log(bottomBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printSuccess(message: string): void {
|
export function printSuccess(message: string): void {
|
||||||
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
if (isAgentMode()) return;
|
||||||
|
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printError(message: string, hint?: string): void {
|
export function printError(message: string, hint?: string): void {
|
||||||
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`);
|
if (isAgentMode()) {
|
||||||
if (hint) {
|
const envelope = {
|
||||||
console.error(` ${dim(hint)}`);
|
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 {
|
export function printWarning(message: string): void {
|
||||||
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
|
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printInfo(message: string): void {
|
export function printInfo(message: string): void {
|
||||||
console.log(`${brand(sym("◆", "*"))} ${message}`);
|
if (isAgentMode()) return;
|
||||||
|
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printScope(ids: Record<string, string | undefined>): void {
|
export function printScope(ids: Record<string, string | undefined>): void {
|
||||||
const parts: string[] = [];
|
if (isAgentMode()) return;
|
||||||
for (const [key, val] of Object.entries(ids)) {
|
const parts: string[] = [];
|
||||||
if (val) {
|
for (const [key, val] of Object.entries(ids)) {
|
||||||
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
|
if (val) {
|
||||||
parts.push(`${label}=${val}`);
|
parts.push(`${key}=${val}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimedStatusContext {
|
export interface TimedStatusContext {
|
||||||
successMsg: string;
|
successMsg: string;
|
||||||
errorMsg: string;
|
errorMsg: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,29 +135,37 @@ export interface TimedStatusContext {
|
|||||||
* Equivalent to Python's timed_status context manager.
|
* Equivalent to Python's timed_status context manager.
|
||||||
*/
|
*/
|
||||||
export async function timedStatus<T>(
|
export async function timedStatus<T>(
|
||||||
message: string,
|
message: string,
|
||||||
fn: (ctx: TimedStatusContext) => Promise<T>,
|
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
if (isAgentMode()) {
|
||||||
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
const start = performance.now();
|
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 {
|
try {
|
||||||
const result = await fn(ctx);
|
const result = await fn(ctx);
|
||||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
if (ctx.successMsg) {
|
if (ctx.successMsg) {
|
||||||
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
|
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
if (ctx.errorMsg) {
|
if (ctx.errorMsg) {
|
||||||
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
|
printError(`${ctx.errorMsg} (${elapsed}s)`);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format helpers using brand colors for external use. */
|
/** Format helpers using brand colors for external use. */
|
||||||
|
|||||||
@@ -3,88 +3,109 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printSuccess, colors } from "../branding.js";
|
import { colors, printError, printSuccess } from "../branding.js";
|
||||||
import {
|
import {
|
||||||
getNestedValue,
|
getNestedValue,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
redactKey,
|
redactKey,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
setNestedValue,
|
setNestedValue,
|
||||||
} from "../config.js";
|
} from "../config.js";
|
||||||
import { formatJsonEnvelope } from "../output.js";
|
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||||
|
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
const { brand, accent, dim } = colors;
|
const { brand, accent, dim } = colors;
|
||||||
|
|
||||||
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||||
const config = loadConfig();
|
setCurrentCommand("config show");
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
if (opts.output === "json") {
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
formatJsonEnvelope({
|
formatAgentEnvelope({
|
||||||
command: "config show",
|
command: "config show",
|
||||||
data: {
|
data: {
|
||||||
defaults: {
|
defaults: {
|
||||||
user_id: config.defaults.userId || null,
|
user_id: config.defaults.userId || null,
|
||||||
agent_id: config.defaults.agentId || null,
|
agent_id: config.defaults.agentId || null,
|
||||||
app_id: config.defaults.appId || null,
|
app_id: config.defaults.appId || null,
|
||||||
run_id: config.defaults.runId || null,
|
run_id: config.defaults.runId || null,
|
||||||
enable_graph: config.defaults.enableGraph,
|
enable_graph: config.defaults.enableGraph,
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
api_key: redactKey(config.platform.apiKey),
|
api_key: redactKey(config.platform.apiKey),
|
||||||
base_url: config.platform.baseUrl,
|
base_url: config.platform.baseUrl,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(` ${brand("◆ mem0 Configuration")}\n`);
|
console.log(` ${brand("◆ mem0 Configuration")}\n`);
|
||||||
|
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: [accent("Key"), accent("Value")],
|
head: [accent("Key"), accent("Value")],
|
||||||
style: { head: [], border: [] },
|
style: { head: [], border: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
||||||
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
|
table.push([
|
||||||
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
|
"defaults.agent_id",
|
||||||
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
|
config.defaults.agentId || dim("(not set)"),
|
||||||
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
]);
|
||||||
table.push(["", ""]);
|
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
|
// Platform
|
||||||
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
|
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
|
||||||
table.push(["platform.base_url", config.platform.baseUrl]);
|
table.push(["platform.base_url", config.platform.baseUrl]);
|
||||||
|
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cmdConfigGet(key: string): void {
|
export function cmdConfigGet(key: string): void {
|
||||||
const config = loadConfig();
|
setCurrentCommand("config get");
|
||||||
const value = getNestedValue(config, key);
|
const config = loadConfig();
|
||||||
|
const value = getNestedValue(config, key);
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
printError(`Unknown config key: ${key}`);
|
printError(`Unknown config key: ${key}`);
|
||||||
} else {
|
} else {
|
||||||
// Redact secrets
|
// Redact secrets
|
||||||
if (key.includes("api_key") || key.split(".").pop() === "key") {
|
const displayValue =
|
||||||
console.log(redactKey(String(value)));
|
key.includes("api_key") || key.split(".").pop() === "key"
|
||||||
} else {
|
? redactKey(String(value))
|
||||||
console.log(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 {
|
export function cmdConfigSet(key: string, value: string): void {
|
||||||
const config = loadConfig();
|
setCurrentCommand("config set");
|
||||||
if (setNestedValue(config, key, value)) {
|
const config = loadConfig();
|
||||||
saveConfig(config);
|
if (setNestedValue(config, key, value)) {
|
||||||
const display = key.includes("key") ? redactKey(value) : value;
|
saveConfig(config);
|
||||||
printSuccess(`${key} = ${display}`);
|
const display = key.includes("key") ? redactKey(value) : value;
|
||||||
} else {
|
if (isAgentMode()) {
|
||||||
printError(`Unknown config key: ${key}`);
|
formatAgentEnvelope({
|
||||||
}
|
command: "config set",
|
||||||
|
data: { key, value: display },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
printSuccess(`${key} = ${display}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printError(`Unknown config key: ${key}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+138
-109
@@ -4,136 +4,165 @@
|
|||||||
|
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
import type { Backend } from "../backend/base.js";
|
||||||
import { formatJson } from "../output.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 { brand, accent, dim } = colors;
|
||||||
|
|
||||||
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
|
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
|
||||||
|
|
||||||
export async function cmdEntitiesList(
|
export async function cmdEntitiesList(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
entityType: string,
|
entityType: string,
|
||||||
opts: { output: string },
|
opts: { output: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!VALID_TYPES.has(entityType)) {
|
setCurrentCommand("entity list");
|
||||||
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
|
if (!VALID_TYPES.has(entityType)) {
|
||||||
process.exit(1);
|
printError(
|
||||||
}
|
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let results: Record<string, unknown>[];
|
let results: Record<string, unknown>[];
|
||||||
try {
|
try {
|
||||||
results = await timedStatus(`Fetching ${entityType}...`, async () => {
|
results = await timedStatus(`Fetching ${entityType}...`, async () => {
|
||||||
return backend.entities(entityType);
|
return backend.entities(entityType);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(
|
printError(
|
||||||
e instanceof Error ? e.message : String(e),
|
e instanceof Error ? e.message : String(e),
|
||||||
"This feature may require the mem0 Platform.",
|
"This feature may require the mem0 Platform.",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
if (opts.output === "json") {
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
formatJson(results);
|
formatAgentEnvelope({
|
||||||
return;
|
command: "entity list",
|
||||||
}
|
data: results,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
printInfo(`No ${entityType} found.`);
|
printInfo(`No ${entityType} found.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const table = new Table({
|
const table = new Table({
|
||||||
head: [accent("Name / ID"), accent("Created")],
|
head: [accent("Name / ID"), accent("Created")],
|
||||||
style: { head: [], border: [] },
|
style: { head: [], border: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const entity of results) {
|
for (const entity of results) {
|
||||||
const name = String(entity.name ?? entity.id ?? "—");
|
const name = String(entity.name ?? entity.id ?? "—");
|
||||||
const created = String(entity.created_at ?? "—").slice(0, 10);
|
const created = String(entity.created_at ?? "—").slice(0, 10);
|
||||||
table.push([name, created]);
|
table.push([name, created]);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
|
console.log(
|
||||||
console.log();
|
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdEntitiesDelete(
|
export async function cmdEntitiesDelete(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
opts: {
|
opts: {
|
||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
appId?: string;
|
appId?: string;
|
||||||
runId?: string;
|
runId?: string;
|
||||||
dryRun?: boolean;
|
dryRun?: boolean;
|
||||||
force: boolean;
|
force: boolean;
|
||||||
output: string;
|
output: string;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
setCurrentCommand("entity delete");
|
||||||
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
|
const { isAgentMode } = await import("../state.js");
|
||||||
process.exit(1);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.dryRun) {
|
const scopeParts: string[] = [];
|
||||||
const scopeParts: string[] = [];
|
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
const scope = scopeParts.join(", ");
|
||||||
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
|
|
||||||
printInfo("No changes made.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts.force) {
|
if (opts.dryRun) {
|
||||||
const scopeParts: string[] = [];
|
printInfo(`Would delete entity ${scope} and all its memories.`);
|
||||||
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
printInfo("No changes made.");
|
||||||
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
return;
|
||||||
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
}
|
||||||
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
|
||||||
const scope = scopeParts.join(", ");
|
|
||||||
|
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
if (!opts.force) {
|
||||||
const answer = await new Promise<string>((resolve) => {
|
const rl = readline.createInterface({
|
||||||
rl.question(
|
input: process.stdin,
|
||||||
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
output: process.stdout,
|
||||||
resolve,
|
});
|
||||||
);
|
const answer = await new Promise<string>((resolve) => {
|
||||||
});
|
rl.question(
|
||||||
rl.close();
|
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||||
if (answer.toLowerCase() !== "y") {
|
resolve,
|
||||||
printInfo("Cancelled.");
|
);
|
||||||
process.exit(0);
|
});
|
||||||
}
|
rl.close();
|
||||||
}
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
let result: Record<string, unknown>;
|
let result: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
result = await timedStatus("Deleting entity...", async () => {
|
result = await timedStatus("Deleting entity...", async () => {
|
||||||
return backend.deleteEntities({
|
return backend.deleteEntities({
|
||||||
userId: opts.userId,
|
userId: opts.userId,
|
||||||
agentId: opts.agentId,
|
agentId: opts.agentId,
|
||||||
appId: opts.appId,
|
appId: opts.appId,
|
||||||
runId: opts.runId,
|
runId: opts.runId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printError(e instanceof Error ? e.message : String(e));
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
if (opts.output === "json") {
|
if (opts.output === "agent") {
|
||||||
formatJson(result);
|
formatAgentEnvelope({
|
||||||
} else if (opts.output !== "quiet") {
|
command: "entity delete",
|
||||||
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
|
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();
|
||||||
|
}
|
||||||
+407
-143
@@ -2,181 +2,445 @@
|
|||||||
* mem0 init — interactive setup wizard.
|
* mem0 init — interactive setup wizard.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
import readline from "node:readline";
|
import readline from "node:readline";
|
||||||
import {
|
|
||||||
printBanner,
|
|
||||||
printError,
|
|
||||||
printInfo,
|
|
||||||
printSuccess,
|
|
||||||
colors,
|
|
||||||
} from "../branding.js";
|
|
||||||
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
|
|
||||||
import { PlatformBackend } from "../backend/platform.js";
|
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 { 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;
|
||||||
|
|
||||||
|
const sourceHeaders = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "node",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!codeValue) {
|
||||||
|
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: sourceHeaders,
|
||||||
|
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: sourceHeaders,
|
||||||
|
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> {
|
function promptSecret(label: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
process.stdout.write(label);
|
process.stdout.write(label);
|
||||||
|
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
process.stdin.setRawMode(true);
|
process.stdin.setRawMode(true);
|
||||||
}
|
}
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
process.stdin.setEncoding("utf-8");
|
process.stdin.setEncoding("utf-8");
|
||||||
|
|
||||||
const chars: string[] = [];
|
const chars: string[] = [];
|
||||||
|
|
||||||
const onData = (key: string) => {
|
const onData = (key: string) => {
|
||||||
for (const ch of key) {
|
for (const ch of key) {
|
||||||
if (ch === "\r" || ch === "\n") {
|
if (ch === "\r" || ch === "\n") {
|
||||||
cleanup();
|
cleanup();
|
||||||
process.stdout.write("\n");
|
process.stdout.write("\n");
|
||||||
resolve(chars.join(""));
|
resolve(chars.join(""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ch === "\x03") {
|
if (ch === "\x03") {
|
||||||
cleanup();
|
cleanup();
|
||||||
reject(new Error("Interrupted"));
|
reject(new Error("Interrupted"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ch === "\x7f" || ch === "\x08") {
|
if (ch === "\x7f" || ch === "\x08") {
|
||||||
// backspace
|
// backspace
|
||||||
if (chars.length > 0) {
|
if (chars.length > 0) {
|
||||||
chars.pop();
|
chars.pop();
|
||||||
process.stdout.write("\b \b");
|
process.stdout.write("\b \b");
|
||||||
}
|
}
|
||||||
} else if (ch === "\x15") {
|
} else if (ch === "\x15") {
|
||||||
// Ctrl+U — clear line
|
// Ctrl+U — clear line
|
||||||
process.stdout.write("\b \b".repeat(chars.length));
|
process.stdout.write("\b \b".repeat(chars.length));
|
||||||
chars.length = 0;
|
chars.length = 0;
|
||||||
} else if (ch >= " ") {
|
} else if (ch >= " ") {
|
||||||
chars.push(ch);
|
chars.push(ch);
|
||||||
process.stdout.write("*");
|
process.stdout.write("*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
process.stdin.removeListener("data", onData);
|
process.stdin.removeListener("data", onData);
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
process.stdin.setRawMode(false);
|
process.stdin.setRawMode(false);
|
||||||
}
|
}
|
||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
};
|
};
|
||||||
|
|
||||||
process.stdin.on("data", onData);
|
process.stdin.on("data", onData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
const rl = readline.createInterface({
|
||||||
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
input: process.stdin,
|
||||||
return new Promise((resolve) => {
|
output: process.stdout,
|
||||||
rl.question(prompt, (answer) => {
|
});
|
||||||
rl.close();
|
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||||
resolve(answer.trim() || defaultValue || "");
|
return new Promise((resolve) => {
|
||||||
});
|
rl.question(prompt, (answer) => {
|
||||||
});
|
rl.close();
|
||||||
|
resolve(answer.trim() || defaultValue || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupPlatform(config: Mem0Config): Promise<void> {
|
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||||
console.log();
|
console.log();
|
||||||
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
|
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")}: `);
|
process.stdout.write(` ${brand("API Key")}: `);
|
||||||
const apiKey = await promptSecret("");
|
const apiKey = await promptSecret("");
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
printError("API key is required.");
|
printError("API key is required.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
config.platform.apiKey = apiKey;
|
config.platform.apiKey = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDefaults(config: Mem0Config): Promise<void> {
|
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("Set default entity IDs (press Enter to skip).\n");
|
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||||
|
|
||||||
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
|
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
if (userId) config.defaults.userId = userId;
|
const userId = await promptLine(
|
||||||
|
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||||
|
_systemUser,
|
||||||
|
);
|
||||||
|
if (userId) config.defaults.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validatePlatform(config: Mem0Config): Promise<void> {
|
async function validatePlatform(config: Mem0Config): Promise<void> {
|
||||||
console.log();
|
console.log();
|
||||||
printInfo("Validating connection...");
|
printInfo("Validating connection...");
|
||||||
try {
|
try {
|
||||||
const backend = new PlatformBackend(config.platform);
|
const backend = new PlatformBackend(config.platform);
|
||||||
const status = await backend.status({
|
const status = await backend.status({
|
||||||
userId: config.defaults.userId || undefined,
|
userId: config.defaults.userId || undefined,
|
||||||
agentId: config.defaults.agentId || undefined,
|
agentId: config.defaults.agentId || undefined,
|
||||||
});
|
});
|
||||||
if (status.connected) {
|
if (status.connected) {
|
||||||
printSuccess("Connected to mem0 Platform!");
|
printSuccess("Connected to mem0 Platform!");
|
||||||
} else {
|
// Cache user_email from ping response for telemetry distinct_id
|
||||||
printError(
|
try {
|
||||||
`Could not connect: ${status.error ?? "Unknown error"}`,
|
const pingData = (await backend.ping()) as Record<string, unknown>;
|
||||||
"Check your API key and try again.",
|
const userEmail = pingData?.user_email as string | undefined;
|
||||||
);
|
if (userEmail) {
|
||||||
}
|
config.platform.userEmail = userEmail;
|
||||||
} catch (e) {
|
}
|
||||||
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
|
} catch {
|
||||||
}
|
/* ignore — telemetry ID will fall back to API key hash */
|
||||||
|
}
|
||||||
|
} 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 } = {}): Promise<void> {
|
export async function runInit(
|
||||||
const config = createDefaultConfig();
|
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;
|
||||||
|
|
||||||
// Non-interactive: both flags provided
|
// Guards
|
||||||
if (opts.apiKey && opts.userId) {
|
if (opts.code && !opts.email) {
|
||||||
config.platform.apiKey = opts.apiKey;
|
printError("--code requires --email.");
|
||||||
config.defaults.userId = opts.userId;
|
process.exit(1);
|
||||||
await validatePlatform(config);
|
}
|
||||||
saveConfig(config);
|
if (opts.email && opts.apiKey) {
|
||||||
printSuccess("Configuration saved to ~/.mem0/config.json");
|
printError("Cannot use both --api-key and --email.");
|
||||||
return;
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-TTY without full flags: error with usage hint
|
// Warn if an existing config with an API key would be overwritten
|
||||||
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
|
if (
|
||||||
printError(
|
!opts.force &&
|
||||||
"Non-interactive terminal detected and missing required flags.",
|
fs.existsSync(CONFIG_FILE) &&
|
||||||
"Usage: mem0 init --api-key <key> --user-id <id>",
|
savedConfig.platform.apiKey
|
||||||
);
|
) {
|
||||||
process.exit(1);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
printBanner();
|
// ── Email login flow ──────────────────────────────────────────────────────
|
||||||
console.log();
|
if (opts.email) {
|
||||||
printInfo("Welcome! Let's set up your mem0 CLI.\n");
|
const email = opts.email.trim().toLowerCase();
|
||||||
|
validateEmail(email);
|
||||||
|
|
||||||
// Use provided API key or prompt
|
printBanner();
|
||||||
if (opts.apiKey) {
|
console.log();
|
||||||
config.platform.apiKey = opts.apiKey;
|
printInfo(`Logging in as ${email}...\n`);
|
||||||
} else {
|
|
||||||
await setupPlatform(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use provided user ID or prompt
|
const result = await emailLogin(email, opts.code, baseUrl);
|
||||||
if (opts.userId) {
|
|
||||||
config.defaults.userId = opts.userId;
|
|
||||||
} else {
|
|
||||||
await setupDefaults(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
await validatePlatform(config);
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
|
if (!apiKeyVal) {
|
||||||
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
saveConfig(config);
|
config.platform.apiKey = apiKeyVal;
|
||||||
console.log();
|
config.platform.baseUrl = baseUrl;
|
||||||
printSuccess("Configuration saved to ~/.mem0/config.json");
|
config.platform.userEmail = email;
|
||||||
console.log();
|
config.defaults.userId =
|
||||||
console.log(` ${dim("Get started:")}`);
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
if (config.defaults.userId) {
|
|
||||||
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
saveConfig(config);
|
||||||
console.log(` ${dim(' mem0 search "preferences"')}`);
|
console.log();
|
||||||
} else {
|
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
|
||||||
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
|
console.log();
|
||||||
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
|
console.log(` ${dim("Get started:")}`);
|
||||||
}
|
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
|
||||||
console.log();
|
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.platform.userEmail = email;
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+647
-425
File diff suppressed because it is too large
Load Diff
+125
-109
@@ -3,137 +3,153 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
|
|
||||||
import type { Backend } from "../backend/base.js";
|
|
||||||
import { formatJsonEnvelope } from "../output.js";
|
|
||||||
import boxen from "boxen";
|
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;
|
const { brand, dim, success, error: errorColor } = colors;
|
||||||
|
|
||||||
const _require = createRequire(import.meta.url);
|
|
||||||
const VERSION: string = _require("../../package.json").version;
|
|
||||||
|
|
||||||
export async function cmdStatus(
|
export async function cmdStatus(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
opts: { userId?: string; agentId?: string; output?: string } = {},
|
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const start = performance.now();
|
setCurrentCommand("status");
|
||||||
let result: Record<string, unknown>;
|
const start = performance.now();
|
||||||
try {
|
let result: Record<string, unknown>;
|
||||||
result = await timedStatus("Checking connection...", async () => {
|
try {
|
||||||
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
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) };
|
} catch (e) {
|
||||||
}
|
result = {
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
connected: false,
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
if (opts.output === "json") {
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
formatJsonEnvelope({
|
formatAgentEnvelope({
|
||||||
command: "status",
|
command: "status",
|
||||||
data: {
|
data: {
|
||||||
connected: result.connected,
|
connected: result.connected,
|
||||||
backend: result.backend ?? null,
|
backend: result.backend ?? null,
|
||||||
base_url: result.base_url ?? null,
|
base_url: result.base_url ?? null,
|
||||||
latency_ms: Math.round(elapsed * 1000),
|
},
|
||||||
},
|
durationMs: Math.round(elapsed * 1000),
|
||||||
durationMs: Math.round(elapsed * 1000),
|
});
|
||||||
});
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
if (result.connected) {
|
if (result.connected) {
|
||||||
lines.push(` ${success("\u25cf")} Connected`);
|
lines.push(` ${success("\u25cf")} Connected`);
|
||||||
} else {
|
} else {
|
||||||
lines.push(` ${errorColor("\u25cf")} Disconnected`);
|
lines.push(` ${errorColor("\u25cf")} Disconnected`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
|
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
|
||||||
if (result.base_url) {
|
if (result.base_url) {
|
||||||
lines.push(` ${dim("API URL:")} ${result.base_url}`);
|
lines.push(` ${dim("API URL:")} ${result.base_url}`);
|
||||||
}
|
}
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
||||||
}
|
if (String(result.error).includes("Authentication failed")) {
|
||||||
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
|
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");
|
const content = lines.join("\n");
|
||||||
console.log();
|
console.log();
|
||||||
console.log(
|
console.log(
|
||||||
boxen(content, {
|
boxen(content, {
|
||||||
title: brand("Connection Status"),
|
title: brand("Connection Status"),
|
||||||
titleAlignment: "left",
|
titleAlignment: "left",
|
||||||
borderColor: "magenta",
|
borderColor: "magenta",
|
||||||
padding: 1,
|
padding: 1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cmdVersion(): void {
|
export function cmdVersion(): void {
|
||||||
console.log(` ${brand("◆ Mem0")} CLI v${VERSION}`);
|
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cmdImport(
|
export async function cmdImport(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
filePath: string,
|
filePath: string,
|
||||||
opts: { userId?: string; agentId?: string; output?: string },
|
opts: { userId?: string; agentId?: string; output?: string },
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let data: Record<string, unknown>[];
|
setCurrentCommand("import");
|
||||||
try {
|
let data: Record<string, unknown>[];
|
||||||
const raw = fs.readFileSync(filePath, "utf-8");
|
try {
|
||||||
const parsed = JSON.parse(raw);
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
data = Array.isArray(parsed) ? parsed : [parsed];
|
const parsed = JSON.parse(raw);
|
||||||
} catch (e) {
|
data = Array.isArray(parsed) ? parsed : [parsed];
|
||||||
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
} catch (e) {
|
||||||
process.exit(1);
|
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||||
}
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
let added = 0;
|
let added = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const item = data[i];
|
const item = data[i];
|
||||||
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
|
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
|
||||||
if (!content) {
|
if (!content) {
|
||||||
failed++;
|
failed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await backend.add(content, undefined, {
|
await backend.add(content, undefined, {
|
||||||
userId: opts.userId ?? (item.user_id as string | undefined),
|
userId: opts.userId ?? (item.user_id as string | undefined),
|
||||||
agentId: opts.agentId ?? (item.agent_id as string | undefined),
|
agentId: opts.agentId ?? (item.agent_id as string | undefined),
|
||||||
metadata: item.metadata as Record<string, unknown> | undefined,
|
metadata: item.metadata as Record<string, unknown> | undefined,
|
||||||
});
|
});
|
||||||
added++;
|
added++;
|
||||||
} catch {
|
} catch {
|
||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple progress indicator
|
// Simple progress indicator
|
||||||
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
||||||
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
|
process.stdout.write(
|
||||||
}
|
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const elapsed = (performance.now() - start) / 1000;
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
console.log(); // Clear progress line
|
console.log(); // Clear progress line
|
||||||
|
|
||||||
if (opts.output === "json") {
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
formatJsonEnvelope({
|
formatAgentEnvelope({
|
||||||
command: "import",
|
command: "import",
|
||||||
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
|
data: {
|
||||||
durationMs: Math.round(elapsed * 1000),
|
added,
|
||||||
});
|
failed,
|
||||||
return;
|
},
|
||||||
}
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
|
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
printError(`${failed} memories failed to import.`);
|
printError(`${failed} memories failed to import.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-99
@@ -18,142 +18,178 @@ export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
|||||||
export const CONFIG_VERSION = 1;
|
export const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
export interface PlatformConfig {
|
export interface PlatformConfig {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
userEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultsConfig {
|
export interface DefaultsConfig {
|
||||||
userId: string;
|
userId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
enableGraph: boolean;
|
enableGraph: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TelemetryConfig {
|
||||||
|
anonymousId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Mem0Config {
|
export interface Mem0Config {
|
||||||
version: number;
|
version: number;
|
||||||
defaults: DefaultsConfig;
|
defaults: DefaultsConfig;
|
||||||
platform: PlatformConfig;
|
platform: PlatformConfig;
|
||||||
|
telemetry: TelemetryConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDefaultConfig(): Mem0Config {
|
export function createDefaultConfig(): Mem0Config {
|
||||||
return {
|
return {
|
||||||
version: CONFIG_VERSION,
|
version: CONFIG_VERSION,
|
||||||
defaults: {
|
defaults: {
|
||||||
userId: "",
|
userId: "",
|
||||||
agentId: "",
|
agentId: "",
|
||||||
appId: "",
|
appId: "",
|
||||||
runId: "",
|
runId: "",
|
||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
baseUrl: DEFAULT_BASE_URL,
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
},
|
userEmail: "",
|
||||||
};
|
},
|
||||||
|
telemetry: {
|
||||||
|
anonymousId: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureConfigDir(): string {
|
export function ensureConfigDir(): string {
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
||||||
return CONFIG_DIR;
|
return CONFIG_DIR;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(): Mem0Config {
|
export function loadConfig(): Mem0Config {
|
||||||
const config = createDefaultConfig();
|
const config = createDefaultConfig();
|
||||||
|
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||||
const data = JSON.parse(raw);
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
config.version = data.version ?? CONFIG_VERSION;
|
config.version = data.version ?? CONFIG_VERSION;
|
||||||
|
|
||||||
const plat = data.platform ?? {};
|
const plat = data.platform ?? {};
|
||||||
config.platform.apiKey = plat.api_key ?? "";
|
config.platform.apiKey = plat.api_key ?? "";
|
||||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||||
|
config.platform.userEmail = plat.user_email ?? "";
|
||||||
|
|
||||||
const defaults = data.defaults ?? {};
|
const defaults = data.defaults ?? {};
|
||||||
config.defaults.userId = defaults.user_id ?? "";
|
config.defaults.userId = defaults.user_id ?? "";
|
||||||
config.defaults.agentId = defaults.agent_id ?? "";
|
config.defaults.agentId = defaults.agent_id ?? "";
|
||||||
config.defaults.appId = defaults.app_id ?? "";
|
config.defaults.appId = defaults.app_id ?? "";
|
||||||
config.defaults.runId = defaults.run_id ?? "";
|
config.defaults.runId = defaults.run_id ?? "";
|
||||||
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
||||||
}
|
|
||||||
|
|
||||||
// Environment variable overrides
|
const telemetry = data.telemetry ?? {};
|
||||||
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
|
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||||
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;
|
// 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 {
|
export function saveConfig(config: Mem0Config): void {
|
||||||
ensureConfigDir();
|
ensureConfigDir();
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
version: config.version,
|
version: config.version,
|
||||||
defaults: {
|
defaults: {
|
||||||
user_id: config.defaults.userId,
|
user_id: config.defaults.userId,
|
||||||
agent_id: config.defaults.agentId,
|
agent_id: config.defaults.agentId,
|
||||||
app_id: config.defaults.appId,
|
app_id: config.defaults.appId,
|
||||||
run_id: config.defaults.runId,
|
run_id: config.defaults.runId,
|
||||||
enable_graph: config.defaults.enableGraph,
|
enable_graph: config.defaults.enableGraph,
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
api_key: config.platform.apiKey,
|
api_key: config.platform.apiKey,
|
||||||
base_url: config.platform.baseUrl,
|
base_url: config.platform.baseUrl,
|
||||||
},
|
user_email: config.platform.userEmail,
|
||||||
};
|
},
|
||||||
|
telemetry: {
|
||||||
|
anonymous_id: config.telemetry.anonymousId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redactKey(key: string): string {
|
export function redactKey(key: string): string {
|
||||||
if (!key) return "(not set)";
|
if (!key) return "(not set)";
|
||||||
if (key.length <= 8) return key.slice(0, 2) + "***";
|
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||||
return key.slice(0, 4) + "..." + key.slice(-4);
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Key map from dotted config path to the config object fields. */
|
/** Key map from dotted config path to the config object fields. */
|
||||||
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||||
"platform.api_key": ["platform", "apiKey"],
|
"platform.api_key": ["platform", "apiKey"],
|
||||||
"platform.base_url": ["platform", "baseUrl"],
|
"platform.base_url": ["platform", "baseUrl"],
|
||||||
"defaults.user_id": ["defaults", "userId"],
|
"platform.user_email": ["platform", "userEmail"],
|
||||||
"defaults.agent_id": ["defaults", "agentId"],
|
"defaults.user_id": ["defaults", "userId"],
|
||||||
"defaults.app_id": ["defaults", "appId"],
|
"defaults.agent_id": ["defaults", "agentId"],
|
||||||
"defaults.run_id": ["defaults", "runId"],
|
"defaults.app_id": ["defaults", "appId"],
|
||||||
"defaults.enable_graph": ["defaults", "enableGraph"],
|
"defaults.run_id": ["defaults", "runId"],
|
||||||
|
"defaults.enable_graph": ["defaults", "enableGraph"],
|
||||||
|
// Short-form aliases
|
||||||
|
api_key: ["platform", "apiKey"],
|
||||||
|
base_url: ["platform", "baseUrl"],
|
||||||
|
user_email: ["platform", "userEmail"],
|
||||||
|
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 {
|
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
|
||||||
const mapping = KEY_MAP[dottedKey];
|
const mapping = KEY_MAP[dottedKey];
|
||||||
if (!mapping) return undefined;
|
if (!mapping) return undefined;
|
||||||
const [section, field] = mapping;
|
const [section, field] = mapping;
|
||||||
return (config[section] as unknown as Record<string, unknown>)[field];
|
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
|
export function setNestedValue(
|
||||||
const mapping = KEY_MAP[dottedKey];
|
config: Mem0Config,
|
||||||
if (!mapping) return false;
|
dottedKey: string,
|
||||||
const [section, field] = mapping;
|
value: string,
|
||||||
const obj = config[section] as unknown as Record<string, unknown>;
|
): 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];
|
const current = obj[field];
|
||||||
if (typeof current === "boolean") {
|
if (typeof current === "boolean") {
|
||||||
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||||
} else if (typeof current === "number") {
|
} else if (typeof current === "number") {
|
||||||
obj[field] = parseInt(value, 10);
|
obj[field] = Number.parseInt(value, 10);
|
||||||
} else {
|
} else {
|
||||||
obj[field] = value;
|
obj[field] = value;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -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;
|
||||||
+284
-280
@@ -4,40 +4,40 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import type { Command, Help, Option, Argument } from "commander";
|
import type { Argument, Command, Help, Option } from "commander";
|
||||||
// Colors imported from chalk directly to match Typer/Rich defaults
|
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||||
|
|
||||||
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||||
|
|
||||||
const cyanBold = chalk.cyan.bold; // option flags, command names
|
const cyanBold = chalk.cyan.bold; // option flags, command names
|
||||||
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
||||||
const yellowBold = chalk.yellow.bold; // metavar <value>
|
const yellowBold = chalk.yellow.bold; // metavar <value>
|
||||||
const yellow = chalk.yellow; // "Usage:" label
|
const yellow = chalk.yellow; // "Usage:" label
|
||||||
const bold = chalk.bold; // command name in usage
|
const bold = chalk.bold; // command name in usage
|
||||||
const dim = chalk.dim; // defaults, descriptions
|
const dim = chalk.dim; // defaults, descriptions
|
||||||
const dimBorder = chalk.dim; // panel borders
|
const dimBorder = chalk.dim; // panel borders
|
||||||
|
|
||||||
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// eslint-disable-next-line no-control-regex
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||||
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
function stripAnsi(str: string): number {
|
function stripAnsi(str: string): number {
|
||||||
return str.replace(ANSI_RE, "").length;
|
return str.replace(ANSI_RE, "").length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command display order (matches Python CLI) ──────────────────────────
|
// ── Command display order (matches Python CLI) ──────────────────────────
|
||||||
|
|
||||||
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
||||||
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||||
{
|
{
|
||||||
panel: "Memory",
|
panel: "Memory",
|
||||||
commands: ["add", "search", "get", "list", "update", "delete"],
|
commands: ["add", "search", "get", "list", "update", "delete"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
panel: "Management",
|
panel: "Management",
|
||||||
commands: ["init", "status", "import", "help", "entity", "config"],
|
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Flat order derived from COMMAND_GROUPS. */
|
/** Flat order derived from COMMAND_GROUPS. */
|
||||||
@@ -46,88 +46,88 @@ const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
|
|||||||
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
||||||
|
|
||||||
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
||||||
add: {
|
add: {
|
||||||
"--user-id": "Scope",
|
"--user-id": "Scope",
|
||||||
"--agent-id": "Scope",
|
"--agent-id": "Scope",
|
||||||
"--app-id": "Scope",
|
"--app-id": "Scope",
|
||||||
"--run-id": "Scope",
|
"--run-id": "Scope",
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
"--user-id": "Scope",
|
"--user-id": "Scope",
|
||||||
"--agent-id": "Scope",
|
"--agent-id": "Scope",
|
||||||
"--app-id": "Scope",
|
"--app-id": "Scope",
|
||||||
"--run-id": "Scope",
|
"--run-id": "Scope",
|
||||||
"--top-k": "Search",
|
"--top-k": "Search",
|
||||||
"--threshold": "Search",
|
"--threshold": "Search",
|
||||||
"--rerank": "Search",
|
"--rerank": "Search",
|
||||||
"--keyword": "Search",
|
"--keyword": "Search",
|
||||||
"--filter": "Search",
|
"--filter": "Search",
|
||||||
"--fields": "Search",
|
"--fields": "Search",
|
||||||
"--graph": "Search",
|
"--graph": "Search",
|
||||||
"--no-graph": "Search",
|
"--no-graph": "Search",
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
"--user-id": "Scope",
|
"--user-id": "Scope",
|
||||||
"--agent-id": "Scope",
|
"--agent-id": "Scope",
|
||||||
"--app-id": "Scope",
|
"--app-id": "Scope",
|
||||||
"--run-id": "Scope",
|
"--run-id": "Scope",
|
||||||
"--page": "Pagination",
|
"--page": "Pagination",
|
||||||
"--page-size": "Pagination",
|
"--page-size": "Pagination",
|
||||||
"--category": "Filters",
|
"--category": "Filters",
|
||||||
"--after": "Filters",
|
"--after": "Filters",
|
||||||
"--before": "Filters",
|
"--before": "Filters",
|
||||||
"--graph": "Filters",
|
"--graph": "Filters",
|
||||||
"--no-graph": "Filters",
|
"--no-graph": "Filters",
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
"--user-id": "Scope",
|
"--user-id": "Scope",
|
||||||
"--agent-id": "Scope",
|
"--agent-id": "Scope",
|
||||||
"--app-id": "Scope",
|
"--app-id": "Scope",
|
||||||
"--run-id": "Scope",
|
"--run-id": "Scope",
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
import: {
|
import: {
|
||||||
"--user-id": "Scope",
|
"--user-id": "Scope",
|
||||||
"--agent-id": "Scope",
|
"--agent-id": "Scope",
|
||||||
"--output": "Output",
|
"--output": "Output",
|
||||||
"--api-key": "Connection",
|
"--api-key": "Connection",
|
||||||
"--base-url": "Connection",
|
"--base-url": "Connection",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const PANEL_ORDER: string[] = [
|
const PANEL_ORDER: string[] = [
|
||||||
"Scope",
|
"Scope",
|
||||||
"Search",
|
"Search",
|
||||||
"Pagination",
|
"Pagination",
|
||||||
"Filters",
|
"Filters",
|
||||||
"Output",
|
"Output",
|
||||||
"Connection",
|
"Connection",
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Panel rendering ─────────────────────────────────────────────────────
|
// ── Panel rendering ─────────────────────────────────────────────────────
|
||||||
@@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [
|
|||||||
* ╰────────────────────────────────╯
|
* ╰────────────────────────────────╯
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function renderPanel(
|
function renderPanel(title: string, rows: string[], width: number): string {
|
||||||
title: string,
|
if (rows.length === 0) return "";
|
||||||
rows: string[],
|
|
||||||
width: number,
|
|
||||||
): string {
|
|
||||||
if (rows.length === 0) return "";
|
|
||||||
|
|
||||||
// Inner width is total width minus the two border chars
|
// Inner width is total width minus the two border chars
|
||||||
const inner = width - 2;
|
const inner = width - 2;
|
||||||
|
|
||||||
// Top border: ╭─ Title ─...─╮
|
// Top border: ╭─ Title ─...─╮
|
||||||
const titleStr = ` ${title} `;
|
const titleStr = ` ${title} `;
|
||||||
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
||||||
const topLine =
|
const topLine =
|
||||||
dimBorder("╭─") +
|
dimBorder("╭─") +
|
||||||
dimBorder(titleStr) +
|
dimBorder(titleStr) +
|
||||||
dimBorder("─".repeat(fillLen)) +
|
dimBorder("─".repeat(fillLen)) +
|
||||||
dimBorder("╮");
|
dimBorder("╮");
|
||||||
|
|
||||||
// Bottom border: ╰─...─╯
|
// Bottom border: ╰─...─╯
|
||||||
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
const bottomLine =
|
||||||
|
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||||
|
|
||||||
// Content rows
|
// Content rows
|
||||||
const contentLines = rows.map((row) => {
|
const contentLines = rows.map((row) => {
|
||||||
const visLen = stripAnsi(row);
|
const visLen = stripAnsi(row);
|
||||||
const pad = Math.max(0, inner - 1 - visLen);
|
const pad = Math.max(0, inner - 1 - visLen);
|
||||||
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
|
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return [topLine, ...contentLines, bottomLine].join("\n");
|
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Format an option term (short + long) ────────────────────────────────
|
// ── Format an option term (short + long) ────────────────────────────────
|
||||||
|
|
||||||
function formatOptionTerm(opt: Option): string {
|
function formatOptionTerm(opt: Option): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (opt.short) parts.push(opt.short);
|
if (opt.short) parts.push(opt.short);
|
||||||
if (opt.long) parts.push(opt.long);
|
if (opt.long) parts.push(opt.long);
|
||||||
let term = parts.join(", ");
|
let term = parts.join(", ");
|
||||||
|
|
||||||
// Append value placeholder for non-boolean options
|
// Append value placeholder for non-boolean options
|
||||||
if (opt.flags) {
|
if (opt.flags) {
|
||||||
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||||
if (match) {
|
if (match) {
|
||||||
term += " " + match[0];
|
term += ` ${match[0]}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return term;
|
return term;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Get the long flag name for panel lookup ─────────────────────────────
|
// ── Get the long flag name for panel lookup ─────────────────────────────
|
||||||
|
|
||||||
function getLongFlag(opt: Option): string {
|
function getLongFlag(opt: Option): string {
|
||||||
if (opt.long) return opt.long;
|
if (opt.long) return opt.long;
|
||||||
return opt.short || "";
|
return opt.short || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Format a default value ──────────────────────────────────────────────
|
// ── Format a default value ──────────────────────────────────────────────
|
||||||
|
|
||||||
function formatDefault(opt: Option): string {
|
function formatDefault(opt: Option): string {
|
||||||
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
||||||
return dim(` [default: ${opt.defaultValue}]`);
|
return dim(` [default: ${opt.defaultValue}]`);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── The main help formatter ─────────────────────────────────────────────
|
// ── The main help formatter ─────────────────────────────────────────────
|
||||||
|
|
||||||
export function richFormatHelp(cmd: Command, helper: Help): string {
|
export function richFormatHelp(cmd: Command, helper: Help): string {
|
||||||
const width = process.stdout.columns || 80;
|
const width = process.stdout.columns || 80;
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
const isRoot = !cmd.parent;
|
const isRoot = !cmd.parent;
|
||||||
|
|
||||||
// ── Usage line ──
|
// ── Usage line ──
|
||||||
const usage = helper.commandUsage(cmd);
|
const usage = helper.commandUsage(cmd);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
|
lines.push(
|
||||||
} else {
|
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||||
// Subcommands: split into command path (bold) and args (yellow)
|
);
|
||||||
const usageParts = usage.split(" ");
|
} else {
|
||||||
const cmdPath: string[] = [];
|
// Subcommands: split into command path (bold) and args (yellow)
|
||||||
const argParts: string[] = [];
|
const usageParts = usage.split(" ");
|
||||||
let pastCmd = false;
|
const cmdPath: string[] = [];
|
||||||
for (const part of usageParts) {
|
const argParts: string[] = [];
|
||||||
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
let pastCmd = false;
|
||||||
cmdPath.push(part);
|
for (const part of usageParts) {
|
||||||
} else {
|
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
||||||
pastCmd = true;
|
cmdPath.push(part);
|
||||||
argParts.push(part);
|
} else {
|
||||||
}
|
pastCmd = true;
|
||||||
}
|
argParts.push(part);
|
||||||
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
|
}
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
// ── Description ──
|
// ── Description ──
|
||||||
const desc = helper.commandDescription(cmd);
|
const desc = helper.commandDescription(cmd);
|
||||||
if (desc) {
|
if (desc) {
|
||||||
// Split multi-line descriptions (e.g., title + tagline)
|
// Split multi-line descriptions (e.g., title + tagline)
|
||||||
const descLines = desc.split("\n");
|
const descLines = desc.split("\n");
|
||||||
for (let i = 0; i < descLines.length; i++) {
|
for (let i = 0; i < descLines.length; i++) {
|
||||||
const dLine = descLines[i];
|
const dLine = descLines[i];
|
||||||
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
||||||
if (i === 0 || dLine.trim() === "") {
|
if (i === 0 || dLine.trim() === "") {
|
||||||
lines.push(` ${dLine}`);
|
lines.push(` ${dLine}`);
|
||||||
} else {
|
} else {
|
||||||
lines.push(` ${dim(dLine)}`);
|
lines.push(` ${dim(dLine)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push("");
|
lines.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Arguments panel (subcommands only) ──
|
// ── Arguments panel (subcommands only) ──
|
||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
const visibleArgs = helper.visibleArguments(cmd);
|
const visibleArgs = helper.visibleArguments(cmd);
|
||||||
if (visibleArgs.length > 0) {
|
if (visibleArgs.length > 0) {
|
||||||
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
|
const maxLen = Math.max(
|
||||||
const argRows = visibleArgs.map((a: Argument) => {
|
...visibleArgs.map((a: Argument) => a.name().length),
|
||||||
const name = cyanBold(a.name().padEnd(maxLen));
|
);
|
||||||
const description = helper.argumentDescription(a);
|
const argRows = visibleArgs.map((a: Argument) => {
|
||||||
return ` ${name} ${description}`;
|
const name = cyanBold(a.name().padEnd(maxLen));
|
||||||
});
|
const description = helper.argumentDescription(a);
|
||||||
const panel = renderPanel("Arguments", argRows, width);
|
return ` ${name} ${description}`;
|
||||||
if (panel) lines.push(panel);
|
});
|
||||||
}
|
const panel = renderPanel("Arguments", argRows, width);
|
||||||
}
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Collect options (grouped into panels for subcommands) ──
|
// ── Collect options (grouped into panels for subcommands) ──
|
||||||
const visibleOpts = helper.visibleOptions(cmd);
|
const visibleOpts = helper.visibleOptions(cmd);
|
||||||
const cmdName = cmd.name();
|
const cmdName = cmd.name();
|
||||||
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
|
const panelMap =
|
||||||
|
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||||
|
|
||||||
const grouped: Record<string, Option[]> = { Options: [] };
|
const grouped: Record<string, Option[]> = { Options: [] };
|
||||||
for (const panelName of PANEL_ORDER) {
|
for (const panelName of PANEL_ORDER) {
|
||||||
grouped[panelName] = [];
|
grouped[panelName] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const opt of visibleOpts) {
|
for (const opt of visibleOpts) {
|
||||||
const flag = getLongFlag(opt);
|
const flag = getLongFlag(opt);
|
||||||
const panel = panelMap[flag];
|
const panel = panelMap[flag];
|
||||||
if (panel && PANEL_ORDER.includes(panel)) {
|
if (panel && PANEL_ORDER.includes(panel)) {
|
||||||
grouped[panel].push(opt);
|
grouped[panel].push(opt);
|
||||||
} else {
|
} else {
|
||||||
grouped["Options"].push(opt);
|
grouped.Options.push(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Collect commands ──
|
// ── Collect commands ──
|
||||||
const visibleCmds = helper.visibleCommands(cmd);
|
const visibleCmds = helper.visibleCommands(cmd);
|
||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||||
if (grouped["Options"].length > 0) {
|
if (grouped.Options.length > 0) {
|
||||||
const optRows = formatOptionRows(grouped["Options"]);
|
const optRows = formatOptionRows(grouped.Options);
|
||||||
const panel = renderPanel("Options", optRows, width);
|
const panel = renderPanel("Options", optRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
if (visibleCmds.length > 0) {
|
if (visibleCmds.length > 0) {
|
||||||
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
||||||
for (const group of COMMAND_GROUPS) {
|
for (const group of COMMAND_GROUPS) {
|
||||||
const groupCmds = group.commands
|
const groupCmds = group.commands
|
||||||
.map((name) => cmdMap.get(name))
|
.map((name) => cmdMap.get(name))
|
||||||
.filter((c): c is Command => c !== undefined);
|
.filter((c): c is Command => c !== undefined);
|
||||||
if (groupCmds.length === 0) continue;
|
if (groupCmds.length === 0) continue;
|
||||||
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
||||||
const cmdRows = groupCmds.map((c) => {
|
const cmdRows = groupCmds.map((c) => {
|
||||||
const name = cyanBold(c.name().padEnd(maxLen));
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
const description = helper.subcommandDescription(c);
|
const description = helper.subcommandDescription(c);
|
||||||
return ` ${name} ${description}`;
|
return ` ${name} ${description}`;
|
||||||
});
|
});
|
||||||
const panel = renderPanel(group.panel, cmdRows, width);
|
const panel = renderPanel(group.panel, cmdRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
||||||
const panelSequence = ["Options", ...PANEL_ORDER];
|
const panelSequence = ["Options", ...PANEL_ORDER];
|
||||||
for (const panelName of panelSequence) {
|
for (const panelName of panelSequence) {
|
||||||
const opts = grouped[panelName];
|
const opts = grouped[panelName];
|
||||||
if (opts && opts.length > 0) {
|
if (opts && opts.length > 0) {
|
||||||
const optRows = formatOptionRows(opts);
|
const optRows = formatOptionRows(opts);
|
||||||
const panel = renderPanel(panelName, optRows, width);
|
const panel = renderPanel(panelName, optRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
||||||
if (visibleCmds.length > 0) {
|
if (visibleCmds.length > 0) {
|
||||||
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
||||||
const cmdRows = visibleCmds.map((c) => {
|
const cmdRows = visibleCmds.map((c) => {
|
||||||
const name = cyanBold(c.name().padEnd(maxLen));
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
const description = helper.subcommandDescription(c);
|
const description = helper.subcommandDescription(c);
|
||||||
return ` ${name} ${description}`;
|
return ` ${name} ${description}`;
|
||||||
});
|
});
|
||||||
const panel = renderPanel("Commands", cmdRows, width);
|
const panel = renderPanel("Commands", cmdRows, width);
|
||||||
if (panel) lines.push(panel);
|
if (panel) lines.push(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push("");
|
lines.push("");
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Format option rows with aligned columns ─────────────────────────────
|
// ── Format option rows with aligned columns ─────────────────────────────
|
||||||
|
|
||||||
function formatOptionRows(opts: Option[]): string[] {
|
function formatOptionRows(opts: Option[]): string[] {
|
||||||
const terms = opts.map((o) => formatOptionTerm(o));
|
const terms = opts.map((o) => formatOptionTerm(o));
|
||||||
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
||||||
|
|
||||||
return opts.map((opt, i) => {
|
return opts.map((opt, i) => {
|
||||||
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
||||||
const desc = opt.description || "";
|
const desc = opt.description || "";
|
||||||
const def = formatDefault(opt);
|
const def = formatDefault(opt);
|
||||||
return ` ${term} ${desc}${def}`;
|
return ` ${term} ${desc}${def}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
||||||
|
|
||||||
function sortCommands(cmds: Command[]): Command[] {
|
function sortCommands(cmds: Command[]): Command[] {
|
||||||
return [...cmds].sort((a, b) => {
|
return [...cmds].sort((a, b) => {
|
||||||
const ai = COMMAND_ORDER.indexOf(a.name());
|
const ai = COMMAND_ORDER.indexOf(a.name());
|
||||||
const bi = COMMAND_ORDER.indexOf(b.name());
|
const bi = COMMAND_ORDER.indexOf(b.name());
|
||||||
// Unknown commands go to end, preserving original order
|
// Unknown commands go to end, preserving original order
|
||||||
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
||||||
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
||||||
return aIdx - bIdx;
|
return aIdx - bIdx;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+687
-393
File diff suppressed because it is too large
Load Diff
+328
-177
@@ -2,229 +2,380 @@
|
|||||||
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Table from "cli-table3";
|
|
||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
import { colors, sym } from "./branding.js";
|
import { colors, sym } from "./branding.js";
|
||||||
|
|
||||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
|
|
||||||
function formatDate(dtStr?: string): string | undefined {
|
function formatDate(dtStr?: string): string | undefined {
|
||||||
if (!dtStr) return undefined;
|
if (!dtStr) return undefined;
|
||||||
try {
|
try {
|
||||||
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
||||||
return dt.toISOString().slice(0, 10);
|
return dt.toISOString().slice(0, 10);
|
||||||
} catch {
|
} catch {
|
||||||
return dtStr?.slice(0, 10);
|
return dtStr?.slice(0, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemoriesText(
|
export function formatMemoriesText(
|
||||||
memories: Record<string, unknown>[],
|
memories: Record<string, unknown>[],
|
||||||
title = "memories",
|
title = "memories",
|
||||||
): void {
|
): void {
|
||||||
const count = memories.length;
|
const count = memories.length;
|
||||||
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
||||||
|
|
||||||
for (let i = 0; i < memories.length; i++) {
|
for (let i = 0; i < memories.length; i++) {
|
||||||
const mem = memories[i];
|
const mem = memories[i];
|
||||||
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
||||||
const score = mem.score as number | undefined;
|
const score = mem.score as number | undefined;
|
||||||
const created = formatDate(mem.created_at as string | undefined);
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
let category: string | undefined;
|
let category: string | undefined;
|
||||||
const cats = mem.categories;
|
const cats = mem.categories;
|
||||||
if (Array.isArray(cats)) {
|
if (Array.isArray(cats)) {
|
||||||
category = cats[0] as string | undefined;
|
category = cats[0] as string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` ${i + 1}. ${memoryText}`);
|
console.log(` ${i + 1}. ${memoryText}`);
|
||||||
|
|
||||||
const details: string[] = [];
|
const details: string[] = [];
|
||||||
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
||||||
if (memId) details.push(`ID: ${memId}`);
|
if (memId) details.push(`ID: ${memId}`);
|
||||||
if (created) details.push(`Created: ${created}`);
|
if (created) details.push(`Created: ${created}`);
|
||||||
if (category) details.push(`Category: ${category}`);
|
if (category) details.push(`Category: ${category}`);
|
||||||
|
|
||||||
if (details.length > 0) {
|
if (details.length > 0) {
|
||||||
console.log(` ${dim(details.join(" · "))}`);
|
console.log(` ${dim(details.join(" · "))}`);
|
||||||
}
|
}
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
|
export function formatMemoriesTable(
|
||||||
const table = new Table({
|
memories: Record<string, unknown>[],
|
||||||
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
|
opts: { showScore?: boolean } = {},
|
||||||
colWidths: [12, 52, 16, 14],
|
): void {
|
||||||
wordWrap: true,
|
const head = opts.showScore
|
||||||
style: { head: [], border: [] },
|
? [
|
||||||
});
|
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) {
|
for (const mem of memories) {
|
||||||
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
const memId = (mem.id as string) ?? "";
|
||||||
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
if (memoryText.length > 60) {
|
if (memoryText.length > 60) {
|
||||||
memoryText = memoryText.slice(0, 57) + "...";
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||||
}
|
}
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
const cat =
|
const cat =
|
||||||
Array.isArray(categories) && categories.length > 0
|
Array.isArray(categories) && categories.length > 0
|
||||||
? (categories[0] as string)
|
? categories.length > 1
|
||||||
: "—";
|
? `${categories[0]} (+${categories.length - 1})`
|
||||||
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
: (categories[0] as string)
|
||||||
table.push([dim(memId), memoryText, cat, created]);
|
: "—";
|
||||||
}
|
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();
|
||||||
console.log(table.toString());
|
console.log(table.toString());
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatJson(data: unknown): void {
|
export function formatJson(data: unknown): void {
|
||||||
console.log(JSON.stringify(data, null, 2));
|
console.log(JSON.stringify(data, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSingleMemory(
|
export function formatSingleMemory(
|
||||||
mem: Record<string, unknown>,
|
mem: Record<string, unknown>,
|
||||||
output = "text",
|
output = "text",
|
||||||
): void {
|
): void {
|
||||||
if (output === "json") {
|
if (output === "json") {
|
||||||
formatJson(mem);
|
formatJson(mem);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
const memId = (mem.id ?? "") as string;
|
const memId = (mem.id ?? "") as string;
|
||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
lines.push(` ${memoryText}`);
|
lines.push(` ${memoryText}`);
|
||||||
lines.push("");
|
lines.push("");
|
||||||
|
|
||||||
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
||||||
const created = formatDate(mem.created_at as string | undefined);
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
||||||
const updated = formatDate(mem.updated_at as string | undefined);
|
const updated = formatDate(mem.updated_at as string | undefined);
|
||||||
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
||||||
const meta = mem.metadata;
|
const meta = mem.metadata;
|
||||||
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||||
const categories = mem.categories;
|
const categories = mem.categories;
|
||||||
if (categories) {
|
if (categories) {
|
||||||
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
|
const catStr = Array.isArray(categories)
|
||||||
lines.push(` ${dim("Categories:")} ${catStr}`);
|
? categories.join(", ")
|
||||||
}
|
: String(categories);
|
||||||
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
const content = lines.join("\n");
|
const content = lines.join("\n");
|
||||||
console.log();
|
console.log();
|
||||||
console.log(
|
console.log(
|
||||||
boxen(content, {
|
boxen(content, {
|
||||||
title: brand("Memory"),
|
title: brand("Memory"),
|
||||||
titleAlignment: "left",
|
titleAlignment: "left",
|
||||||
borderColor: "magenta",
|
borderColor: "magenta",
|
||||||
padding: 1,
|
padding: 1,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatAddResult(
|
export function formatAddResult(
|
||||||
result: Record<string, unknown> | Record<string, unknown>[],
|
result: Record<string, unknown> | Record<string, unknown>[],
|
||||||
output = "text",
|
output = "text",
|
||||||
): void {
|
): void {
|
||||||
if (output === "json") {
|
if (output === "json") {
|
||||||
formatJson(result);
|
formatJson(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (output === "quiet") return;
|
if (output === "quiet") return;
|
||||||
|
|
||||||
const results: Record<string, unknown>[] = Array.isArray(result)
|
const results: Record<string, unknown>[] = Array.isArray(result)
|
||||||
? result
|
? result
|
||||||
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||||
|
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
console.log(` ${dim("No memories extracted.")}`);
|
console.log(` ${dim("No memories extracted.")}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
for (const r of results) {
|
const seenPendingEvents = new Set<string>();
|
||||||
// Detect async PENDING response
|
for (const r of results) {
|
||||||
if (r.status === "PENDING") {
|
// Detect async PENDING response
|
||||||
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
|
if (r.status === "PENDING") {
|
||||||
const icon = accent(sym("⧗", "..."));
|
const eventId = (r.event_id as string) ?? "";
|
||||||
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
|
// Deduplicate PENDING entries with the same event_id
|
||||||
if (eventId) parts.push(dim(`(event ${eventId})`));
|
if (eventId && seenPendingEvents.has(eventId)) continue;
|
||||||
console.log(parts.join(" "));
|
if (eventId) seenPendingEvents.add(eventId);
|
||||||
continue;
|
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 event = (r.event ?? "ADD") as string;
|
||||||
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") 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);
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
let icon: string;
|
let icon: string;
|
||||||
let label: string;
|
let label: string;
|
||||||
if (event === "ADD") {
|
if (event === "ADD") {
|
||||||
icon = success("+");
|
icon = success("+");
|
||||||
label = "Added";
|
label = "Added";
|
||||||
} else if (event === "UPDATE") {
|
} else if (event === "UPDATE") {
|
||||||
icon = accent("~");
|
icon = accent("~");
|
||||||
label = "Updated";
|
label = "Updated";
|
||||||
} else if (event === "DELETE") {
|
} else if (event === "DELETE") {
|
||||||
icon = errorColor("-");
|
icon = errorColor("-");
|
||||||
label = "Deleted";
|
label = "Deleted";
|
||||||
} else if (event === "NOOP") {
|
} else if (event === "NOOP") {
|
||||||
icon = dim("·");
|
icon = dim("·");
|
||||||
label = "No change";
|
label = "No change";
|
||||||
} else {
|
} else {
|
||||||
icon = dim("?");
|
icon = dim("?");
|
||||||
label = event;
|
label = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
||||||
if (memory) parts.push(memory);
|
if (memory) parts.push(memory);
|
||||||
if (memId) parts.push(dim(`(${memId})`));
|
if (memId) parts.push(dim(`(${memId})`));
|
||||||
console.log(parts.join(" "));
|
console.log(parts.join(" "));
|
||||||
}
|
}
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatJsonEnvelope(opts: {
|
export function formatJsonEnvelope(opts: {
|
||||||
command: string;
|
command: string;
|
||||||
data: unknown;
|
data: unknown;
|
||||||
durationMs?: number;
|
durationMs?: number;
|
||||||
scope?: Record<string, string | undefined>;
|
scope?: Record<string, string | undefined>;
|
||||||
count?: number;
|
count?: number;
|
||||||
status?: string;
|
status?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}): void {
|
}): void {
|
||||||
const envelope: Record<string, unknown> = {
|
const envelope: Record<string, unknown> = {
|
||||||
status: opts.status ?? "success",
|
status: opts.status ?? "success",
|
||||||
command: opts.command,
|
command: opts.command,
|
||||||
};
|
};
|
||||||
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||||
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
||||||
if (opts.count !== undefined) envelope.count = opts.count;
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
if (opts.error) envelope.error = opts.error;
|
if (opts.error) envelope.error = opts.error;
|
||||||
envelope.data = opts.data;
|
envelope.data = opts.data;
|
||||||
console.log(JSON.stringify(envelope, null, 2));
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key in obj) result[key] = obj[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||||
|
if (data === null || data === undefined) return data;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "add": {
|
||||||
|
const items = Array.isArray(data) ? data : [data];
|
||||||
|
return items.map((item) => {
|
||||||
|
const r = item as Record<string, unknown>;
|
||||||
|
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
|
||||||
|
return pick(r, ["id", "memory", "event"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "get": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, [
|
||||||
|
"id",
|
||||||
|
"memory",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"categories",
|
||||||
|
"metadata",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
case "update": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, ["id", "memory"]);
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
case "delete-all":
|
||||||
|
case "entity delete":
|
||||||
|
return data;
|
||||||
|
case "entity list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) => ({
|
||||||
|
name: (r.name ?? r.id) as string,
|
||||||
|
...pick(r, ["type", "count"]),
|
||||||
|
}));
|
||||||
|
case "event list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
|
||||||
|
);
|
||||||
|
case "event status": {
|
||||||
|
const ev = data as Record<string, unknown>;
|
||||||
|
const rawResults =
|
||||||
|
(ev.results as Record<string, unknown>[] | undefined) ?? [];
|
||||||
|
const sanitizedResults = rawResults.map((r) => {
|
||||||
|
const nested = r.data as Record<string, unknown> | undefined;
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
event: r.event,
|
||||||
|
user_id: r.user_id,
|
||||||
|
memory: nested?.memory ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...pick(ev, [
|
||||||
|
"id",
|
||||||
|
"event_type",
|
||||||
|
"status",
|
||||||
|
"latency",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]),
|
||||||
|
results: sanitizedResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAgentEnvelope(opts: {
|
||||||
|
command: string;
|
||||||
|
data: unknown;
|
||||||
|
durationMs?: number;
|
||||||
|
scope?: Record<string, string | undefined>;
|
||||||
|
count?: number;
|
||||||
|
}): void {
|
||||||
|
const envelope: Record<string, unknown> = {
|
||||||
|
status: "success",
|
||||||
|
command: opts.command,
|
||||||
|
};
|
||||||
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||||
|
if (opts.scope) {
|
||||||
|
const filtered = Object.fromEntries(
|
||||||
|
Object.entries(opts.scope).filter(([, v]) => v),
|
||||||
|
);
|
||||||
|
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
|
||||||
|
}
|
||||||
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
|
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||||
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printResultSummary(opts: {
|
export function printResultSummary(opts: {
|
||||||
count: number;
|
count: number;
|
||||||
durationSecs?: number;
|
durationSecs?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
scopeIds?: Record<string, string | undefined>;
|
scopeIds?: Record<string, string | undefined>;
|
||||||
}): void {
|
}): void {
|
||||||
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
||||||
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
||||||
if (opts.scopeIds) {
|
if (opts.scopeIds) {
|
||||||
const scopeParts = Object.entries(opts.scopeIds)
|
const scopeParts = Object.entries(opts.scopeIds)
|
||||||
.filter(([, v]) => v)
|
.filter(([, v]) => v)
|
||||||
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
|
.map(([k, v]) => `${k}=${v}`);
|
||||||
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||||
}
|
}
|
||||||
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
if (opts.durationSecs !== undefined)
|
||||||
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||||
|
|
||||||
console.log(` ${dim(parts.join(" · "))}`);
|
console.log(` ${dim(parts.join(" · "))}`);
|
||||||
console.log();
|
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,153 @@
|
|||||||
|
/**
|
||||||
|
* CLI telemetry — anonymous usage tracking via PostHog.
|
||||||
|
*
|
||||||
|
* Sends fire-and-forget events by spawning a detached child process
|
||||||
|
* (telemetry-sender.cjs). The parent CLI process exits immediately;
|
||||||
|
* the child handles email resolution, caching, and the HTTP POST.
|
||||||
|
*
|
||||||
|
* Disable with: MEM0_TELEMETRY=false
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { createHash, randomUUID } from "node:crypto";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { CONFIG_FILE, loadConfig, saveConfig } from "./config.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
|
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||||
|
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
|
||||||
|
|
||||||
|
function isTelemetryEnabled(): boolean {
|
||||||
|
try {
|
||||||
|
return process.env.MEM0_TELEMETRY !== "false";
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a persistent per-machine anonymous ID, generating one if needed.
|
||||||
|
*
|
||||||
|
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
||||||
|
* repeat runs on the same machine share one PostHog identity instead of
|
||||||
|
* collapsing into a single shared fallback string.
|
||||||
|
*/
|
||||||
|
function getOrCreateAnonymousId(): string {
|
||||||
|
const config = loadConfig();
|
||||||
|
if (config.telemetry.anonymousId) {
|
||||||
|
return config.telemetry.anonymousId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||||
|
config.telemetry.anonymousId = newId;
|
||||||
|
try {
|
||||||
|
saveConfig(config);
|
||||||
|
} catch {
|
||||||
|
/* ignore persistence failure — still return the generated ID */
|
||||||
|
}
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a stable anonymous identifier for the current user.
|
||||||
|
*
|
||||||
|
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
||||||
|
* persistent per-machine anonymous ID.
|
||||||
|
*/
|
||||||
|
function getDistinctId(): string {
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
if (config.platform.userEmail) {
|
||||||
|
return config.platform.userEmail;
|
||||||
|
}
|
||||||
|
if (config.platform.apiKey) {
|
||||||
|
return createHash("md5").update(config.platform.apiKey).digest("hex");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return getOrCreateAnonymousId();
|
||||||
|
} catch {
|
||||||
|
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a PostHog event (non-blocking, returns void, never throws).
|
||||||
|
* Spawns telemetry-sender.cjs as a detached subprocess.
|
||||||
|
*
|
||||||
|
* When `preResolvedEmail` is provided (e.g. from an upfront ping
|
||||||
|
* validation), it is used directly as the PostHog distinct ID and the
|
||||||
|
* subprocess skips its own `/v1/ping/` call.
|
||||||
|
*/
|
||||||
|
export function captureEvent(
|
||||||
|
eventName: string,
|
||||||
|
properties: Record<string, unknown> = {},
|
||||||
|
preResolvedEmail?: string,
|
||||||
|
): void {
|
||||||
|
if (!isTelemetryEnabled()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = loadConfig();
|
||||||
|
const distinctId = preResolvedEmail || getDistinctId();
|
||||||
|
|
||||||
|
// Detect anonymous → identified transition. If a stored anonymous_id
|
||||||
|
// exists and we just resolved to a real identity, fire a one-shot
|
||||||
|
// $identify event so PostHog stitches the pre-signup history onto
|
||||||
|
// the authenticated profile. Clear the stored id so we don't re-alias.
|
||||||
|
let anonIdToAlias: string | null = null;
|
||||||
|
if (
|
||||||
|
distinctId &&
|
||||||
|
!distinctId.startsWith("cli-anon-") &&
|
||||||
|
config.telemetry.anonymousId
|
||||||
|
) {
|
||||||
|
anonIdToAlias = config.telemetry.anonymousId;
|
||||||
|
config.telemetry.anonymousId = "";
|
||||||
|
try {
|
||||||
|
saveConfig(config);
|
||||||
|
} catch {
|
||||||
|
/* ignore — alias may double-fire next run, harmless */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
api_key: POSTHOG_API_KEY,
|
||||||
|
distinct_id: distinctId,
|
||||||
|
event: eventName,
|
||||||
|
properties: {
|
||||||
|
source: "CLI",
|
||||||
|
language: "node",
|
||||||
|
cli_version: CLI_VERSION,
|
||||||
|
node_version: process.version,
|
||||||
|
os: process.platform,
|
||||||
|
...properties,
|
||||||
|
$process_person_profile: false,
|
||||||
|
$lib: "posthog-node",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
payload,
|
||||||
|
posthogHost: POSTHOG_HOST,
|
||||||
|
needsEmail: !distinctId || !distinctId.includes("@"),
|
||||||
|
mem0ApiKey: config.platform.apiKey || "",
|
||||||
|
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
|
||||||
|
configPath: CONFIG_FILE,
|
||||||
|
anonDistinctIdToAlias: anonIdToAlias,
|
||||||
|
};
|
||||||
|
|
||||||
|
const child = spawn(
|
||||||
|
process.execPath,
|
||||||
|
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||||
|
{ detached: true, stdio: "ignore" },
|
||||||
|
);
|
||||||
|
child.unref();
|
||||||
|
} catch {
|
||||||
|
/* silently swallow */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,129 @@
|
|||||||
|
/**
|
||||||
|
* Standalone telemetry sender — runs as a detached child process.
|
||||||
|
*
|
||||||
|
* Usage: node telemetry-sender.cjs '<json context>'
|
||||||
|
*
|
||||||
|
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||||
|
* of the parent CLI process. It:
|
||||||
|
*
|
||||||
|
* 1. Resolves the user's email via /v1/ping/ if not already cached
|
||||||
|
* 2. Caches the email in ~/.mem0/config.json for future runs
|
||||||
|
* 3. Sends the PostHog event
|
||||||
|
*
|
||||||
|
* All errors are silently swallowed — this process must never produce output
|
||||||
|
* or affect the user experience.
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const https = require("https");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
function httpsRequest(url, method, headers, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const u = new URL(url);
|
||||||
|
const opts = {
|
||||||
|
hostname: u.hostname,
|
||||||
|
path: u.pathname + u.search,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
timeout: 10000,
|
||||||
|
};
|
||||||
|
const req = https.request(opts, (res) => {
|
||||||
|
let data = "";
|
||||||
|
res.on("data", (chunk) => (data += chunk));
|
||||||
|
res.on("end", () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(data));
|
||||||
|
} catch {
|
||||||
|
resolve({});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.on("timeout", () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("timeout"));
|
||||||
|
});
|
||||||
|
if (body) {
|
||||||
|
req.end(body);
|
||||||
|
} else {
|
||||||
|
req.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveAndCacheEmail(ctx, payload) {
|
||||||
|
try {
|
||||||
|
const pingUrl = ctx.mem0BaseUrl.replace(/\/+$/, "") + "/v1/ping/";
|
||||||
|
const data = await httpsRequest(pingUrl, "GET", {
|
||||||
|
Authorization: "Token " + ctx.mem0ApiKey,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
});
|
||||||
|
if (data.user_email) {
|
||||||
|
payload.distinct_id = data.user_email;
|
||||||
|
cacheEmail(ctx.configPath, data.user_email);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silently swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheEmail(configPath, email) {
|
||||||
|
if (!configPath) return;
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(configPath, "utf-8");
|
||||||
|
const cfg = JSON.parse(raw);
|
||||||
|
if (!cfg.platform) cfg.platform = {};
|
||||||
|
cfg.platform.user_email = email;
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
||||||
|
} catch {
|
||||||
|
// silently swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPosthogEvent(posthogHost, payload) {
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify(payload);
|
||||||
|
await httpsRequest(posthogHost, "POST", {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": Buffer.byteLength(body),
|
||||||
|
}, body);
|
||||||
|
} catch {
|
||||||
|
// silently swallow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendIdentifyEvent(ctx, payload, anonId) {
|
||||||
|
const identifyPayload = {
|
||||||
|
api_key: payload.api_key,
|
||||||
|
event: "$identify",
|
||||||
|
distinct_id: payload.distinct_id,
|
||||||
|
properties: {
|
||||||
|
$anon_distinct_id: anonId,
|
||||||
|
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const ctx = JSON.parse(process.argv[2]);
|
||||||
|
const payload = ctx.payload;
|
||||||
|
|
||||||
|
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||||
|
await resolveAndCacheEmail(ctx, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire $identify *after* email resolution so PostHog links the stored
|
||||||
|
// anonymous id directly to the final identity (email, not the api-key
|
||||||
|
// hash). The regular event is sent next so it lands under the merged
|
||||||
|
// profile.
|
||||||
|
if (ctx.anonDistinctIdToAlias) {
|
||||||
|
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendPosthogEvent(ctx.posthogHost, payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(() => {});
|
||||||
@@ -80,19 +80,19 @@ describe("printWarning", () => {
|
|||||||
describe("printInfo", () => {
|
describe("printInfo", () => {
|
||||||
it("prints info message", () => {
|
it("prints info message", () => {
|
||||||
printInfo("Important note");
|
printInfo("Important note");
|
||||||
expect(output).toContain("Important note");
|
expect(errOutput).toContain("Important note");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("printScope", () => {
|
describe("printScope", () => {
|
||||||
it("prints scope when IDs present", () => {
|
it("prints scope when IDs present", () => {
|
||||||
printScope({ user_id: "alice", agent_id: "bot" });
|
printScope({ user_id: "alice", agent_id: "bot" });
|
||||||
expect(output).toContain("alice");
|
expect(errOutput).toContain("alice");
|
||||||
expect(output).toContain("bot");
|
expect(errOutput).toContain("bot");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prints nothing when no IDs", () => {
|
it("prints nothing when no IDs", () => {
|
||||||
printScope({});
|
printScope({});
|
||||||
expect(output).toBe("");
|
expect(errOutput).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,13 +44,6 @@ describe("CLI Integration — help and version", () => {
|
|||||||
expect(result.stdout).toContain("search");
|
expect(result.stdout).toContain("search");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows version with --version", () => {
|
|
||||||
const result = run(["--version"]);
|
|
||||||
expect(result.exitCode).toBe(0);
|
|
||||||
expect(result.stdout).toContain("0.1.0");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it("help --json produces valid JSON", () => {
|
it("help --json produces valid JSON", () => {
|
||||||
const result = run(["help", "--json"]);
|
const result = run(["help", "--json"]);
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { createMockBackend } from "./setup.js";
|
import { createMockBackend } from "./setup.js";
|
||||||
import type { Backend } from "../src/backend/base.js";
|
import type { Backend } from "../src/backend/base.js";
|
||||||
|
import { setAgentMode } from "../src/state.js";
|
||||||
|
|
||||||
let mockBackend: Backend;
|
let mockBackend: Backend;
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ import { afterEach } from "vitest";
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
console.log = originalLog;
|
console.log = originalLog;
|
||||||
console.error = originalError;
|
console.error = originalError;
|
||||||
|
setAgentMode(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cmdAdd", () => {
|
describe("cmdAdd", () => {
|
||||||
@@ -84,6 +86,59 @@ describe("cmdAdd", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cmdAdd deduplicates PENDING", () => {
|
||||||
|
const DUPLICATE_PENDING = {
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("text shows one pending block", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("json shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("agent shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
expect(data.count).toBe(1);
|
||||||
|
expect(data.data).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("cmdSearch", () => {
|
describe("cmdSearch", () => {
|
||||||
it("searches and shows results in text mode", async () => {
|
it("searches and shows results in text mode", async () => {
|
||||||
const { cmdSearch } = await import("../src/commands/memory.js");
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
@@ -125,7 +180,7 @@ describe("cmdSearch", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +221,7 @@ describe("cmdList", () => {
|
|||||||
enableGraph: false,
|
enableGraph: false,
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output).toContain("No memories found");
|
expect(errOutput).toContain("No memories found");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -198,13 +253,6 @@ describe("cmdDeleteAll", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cmdVersion", () => {
|
|
||||||
it("shows version", async () => {
|
|
||||||
const { cmdVersion } = await import("../src/commands/utils.js");
|
|
||||||
cmdVersion();
|
|
||||||
expect(output).toContain("0.1.0");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("cmdEntitiesList", () => {
|
describe("cmdEntitiesList", () => {
|
||||||
it("lists users in table mode", async () => {
|
it("lists users in table mode", async () => {
|
||||||
@@ -219,3 +267,168 @@ describe("cmdEntitiesList", () => {
|
|||||||
expect(output).toContain("alice");
|
expect(output).toContain("alice");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("cmdEventList", () => {
|
||||||
|
it("lists events in table mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect(output).toContain("evt-abc-");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists events in json mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("evt-def-456-ghi-789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty message when no events", async () => {
|
||||||
|
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect((output + errOutput).toLowerCase()).toContain("no events");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdEventStatus", () => {
|
||||||
|
it("shows event details in text mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows event details in json mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agent mode", () => {
|
||||||
|
it("cmdAdd outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test preference", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("add");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.scope).toMatchObject({ user_id: "alice" });
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdSearch outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("search");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
const keys = Object.keys(parsed.data[0]);
|
||||||
|
expect(keys).toContain("id");
|
||||||
|
expect(keys).toContain("memory");
|
||||||
|
expect(keys).toContain("score");
|
||||||
|
expect(keys).toContain("created_at");
|
||||||
|
expect(keys).toContain("categories");
|
||||||
|
expect(keys).not.toContain("user_id");
|
||||||
|
expect(keys).not.toContain("agent_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdGet outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("get");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
|
||||||
|
expect(Object.keys(parsed.data)).not.toContain("user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdUpdate outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||||
|
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("update");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdDelete outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("delete");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(
|
||||||
|
["created_at", "event_type", "id", "latency", "status"],
|
||||||
|
);
|
||||||
|
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventStatus outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event status");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
|
||||||
|
expect(parsed.data.results[0]).toHaveProperty("memory");
|
||||||
|
expect(parsed.data.results[0]).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
formatSingleMemory,
|
formatSingleMemory,
|
||||||
formatAddResult,
|
formatAddResult,
|
||||||
printResultSummary,
|
printResultSummary,
|
||||||
|
sanitizeAgentData,
|
||||||
} from "../src/output.js";
|
} from "../src/output.js";
|
||||||
|
|
||||||
let output: string;
|
let output: string;
|
||||||
@@ -98,6 +99,18 @@ describe("formatAddResult", () => {
|
|||||||
});
|
});
|
||||||
expect(output).toContain("Queued");
|
expect(output).toContain("Queued");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("deduplicates PENDING entries with same event_id", () => {
|
||||||
|
formatAddResult({
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Should show only one PENDING block despite two entries with same event_id
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("printResultSummary", () => {
|
describe("printResultSummary", () => {
|
||||||
@@ -113,3 +126,73 @@ describe("printResultSummary", () => {
|
|||||||
expect(output).not.toContain("results");
|
expect(output).not.toContain("results");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("sanitizeAgentData", () => {
|
||||||
|
it("projects add results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through PENDING add items", () => {
|
||||||
|
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects search results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("user_id");
|
||||||
|
expect(result[0]).toHaveProperty("score");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects list results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
|
||||||
|
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects get result", () => {
|
||||||
|
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
|
||||||
|
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
|
||||||
|
expect(result).not.toHaveProperty("user_id");
|
||||||
|
expect(result).toHaveProperty("metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects update result", () => {
|
||||||
|
const raw = { id: "abc", memory: "updated", extra: "noise" };
|
||||||
|
const result = sanitizeAgentData("update", raw);
|
||||||
|
expect(result).toEqual({ id: "abc", memory: "updated" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects event list results", () => {
|
||||||
|
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
|
||||||
|
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("updated_at");
|
||||||
|
expect(result[0]).not.toHaveProperty("graph_status");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flattens event status results", () => {
|
||||||
|
const raw = {
|
||||||
|
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
|
||||||
|
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
|
||||||
|
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
|
||||||
|
};
|
||||||
|
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
|
||||||
|
const firstResult = (result.results as Record<string, unknown>[])[0];
|
||||||
|
expect(firstResult).toHaveProperty("memory", "dark mode");
|
||||||
|
expect(firstResult).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through status/config/import commands unchanged", () => {
|
||||||
|
const data = { key: "value", other: "stuff" };
|
||||||
|
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
|
||||||
|
expect(sanitizeAgentData(cmd, data)).toEqual(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null data", () => {
|
||||||
|
expect(sanitizeAgentData("add", null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -71,5 +71,42 @@ export function createMockBackend(): Backend {
|
|||||||
{ name: "alice", count: 5 },
|
{ name: "alice", count: 5 },
|
||||||
{ name: "bob", count: 3 },
|
{ name: "bob", count: 3 },
|
||||||
]),
|
]),
|
||||||
|
listEvents: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: null,
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evt-def-456-ghi-789",
|
||||||
|
event_type: "SEARCH",
|
||||||
|
status: "PENDING",
|
||||||
|
graph_status: null,
|
||||||
|
latency: null,
|
||||||
|
created_at: "2026-04-01T10:01:00Z",
|
||||||
|
updated_at: "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
getEvent: vi.fn().mockResolvedValue({
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: "SUCCEEDED",
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: "mem-abc-123",
|
||||||
|
event: "ADD",
|
||||||
|
user_id: "alice",
|
||||||
|
data: { memory: "User prefers dark mode" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
},
|
||||||
|
});
|
||||||
+315
-5
@@ -1,6 +1,12 @@
|
|||||||
# mem0 CLI
|
# mem0 CLI (Python)
|
||||||
|
|
||||||
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents.
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python **3.10+**
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -18,22 +24,326 @@ pip install mem0-cli
|
|||||||
|
|
||||||
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
|
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
|
||||||
|
|
||||||
## Quick Start
|
## Quick start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Set up your configuration
|
# Interactive setup wizard
|
||||||
mem0 init
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
# Add a memory
|
# Add a memory
|
||||||
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
# Search memories
|
# Search memories
|
||||||
mem0 search "What are Alice's preferences?" --user-id alice
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
# List all memories
|
# List all memories for a user
|
||||||
mem0 list --user-id alice
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/python
|
||||||
|
python -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Run during development
|
||||||
|
python -m mem0_cli --help
|
||||||
|
mem0 add "test memory" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
1. Update `version` in `pyproject.toml`
|
||||||
|
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
|
||||||
|
|
||||||
|
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mem0-cli"
|
name = "mem0-cli"
|
||||||
version = "0.1.0"
|
version = "0.2.3"
|
||||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.2.3"
|
||||||
|
|||||||
+255
-30
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import json as _json
|
import json as _json
|
||||||
|
import os
|
||||||
|
import stat as _stat_mod
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from mem0_cli import __version__
|
from mem0_cli import __version__
|
||||||
from mem0_cli.branding import BRAND_COLOR, print_error
|
from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
@@ -43,7 +46,52 @@ entity_app = typer.Typer(
|
|||||||
no_args_is_help=True,
|
no_args_is_help=True,
|
||||||
rich_markup_mode="rich",
|
rich_markup_mode="rich",
|
||||||
)
|
)
|
||||||
# entity_app registered after Memory commands to control panel ordering
|
|
||||||
|
event_app = typer.Typer(
|
||||||
|
name="event",
|
||||||
|
help="Inspect background processing events.",
|
||||||
|
no_args_is_help=True,
|
||||||
|
rich_markup_mode="rich",
|
||||||
|
)
|
||||||
|
# entity_app and event_app registered after Memory commands to control panel ordering
|
||||||
|
|
||||||
|
|
||||||
|
# ── Validated user identity (set by _get_backend_and_config) ──────────────
|
||||||
|
|
||||||
|
_validated_user_email: str | None = None
|
||||||
|
|
||||||
|
# ── Telemetry helper ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _fire_telemetry(command_name: str, extra: dict | None = None) -> None:
|
||||||
|
"""Fire a PostHog telemetry event (non-blocking, never fails)."""
|
||||||
|
try:
|
||||||
|
from mem0_cli.telemetry import capture_event
|
||||||
|
|
||||||
|
props = {"command": command_name}
|
||||||
|
if extra:
|
||||||
|
props.update(extra)
|
||||||
|
capture_event(f"cli.{command_name}", props, pre_resolved_email=_validated_user_email)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@config_app.callback(invoke_without_command=True)
|
||||||
|
def _config_callback(ctx: typer.Context) -> None:
|
||||||
|
if ctx.invoked_subcommand:
|
||||||
|
_fire_telemetry(f"config.{ctx.invoked_subcommand}")
|
||||||
|
|
||||||
|
|
||||||
|
@entity_app.callback(invoke_without_command=True)
|
||||||
|
def _entity_callback(ctx: typer.Context) -> None:
|
||||||
|
if ctx.invoked_subcommand:
|
||||||
|
_fire_telemetry(f"entity.{ctx.invoked_subcommand}")
|
||||||
|
|
||||||
|
|
||||||
|
@event_app.callback(invoke_without_command=True)
|
||||||
|
def _event_callback(ctx: typer.Context) -> None:
|
||||||
|
if ctx.invoked_subcommand:
|
||||||
|
_fire_telemetry(f"event.{ctx.invoked_subcommand}")
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
@@ -53,9 +101,16 @@ def _get_backend_and_config(
|
|||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
):
|
):
|
||||||
"""Build and return the Platform backend plus the loaded config."""
|
"""Build and return the Platform backend plus the loaded config.
|
||||||
|
|
||||||
|
Validates the API key upfront via ``/v1/ping/`` and caches the
|
||||||
|
resolved user email for telemetry.
|
||||||
|
"""
|
||||||
|
global _validated_user_email
|
||||||
|
|
||||||
from mem0_cli.backend import get_backend
|
from mem0_cli.backend import get_backend
|
||||||
from mem0_cli.config import load_config
|
from mem0_cli.backend.platform import AuthError
|
||||||
|
from mem0_cli.config import load_config, save_config
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
@@ -72,7 +127,29 @@ def _get_backend_and_config(
|
|||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
return get_backend(config), config
|
backend = get_backend(config)
|
||||||
|
|
||||||
|
# Validate the API key upfront with a fast timeout
|
||||||
|
try:
|
||||||
|
ping_data = backend.ping(timeout=5.0)
|
||||||
|
email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
|
||||||
|
if email:
|
||||||
|
_validated_user_email = email
|
||||||
|
if config.platform.user_email != email:
|
||||||
|
config.platform.user_email = email
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
save_config(config)
|
||||||
|
except AuthError:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"Invalid or expired API key.",
|
||||||
|
hint="Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
except Exception:
|
||||||
|
print_warning(err_console, "Could not validate API key (network issue). Proceeding anyway.")
|
||||||
|
|
||||||
|
return backend, config
|
||||||
|
|
||||||
|
|
||||||
def _get_backend(
|
def _get_backend(
|
||||||
@@ -114,9 +191,22 @@ def _resolve_ids(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _stdin_is_piped() -> bool:
|
||||||
|
"""Return True only when stdin is an actual pipe or file redirect — not a bare open fd."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||||
|
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _read_stdin() -> str | None:
|
def _read_stdin() -> str | None:
|
||||||
"""Read from stdin if it is piped (not a TTY)."""
|
"""Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
|
||||||
if not sys.stdin.isatty():
|
if _stdin_is_piped():
|
||||||
return sys.stdin.read().strip() or None
|
return sys.stdin.read().strip() or None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -128,12 +218,26 @@ def _read_stdin() -> str | None:
|
|||||||
def main_callback(
|
def main_callback(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
version: bool = typer.Option(False, "--version", help="Show version and exit."),
|
version: bool = typer.Option(False, "--version", help="Show version and exit."),
|
||||||
|
json_agent: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--json",
|
||||||
|
"--agent",
|
||||||
|
help="Output as JSON for agent/programmatic use.",
|
||||||
|
is_eager=False,
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if json_agent:
|
||||||
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
|
set_agent_mode(True)
|
||||||
if version:
|
if version:
|
||||||
from mem0_cli.commands.utils import cmd_version
|
from mem0_cli.commands.utils import cmd_version
|
||||||
|
|
||||||
|
_fire_telemetry("version")
|
||||||
cmd_version()
|
cmd_version()
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
|
if ctx.invoked_subcommand:
|
||||||
|
_fire_telemetry(ctx.invoked_subcommand)
|
||||||
|
|
||||||
|
|
||||||
# ── Memory: add ───────────────────────────────────────────────────────────
|
# ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
@@ -253,8 +357,12 @@ def search(
|
|||||||
help="Specific fields to return (comma-separated).",
|
help="Specific fields to return (comma-separated).",
|
||||||
rich_help_panel="Search",
|
rich_help_panel="Search",
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
|
False, "--graph", help="Enable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -269,7 +377,7 @@ def search(
|
|||||||
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Search memories by semantic query.
|
"""Query your memory store — semantic, keyword, or hybrid retrieval.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
mem0 search "preferences" --user-id alice
|
mem0 search "preferences" --user-id alice
|
||||||
@@ -281,8 +389,8 @@ def search(
|
|||||||
# STEP 7: stdin fallback for query
|
# STEP 7: stdin fallback for query
|
||||||
if query is None:
|
if query is None:
|
||||||
query = _read_stdin()
|
query = _read_stdin()
|
||||||
if query is None:
|
if not query or not query.strip():
|
||||||
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
|
print_error(err_console, "Search query cannot be empty.")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
backend, config = _get_backend_and_config(api_key, base_url)
|
backend, config = _get_backend_and_config(api_key, base_url)
|
||||||
@@ -372,8 +480,12 @@ def list_cmd(
|
|||||||
before: str | None = typer.Option(
|
before: str | None = typer.Option(
|
||||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||||
),
|
),
|
||||||
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
|
graph: bool = typer.Option(
|
||||||
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
|
False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
|
no_graph: bool = typer.Option(
|
||||||
|
False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -463,11 +575,19 @@ def update(
|
|||||||
|
|
||||||
@app.command(rich_help_panel="Memory")
|
@app.command(rich_help_panel="Memory")
|
||||||
def delete(
|
def delete(
|
||||||
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
|
memory_id: str | None = typer.Argument(
|
||||||
|
None, help="Memory ID to delete (omit when using --all or --entity)."
|
||||||
|
),
|
||||||
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
|
||||||
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
|
entity: bool = typer.Option(
|
||||||
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
|
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
),
|
||||||
|
project: bool = typer.Option(
|
||||||
|
False, "--project", help="With --all: delete ALL memories project-wide."
|
||||||
|
),
|
||||||
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
user_id: str | None = typer.Option(
|
user_id: str | None = typer.Option(
|
||||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||||
@@ -522,12 +642,14 @@ def delete(
|
|||||||
|
|
||||||
# ── Dispatch ─────────────────────────────────────────────────────
|
# ── Dispatch ─────────────────────────────────────────────────────
|
||||||
if memory_id is not None:
|
if memory_id is not None:
|
||||||
|
_fire_telemetry("delete", {"delete_mode": "single"})
|
||||||
from mem0_cli.commands.memory import cmd_delete
|
from mem0_cli.commands.memory import cmd_delete
|
||||||
|
|
||||||
backend = _get_backend(api_key, base_url)
|
backend = _get_backend(api_key, base_url)
|
||||||
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
|
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
|
||||||
|
|
||||||
elif all_:
|
elif all_:
|
||||||
|
_fire_telemetry("delete", {"delete_mode": "all"})
|
||||||
from mem0_cli.commands.memory import cmd_delete_all
|
from mem0_cli.commands.memory import cmd_delete_all
|
||||||
|
|
||||||
backend, config = _get_backend_and_config(api_key, base_url)
|
backend, config = _get_backend_and_config(api_key, base_url)
|
||||||
@@ -535,6 +657,7 @@ def delete(
|
|||||||
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
|
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
|
||||||
|
|
||||||
else: # --entity
|
else: # --entity
|
||||||
|
_fire_telemetry("delete", {"delete_mode": "entity"})
|
||||||
from mem0_cli.commands.entities import cmd_entities_delete
|
from mem0_cli.commands.entities import cmd_entities_delete
|
||||||
|
|
||||||
backend = _get_backend(api_key, base_url)
|
backend = _get_backend(api_key, base_url)
|
||||||
@@ -641,14 +764,12 @@ def entity_delete(
|
|||||||
agent_id: str | None = typer.Option(
|
agent_id: str | None = typer.Option(
|
||||||
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
|
||||||
),
|
),
|
||||||
app_id: str | None = typer.Option(
|
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
|
||||||
None, "--app-id", help="App ID.", rich_help_panel="Scope"
|
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
|
||||||
),
|
|
||||||
run_id: str | None = typer.Option(
|
|
||||||
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
|
|
||||||
),
|
|
||||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||||
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||||
|
),
|
||||||
output: str = typer.Option(
|
output: str = typer.Option(
|
||||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||||
),
|
),
|
||||||
@@ -688,23 +809,98 @@ def entity_delete(
|
|||||||
app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
app.add_typer(entity_app, name="entity", rich_help_panel="Management")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Event subcommands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@event_app.command("list")
|
||||||
|
def event_list(
|
||||||
|
output: str = typer.Option(
|
||||||
|
"table", "--output", "-o", help="Output: table, json.", rich_help_panel="Output"
|
||||||
|
),
|
||||||
|
api_key: str | None = typer.Option(
|
||||||
|
None,
|
||||||
|
"--api-key",
|
||||||
|
help="Override API key.",
|
||||||
|
envvar="MEM0_API_KEY",
|
||||||
|
rich_help_panel="Connection",
|
||||||
|
),
|
||||||
|
base_url: str | None = typer.Option(
|
||||||
|
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""List recent background processing events.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mem0 event list
|
||||||
|
mem0 event list -o json
|
||||||
|
"""
|
||||||
|
from mem0_cli.commands.events_cmd import cmd_event_list
|
||||||
|
|
||||||
|
backend = _get_backend(api_key, base_url)
|
||||||
|
cmd_event_list(backend, output=output)
|
||||||
|
|
||||||
|
|
||||||
|
@event_app.command("status")
|
||||||
|
def event_status(
|
||||||
|
event_id: str = typer.Argument(..., help="Event ID to inspect."),
|
||||||
|
output: str = typer.Option(
|
||||||
|
"text", "--output", "-o", help="Output: text, json.", rich_help_panel="Output"
|
||||||
|
),
|
||||||
|
api_key: str | None = typer.Option(
|
||||||
|
None,
|
||||||
|
"--api-key",
|
||||||
|
help="Override API key.",
|
||||||
|
envvar="MEM0_API_KEY",
|
||||||
|
rich_help_panel="Connection",
|
||||||
|
),
|
||||||
|
base_url: str | None = typer.Option(
|
||||||
|
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Check the status of a specific background event.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
mem0 event status <event-id>
|
||||||
|
mem0 event status <event-id> -o json
|
||||||
|
"""
|
||||||
|
from mem0_cli.commands.events_cmd import cmd_event_status
|
||||||
|
|
||||||
|
backend = _get_backend(api_key, base_url)
|
||||||
|
cmd_event_status(backend, event_id, output=output)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Event subgroup ──
|
||||||
|
app.add_typer(event_app, name="event", rich_help_panel="Management")
|
||||||
|
|
||||||
|
|
||||||
# ── Management commands ───────────────────────────────────────────────────
|
# ── Management commands ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@app.command(rich_help_panel="Management")
|
@app.command(rich_help_panel="Management")
|
||||||
def init(
|
def init(
|
||||||
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
|
||||||
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
|
user_id: str | None = typer.Option(
|
||||||
|
None, "--user-id", "-u", help="Default user ID (skip prompt)."
|
||||||
|
),
|
||||||
|
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
|
||||||
|
code: str | None = typer.Option(
|
||||||
|
None, "--code", help="Verification code (use with --email for non-interactive login)."
|
||||||
|
),
|
||||||
|
force: bool = typer.Option(
|
||||||
|
False, "--force", help="Overwrite existing config without confirmation."
|
||||||
|
),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
mem0 init
|
mem0 init
|
||||||
mem0 init --api-key m0-xxx --user-id alice
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
mem0 init --email alice@company.com --code 482901
|
||||||
"""
|
"""
|
||||||
from mem0_cli.commands.init_cmd import run_init
|
from mem0_cli.commands.init_cmd import run_init
|
||||||
|
|
||||||
run_init(api_key=api_key, user_id=user_id)
|
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
||||||
|
|
||||||
|
|
||||||
# (entity_app registered at module level, below sub-group definitions)
|
# (entity_app registered at module level, below sub-group definitions)
|
||||||
@@ -743,7 +939,6 @@ def status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("import", rich_help_panel="Management")
|
@app.command("import", rich_help_panel="Management")
|
||||||
def import_cmd(
|
def import_cmd(
|
||||||
file_path: str = typer.Argument(..., help="JSON file to import."),
|
file_path: str = typer.Argument(..., help="JSON file to import."),
|
||||||
@@ -810,7 +1005,7 @@ def _build_help_json() -> dict:
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"description": "Search memories by semantic query.",
|
"description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||||
"usage": "mem0 search <query> [OPTIONS]",
|
"usage": "mem0 search <query> [OPTIONS]",
|
||||||
"arguments": {"query": {"description": "Search query.", "required": False}},
|
"arguments": {"query": {"description": "Search query.", "required": False}},
|
||||||
"options": {
|
"options": {
|
||||||
@@ -914,6 +1109,24 @@ def _build_help_json() -> dict:
|
|||||||
"value": {"description": "Value to set.", "required": True},
|
"value": {"description": "Value to set.", "required": True},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"event": {
|
||||||
|
"description": "Inspect background processing events.",
|
||||||
|
"subcommands": {
|
||||||
|
"list": {
|
||||||
|
"description": "List recent background processing events.",
|
||||||
|
"usage": "mem0 event list [OPTIONS]",
|
||||||
|
"options": {"--output, -o": "Output format: table, json."},
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "Check the status of a specific background event.",
|
||||||
|
"usage": "mem0 event status <event_id> [OPTIONS]",
|
||||||
|
"arguments": {
|
||||||
|
"event_id": {"description": "Event ID to inspect.", "required": True}
|
||||||
|
},
|
||||||
|
"options": {"--output, -o": "Output format: text, json."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"description": "Manage entities.",
|
"description": "Manage entities.",
|
||||||
"subcommands": {
|
"subcommands": {
|
||||||
@@ -965,6 +1178,7 @@ def _build_help_json() -> dict:
|
|||||||
"global_options": {
|
"global_options": {
|
||||||
"--api-key": "Override API key (env: MEM0_API_KEY).",
|
"--api-key": "Override API key (env: MEM0_API_KEY).",
|
||||||
"--base-url": "Override API base URL.",
|
"--base-url": "Override API base URL.",
|
||||||
|
"--json / --agent": "Output as JSON for agent/programmatic use.",
|
||||||
"--help": "Show help for a command.",
|
"--help": "Show help for a command.",
|
||||||
"--version": "Show version and exit.",
|
"--version": "Show version and exit.",
|
||||||
},
|
},
|
||||||
@@ -994,7 +1208,7 @@ def help(
|
|||||||
console.print("Usage: mem0 <command> [OPTIONS]\n")
|
console.print("Usage: mem0 <command> [OPTIONS]\n")
|
||||||
console.print("[bold]Commands:[/]")
|
console.print("[bold]Commands:[/]")
|
||||||
console.print(" add Add a memory from text, messages, file, or stdin")
|
console.print(" add Add a memory from text, messages, file, or stdin")
|
||||||
console.print(" search Search memories by semantic query")
|
console.print(" search Query your memory store (semantic, keyword, hybrid)")
|
||||||
console.print(" get Get a specific memory by ID")
|
console.print(" get Get a specific memory by ID")
|
||||||
console.print(" list List memories with optional filters")
|
console.print(" list List memories with optional filters")
|
||||||
console.print(" update Update a memory's text or metadata")
|
console.print(" update Update a memory's text or metadata")
|
||||||
@@ -1002,6 +1216,7 @@ def help(
|
|||||||
console.print(" import Import memories from a JSON file")
|
console.print(" import Import memories from a JSON file")
|
||||||
console.print(" config Manage configuration (show, get, set)")
|
console.print(" config Manage configuration (show, get, set)")
|
||||||
console.print(" entity Manage entities (list, delete)")
|
console.print(" entity Manage entities (list, delete)")
|
||||||
|
console.print(" event Inspect background events (list, status)")
|
||||||
console.print(" init Interactive setup wizard")
|
console.print(" init Interactive setup wizard")
|
||||||
console.print(" status Check connectivity and authentication")
|
console.print(" status Check connectivity and authentication")
|
||||||
console.print()
|
console.print()
|
||||||
@@ -1018,4 +1233,14 @@ app.add_typer(config_app, name="config", rich_help_panel="Management")
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||||
|
_json_flags = {"--json", "--agent"}
|
||||||
|
if any(a in _json_flags for a in sys.argv[1:]):
|
||||||
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
|
set_agent_mode(True)
|
||||||
|
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
||||||
|
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ class Backend(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
def entities(self, entity_type: str) -> list[dict]: ...
|
def entities(self, entity_type: str) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_events(self) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_event(self, event_id: str) -> dict: ...
|
||||||
|
|
||||||
|
|
||||||
def get_backend(config: Mem0Config) -> Backend:
|
def get_backend(config: Mem0Config) -> Backend:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from mem0_cli import __version__
|
||||||
from mem0_cli.backend.base import Backend
|
from mem0_cli.backend.base import Backend
|
||||||
from mem0_cli.config import PlatformConfig
|
from mem0_cli.config import PlatformConfig
|
||||||
|
|
||||||
@@ -21,11 +22,17 @@ class PlatformBackend(Backend):
|
|||||||
headers={
|
headers={
|
||||||
"Authorization": f"Token {config.api_key}",
|
"Authorization": f"Token {config.api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "python",
|
||||||
|
"X-Mem0-Client-Version": __version__,
|
||||||
},
|
},
|
||||||
timeout=30.0,
|
timeout=30.0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||||
resp = self._client.request(method, path, **kwargs)
|
resp = self._client.request(method, path, **kwargs)
|
||||||
if resp.status_code == 401:
|
if resp.status_code == 401:
|
||||||
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||||
@@ -86,6 +93,7 @@ class PlatformBackend(Backend):
|
|||||||
payload["categories"] = categories
|
payload["categories"] = categories
|
||||||
if enable_graph:
|
if enable_graph:
|
||||||
payload["enable_graph"] = True
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
return self._request("POST", "/v1/memories/", json=payload)
|
return self._request("POST", "/v1/memories/", json=payload)
|
||||||
|
|
||||||
@@ -165,6 +173,7 @@ class PlatformBackend(Backend):
|
|||||||
payload["fields"] = fields
|
payload["fields"] = fields
|
||||||
if enable_graph:
|
if enable_graph:
|
||||||
payload["enable_graph"] = True
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
result = self._request("POST", "/v2/memories/search/", json=payload)
|
result = self._request("POST", "/v2/memories/search/", json=payload)
|
||||||
return (
|
return (
|
||||||
@@ -174,7 +183,7 @@ class PlatformBackend(Backend):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get(self, memory_id: str) -> dict:
|
def get(self, memory_id: str) -> dict:
|
||||||
return self._request("GET", f"/v1/memories/{memory_id}/")
|
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||||
|
|
||||||
def list_memories(
|
def list_memories(
|
||||||
self,
|
self,
|
||||||
@@ -213,6 +222,7 @@ class PlatformBackend(Backend):
|
|||||||
payload["filters"] = api_filters
|
payload["filters"] = api_filters
|
||||||
if enable_graph:
|
if enable_graph:
|
||||||
payload["enable_graph"] = True
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
result = self._request("POST", "/v2/memories/", json=payload, params=params)
|
result = self._request("POST", "/v2/memories/", json=payload, params=params)
|
||||||
return (
|
return (
|
||||||
@@ -229,6 +239,7 @@ class PlatformBackend(Backend):
|
|||||||
payload["text"] = content
|
payload["text"] = content
|
||||||
if metadata:
|
if metadata:
|
||||||
payload["metadata"] = metadata
|
payload["metadata"] = metadata
|
||||||
|
payload["source"] = "CLI"
|
||||||
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@@ -242,7 +253,7 @@ class PlatformBackend(Backend):
|
|||||||
run_id: str | None = None,
|
run_id: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
if all:
|
if all:
|
||||||
params: dict[str, str] = {}
|
params: dict[str, str] = {"source": "CLI"}
|
||||||
if user_id:
|
if user_id:
|
||||||
params["user_id"] = user_id
|
params["user_id"] = user_id
|
||||||
if agent_id:
|
if agent_id:
|
||||||
@@ -253,7 +264,7 @@ class PlatformBackend(Backend):
|
|||||||
params["run_id"] = run_id
|
params["run_id"] = run_id
|
||||||
return self._request("DELETE", "/v1/memories/", params=params)
|
return self._request("DELETE", "/v1/memories/", params=params)
|
||||||
elif memory_id:
|
elif memory_id:
|
||||||
return self._request("DELETE", f"/v1/memories/{memory_id}/")
|
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||||
else:
|
else:
|
||||||
raise ValueError("Either memory_id or --all is required")
|
raise ValueError("Either memory_id or --all is required")
|
||||||
|
|
||||||
@@ -265,18 +276,37 @@ class PlatformBackend(Backend):
|
|||||||
app_id: str | None = None,
|
app_id: str | None = None,
|
||||||
run_id: str | None = None,
|
run_id: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
params: dict[str, str] = {}
|
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
if user_id:
|
type_map = {
|
||||||
params["user_id"] = user_id
|
"user": user_id,
|
||||||
if agent_id:
|
"agent": agent_id,
|
||||||
params["agent_id"] = agent_id
|
"app": app_id,
|
||||||
if app_id:
|
"run": run_id,
|
||||||
params["app_id"] = app_id
|
}
|
||||||
if run_id:
|
entities = {t: v for t, v in type_map.items() if v}
|
||||||
params["run_id"] = run_id
|
if not entities:
|
||||||
if not params:
|
|
||||||
raise ValueError("At least one entity ID is required for delete_entities.")
|
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||||
return self._request("DELETE", "/v1/entities/", params=params)
|
# Delete each provided entity via the v2 path-based endpoint
|
||||||
|
result: dict = {}
|
||||||
|
for entity_type, entity_id in entities.items():
|
||||||
|
result = self._request(
|
||||||
|
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def ping(self, timeout: float | None = None) -> dict:
|
||||||
|
"""Call the ping endpoint and return the raw response.
|
||||||
|
|
||||||
|
When *timeout* is given it overrides the client-level timeout so that
|
||||||
|
validation pings can fail fast without blocking the user.
|
||||||
|
"""
|
||||||
|
if timeout is not None:
|
||||||
|
resp = self._client.get("/v1/ping/", timeout=timeout)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
return self._request("GET", "/v1/ping/")
|
||||||
|
|
||||||
def status(
|
def status(
|
||||||
self,
|
self,
|
||||||
@@ -284,19 +314,9 @@ class PlatformBackend(Backend):
|
|||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
agent_id: str | None = None,
|
agent_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Check connectivity by making a lightweight API call."""
|
"""Check connectivity using the ping endpoint."""
|
||||||
try:
|
try:
|
||||||
# If entity IDs are available, validate with a minimal memories list
|
self.ping()
|
||||||
if user_id or agent_id:
|
|
||||||
payload: dict[str, Any] = {}
|
|
||||||
params = {"page": "1", "page_size": "1"}
|
|
||||||
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
|
|
||||||
if api_filters:
|
|
||||||
payload["filters"] = api_filters
|
|
||||||
self._request("POST", "/v2/memories/", json=payload, params=params)
|
|
||||||
else:
|
|
||||||
# No entity IDs — use entities endpoint to validate API key
|
|
||||||
self._request("GET", "/v1/entities/")
|
|
||||||
return {"connected": True, "backend": "platform", "base_url": self.base_url}
|
return {"connected": True, "backend": "platform", "base_url": self.base_url}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"connected": False, "backend": "platform", "error": str(e)}
|
return {"connected": False, "backend": "platform", "error": str(e)}
|
||||||
@@ -311,6 +331,12 @@ class PlatformBackend(Backend):
|
|||||||
items = [e for e in items if e.get("type", "").lower() == target_type]
|
items = [e for e in items if e.get("type", "").lower() == target_type]
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def list_events(self) -> list[dict]:
|
||||||
|
result = self._request("GET", "/v1/events/")
|
||||||
|
return result if isinstance(result, list) else result.get("results", [])
|
||||||
|
|
||||||
|
def get_event(self, event_id: str) -> dict:
|
||||||
|
return self._request("GET", f"/v1/event/{event_id}/")
|
||||||
|
|
||||||
|
|
||||||
class AuthError(Exception):
|
class AuthError(Exception):
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ def _sym(fancy: str, plain: str) -> str:
|
|||||||
|
|
||||||
def print_banner(console: Console) -> None:
|
def print_banner(console: Console) -> None:
|
||||||
"""Print the mem0 welcome banner."""
|
"""Print the mem0 welcome banner."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
||||||
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
||||||
|
|
||||||
@@ -61,11 +65,28 @@ def print_banner(console: Console) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def print_success(console: Console, message: str) -> None:
|
def print_success(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
sym = _sym("✓", "[ok]")
|
sym = _sym("✓", "[ok]")
|
||||||
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
||||||
|
from mem0_cli.state import get_current_command, is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
envelope = {
|
||||||
|
"status": "error",
|
||||||
|
"command": get_current_command(),
|
||||||
|
"error": message,
|
||||||
|
"data": None,
|
||||||
|
}
|
||||||
|
print(_json.dumps(envelope))
|
||||||
|
return
|
||||||
sym = _sym("✗", "[error]")
|
sym = _sym("✗", "[error]")
|
||||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||||
if hint:
|
if hint:
|
||||||
@@ -73,11 +94,19 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
|||||||
|
|
||||||
|
|
||||||
def print_warning(console: Console, message: str) -> None:
|
def print_warning(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
sym = _sym("⚠", "[warn]")
|
sym = _sym("⚠", "[warn]")
|
||||||
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
def print_info(console: Console, message: str) -> None:
|
def print_info(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
sym = _sym("◆", "*")
|
sym = _sym("◆", "*")
|
||||||
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
@@ -89,7 +118,9 @@ def timed_status(console: Console, message: str):
|
|||||||
The spinner and timing output are sent to stderr (via ``_err``) so they
|
The spinner and timing output are sent to stderr (via ``_err``) so they
|
||||||
never contaminate machine-readable stdout. The *console* parameter is
|
never contaminate machine-readable stdout. The *console* parameter is
|
||||||
kept for backward compatibility but is not used for spinner output.
|
kept for backward compatibility but is not used for spinner output.
|
||||||
|
In agent mode the spinner is suppressed entirely.
|
||||||
"""
|
"""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
class _Ctx:
|
class _Ctx:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -97,6 +128,13 @@ def timed_status(console: Console, message: str):
|
|||||||
self.error_msg = ""
|
self.error_msg = ""
|
||||||
|
|
||||||
ctx = _Ctx()
|
ctx = _Ctx()
|
||||||
|
if is_agent_mode():
|
||||||
|
try:
|
||||||
|
yield ctx
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
return
|
||||||
|
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
try:
|
try:
|
||||||
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
||||||
@@ -105,6 +143,11 @@ def timed_status(console: Console, message: str):
|
|||||||
elapsed = time.perf_counter() - start
|
elapsed = time.perf_counter() - start
|
||||||
if ctx.error_msg:
|
if ctx.error_msg:
|
||||||
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
||||||
|
if "Authentication failed" in ctx.error_msg:
|
||||||
|
_err.print(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
||||||
|
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
elapsed = time.perf_counter() - start
|
elapsed = time.perf_counter() - start
|
||||||
@@ -114,11 +157,14 @@ def timed_status(console: Console, message: str):
|
|||||||
|
|
||||||
def print_scope(console: Console, **ids: str | None) -> None:
|
def print_scope(console: Console, **ids: str | None) -> None:
|
||||||
"""Show active entity scope if any IDs are set."""
|
"""Show active entity scope if any IDs are set."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
parts = []
|
parts = []
|
||||||
for key, val in ids.items():
|
for key, val in ids.items():
|
||||||
if val:
|
if val:
|
||||||
label = key.replace("_", " ").replace("id", "ID").strip()
|
parts.append(f"{key}={val}")
|
||||||
parts.append(f"{label}={val}")
|
|
||||||
if parts:
|
if parts:
|
||||||
scope_str = ", ".join(parts)
|
scope_str = ", ".join(parts)
|
||||||
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
||||||
|
|||||||
@@ -20,12 +20,17 @@ err_console = Console(stderr=True)
|
|||||||
|
|
||||||
def cmd_config_show(*, output: str = "text") -> None:
|
def cmd_config_show(*, output: str = "text") -> None:
|
||||||
"""Display current configuration (secrets redacted)."""
|
"""Display current configuration (secrets redacted)."""
|
||||||
from mem0_cli.output import format_json_envelope
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config show")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
if output == "json":
|
if output in ("json", "agent"):
|
||||||
format_json_envelope(
|
format_agent_envelope(
|
||||||
console,
|
console,
|
||||||
command="config show",
|
command="config show",
|
||||||
data={
|
data={
|
||||||
@@ -84,25 +89,44 @@ def cmd_config_show(*, output: str = "text") -> None:
|
|||||||
|
|
||||||
def cmd_config_get(key: str) -> None:
|
def cmd_config_get(key: str) -> None:
|
||||||
"""Get a config value."""
|
"""Get a config value."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config get")
|
||||||
config = load_config()
|
config = load_config()
|
||||||
value = get_nested_value(config, key)
|
value = get_nested_value(config, key)
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
print_error(err_console, f"Unknown config key: {key}")
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
|
return
|
||||||
|
|
||||||
|
display_value = (
|
||||||
|
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config get", data={"key": key, "value": display_value}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Redact secrets
|
console.print(display_value)
|
||||||
if "api_key" in key or "key" in key.split(".")[-1:]:
|
|
||||||
console.print(redact_key(str(value)))
|
|
||||||
else:
|
|
||||||
console.print(str(value))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_config_set(key: str, value: str) -> None:
|
def cmd_config_set(key: str, value: str) -> None:
|
||||||
"""Set a config value."""
|
"""Set a config value."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config set")
|
||||||
config = load_config()
|
config = load_config()
|
||||||
if set_nested_value(config, key, value):
|
if set_nested_value(config, key, value):
|
||||||
save_config(config)
|
save_config(config)
|
||||||
display = redact_key(value) if "key" in key else value
|
display = redact_key(value) if "key" in key else value
|
||||||
print_success(console, f"{key} = {display}")
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config set", data={"key": key, "value": display}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(console, f"{key} = {display}")
|
||||||
else:
|
else:
|
||||||
print_error(err_console, f"Unknown config key: {key}")
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
|
|||||||
print_success,
|
print_success,
|
||||||
timed_status,
|
timed_status,
|
||||||
)
|
)
|
||||||
from mem0_cli.output import format_json
|
from mem0_cli.output import format_agent_envelope, format_json
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
@@ -26,9 +26,16 @@ err_console = Console(stderr=True)
|
|||||||
|
|
||||||
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
||||||
"""List entities of a given type."""
|
"""List entities of a given type."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
valid_types = {"users", "agents", "apps", "runs"}
|
valid_types = {"users", "agents", "apps", "runs"}
|
||||||
if entity_type not in valid_types:
|
if entity_type not in valid_types:
|
||||||
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
|
print_error(
|
||||||
|
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
@@ -40,6 +47,16 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if output == "json":
|
if output == "json":
|
||||||
format_json(console, results)
|
format_json(console, results)
|
||||||
return
|
return
|
||||||
@@ -75,39 +92,39 @@ def cmd_entities_delete(
|
|||||||
output: str,
|
output: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete an entity and all its memories (cascade delete)."""
|
"""Delete an entity and all its memories (cascade delete)."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
if not any([user_id, agent_id, app_id, run_id]):
|
if not any([user_id, agent_id, app_id, run_id]):
|
||||||
print_error(err_console, "Provide at least one of --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)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
scope_parts = []
|
||||||
|
if user_id:
|
||||||
|
scope_parts.append(f"user={user_id}")
|
||||||
|
if agent_id:
|
||||||
|
scope_parts.append(f"agent={agent_id}")
|
||||||
|
if app_id:
|
||||||
|
scope_parts.append(f"app={app_id}")
|
||||||
|
if run_id:
|
||||||
|
scope_parts.append(f"run={run_id}")
|
||||||
|
scope_str = ", ".join(scope_parts)
|
||||||
|
|
||||||
if dry_run:
|
if dry_run:
|
||||||
scope_parts = []
|
print_info(console, f"Would delete entity {scope_str} and all its memories.")
|
||||||
if user_id:
|
|
||||||
scope_parts.append(f"user={user_id}")
|
|
||||||
if agent_id:
|
|
||||||
scope_parts.append(f"agent={agent_id}")
|
|
||||||
if app_id:
|
|
||||||
scope_parts.append(f"app={app_id}")
|
|
||||||
if run_id:
|
|
||||||
scope_parts.append(f"run={run_id}")
|
|
||||||
scope = ", ".join(scope_parts)
|
|
||||||
print_info(console, f"Would delete entity {scope} and all its memories.")
|
|
||||||
print_info(console, "No changes made (dry run).")
|
print_info(console, "No changes made (dry run).")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not force:
|
if not force:
|
||||||
scope_parts = []
|
|
||||||
if user_id:
|
|
||||||
scope_parts.append(f"user={user_id}")
|
|
||||||
if agent_id:
|
|
||||||
scope_parts.append(f"agent={agent_id}")
|
|
||||||
if app_id:
|
|
||||||
scope_parts.append(f"app={app_id}")
|
|
||||||
if run_id:
|
|
||||||
scope_parts.append(f"run={run_id}")
|
|
||||||
scope = ", ".join(scope_parts)
|
|
||||||
|
|
||||||
confirm = typer.confirm(
|
confirm = typer.confirm(
|
||||||
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
|
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
|
||||||
)
|
)
|
||||||
if not confirm:
|
if not confirm:
|
||||||
print_info(console, "Cancelled.")
|
print_info(console, "Cancelled.")
|
||||||
@@ -127,7 +144,25 @@ def cmd_entities_delete(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity delete",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
format_json(console, result)
|
format_json(console, result)
|
||||||
elif output != "quiet":
|
elif output != "quiet":
|
||||||
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Event commands: list and status."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
ACCENT_COLOR,
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
WARNING_COLOR,
|
||||||
|
print_info,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
from mem0_cli.output import format_agent_envelope, format_json
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
_STATUS_STYLE = {
|
||||||
|
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
|
||||||
|
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
|
||||||
|
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
|
||||||
|
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _status_styled(status: str) -> str:
|
||||||
|
return _STATUS_STYLE.get(status.upper(), status)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
|
||||||
|
"""List recent background events."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching events...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.list_events()
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No events found.")
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
header_style=f"bold {ACCENT_COLOR}",
|
||||||
|
row_styles=["", "dim"],
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
|
||||||
|
table.add_column("Type", max_width=14)
|
||||||
|
table.add_column("Status", max_width=12)
|
||||||
|
table.add_column("Latency", max_width=10, justify="right")
|
||||||
|
table.add_column("Created", max_width=20)
|
||||||
|
|
||||||
|
for ev in results:
|
||||||
|
ev_id = str(ev.get("id", ""))[:8]
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
|
||||||
|
"""Get the status of a specific background event."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching event...") as _ts:
|
||||||
|
try:
|
||||||
|
ev = backend.get_event(event_id)
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event status",
|
||||||
|
data=ev,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, ev)
|
||||||
|
return
|
||||||
|
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
|
||||||
|
results = ev.get("results")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||||
|
|
||||||
|
if results:
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
|
||||||
|
for r in results:
|
||||||
|
mem_id = str(r.get("id", ""))[:8]
|
||||||
|
data = r.get("data", {})
|
||||||
|
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
|
||||||
|
ev_name = str(r.get("event", ""))
|
||||||
|
user = str(r.get("user_id", ""))
|
||||||
|
detail = f"{ev_name} {memory}"
|
||||||
|
if user:
|
||||||
|
detail += f" [{DIM_COLOR}](user_id={user})[/]"
|
||||||
|
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Event Status[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
import typer
|
import typer
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
@@ -16,7 +19,7 @@ from mem0_cli.branding import (
|
|||||||
print_info,
|
print_info,
|
||||||
print_success,
|
print_success,
|
||||||
)
|
)
|
||||||
from mem0_cli.config import Mem0Config, save_config
|
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
@@ -84,7 +87,96 @@ def _prompt_secret(label: str) -> str:
|
|||||||
return "".join(chars)
|
return "".join(chars)
|
||||||
|
|
||||||
|
|
||||||
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
_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("/")
|
||||||
|
_source_headers = {
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "python",
|
||||||
|
}
|
||||||
|
|
||||||
|
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},
|
||||||
|
headers=_source_headers,
|
||||||
|
)
|
||||||
|
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()},
|
||||||
|
headers=_source_headers,
|
||||||
|
)
|
||||||
|
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.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||||
@@ -93,6 +185,85 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
|||||||
"""
|
"""
|
||||||
config = Mem0Config()
|
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.platform.user_email = email
|
||||||
|
config.defaults.user_id = (
|
||||||
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
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
|
# Fully non-interactive when both flags provided
|
||||||
if api_key and user_id:
|
if api_key and user_id:
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
@@ -102,20 +273,56 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
|
|||||||
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Non-TTY without full flags -> error
|
|
||||||
if not sys.stdin.isatty() and (not api_key or not user_id):
|
|
||||||
print_error(
|
|
||||||
err_console,
|
|
||||||
"Non-interactive terminal detected and required flags missing.",
|
|
||||||
hint="Run: mem0 init --api-key <key> --user-id <id>",
|
|
||||||
)
|
|
||||||
raise typer.Exit(1)
|
|
||||||
|
|
||||||
print_banner(console)
|
print_banner(console)
|
||||||
console.print()
|
console.print()
|
||||||
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||||
|
|
||||||
# Use provided flags or prompt
|
# 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.platform.user_email = email_addr
|
||||||
|
config.defaults.user_id = (
|
||||||
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
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:
|
if api_key:
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
else:
|
else:
|
||||||
@@ -162,9 +369,10 @@ def _setup_defaults(config: Mem0Config) -> None:
|
|||||||
console.print()
|
console.print()
|
||||||
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
||||||
|
|
||||||
|
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
user_id = Prompt.ask(
|
user_id = Prompt.ask(
|
||||||
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||||
default="mem0-cli",
|
default=_default_user,
|
||||||
)
|
)
|
||||||
if user_id:
|
if user_id:
|
||||||
config.defaults.user_id = user_id
|
config.defaults.user_id = user_id
|
||||||
@@ -184,11 +392,19 @@ def _validate_platform(config: Mem0Config) -> None:
|
|||||||
)
|
)
|
||||||
if status.get("connected"):
|
if status.get("connected"):
|
||||||
print_success(console, "Connected to mem0 Platform!")
|
print_success(console, "Connected to mem0 Platform!")
|
||||||
|
# Cache user_email from ping response for telemetry distinct_id
|
||||||
|
try:
|
||||||
|
ping_data = backend.ping()
|
||||||
|
user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
|
||||||
|
if user_email:
|
||||||
|
config.platform.user_email = user_email
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
print_error(
|
print_error(
|
||||||
err_console,
|
err_console,
|
||||||
f"Could not connect: {status.get('error', 'Unknown error')}",
|
f"Could not connect: {status.get('error', 'Unknown error')}",
|
||||||
hint="Check your API key and try again.",
|
hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print_error(err_console, f"Connection test failed: {e}")
|
print_error(err_console, f"Connection test failed: {e}")
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import stat as _stat_mod
|
||||||
import sys
|
import sys
|
||||||
import time as _time
|
import time as _time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -20,6 +22,7 @@ from mem0_cli.branding import (
|
|||||||
)
|
)
|
||||||
from mem0_cli.output import (
|
from mem0_cli.output import (
|
||||||
format_add_result,
|
format_add_result,
|
||||||
|
format_agent_envelope,
|
||||||
format_json,
|
format_json,
|
||||||
format_memories_table,
|
format_memories_table,
|
||||||
format_memories_text,
|
format_memories_text,
|
||||||
@@ -31,6 +34,19 @@ console = Console()
|
|||||||
err_console = Console(stderr=True)
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _stdin_is_piped() -> bool:
|
||||||
|
"""Return True only when stdin is an actual pipe or file redirect."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||||
|
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def cmd_add(
|
def cmd_add(
|
||||||
backend: Backend,
|
backend: Backend,
|
||||||
text: str | None,
|
text: str | None,
|
||||||
@@ -50,6 +66,11 @@ def cmd_add(
|
|||||||
output: str = "text",
|
output: str = "text",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a memory."""
|
"""Add a memory."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("add")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
msgs = None
|
msgs = None
|
||||||
content = text
|
content = text
|
||||||
|
|
||||||
@@ -70,8 +91,8 @@ def cmd_add(
|
|||||||
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
# Read from stdin if no text and stdin is piped
|
# Read from stdin only if stdin is an actual pipe or file redirect
|
||||||
elif not content and not sys.stdin.isatty():
|
elif not content and _stdin_is_piped():
|
||||||
content = sys.stdin.read().strip()
|
content = sys.stdin.read().strip()
|
||||||
|
|
||||||
if not content and not msgs:
|
if not content and not msgs:
|
||||||
@@ -95,6 +116,21 @@ def cmd_add(
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
cats = [c.strip() for c in categories.split(",")]
|
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:
|
with timed_status(err_console, "Adding memory...") as ts:
|
||||||
try:
|
try:
|
||||||
result = backend.add(
|
result = backend.add(
|
||||||
@@ -113,24 +149,66 @@ def cmd_add(
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
ts.error_msg = str(e)
|
ts.error_msg = str(e)
|
||||||
print_error(err_console, str(e))
|
|
||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
if output == "quiet":
|
if output == "quiet":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||||
|
results_list = result if isinstance(result, list) else result.get("results", [result])
|
||||||
|
seen_events: set[str] = set()
|
||||||
|
deduped: list[dict] = []
|
||||||
|
for r in results_list:
|
||||||
|
if r.get("status") == "PENDING":
|
||||||
|
eid = r.get("event_id", "")
|
||||||
|
if eid and eid in seen_events:
|
||||||
|
continue
|
||||||
|
if eid:
|
||||||
|
seen_events.add(eid)
|
||||||
|
deduped.append(r)
|
||||||
|
# Write back so downstream formatters see deduplicated data
|
||||||
|
if isinstance(result, dict) and "results" in result:
|
||||||
|
result = {**result, "results": deduped}
|
||||||
|
else:
|
||||||
|
result = deduped
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="add",
|
||||||
|
data=deduped,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(deduped),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if output == "json":
|
if output == "json":
|
||||||
format_add_result(console, result, output)
|
format_add_result(console, result, output)
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||||
# Count results
|
count = len(deduped)
|
||||||
results = result if isinstance(result, list) else result.get("results", [result])
|
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
|
||||||
count = len(results) if results else 0
|
if all_pending:
|
||||||
print_success(
|
print_success(
|
||||||
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
console,
|
||||||
)
|
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(
|
||||||
|
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||||
|
)
|
||||||
format_add_result(console, result, output)
|
format_add_result(console, result, output)
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +230,11 @@ def cmd_search(
|
|||||||
output: str = "text",
|
output: str = "text",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Search memories."""
|
"""Search memories."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("search")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
filters = None
|
filters = None
|
||||||
if filter_json:
|
if filter_json:
|
||||||
try:
|
try:
|
||||||
@@ -164,6 +247,13 @@ def cmd_search(
|
|||||||
if fields:
|
if fields:
|
||||||
field_list = [f.strip() for f in fields.split(",")]
|
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()
|
_start = _time.perf_counter()
|
||||||
with timed_status(err_console, "Searching memories...") as _ts:
|
with timed_status(err_console, "Searching memories...") as _ts:
|
||||||
try:
|
try:
|
||||||
@@ -186,11 +276,35 @@ def cmd_search(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_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":
|
if output == "json":
|
||||||
format_json(console, results)
|
format_json(console, results)
|
||||||
elif output == "table":
|
elif output == "table":
|
||||||
if results:
|
if results:
|
||||||
format_memories_table(console, results)
|
format_memories_table(console, results, show_score=True)
|
||||||
print_result_summary(
|
print_result_summary(
|
||||||
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||||
)
|
)
|
||||||
@@ -212,6 +326,11 @@ def cmd_search(
|
|||||||
|
|
||||||
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||||
"""Get a specific memory by ID."""
|
"""Get a specific memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("get")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
with timed_status(err_console, "Fetching memory...") as _ts:
|
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||||
try:
|
try:
|
||||||
result = backend.get(memory_id)
|
result = backend.get(memory_id)
|
||||||
@@ -219,7 +338,10 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
|||||||
print_error(err_console, str(e))
|
print_error(err_console, str(e))
|
||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
format_single_memory(console, result, output)
|
if output == "agent":
|
||||||
|
format_agent_envelope(console, command="get", data=result)
|
||||||
|
else:
|
||||||
|
format_single_memory(console, result, output)
|
||||||
|
|
||||||
|
|
||||||
def cmd_list(
|
def cmd_list(
|
||||||
@@ -238,6 +360,18 @@ def cmd_list(
|
|||||||
output: str = "table",
|
output: str = "table",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""List memories."""
|
"""List memories."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if page_size < 1:
|
||||||
|
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()
|
_start = _time.perf_counter()
|
||||||
with timed_status(err_console, "Listing memories...") as _ts:
|
with timed_status(err_console, "Listing memories...") as _ts:
|
||||||
try:
|
try:
|
||||||
@@ -258,8 +392,28 @@ def cmd_list(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output == "quiet":
|
||||||
format_json(console, results)
|
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":
|
elif output == "table":
|
||||||
if results:
|
if results:
|
||||||
format_memories_table(console, results)
|
format_memories_table(console, results)
|
||||||
@@ -301,6 +455,11 @@ def cmd_update(
|
|||||||
output: str,
|
output: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update a memory."""
|
"""Update a memory."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("update")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
meta = None
|
meta = None
|
||||||
if metadata:
|
if metadata:
|
||||||
try:
|
try:
|
||||||
@@ -318,7 +477,14 @@ def cmd_update(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="update",
|
||||||
|
data=result,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
format_json(console, result)
|
format_json(console, result)
|
||||||
elif output != "quiet":
|
elif output != "quiet":
|
||||||
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||||
@@ -333,6 +499,11 @@ def cmd_delete(
|
|||||||
output: str,
|
output: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete a single memory by ID."""
|
"""Delete a single memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
if dry_run:
|
if dry_run:
|
||||||
# Fetch and display what would be deleted
|
# Fetch and display what would be deleted
|
||||||
try:
|
try:
|
||||||
@@ -353,7 +524,14 @@ def cmd_delete(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete",
|
||||||
|
data={"id": memory_id, "deleted": True},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
format_json(console, result)
|
format_json(console, result)
|
||||||
elif output != "quiet":
|
elif output != "quiet":
|
||||||
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||||
@@ -372,12 +550,17 @@ def cmd_delete_all(
|
|||||||
output: str,
|
output: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete all memories matching a scope."""
|
"""Delete all memories matching a scope."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete-all")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
if all_:
|
if all_:
|
||||||
# Project-wide wipe using wildcard entity IDs
|
# Project-wide wipe using wildcard entity IDs
|
||||||
if dry_run:
|
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||||
print_info(console, "Would delete ALL memories project-wide.")
|
|
||||||
print_info(console, "No changes made (dry run).")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not force:
|
if not force:
|
||||||
confirm = typer.confirm(
|
confirm = typer.confirm(
|
||||||
@@ -402,7 +585,14 @@ def cmd_delete_all(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True, "scope": "project"},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
format_json(console, result)
|
format_json(console, result)
|
||||||
elif output != "quiet":
|
elif output != "quiet":
|
||||||
if isinstance(result, dict) and "message" in result:
|
if isinstance(result, dict) and "message" in result:
|
||||||
@@ -460,7 +650,25 @@ def cmd_delete_all(
|
|||||||
raise typer.Exit(1) from None
|
raise typer.Exit(1) from None
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
format_json(console, result)
|
format_json(console, result)
|
||||||
elif output != "quiet":
|
elif output != "quiet":
|
||||||
if isinstance(result, dict) and "message" in result:
|
if isinstance(result, dict) and "message" in result:
|
||||||
|
|||||||
@@ -35,22 +35,26 @@ def cmd_status(
|
|||||||
output: str = "text",
|
output: str = "text",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Check connectivity and auth."""
|
"""Check connectivity and auth."""
|
||||||
from mem0_cli.output import format_json_envelope
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
with timed_status(err_console, "Checking connection...") as _ts:
|
with timed_status(err_console, "Checking connection...") as _ts:
|
||||||
result = backend.status(user_id=user_id, agent_id=agent_id)
|
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output in ("json", "agent"):
|
||||||
format_json_envelope(
|
format_agent_envelope(
|
||||||
console,
|
console,
|
||||||
command="status",
|
command="status",
|
||||||
data={
|
data={
|
||||||
"connected": result.get("connected", False),
|
"connected": result.get("connected", False),
|
||||||
"backend": result.get("backend", "?"),
|
"backend": result.get("backend", "?"),
|
||||||
"base_url": result.get("base_url", ""),
|
"base_url": result.get("base_url", ""),
|
||||||
"latency_ms": int(_elapsed * 1000),
|
|
||||||
},
|
},
|
||||||
duration_ms=int(_elapsed * 1000),
|
duration_ms=int(_elapsed * 1000),
|
||||||
)
|
)
|
||||||
@@ -67,6 +71,14 @@ def cmd_status(
|
|||||||
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
||||||
if result.get("error"):
|
if result.get("error"):
|
||||||
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
||||||
|
if "Authentication failed" in str(result["error"]):
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||||
|
|
||||||
content = "\n".join(lines)
|
content = "\n".join(lines)
|
||||||
@@ -96,7 +108,12 @@ def cmd_import(
|
|||||||
output: str = "text",
|
output: str = "text",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Import memories from a JSON file."""
|
"""Import memories from a JSON file."""
|
||||||
from mem0_cli.output import format_json_envelope
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("import")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(Path(file_path).read_text())
|
data = json.loads(Path(file_path).read_text())
|
||||||
@@ -110,7 +127,9 @@ def cmd_import(
|
|||||||
added = 0
|
added = 0
|
||||||
failed = 0
|
failed = 0
|
||||||
_start = _time.perf_counter()
|
_start = _time.perf_counter()
|
||||||
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
|
for item in track(
|
||||||
|
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
|
||||||
|
):
|
||||||
content = item.get("memory", item.get("text", item.get("content", "")))
|
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||||
if not content:
|
if not content:
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -127,11 +146,13 @@ def cmd_import(
|
|||||||
failed += 1
|
failed += 1
|
||||||
_elapsed = _time.perf_counter() - _start
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
if output == "json":
|
if output in ("json", "agent"):
|
||||||
format_json_envelope(
|
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
|
||||||
|
format_agent_envelope(
|
||||||
console,
|
console,
|
||||||
command="import",
|
command="import",
|
||||||
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
|
data={"added": added, "failed": failed},
|
||||||
|
scope=scope or None,
|
||||||
duration_ms=int(_elapsed * 1000),
|
duration_ms=int(_elapsed * 1000),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ CONFIG_VERSION = 1
|
|||||||
class PlatformConfig:
|
class PlatformConfig:
|
||||||
api_key: str = ""
|
api_key: str = ""
|
||||||
base_url: str = DEFAULT_BASE_URL
|
base_url: str = DEFAULT_BASE_URL
|
||||||
|
user_email: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -38,11 +39,29 @@ class DefaultsConfig:
|
|||||||
enable_graph: bool = False
|
enable_graph: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TelemetryConfig:
|
||||||
|
anonymous_id: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Mem0Config:
|
class Mem0Config:
|
||||||
version: int = CONFIG_VERSION
|
version: int = CONFIG_VERSION
|
||||||
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||||
|
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
||||||
|
|
||||||
|
|
||||||
|
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||||
|
"api_key": "platform.api_key",
|
||||||
|
"base_url": "platform.base_url",
|
||||||
|
"user_email": "platform.user_email",
|
||||||
|
"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:
|
def ensure_config_dir() -> Path:
|
||||||
@@ -65,6 +84,7 @@ def load_config() -> Mem0Config:
|
|||||||
plat = data.get("platform", {})
|
plat = data.get("platform", {})
|
||||||
config.platform.api_key = plat.get("api_key", "")
|
config.platform.api_key = plat.get("api_key", "")
|
||||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||||
|
config.platform.user_email = plat.get("user_email", "")
|
||||||
|
|
||||||
defaults = data.get("defaults", {})
|
defaults = data.get("defaults", {})
|
||||||
config.defaults.user_id = defaults.get("user_id", "")
|
config.defaults.user_id = defaults.get("user_id", "")
|
||||||
@@ -73,6 +93,9 @@ def load_config() -> Mem0Config:
|
|||||||
config.defaults.run_id = defaults.get("run_id", "")
|
config.defaults.run_id = defaults.get("run_id", "")
|
||||||
config.defaults.enable_graph = defaults.get("enable_graph", False)
|
config.defaults.enable_graph = defaults.get("enable_graph", False)
|
||||||
|
|
||||||
|
telemetry = data.get("telemetry", {})
|
||||||
|
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
||||||
|
|
||||||
# Environment variable overrides
|
# Environment variable overrides
|
||||||
env_key = os.environ.get("MEM0_API_KEY")
|
env_key = os.environ.get("MEM0_API_KEY")
|
||||||
if env_key:
|
if env_key:
|
||||||
@@ -121,6 +144,10 @@ def save_config(config: Mem0Config) -> None:
|
|||||||
"platform": {
|
"platform": {
|
||||||
"api_key": config.platform.api_key,
|
"api_key": config.platform.api_key,
|
||||||
"base_url": config.platform.base_url,
|
"base_url": config.platform.base_url,
|
||||||
|
"user_email": config.platform.user_email,
|
||||||
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"anonymous_id": config.telemetry.anonymous_id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +167,8 @@ def redact_key(key: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||||
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
|
"""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(".")
|
parts = dotted_key.split(".")
|
||||||
obj: Any = config
|
obj: Any = config
|
||||||
for part in parts:
|
for part in parts:
|
||||||
@@ -153,6 +181,7 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
|||||||
|
|
||||||
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||||
"""Set a config value by dotted path. Returns True on success."""
|
"""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(".")
|
parts = dotted_key.split(".")
|
||||||
obj: Any = config
|
obj: Any = config
|
||||||
for part in parts[:-1]:
|
for part in parts[:-1]:
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
|||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
def format_memories_table(console: Console, memories: list[dict]) -> None:
|
def format_memories_table(
|
||||||
|
console: Console, memories: list[dict], *, show_score: bool = False
|
||||||
|
) -> None:
|
||||||
"""Render memories in a rich table."""
|
"""Render memories in a rich table."""
|
||||||
table = Table(
|
table = Table(
|
||||||
border_style=BRAND_COLOR,
|
border_style=BRAND_COLOR,
|
||||||
@@ -57,20 +59,34 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
|
|||||||
row_styles=["", "dim"],
|
row_styles=["", "dim"],
|
||||||
padding=(0, 1),
|
padding=(0, 1),
|
||||||
)
|
)
|
||||||
table.add_column("ID", style="dim", max_width=10)
|
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("Memory", max_width=50, no_wrap=False)
|
||||||
table.add_column("Category", max_width=14)
|
table.add_column("Category", max_width=14)
|
||||||
table.add_column("Created", max_width=12)
|
table.add_column("Created", max_width=12)
|
||||||
|
|
||||||
for mem in memories:
|
for mem in memories:
|
||||||
mem_id = mem.get("id", "")[:8]
|
mem_id = mem.get("id", "")
|
||||||
memory_text = mem.get("memory", mem.get("text", ""))
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
if len(memory_text) > 60:
|
if len(memory_text) > 60:
|
||||||
memory_text = memory_text[:57] + "..."
|
memory_text = memory_text[:57] + "..."
|
||||||
categories = mem.get("categories", [])
|
categories = mem.get("categories", [])
|
||||||
cat = categories[0] if isinstance(categories, list) and categories else "—"
|
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 "—"
|
created = _format_date(mem.get("created_at")) or "—"
|
||||||
table.add_row(mem_id, memory_text, cat, created)
|
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()
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@@ -139,16 +155,23 @@ def format_add_result(console: Console, result: dict | list, output: str = "text
|
|||||||
return
|
return
|
||||||
|
|
||||||
console.print()
|
console.print()
|
||||||
|
seen_pending_events: set[str] = set()
|
||||||
for r in results:
|
for r in results:
|
||||||
# Detect async PENDING response from Platform API
|
# Detect async PENDING response from Platform API
|
||||||
if r.get("status") == "PENDING":
|
if r.get("status") == "PENDING":
|
||||||
event_id = r.get("event_id", "")[:8]
|
event_id = r.get("event_id", "")
|
||||||
|
# Deduplicate PENDING entries with the same event_id
|
||||||
|
if event_id and event_id in seen_pending_events:
|
||||||
|
continue
|
||||||
|
if event_id:
|
||||||
|
seen_pending_events.add(event_id)
|
||||||
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
||||||
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
||||||
parts.append("[white]Processing in background[/]")
|
parts.append("[white]Processing in background[/]")
|
||||||
if event_id:
|
|
||||||
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
|
|
||||||
console.print(" ".join(parts))
|
console.print(" ".join(parts))
|
||||||
|
if event_id:
|
||||||
|
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event = r.get("event", "ADD")
|
event = r.get("event", "ADD")
|
||||||
@@ -209,6 +232,100 @@ def format_json_envelope(
|
|||||||
console.print_json(json.dumps(envelope, default=str))
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||||
|
"""Project API response data to minimal relevant fields for agent consumption."""
|
||||||
|
|
||||||
|
def pick(obj: dict, keys: list) -> dict:
|
||||||
|
return {k: obj[k] for k in keys if k in obj}
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "add":
|
||||||
|
items = data if isinstance(data, list) else [data]
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if item.get("status") == "PENDING":
|
||||||
|
result.append(pick(item, ["status", "event_id"]))
|
||||||
|
else:
|
||||||
|
result.append(pick(item, ["id", "memory", "event"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "search":
|
||||||
|
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "list":
|
||||||
|
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "get":
|
||||||
|
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||||
|
|
||||||
|
if command == "update":
|
||||||
|
return pick(data, ["id", "memory"])
|
||||||
|
|
||||||
|
if command in ("delete", "delete-all", "entity delete"):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "entity list":
|
||||||
|
result = []
|
||||||
|
for r in data:
|
||||||
|
item = pick(r, ["type", "count"])
|
||||||
|
item["name"] = r.get("name") or r.get("id", "")
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "event list":
|
||||||
|
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
|
||||||
|
|
||||||
|
if command == "event status":
|
||||||
|
ev = data
|
||||||
|
raw_results = ev.get("results") or []
|
||||||
|
sanitized_results = []
|
||||||
|
for r in raw_results:
|
||||||
|
nested = r.get("data") or {}
|
||||||
|
memory = nested.get("memory") if isinstance(nested, dict) else None
|
||||||
|
sanitized_results.append(
|
||||||
|
{
|
||||||
|
"id": r.get("id"),
|
||||||
|
"event": r.get("event"),
|
||||||
|
"user_id": r.get("user_id"),
|
||||||
|
"memory": memory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
|
||||||
|
result["results"] = sanitized_results
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Pass-through: status, import, config show/get/set
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def format_agent_envelope(
|
||||||
|
console: Console,
|
||||||
|
*,
|
||||||
|
command: str,
|
||||||
|
data: Any,
|
||||||
|
duration_ms: int | None = None,
|
||||||
|
scope: dict | None = None,
|
||||||
|
count: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
|
||||||
|
envelope: dict[str, Any] = {
|
||||||
|
"status": "success",
|
||||||
|
"command": command,
|
||||||
|
}
|
||||||
|
if duration_ms is not None:
|
||||||
|
envelope["duration_ms"] = duration_ms
|
||||||
|
if scope:
|
||||||
|
filtered = {k: v for k, v in scope.items() if v}
|
||||||
|
if filtered:
|
||||||
|
envelope["scope"] = filtered
|
||||||
|
if count is not None:
|
||||||
|
envelope["count"] = count
|
||||||
|
envelope["data"] = sanitize_agent_data(command, data)
|
||||||
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
def print_result_summary(
|
def print_result_summary(
|
||||||
console: Console,
|
console: Console,
|
||||||
count: int,
|
count: int,
|
||||||
@@ -221,7 +338,7 @@ def print_result_summary(
|
|||||||
parts = [f"{count} result{'s' if count != 1 else ''}"]
|
parts = [f"{count} result{'s' if count != 1 else ''}"]
|
||||||
if page is not None:
|
if page is not None:
|
||||||
parts.append(f"page {page}")
|
parts.append(f"page {page}")
|
||||||
scope_parts = [f"{k.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
|
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
|
||||||
if scope_parts:
|
if scope_parts:
|
||||||
parts.append(", ".join(scope_parts))
|
parts.append(", ".join(scope_parts))
|
||||||
if duration_secs is not None:
|
if duration_secs is not None:
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
"""CLI telemetry — anonymous usage tracking via PostHog.
|
||||||
|
|
||||||
|
Sends fire-and-forget events to PostHog by spawning a detached subprocess
|
||||||
|
(telemetry_sender.py). The parent CLI process exits immediately; the
|
||||||
|
subprocess handles email resolution, caching, and the HTTP POST.
|
||||||
|
|
||||||
|
Disable with: MEM0_TELEMETRY=false
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
|
||||||
|
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_telemetry_enabled() -> bool:
|
||||||
|
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
|
||||||
|
return val not in ("false", "0", "no")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_anonymous_id() -> str:
|
||||||
|
"""Return a persistent per-machine anonymous ID, generating one if needed.
|
||||||
|
|
||||||
|
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
|
||||||
|
repeat runs on the same machine share one PostHog identity instead of
|
||||||
|
collapsing into a single shared fallback string.
|
||||||
|
"""
|
||||||
|
from mem0_cli.config import load_config, save_config
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
if config.telemetry.anonymous_id:
|
||||||
|
return config.telemetry.anonymous_id
|
||||||
|
|
||||||
|
new_id = f"cli-anon-{uuid.uuid4().hex}"
|
||||||
|
config.telemetry.anonymous_id = new_id
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
save_config(config)
|
||||||
|
return new_id
|
||||||
|
|
||||||
|
|
||||||
|
def _get_distinct_id() -> str:
|
||||||
|
"""Return a stable anonymous identifier for the current user.
|
||||||
|
|
||||||
|
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
|
||||||
|
persistent per-machine anonymous ID.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from mem0_cli.config import load_config
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
if config.platform.user_email:
|
||||||
|
return config.platform.user_email
|
||||||
|
if config.platform.api_key:
|
||||||
|
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return _get_or_create_anonymous_id()
|
||||||
|
except Exception:
|
||||||
|
return f"cli-anon-{uuid.uuid4().hex}"
|
||||||
|
|
||||||
|
|
||||||
|
def capture_event(
|
||||||
|
event_name: str,
|
||||||
|
properties: dict[str, Any] | None = None,
|
||||||
|
pre_resolved_email: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Fire a PostHog event via a detached subprocess (non-blocking).
|
||||||
|
|
||||||
|
When *pre_resolved_email* is provided (e.g. from an upfront ping
|
||||||
|
validation), it is used directly as the PostHog distinct ID and the
|
||||||
|
subprocess skips its own ``/v1/ping/`` call.
|
||||||
|
"""
|
||||||
|
if not _is_telemetry_enabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mem0_cli import __version__
|
||||||
|
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||||
|
|
||||||
|
# Detect anonymous → identified transition. If a stored anonymous_id
|
||||||
|
# exists and we just resolved to a real identity, fire a one-shot
|
||||||
|
# $identify event so PostHog stitches the pre-signup history onto
|
||||||
|
# the authenticated profile. Clear the stored id so we don't re-alias.
|
||||||
|
anon_id_to_alias: str | None = None
|
||||||
|
if (
|
||||||
|
distinct_id
|
||||||
|
and not distinct_id.startswith("cli-anon-")
|
||||||
|
and config.telemetry.anonymous_id
|
||||||
|
):
|
||||||
|
anon_id_to_alias = config.telemetry.anonymous_id
|
||||||
|
config.telemetry.anonymous_id = ""
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"api_key": POSTHOG_API_KEY,
|
||||||
|
"distinct_id": distinct_id,
|
||||||
|
"event": event_name,
|
||||||
|
"properties": {
|
||||||
|
"source": "CLI",
|
||||||
|
"language": "python",
|
||||||
|
"cli_version": __version__,
|
||||||
|
"agent_mode": is_agent_mode(),
|
||||||
|
"python_version": sys.version,
|
||||||
|
"os": sys.platform,
|
||||||
|
"os_version": platform.version(),
|
||||||
|
"$process_person_profile": False,
|
||||||
|
"$lib": "posthog-python",
|
||||||
|
**(properties or {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"payload": payload,
|
||||||
|
"posthog_host": POSTHOG_HOST,
|
||||||
|
"needs_email": not distinct_id or "@" not in distinct_id,
|
||||||
|
"mem0_api_key": config.platform.api_key or "",
|
||||||
|
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
|
||||||
|
"config_path": str(CONFIG_FILE),
|
||||||
|
"anon_distinct_id_to_alias": anon_id_to_alias,
|
||||||
|
}
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
close_fds=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
"""Standalone telemetry sender — runs as a detached subprocess.
|
||||||
|
|
||||||
|
Usage: python -m mem0_cli.telemetry_sender '<json context>'
|
||||||
|
|
||||||
|
This module is spawned by telemetry.capture_event() and runs independently
|
||||||
|
of the parent CLI process. It:
|
||||||
|
|
||||||
|
1. Resolves the user's email via /v1/ping/ if not already cached
|
||||||
|
2. Caches the email in ~/.mem0/config.json for future runs
|
||||||
|
3. Sends the PostHog event
|
||||||
|
|
||||||
|
All errors are silently swallowed — this process must never produce output
|
||||||
|
or affect the user experience.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ctx = json.loads(sys.argv[1])
|
||||||
|
payload = ctx["payload"]
|
||||||
|
|
||||||
|
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
||||||
|
_resolve_and_cache_email(ctx, payload)
|
||||||
|
|
||||||
|
# Fire $identify *after* email resolution so PostHog links the stored
|
||||||
|
# anonymous id directly to the final identity (email, not the api-key
|
||||||
|
# hash). The regular event is sent next so it lands under the merged
|
||||||
|
# profile.
|
||||||
|
anon_id = ctx.get("anon_distinct_id_to_alias")
|
||||||
|
if anon_id:
|
||||||
|
_send_identify_event(ctx, payload, anon_id)
|
||||||
|
|
||||||
|
_send_posthog_event(ctx["posthog_host"], payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_identify_event(ctx: dict, payload: dict, anon_id: str) -> None:
|
||||||
|
"""Send a PostHog $identify event aliasing anon_id → payload['distinct_id']."""
|
||||||
|
identify_payload = {
|
||||||
|
"api_key": payload["api_key"],
|
||||||
|
"event": "$identify",
|
||||||
|
"distinct_id": payload["distinct_id"],
|
||||||
|
"properties": {
|
||||||
|
"$anon_distinct_id": anon_id,
|
||||||
|
"$lib": payload.get("properties", {}).get("$lib", "posthog-python"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_send_posthog_event(ctx["posthog_host"], identify_payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
|
||||||
|
"""Call /v1/ping/ to get the user's email, update the payload, and cache it."""
|
||||||
|
try:
|
||||||
|
ping_url = ctx["mem0_base_url"].rstrip("/") + "/v1/ping/"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
ping_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": "Token " + ctx["mem0_api_key"],
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
resp = urllib.request.urlopen(req, timeout=10)
|
||||||
|
data = json.loads(resp.read())
|
||||||
|
email = data.get("user_email")
|
||||||
|
if email:
|
||||||
|
payload["distinct_id"] = email
|
||||||
|
_cache_email(ctx.get("config_path"), email)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_email(config_path: str | None, email: str) -> None:
|
||||||
|
"""Write user_email into the config file for future runs."""
|
||||||
|
if not config_path:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(config_path) as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
cfg.setdefault("platform", {})["user_email"] = email
|
||||||
|
with open(config_path, "w") as f:
|
||||||
|
json.dump(cfg, f, indent=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _send_posthog_event(posthog_host: str, payload: dict) -> None:
|
||||||
|
"""POST the event to PostHog."""
|
||||||
|
try:
|
||||||
|
body = json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
posthog_host,
|
||||||
|
data=body,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
main()
|
||||||
@@ -96,6 +96,43 @@ def mock_backend():
|
|||||||
{"name": "alice", "count": 5},
|
{"name": "alice", "count": 5},
|
||||||
{"name": "bob", "count": 3},
|
{"name": "bob", "count": 3},
|
||||||
]
|
]
|
||||||
|
backend.list_events.return_value = [
|
||||||
|
{
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "evt-def-456-ghi-789",
|
||||||
|
"event_type": "SEARCH",
|
||||||
|
"status": "PENDING",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": None,
|
||||||
|
"created_at": "2026-04-01T10:01:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
backend.get_event.return_value = {
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": "SUCCEEDED",
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "mem-abc-123",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"data": {"memory": "User prefers dark mode"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,19 @@ boundaries).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
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(
|
def _run(
|
||||||
args: list[str],
|
args: list[str],
|
||||||
@@ -28,22 +36,35 @@ def _run(
|
|||||||
reads config from ``<home_dir>/.mem0/config.json`` instead
|
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||||
of the user's real config. This is critical for tests that
|
of the user's real config. This is critical for tests that
|
||||||
depend on a clean (no API key) or custom config state.
|
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()
|
env = os.environ.copy()
|
||||||
# Strip all MEM0_ env vars so tests start clean
|
# Strip all MEM0_ env vars so tests start clean
|
||||||
for key in list(env.keys()):
|
for key in list(env.keys()):
|
||||||
if key.startswith("MEM0_"):
|
if key.startswith("MEM0_"):
|
||||||
del env[key]
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
if home_dir:
|
if home_dir:
|
||||||
env["HOME"] = home_dir
|
env["HOME"] = home_dir
|
||||||
if env_override:
|
if env_override:
|
||||||
env.update(env_override)
|
env.update(env_override)
|
||||||
return subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "mem0_cli", *args],
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -62,12 +83,6 @@ class TestCLIIntegration:
|
|||||||
assert "add" in result.stdout
|
assert "add" in result.stdout
|
||||||
assert "search" in result.stdout
|
assert "search" in result.stdout
|
||||||
|
|
||||||
def test_version_flag(self):
|
|
||||||
result = _run(["--version"])
|
|
||||||
assert result.returncode == 0
|
|
||||||
assert "0.1.0" in result.stdout
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_help(self):
|
def test_add_help(self):
|
||||||
result = _run(["add", "--help"])
|
result = _run(["add", "--help"])
|
||||||
assert result.returncode == 0
|
assert result.returncode == 0
|
||||||
@@ -161,7 +176,12 @@ class TestCLIIsolated:
|
|||||||
)
|
)
|
||||||
assert result.returncode != 0
|
assert result.returncode != 0
|
||||||
combined = result.stderr + result.stdout
|
combined = result.stderr + result.stdout
|
||||||
assert "memory ID" in combined.lower() or "--all" in combined or "--entity" in combined or "Error" in combined
|
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):
|
def test_config_show_clean(self, clean_home):
|
||||||
"""config show with no config should still work."""
|
"""config show with no config should still work."""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import typing
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ from mem0_cli.commands.config_cmd import (
|
|||||||
cmd_config_show,
|
cmd_config_show,
|
||||||
)
|
)
|
||||||
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list
|
from mem0_cli.commands.entities import cmd_entities_delete, cmd_entities_list
|
||||||
|
from mem0_cli.commands.events_cmd import cmd_event_list, cmd_event_status
|
||||||
from mem0_cli.commands.memory import (
|
from mem0_cli.commands.memory import (
|
||||||
cmd_add,
|
cmd_add,
|
||||||
cmd_delete,
|
cmd_delete,
|
||||||
@@ -28,7 +30,6 @@ from mem0_cli.commands.memory import (
|
|||||||
from mem0_cli.commands.utils import (
|
from mem0_cli.commands.utils import (
|
||||||
cmd_import,
|
cmd_import,
|
||||||
cmd_status,
|
cmd_status,
|
||||||
cmd_version,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
@@ -88,7 +89,7 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
@@ -113,7 +114,7 @@ class TestAddCommand:
|
|||||||
metadata='{"source": "test"}',
|
metadata='{"source": "test"}',
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
@@ -139,7 +140,7 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="json",
|
output="json",
|
||||||
)
|
)
|
||||||
@@ -165,7 +166,7 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="quiet",
|
output="quiet",
|
||||||
)
|
)
|
||||||
@@ -176,30 +177,28 @@ class TestAddCommand:
|
|||||||
def test_add_no_content_exits(self, mock_backend):
|
def test_add_no_content_exits(self, mock_backend):
|
||||||
console, _buf = _make_console()
|
console, _buf = _make_console()
|
||||||
err_console, _err_buf = _make_err_console()
|
err_console, _err_buf = _make_err_console()
|
||||||
# Patch stdin.isatty to return True so it doesn't try to read stdin
|
|
||||||
with (
|
with (
|
||||||
patch("mem0_cli.commands.memory.console", console),
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
patch("mem0_cli.commands.memory.sys") as mock_sys,
|
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||||
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
mock_sys.stdin.isatty.return_value = True
|
cmd_add(
|
||||||
with pytest.raises((SystemExit, ClickExit)):
|
mock_backend,
|
||||||
cmd_add(
|
None,
|
||||||
mock_backend,
|
user_id="alice",
|
||||||
None,
|
agent_id=None,
|
||||||
user_id="alice",
|
app_id=None,
|
||||||
agent_id=None,
|
run_id=None,
|
||||||
app_id=None,
|
messages=None,
|
||||||
run_id=None,
|
file=None,
|
||||||
messages=None,
|
metadata=None,
|
||||||
file=None,
|
immutable=False,
|
||||||
metadata=None,
|
no_infer=False,
|
||||||
immutable=False,
|
expires=None,
|
||||||
no_infer=False,
|
categories=None,
|
||||||
expires=None,
|
output="text",
|
||||||
categories=None,
|
)
|
||||||
output="text",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_add_invalid_metadata_json(self, mock_backend):
|
def test_add_invalid_metadata_json(self, mock_backend):
|
||||||
console, _buf = _make_console()
|
console, _buf = _make_console()
|
||||||
@@ -221,7 +220,7 @@ class TestAddCommand:
|
|||||||
metadata="not-json",
|
metadata="not-json",
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
@@ -247,7 +246,7 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
@@ -272,13 +271,73 @@ class TestAddCommand:
|
|||||||
metadata=None,
|
metadata=None,
|
||||||
immutable=False,
|
immutable=False,
|
||||||
no_infer=False,
|
no_infer=False,
|
||||||
expires=None,
|
expires=None,
|
||||||
categories="health,prefs",
|
categories="health,prefs",
|
||||||
output="text",
|
output="text",
|
||||||
)
|
)
|
||||||
mock_backend.add.assert_called_once()
|
mock_backend.add.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddDeduplicatesPending:
|
||||||
|
"""Ensure duplicate PENDING entries with the same event_id are collapsed."""
|
||||||
|
|
||||||
|
DUPLICATE_PENDING: typing.ClassVar[dict] = {
|
||||||
|
"results": [
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _run_add(self, mock_backend, output):
|
||||||
|
mock_backend.add.return_value = self.DUPLICATE_PENDING
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_add(
|
||||||
|
mock_backend,
|
||||||
|
"test",
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
messages=None,
|
||||||
|
file=None,
|
||||||
|
metadata=None,
|
||||||
|
immutable=False,
|
||||||
|
no_infer=False,
|
||||||
|
expires=None,
|
||||||
|
categories=None,
|
||||||
|
output=output,
|
||||||
|
)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
def test_text_shows_one_pending(self, mock_backend):
|
||||||
|
raw = self._run_add(mock_backend, "text")
|
||||||
|
assert raw.count("Queued") == 1
|
||||||
|
|
||||||
|
def test_json_shows_one_pending(self, mock_backend):
|
||||||
|
raw = self._run_add(mock_backend, "json")
|
||||||
|
data = json.loads(raw)
|
||||||
|
results = data.get("results", data)
|
||||||
|
pending = [r for r in results if r.get("status") == "PENDING"]
|
||||||
|
assert len(pending) == 1
|
||||||
|
|
||||||
|
def test_agent_shows_one_pending(self, mock_backend):
|
||||||
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
|
set_agent_mode(True)
|
||||||
|
try:
|
||||||
|
raw = self._run_add(mock_backend, "agent")
|
||||||
|
finally:
|
||||||
|
set_agent_mode(False)
|
||||||
|
data = json.loads(raw)
|
||||||
|
assert data["count"] == 1
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
|
||||||
|
|
||||||
class TestSearchCommand:
|
class TestSearchCommand:
|
||||||
def test_search_text(self, mock_backend):
|
def test_search_text(self, mock_backend):
|
||||||
console, buf = _make_console()
|
console, buf = _make_console()
|
||||||
@@ -618,28 +677,6 @@ class TestDeleteAllCommand:
|
|||||||
run_id="*",
|
run_id="*",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_all_project_wide_dry_run(self, mock_backend):
|
|
||||||
console, buf = _make_console()
|
|
||||||
err_console, _err_buf = _make_err_console()
|
|
||||||
with (
|
|
||||||
patch("mem0_cli.commands.memory.console", console),
|
|
||||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
|
||||||
):
|
|
||||||
cmd_delete_all(
|
|
||||||
mock_backend,
|
|
||||||
force=True,
|
|
||||||
all_=True,
|
|
||||||
dry_run=True,
|
|
||||||
user_id=None,
|
|
||||||
agent_id=None,
|
|
||||||
app_id=None,
|
|
||||||
run_id=None,
|
|
||||||
output="text",
|
|
||||||
)
|
|
||||||
output = buf.getvalue()
|
|
||||||
assert "project-wide" in output.lower()
|
|
||||||
mock_backend.delete.assert_not_called()
|
|
||||||
|
|
||||||
def test_delete_all_project_wide_async_response(self, mock_backend):
|
def test_delete_all_project_wide_async_response(self, mock_backend):
|
||||||
mock_backend.delete.return_value = {"message": "Memories deletion started..."}
|
mock_backend.delete.return_value = {"message": "Memories deletion started..."}
|
||||||
console, buf = _make_console()
|
console, buf = _make_console()
|
||||||
@@ -704,15 +741,6 @@ class TestStatusCommand:
|
|||||||
assert '"status"' in output
|
assert '"status"' in output
|
||||||
|
|
||||||
|
|
||||||
class TestVersionCommand:
|
|
||||||
def test_version(self):
|
|
||||||
console, buf = _make_console()
|
|
||||||
with patch("mem0_cli.commands.utils.console", console):
|
|
||||||
cmd_version()
|
|
||||||
output = buf.getvalue()
|
|
||||||
assert "0.1.0" in output
|
|
||||||
|
|
||||||
|
|
||||||
class TestImportCommand:
|
class TestImportCommand:
|
||||||
def test_import_json(self, mock_backend, tmp_path):
|
def test_import_json(self, mock_backend, tmp_path):
|
||||||
file_path = tmp_path / "import.json"
|
file_path = tmp_path / "import.json"
|
||||||
@@ -888,6 +916,28 @@ class TestEntitiesDeleteCommand:
|
|||||||
output = buf.getvalue()
|
output = buf.getvalue()
|
||||||
assert "deleted" in output.lower()
|
assert "deleted" in output.lower()
|
||||||
|
|
||||||
|
def test_delete_entity_agent_id(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.entities.console", console),
|
||||||
|
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_entities_delete(
|
||||||
|
mock_backend,
|
||||||
|
user_id=None,
|
||||||
|
agent_id="bot1",
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
force=True,
|
||||||
|
output="text",
|
||||||
|
)
|
||||||
|
mock_backend.delete_entities.assert_called_once_with(
|
||||||
|
user_id=None, agent_id="bot1", app_id=None, run_id=None
|
||||||
|
)
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "deleted" in output.lower()
|
||||||
|
|
||||||
def test_delete_entity_no_id_exits(self, mock_backend):
|
def test_delete_entity_no_id_exits(self, mock_backend):
|
||||||
console, _buf = _make_console()
|
console, _buf = _make_console()
|
||||||
err_console, _err_buf = _make_err_console()
|
err_console, _err_buf = _make_err_console()
|
||||||
@@ -1024,3 +1074,388 @@ class TestEnableGraph:
|
|||||||
)
|
)
|
||||||
call_kwargs = mock_backend.list_memories.call_args
|
call_kwargs = mock_backend.list_memories.call_args
|
||||||
assert call_kwargs.kwargs.get("enable_graph") is True
|
assert call_kwargs.kwargs.get("enable_graph") is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestEventCommands:
|
||||||
|
def test_event_list_table(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_list(mock_backend, output="table")
|
||||||
|
out = buf.getvalue()
|
||||||
|
assert "evt-abc-" in out
|
||||||
|
assert "ADD" in out
|
||||||
|
assert "SUCCEEDED" in out
|
||||||
|
|
||||||
|
def test_event_list_json(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_list(mock_backend, output="json")
|
||||||
|
out = buf.getvalue()
|
||||||
|
assert "evt-abc-123-def-456" in out
|
||||||
|
assert "evt-def-456-ghi-789" in out
|
||||||
|
|
||||||
|
def test_event_list_empty(self, mock_backend):
|
||||||
|
mock_backend.list_events.return_value = []
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_list(mock_backend, output="table")
|
||||||
|
out = buf.getvalue()
|
||||||
|
assert "No events" in out
|
||||||
|
|
||||||
|
def test_event_status_text(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||||
|
out = buf.getvalue()
|
||||||
|
assert "evt-abc-123-def-456" in out
|
||||||
|
assert "SUCCEEDED" in out
|
||||||
|
|
||||||
|
def test_event_status_json(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="json")
|
||||||
|
out = buf.getvalue()
|
||||||
|
assert "evt-abc-123-def-456" in out
|
||||||
|
assert "ADD" in out
|
||||||
|
|
||||||
|
|
||||||
|
class TestAgentMode:
|
||||||
|
"""Tests for --json/--agent mode: structured JSON envelope output."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Enable agent mode before each test."""
|
||||||
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
|
set_agent_mode(True)
|
||||||
|
|
||||||
|
def teardown_method(self):
|
||||||
|
"""Reset agent mode after each test."""
|
||||||
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
|
set_agent_mode(False)
|
||||||
|
|
||||||
|
# ── add ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_add_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_add(
|
||||||
|
mock_backend,
|
||||||
|
"I prefer dark mode",
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
messages=None,
|
||||||
|
file=None,
|
||||||
|
metadata=None,
|
||||||
|
immutable=False,
|
||||||
|
no_infer=False,
|
||||||
|
expires=None,
|
||||||
|
categories=None,
|
||||||
|
output="text", # will be overridden to "agent"
|
||||||
|
)
|
||||||
|
raw = buf.getvalue()
|
||||||
|
data = json.loads(raw)
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "add"
|
||||||
|
assert "data" in data
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
assert data["count"] == 1
|
||||||
|
assert set(data["data"][0].keys()) == {"id", "memory", "event"}
|
||||||
|
|
||||||
|
def test_add_agent_mode_scope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_add(
|
||||||
|
mock_backend,
|
||||||
|
"test",
|
||||||
|
user_id="bob",
|
||||||
|
agent_id="agent1",
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
messages=None,
|
||||||
|
file=None,
|
||||||
|
metadata=None,
|
||||||
|
immutable=False,
|
||||||
|
no_infer=False,
|
||||||
|
expires=None,
|
||||||
|
categories=None,
|
||||||
|
output="text",
|
||||||
|
)
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["scope"]["user_id"] == "bob"
|
||||||
|
assert data["scope"]["agent_id"] == "agent1"
|
||||||
|
|
||||||
|
# ── search ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_search_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_search(
|
||||||
|
mock_backend,
|
||||||
|
"dark mode",
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
top_k=10,
|
||||||
|
threshold=0.3,
|
||||||
|
rerank=False,
|
||||||
|
keyword=False,
|
||||||
|
filter_json=None,
|
||||||
|
fields=None,
|
||||||
|
output="text",
|
||||||
|
)
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "search"
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert "duration_ms" in data
|
||||||
|
assert set(data["data"][0].keys()) == {"id", "memory", "score", "created_at", "categories"}
|
||||||
|
|
||||||
|
# ── list ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_list_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_list(
|
||||||
|
mock_backend,
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
page=1,
|
||||||
|
page_size=100,
|
||||||
|
category=None,
|
||||||
|
after=None,
|
||||||
|
before=None,
|
||||||
|
output="table", # will be overridden to "agent"
|
||||||
|
)
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "list"
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert data["scope"]["user_id"] == "alice"
|
||||||
|
assert set(data["data"][0].keys()) == {"id", "memory", "created_at", "categories"}
|
||||||
|
|
||||||
|
# ── get ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_get_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_get(mock_backend, "abc-123-def-456", output="text")
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "get"
|
||||||
|
assert isinstance(data["data"], dict)
|
||||||
|
assert data["data"]["id"] == "abc-123-def-456"
|
||||||
|
assert "memory" in data["data"]
|
||||||
|
assert set(data["data"].keys()) >= {"id", "memory"}
|
||||||
|
|
||||||
|
# ── update ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_update_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_update(mock_backend, "abc-123", "Updated content", metadata=None, output="text")
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "update"
|
||||||
|
assert isinstance(data["data"], dict)
|
||||||
|
assert "memory" in data["data"]
|
||||||
|
assert "duration_ms" in data
|
||||||
|
|
||||||
|
# ── delete ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_delete_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_delete(mock_backend, "abc-123-def-456", output="text")
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "delete"
|
||||||
|
assert data["data"]["id"] == "abc-123-def-456"
|
||||||
|
assert data["data"]["deleted"] is True
|
||||||
|
assert "duration_ms" in data
|
||||||
|
|
||||||
|
# ── event list ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_event_list_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_list(mock_backend, output="table")
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "event list"
|
||||||
|
assert isinstance(data["data"], list)
|
||||||
|
assert data["count"] == 2
|
||||||
|
assert "duration_ms" in data
|
||||||
|
assert set(data["data"][0].keys()) == {
|
||||||
|
"id",
|
||||||
|
"event_type",
|
||||||
|
"status",
|
||||||
|
"latency",
|
||||||
|
"created_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── event status ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_event_status_agent_mode_envelope(self, mock_backend):
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.events_cmd.console", console),
|
||||||
|
patch("mem0_cli.commands.events_cmd.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_event_status(mock_backend, "evt-abc-123-def-456", output="text")
|
||||||
|
data = json.loads(buf.getvalue())
|
||||||
|
assert data["status"] == "success"
|
||||||
|
assert data["command"] == "event status"
|
||||||
|
assert isinstance(data["data"], dict)
|
||||||
|
assert data["data"]["id"] == "evt-abc-123-def-456"
|
||||||
|
assert "duration_ms" in data
|
||||||
|
assert set(data["data"]["results"][0].keys()) == {"id", "event", "user_id", "memory"}
|
||||||
|
assert "data" not in data["data"]["results"][0]
|
||||||
|
|
||||||
|
# ── error handling ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def test_error_in_agent_mode_produces_json_to_stdout(self, mock_backend):
|
||||||
|
"""Errors in agent mode must emit a JSON envelope to stdout, not stderr."""
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
mock_backend.get.side_effect = Exception("Memory not found")
|
||||||
|
console, _buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
|
||||||
|
captured_stdout = StringIO()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
patch("sys.stdout", captured_stdout),
|
||||||
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
|
):
|
||||||
|
cmd_get(mock_backend, "bad-id", output="text")
|
||||||
|
|
||||||
|
stdout_output = captured_stdout.getvalue()
|
||||||
|
# The error JSON envelope must be on stdout
|
||||||
|
error_data = json.loads(stdout_output)
|
||||||
|
assert error_data["status"] == "error"
|
||||||
|
assert "error" in error_data
|
||||||
|
assert error_data["data"] is None
|
||||||
|
|
||||||
|
def test_branding_suppressed_in_agent_mode(self, mock_backend):
|
||||||
|
"""Scope line and success message must be absent in agent mode output."""
|
||||||
|
console, buf = _make_console()
|
||||||
|
err_console, _err_buf = _make_err_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
|
):
|
||||||
|
cmd_add(
|
||||||
|
mock_backend,
|
||||||
|
"branding test",
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
messages=None,
|
||||||
|
file=None,
|
||||||
|
metadata=None,
|
||||||
|
immutable=False,
|
||||||
|
no_infer=False,
|
||||||
|
expires=None,
|
||||||
|
categories=None,
|
||||||
|
output="text",
|
||||||
|
)
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Must be valid JSON only — no human-readable branding
|
||||||
|
data = json.loads(output)
|
||||||
|
assert data["status"] == "success"
|
||||||
|
# "Scope:" and "Memory processed" must NOT appear in the raw output
|
||||||
|
assert "Scope:" not in output
|
||||||
|
assert "Memory processed" not in output
|
||||||
|
assert "spinner" not in output.lower()
|
||||||
|
|
||||||
|
def test_no_spinner_in_agent_mode(self, mock_backend):
|
||||||
|
"""timed_status must not emit spinner output in agent mode."""
|
||||||
|
err_buf = StringIO()
|
||||||
|
err_console_buf = Console(file=err_buf, force_terminal=False, no_color=True, width=120)
|
||||||
|
console, _buf = _make_console()
|
||||||
|
with (
|
||||||
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
|
patch("mem0_cli.commands.memory.err_console", err_console_buf),
|
||||||
|
):
|
||||||
|
cmd_search(
|
||||||
|
mock_backend,
|
||||||
|
"query",
|
||||||
|
user_id="alice",
|
||||||
|
agent_id=None,
|
||||||
|
app_id=None,
|
||||||
|
run_id=None,
|
||||||
|
top_k=5,
|
||||||
|
threshold=0.3,
|
||||||
|
rerank=False,
|
||||||
|
keyword=False,
|
||||||
|
filter_json=None,
|
||||||
|
fields=None,
|
||||||
|
output="text",
|
||||||
|
)
|
||||||
|
# The err_buf captures what would have been spinner/timing noise
|
||||||
|
# In agent mode it should be empty (no status lines printed)
|
||||||
|
err_output = err_buf.getvalue()
|
||||||
|
assert "Searching" not in err_output
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from mem0_cli.output import (
|
|||||||
format_memories_table,
|
format_memories_table,
|
||||||
format_memories_text,
|
format_memories_text,
|
||||||
format_single_memory,
|
format_single_memory,
|
||||||
|
sanitize_agent_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -129,8 +130,153 @@ class TestAddResult:
|
|||||||
output = buf.getvalue()
|
output = buf.getvalue()
|
||||||
assert output.strip() == ""
|
assert output.strip() == ""
|
||||||
|
|
||||||
|
def test_format_add_result_deduplicates_pending_by_event_id(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Should show only one PENDING block despite two entries with same event_id
|
||||||
|
assert output.count("evt-dup") == 2 # event_id line + status hint line
|
||||||
|
assert output.count("Queued") == 1
|
||||||
|
|
||||||
def test_format_add_result_empty(self):
|
def test_format_add_result_empty(self):
|
||||||
console, buf = _make_console()
|
console, buf = _make_console()
|
||||||
format_add_result(console, {"results": []}, "text")
|
format_add_result(console, {"results": []}, "text")
|
||||||
output = buf.getvalue()
|
output = buf.getvalue()
|
||||||
assert "No memories extracted" in output
|
assert "No memories extracted" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestSanitizeAgentData:
|
||||||
|
def test_add_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"event": "ADD",
|
||||||
|
"metadata": {"x": 1},
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
|
||||||
|
|
||||||
|
def test_add_pending_passthrough(self):
|
||||||
|
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
|
||||||
|
|
||||||
|
def test_search_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
"agent_id": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("search", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("list", raw)
|
||||||
|
assert result == [
|
||||||
|
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_get_projects_fields(self):
|
||||||
|
raw = {
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"categories": ["a"],
|
||||||
|
"metadata": {"k": "v"},
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("get", raw)
|
||||||
|
assert "user_id" not in result
|
||||||
|
assert "id" in result and "memory" in result
|
||||||
|
|
||||||
|
def test_update_projects_fields(self):
|
||||||
|
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
|
||||||
|
result = sanitize_agent_data("update", raw)
|
||||||
|
assert result == {"id": "abc", "memory": "updated"}
|
||||||
|
|
||||||
|
def test_event_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("event list", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert "updated_at" not in result[0]
|
||||||
|
assert "graph_status" not in result[0]
|
||||||
|
|
||||||
|
def test_event_status_flattens_results(self):
|
||||||
|
raw = {
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"results": [
|
||||||
|
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("event status", raw)
|
||||||
|
assert result["results"][0] == {
|
||||||
|
"id": "mem-1",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"memory": "dark mode",
|
||||||
|
}
|
||||||
|
assert "data" not in result["results"][0]
|
||||||
|
|
||||||
|
def test_passthrough_commands(self):
|
||||||
|
for cmd in ("status", "import", "config show", "config get", "config set"):
|
||||||
|
data = {"key": "value", "other": "stuff"}
|
||||||
|
assert sanitize_agent_data(cmd, data) == data
|
||||||
|
|
||||||
|
def test_none_data(self):
|
||||||
|
assert sanitize_agent_data("add", None) is None
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ description: "REST APIs for memory management, search, and entity operations"
|
|||||||
Mem0 provides a comprehensive REST API for integrating advanced memory capabilities into your applications. Create, search, update, and manage memories across users, agents, and custom entities with simple HTTP requests.
|
Mem0 provides a comprehensive REST API for integrating advanced memory capabilities into your applications. Create, search, update, and manage memories across users, agents, and custom entities with simple HTTP requests.
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
**Quick start:** Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys) and make your first memory operation in minutes.
|
**Quick start:** Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a> and make your first memory operation in minutes.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -87,7 +87,7 @@ All API requests require authentication using Token-based authentication. Includ
|
|||||||
Authorization: Token <your-api-key>
|
Authorization: Token <your-api-key>
|
||||||
```
|
```
|
||||||
|
|
||||||
Get your API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
|
Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
**Keep your API key secure.** Never expose it in client-side code or public repositories. Use environment variables and server-side requests only.
|
**Keep your API key secure.** Never expose it in client-side code or public repositories. Use environment variables and server-side requests only.
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ Provide at least one message or direct memory string. Most callers supply `messa
|
|||||||
| `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. |
|
| `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. |
|
||||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
||||||
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
||||||
| `async_mode` | boolean (default `true`) | Optional | Controls asynchronous processing. Most clients leave this enabled. |
|
|
||||||
| `output_format` | string (default `v1.1`) | Optional | Response format. `v1.1` wraps results in a `results` array. |
|
|
||||||
|
|
||||||
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
|
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
|
||||||
|
|
||||||
@@ -83,20 +81,3 @@ Successful requests return an array of events queued for processing. Each event
|
|||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
## Graph relationships
|
|
||||||
|
|
||||||
Add Memories can enrich the knowledge graph on write. Set `enable_graph: true` to create entity nodes and relationships for the stored memory. Use this when you want downstream `get_all` or search calls to traverse connected entities.
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```json Graph-aware request
|
|
||||||
{
|
|
||||||
"user_id": "alice",
|
|
||||||
"messages": [
|
|
||||||
{ "role": "user", "content": "I met with Dr. Lee at General Hospital." }
|
|
||||||
],
|
|
||||||
"enable_graph": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
</CodeGroup>
|
|
||||||
|
|
||||||
The response follows the same format, and related entities become available in [Graph Memory](/platform/features/graph-memory) queries.
|
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ To retrieve graph memory relationships between entities, pass `output_format="v1
|
|||||||
memories = client.get_all(
|
memories = client.get_all(
|
||||||
filters={
|
filters={
|
||||||
"user_id": "alex"
|
"user_id": "alex"
|
||||||
},
|
}
|
||||||
output_format="v1.1"
|
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Example with the mem0 Python package:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from mem0 import MemoryClient
|
from mem0 import MemoryClient
|
||||||
client = MemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID')
|
client = MemoryClient(api_key="your-api-key")
|
||||||
```
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -41,10 +41,7 @@ client = MemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID')
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { MemoryClient } from "mem0ai";
|
import { MemoryClient } from "mem0ai";
|
||||||
const client = new MemoryClient({
|
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||||
organizationId: "YOUR_ORG_ID",
|
|
||||||
projectId: "YOUR_PROJECT_ID"
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
@@ -82,7 +79,7 @@ new_project = client.project.create(
|
|||||||
|
|
||||||
### Update Project Settings
|
### Update Project Settings
|
||||||
|
|
||||||
Modify project configuration including custom instructions, categories, and graph settings:
|
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Update project with custom categories
|
# Update project with custom categories
|
||||||
@@ -98,8 +95,8 @@ client.project.update(
|
|||||||
custom_instructions="..."
|
custom_instructions="..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Enable graph memory for the project
|
# Use the input language for memory storage and retrieval
|
||||||
client.project.update(enable_graph=True)
|
client.project.update(multilingual=True)
|
||||||
|
|
||||||
# Update multiple settings at once
|
# Update multiple settings at once
|
||||||
client.project.update(
|
client.project.update(
|
||||||
@@ -108,7 +105,7 @@ client.project.update(
|
|||||||
{"personal_info": "User personal information and preferences"},
|
{"personal_info": "User personal information and preferences"},
|
||||||
{"work_context": "Professional context and work-related information"}
|
{"work_context": "Professional context and work-related information"}
|
||||||
],
|
],
|
||||||
enable_graph=True
|
multilingual=True
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -168,11 +165,11 @@ All project methods are available in async mode:
|
|||||||
from mem0 import AsyncMemoryClient
|
from mem0 import AsyncMemoryClient
|
||||||
|
|
||||||
async def manage_project():
|
async def manage_project():
|
||||||
client = AsyncMemoryClient(org_id='YOUR_ORG_ID', project_id='YOUR_PROJECT_ID')
|
client = AsyncMemoryClient(api_key="your-api-key")
|
||||||
|
|
||||||
# All methods support async/await
|
# All methods support async/await
|
||||||
project_info = await client.project.get()
|
project_info = await client.project.get()
|
||||||
await client.project.update(enable_graph=True)
|
await client.project.update(multilingual=True)
|
||||||
members = await client.project.get_members()
|
members = await client.project.get_members()
|
||||||
|
|
||||||
# To call the async function properly
|
# To call the async function properly
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: "Highlights"
|
||||||
|
description: "Major product launches, headline features, and milestones for Mem0."
|
||||||
|
mode: "wide"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label="2026-04-06" description="Mem0 Skill Graph">
|
||||||
|
|
||||||
|
**Mem0 Skill Graph — In-Context Documentation for AI Agents**
|
||||||
|
|
||||||
|
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow — no doc searching required. Three interconnected skills launched:
|
||||||
|
|
||||||
|
- **mem0 Core Skill** — Complete Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more
|
||||||
|
- **mem0-cli Skill** — Terminal command reference, configuration walkthroughs, and CI/CD recipes
|
||||||
|
- **mem0-vercel-ai-sdk Skill** — Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-06" description="Mem0 CLI v0.2.2">
|
||||||
|
|
||||||
|
**Official Mem0 CLI — Now on PyPI and npm**
|
||||||
|
|
||||||
|
A full-featured command-line interface for Mem0, available in both Python and Node.js:
|
||||||
|
|
||||||
|
- **Install:** `pip install mem0-cli` or `npm install -g @mem0/cli`
|
||||||
|
- **Full command suite** — `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`
|
||||||
|
- **Interactive setup** — `mem0 init` with email verification or direct API key entry
|
||||||
|
- **Works everywhere** — Platform (Mem0 Cloud) and self-hosted OSS modes
|
||||||
|
- **Scriptable** — `--json` flag for CI/CD pipelines and automation
|
||||||
|
- **Dual SDK** — Same commands, same experience across Python and Node.js
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-04" description="OpenClaw v1.0.4">
|
||||||
|
|
||||||
|
**OpenClaw Plugin — Production-Ready**
|
||||||
|
|
||||||
|
The OpenClaw Mem0 plugin went from initial release to production-ready in one week (v1.0.0 → v1.0.4):
|
||||||
|
|
||||||
|
- **Skills-based memory architecture** — New extraction pipeline with skill-loader, batched extraction, and domain-aware memory triage
|
||||||
|
- **Dream gate** — Automatic memory consolidation during idle periods for higher-quality long-term recall
|
||||||
|
- **Interactive CLI** — `openclaw mem0 init`, `status`, `config`, `import`, and `event` commands
|
||||||
|
- **Unified tool naming** — `memory_add` and `memory_delete` replace 4 legacy tools, matching the platform API
|
||||||
|
- **Security hardened** — Path traversal protection, pinned dependencies, 329 tests across 10 files
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-02" description="Mem0 Plugin for AI Editors">
|
||||||
|
|
||||||
|
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
||||||
|
|
||||||
|
Launched a unified Mem0 plugin across three major AI development environments — Claude Code and Cursor first (March 25), then Codex (April 2):
|
||||||
|
|
||||||
|
- **9 MCP memory tools** — add, search, get, update, delete, bulk delete, entity management via `mcp.mem0.ai`
|
||||||
|
- **Lifecycle hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
|
||||||
|
- **Cloud MCP server** — Managed endpoint replaces local MCP and Smithery setup
|
||||||
|
- **Streamable HTTP transport** — New MCP transport protocol for real-time streaming
|
||||||
|
- **Codex-specific skill** — Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-21" description="New Providers">
|
||||||
|
|
||||||
|
**Apache AGE, Turbopuffer, MiniMax, and pgvector for Node.js**
|
||||||
|
|
||||||
|
Major expansion of the provider ecosystem:
|
||||||
|
|
||||||
|
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
|
||||||
|
- **Turbopuffer** — New vector database provider for Python SDK
|
||||||
|
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
||||||
|
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
||||||
|
- **Reasoning models** — `reasoning_effort` parameter for OpenAI o1/o3-style models
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-14" description="Mem0 Platform Skill">
|
||||||
|
|
||||||
|
**Mem0 Platform Skill on skills.sh**
|
||||||
|
|
||||||
|
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
|
||||||
|
|
||||||
|
</Update>
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
title: "OpenClaw"
|
||||||
|
description: "Release notes for the OpenClaw plugin and agent harness."
|
||||||
|
mode: "wide"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label="2026-04-11" description="v1.0.6">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Telemetry:** Replaced shared `"anonymous-openclaw"` fallback with a persistent per-machine random hash (`openclaw-anon-<uuid>`), so anonymous plugin users are counted individually in PostHog ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch anonymous history onto the authenticated profile ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Fixed event loss on short-lived CLI invocations — added `beforeExit` handler to flush queued events before the process exits ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Added lazy `/v1/ping/` email resolution so users who configure API key outside `mem0 init` show as their email in PostHog, not an md5 hash ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
- **Telemetry:** Unified CLI event prefix from `openclaw.<cmd>` to `openclaw.cli.<cmd>` on the needsSetup branch to match the authenticated branch ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **API:** Added `source: "OPENCLAW"` to all provider calls (`add`, `search`, `getAll`) across tools, CLI commands, recall, and the OSS backend adapter ([#4790](https://github.com/mem0ai/mem0/pull/4790))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-07" description="v1.0.5">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
|
||||||
|
- **OSS pgvector crash** ([#4727](https://github.com/mem0ai/mem0/issues/4727)): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
|
||||||
|
|
||||||
|
**Removed:**
|
||||||
|
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
|
||||||
|
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-04" description="v1.0.4">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
|
||||||
|
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
|
||||||
|
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
|
||||||
|
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status`, `openclaw mem0 config show`, `openclaw mem0 config set`
|
||||||
|
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
|
||||||
|
- **`event list` / `event status` CLI commands**: Monitor background processing events
|
||||||
|
- **`fs-safe.ts` module**: Isolated filesystem wrappers in a separate entry point
|
||||||
|
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
|
||||||
|
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
|
||||||
|
- **Test suite**: 329 tests across 10 test files
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts`
|
||||||
|
- **Code splitting**: tsup builds with `splitting: true` and two entry points
|
||||||
|
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`)
|
||||||
|
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race`
|
||||||
|
- **Auto-capture fire-and-forget**: `provider.add()` runs in background via `.then()/.catch()`
|
||||||
|
- **Auto-capture minimum content gate**: Skips extraction when total user content is fewer than 50 chars
|
||||||
|
|
||||||
|
**Removed:**
|
||||||
|
- `memory_store` tool — replaced by `memory_add`
|
||||||
|
- `memory_forget` tool — replaced by `memory_delete`
|
||||||
|
- `memory_delete_all` tool — merged into `memory_delete`
|
||||||
|
- `memory_history` tool and `history` CLI command — deprecated
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-03" description="v1.0.3">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Security**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal
|
||||||
|
- **Noise filter**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction`
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-02" description="v1.0.2">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Security**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — plugin-side env resolution was redundant and triggered static analysis warnings ([#4676](https://github.com/mem0ai/mem0/pull/4676))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-02" description="v1.0.1">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **CD workflow**: Added continuous deployment workflow with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
|
||||||
|
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` ([#4667](https://github.com/mem0ai/mem0/pull/4667))
|
||||||
|
- **LICENSE**: Added Apache-2.0 license file ([#4667](https://github.com/mem0ai/mem0/pull/4667))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Dream gate**: Fixed cheap-first ordering, session isolation, and verified completion ([#4666](https://github.com/mem0ai/mem0/pull/4666))
|
||||||
|
- **Graceful startup**: Plugin now starts gracefully when no API key is configured ([#4669](https://github.com/mem0ai/mem0/pull/4669))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-01" description="v1.0.0">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction ([#4624](https://github.com/mem0ai/mem0/pull/4624))
|
||||||
|
- **Dream gate**: Memory consolidation and dream-cycle processing during idle periods
|
||||||
|
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
|
||||||
|
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
|
||||||
|
- **Memory dream skill**: Skill for memory consolidation during idle periods
|
||||||
|
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-26" description="v0.4.1">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Credential detection**: Improved detection of credentials, API keys, and secrets in extraction instructions (#4552)
|
||||||
|
- **Standalone timestamps**: Prevented extraction of standalone timestamps as memories (#4550)
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-16" description="v0.4.0">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers
|
||||||
|
- **Subagent hallucination prevention**: Detects ephemeral subagent sessions and routes recall to parent namespace
|
||||||
|
- **Dynamic recall thresholding**: Memories scoring less than 50% of top result are dropped
|
||||||
|
- **SQLite resilience**: Init error recovery with automatic retry for OSS mode
|
||||||
|
- **`disableHistory` config option**: New `oss.disableHistory` flag
|
||||||
|
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision
|
||||||
|
- Recall candidate pool increased to `topK * 2` for better filtering headroom
|
||||||
|
- Relaxed extraction instructions: related facts kept together to preserve context
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Concurrent session race condition**: Lifecycle hooks now use `ctx.sessionKey` directly instead of a shared mutable variable
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-12" description="v0.3.1">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Message filtering pipeline**: Multi-stage noise removal before extraction
|
||||||
|
- **Broad recall for new sessions**: Short or new-session prompts trigger secondary broad search
|
||||||
|
- **Client-side threshold filtering**: Safety net that drops low-relevance results
|
||||||
|
- **Temporal anchoring**: Extraction instructions now include current date
|
||||||
|
- 55 unit tests covering filtering and isolation helpers
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Extraction window expanded from last 10 to last 20 messages
|
||||||
|
- Rewritten custom extraction instructions for conciseness and deduplication
|
||||||
|
- Refactored monolithic `index.ts` (1772 lines) into 6 focused modules
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-10" description="v0.3.0">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- Updated `mem0ai` dependency with sqlite3 to better-sqlite3 migration (#4270)
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-09" description="v0.2.0">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- Per-agent memory isolation for multi-agent setups via `agentId`
|
||||||
|
- "Understanding userId" section in docs
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Updated config examples to use concrete `userId` values instead of placeholders
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- Migrated platform search to Mem0 v2 API
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-02-19" description="v0.1.2">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- Source field for openclaw memory entries
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- Auto-recall injection and auto-capture message drop
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-02-02" description="v0.1.0">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- Initial release of the OpenClaw Mem0 plugin
|
||||||
|
- Platform mode (Mem0 Cloud) and open-source mode support
|
||||||
|
- Auto-recall: inject relevant memories before each turn
|
||||||
|
- Auto-capture: store facts after each turn
|
||||||
|
- Configurable `topK`, `threshold`, and `apiVersion` options
|
||||||
|
|
||||||
|
</Update>
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
---
|
||||||
|
title: "Platform"
|
||||||
|
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
|
||||||
|
mode: "wide"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label="2025-07-23" description="">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Memory:** Fixed ADD functionality
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-19" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **UI:** Added Settings UI and latency display
|
||||||
|
- **Performance:** Neo4j query optimization
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-18" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **UI:** Updated Event UI
|
||||||
|
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Memory:** Fixed duplicate memory index sentry error
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-17" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **UI:** New Settings Page
|
||||||
|
- **Memory:** Duplicate memories entities support
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-16" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **API:** Fixed pagination for get all API
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-12" description="">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Graph:** Fixed social graph bugs and connection issues
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-11" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Rate Limiting:** New rate limit for V2 Search
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Slack:** Fixed Slack rate limit error with backend improvements
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-10" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Performance:**
|
||||||
|
- Changed connection pooling time to 5 minutes
|
||||||
|
- Separated graph lambdas for better performance
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-09" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Graph:** Graph Optimizations V2 and memory improvements
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-08" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Database:** Added read replica support for improved database performance
|
||||||
|
- **UI:** Implemented UI changes for Users Page
|
||||||
|
- **Feedback:** Enabled feedback functionality
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Serializer:** Fixed GET ALL Serializer
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-05" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **UI:** User Page Revamp and New Users Page
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-04" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Users:** New Users Page implementation
|
||||||
|
- **Tools:** Added script to backfill memory categories
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Filters:** Fixed Filters Get All functionality
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-03" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Graph:** Graph Memory optimization
|
||||||
|
- **Memory:** Fixed exact memories and semantically similar memories retrieval
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-02" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-07-01" description="">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Memory:** Fixed old_memory issue in Async memory addition lambda
|
||||||
|
- **Events:** Fixed missing events
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-30" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Graph:** Improvements to graph memory and added user to LTM-STM
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-28" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Graph:** Added support for SQS in graph memory addition
|
||||||
|
- **Testing:** Added Locust load testing script and Grafana Dashboard
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-27" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
|
||||||
|
- **Performance:** Improved Neo4j performance
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-26" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Memory:** Edit Memory From Drawer functionality
|
||||||
|
- **API:** Added Topic Suggestions API Endpoint
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-25" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
|
||||||
|
- **Memory:** Editable Metadata in Memories
|
||||||
|
- **UI:** Memory Actions Badges
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-19" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Rate Limiting:** Implemented comprehensive rate limiting system
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Performance:** Added performance indexes for memory stats query
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Search:** Fixed search events not respecting top-k parameter
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-18" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
|
||||||
|
- **Playground:** Added Claude 4 support on Playground
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Memory:** Added ability to update memory metadata
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-17" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **UI:** New Memories Page UI design
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-16" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-13" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-11" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **OMM:** Added OMM Script and UI functionality
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **API:** Added filters validation to semantic_search_v2 endpoint
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-09" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Intercom:** Set Intercom events for ADD and SEARCH operations
|
||||||
|
- **OpenMemory:** Added Posthog integration and feedback functionality
|
||||||
|
- **MCP:** New JavaScript MCP Server with feedback support
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Structured Data:** Enhanced structured data handling in memory management
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-06" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **OAuth:** Added Mem0 OAuth integration
|
||||||
|
- **OMM:** Added OMM-Mem0 sync for deleted memories
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-05" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-06-02" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **OpenMemory Cloud:** Added OpenMemory Cloud support
|
||||||
|
- **Structured Data:** Added 'structured_attributes' field to Memory model
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-05-30" description="">
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Projects:** Added version and enable_graph to project views
|
||||||
|
- **OpenMemory:** Added Postgres support for OpenMemory
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-05-19" description="">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
@@ -1,13 +1,40 @@
|
|||||||
---
|
---
|
||||||
title: "Product Updates"
|
title: "SDK & Tools"
|
||||||
description: "Latest releases, bug fixes, and improvements for the Mem0 Python and TypeScript SDKs."
|
description: "Release notes for the Mem0 Python SDK, TypeScript SDK, Vercel AI SDK, CLI, and editor plugins."
|
||||||
mode: "wide"
|
mode: "wide"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
|
|
||||||
|
<Update label="2026-04-04" description="v1.0.11">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **SDK:** Added `multilingual` parameter to project update ([#4314](https://github.com/mem0ai/mem0/pull/4314))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **LLMs:** Fixed Groq model configuration ([#4700](https://github.com/mem0ai/mem0/pull/4700))
|
||||||
|
- **Core:** Prevented thread and memory leaks from PostHog telemetry ([#4535](https://github.com/mem0ai/mem0/pull/4535))
|
||||||
|
- **Vector Stores:** Used `DatetimeRange` for datetime string values in Qdrant range filters ([#4659](https://github.com/mem0ai/mem0/pull/4659))
|
||||||
|
- **Configs:** Added missing `ConfigDict` to vector store configs (Elasticsearch, MongoDB, Neptune, OpenSearch, PGVector, Supabase, Valkey) ([#4656](https://github.com/mem0ai/mem0/pull/4656))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-01" description="v1.0.10">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **LLMs:** Added MiniMax provider support for AWS Bedrock ([#4609](https://github.com/mem0ai/mem0/pull/4609))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Configs:** Migrated CassandraConfig and AzureMySQLConfig to pydantic v2 ConfigDict ([#4646](https://github.com/mem0ai/mem0/pull/4646))
|
||||||
|
- **LLMs:** Forward `response_format` to OpenAI-compatible API for DeepSeek ([#4635](https://github.com/mem0ai/mem0/pull/4635))
|
||||||
|
- **LLMs:** Forward `response_format` to OpenAI-compatible API for vLLM ([#4608](https://github.com/mem0ai/mem0/pull/4608))
|
||||||
|
- **Vector Stores:** Only list authorized collections when listing MongoDB collections ([#3888](https://github.com/mem0ai/mem0/pull/3888))
|
||||||
|
- **Core:** Reset graph database in `Memory.reset()` ([#4185](https://github.com/mem0ai/mem0/pull/4185))
|
||||||
|
- **Core:** Make `AsyncMemory.from_config` a regular classmethod ([#4183](https://github.com/mem0ai/mem0/pull/4183))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-03-28" description="v1.0.9">
|
<Update label="2026-03-28" description="v1.0.9">
|
||||||
|
|
||||||
**New Features & Updates:**
|
**New Features & Updates:**
|
||||||
@@ -816,6 +843,21 @@ mode: "wide"
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
|
<Update label="2026-04-04" description="v2.4.6">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **Client:** Added `multilingual` parameter to project update types ([#4314](https://github.com/mem0ai/mem0/pull/4314))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-01" description="v2.4.5">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **OSS:** Replace `.single()` with `.maybeSingle()` in SupabaseDB.get() to handle missing rows ([#4599](https://github.com/mem0ai/mem0/pull/4599))
|
||||||
|
- **Embeddings:** Pass dimensions parameter to OpenAI embeddings API ([#4632](https://github.com/mem0ai/mem0/pull/4632))
|
||||||
|
- **OSS:** Extract JSON from chatty LLM responses in fact retrieval ([#4533](https://github.com/mem0ai/mem0/pull/4533))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-03-28" description="v2.4.4">
|
<Update label="2026-03-28" description="v2.4.4">
|
||||||
|
|
||||||
@@ -1116,352 +1158,152 @@ mode: "wide"
|
|||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Platform">
|
<Tab title="CLI">
|
||||||
|
|
||||||
<Update label="2025-07-23" description="">
|
<Update label="2026-04-11" description="Python v0.2.3 / Node v0.2.3">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **Memory:** Fixed ADD functionality
|
- **Telemetry:** Replaced shared `"anonymous-cli"` fallback with a persistent per-machine random hash (`cli-anon-<uuid>`), so anonymous CLI users are counted individually in PostHog instead of collapsing into one identity ([#4789](https://github.com/mem0ai/mem0/pull/4789))
|
||||||
|
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch pre-signup anonymous history onto the authenticated user profile ([#4789](https://github.com/mem0ai/mem0/pull/4789))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **API:** All API calls now include `source=CLI` in request bodies (POST/PUT) and query params (GET/DELETE) for server-side attribution ([#4789](https://github.com/mem0ai/mem0/pull/4789))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-07-19" description="">
|
<Update label="2026-04-06" description="Python v0.2.2 / Node v0.2.2">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **UI:** Added Settings UI and latency display
|
- **Telemetry:** Added PostHog telemetry and source tracking to both Python and Node CLIs ([#4699](https://github.com/mem0ai/mem0/pull/4699))
|
||||||
- **Performance:** Neo4j query optimization
|
- **Validation:** API key validated upfront via `/v1/ping/` on startup — fail-fast with a helpful error instead of cryptic 401s ([#4701](https://github.com/mem0ai/mem0/pull/4701))
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
|
- **CD:** Fixed OIDC trusted publishing with `npx npm@latest` ([#4724](https://github.com/mem0ai/mem0/pull/4724))
|
||||||
|
- **CD:** Removed npm self-upgrade from CD workflows ([#4723](https://github.com/mem0ai/mem0/pull/4723))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-07-18" description="">
|
<Update label="2026-04-03" description="Python v0.2.1 / Node v0.2.1">
|
||||||
|
|
||||||
**Improvements:**
|
**New Features:**
|
||||||
- **UI:** Updated Event UI
|
- **Docs:** Comprehensive README with installation, usage examples, and purple branding ([#4680](https://github.com/mem0ai/mem0/pull/4680))
|
||||||
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **Memory:** Fixed duplicate memory index sentry error
|
- **npm:** Added `repository` field to Node packages for npm provenance ([#4671](https://github.com/mem0ai/mem0/pull/4671))
|
||||||
|
- **CD:** Added CD workflows for Node SDK packages with OIDC trusted publishing ([#4670](https://github.com/mem0ai/mem0/pull/4670))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-07-17" description="">
|
<Update label="2026-04-02" description="Python v0.2.0 / Node v0.1.1">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **UI:** New Settings Page
|
- **`event` commands:** `mem0 event list` shows recent background processing events in a table; `mem0 event status <id>` shows full detail including nested memory results ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
- **Memory:** Duplicate memories entities support
|
- **`--json` / `--agent` flag:** Root-level flag switches all command output to a structured JSON envelope for programmatic/agent consumption. Envelope format: `{"status", "command", "duration_ms", "scope", "count", "data"}` ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
|
- **Agent output sanitization:** Raw API responses projected to only relevant fields per command (e.g., `add` → `{id, memory, event}`, `search` → `{id, memory, score, created_at, categories}`) ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
**Improvements:**
|
- **Email login:** Added email verification code login to `mem0 init` ([#4623](https://github.com/mem0ai/mem0/pull/4623))
|
||||||
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
|
- **Brand update:** Updated color palette from purple to golden ([#4664](https://github.com/mem0ai/mem0/pull/4664))
|
||||||
|
- **CI/CD:** Added CI pipelines and CD workflows for both CLIs ([#4640](https://github.com/mem0ai/mem0/pull/4640), [#4653](https://github.com/mem0ai/mem0/pull/4653))
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-16" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **API:** Fixed pagination for get all API
|
- **Node:** Fixed critical `MODULE_NOT_FOUND` crash on `status`, `import`, and all commands when installed globally — replaced runtime `createRequire` with build-time version injection ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
|
- **Node:** API errors now show full response detail instead of bare "Bad Request" ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
</Update>
|
- **Python:** Fixed double error printing on all commands ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
|
- **`status` command:** Replaced heavyweight `/v1/entities/` check with dedicated `GET /v1/ping/` endpoint ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
<Update label="2025-07-12" description="">
|
- **`add` command:** Deduplicated PENDING results from API; changed misleading count message ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
|
- **`init` command:** Partial flags now work in non-TTY; warns before overwriting existing config; added `--force` flag ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
**Bug Fixes:**
|
- **`delete` command:** Fixed entity delete via v2 API for all entity types ([#4649](https://github.com/mem0ai/mem0/pull/4649))
|
||||||
- **Graph:** Fixed social graph bugs and connection issues
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-11" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Rate Limiting:** New rate limit for V2 Search
|
- Tables now show full UUIDs (was truncated to 8 chars, making `mem0 get <id>` fail) ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
|
- Search table includes Score column ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
**Bug Fixes:**
|
- `config get api_key` short-form aliases added ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
- **Slack:** Fixed Slack rate limit error with backend improvements
|
- Client-side validation for `--expires`, `--page-size`, `--page`, `--top-k`, `--threshold`, and empty content ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
|
- `printInfo` / `printScope` moved to stderr to avoid contaminating JSON piping ([#4636](https://github.com/mem0ai/mem0/pull/4636))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-07-10" description="">
|
<Update label="2026-03-26" description="Python v0.1.0 / Node v0.1.0">
|
||||||
|
|
||||||
**Improvements:**
|
**Initial Release — Official Mem0 CLI**
|
||||||
- **Performance:**
|
|
||||||
- Changed connection pooling time to 5 minutes
|
|
||||||
- Separated graph lambdas for better performance
|
|
||||||
|
|
||||||
</Update>
|
A full-featured command-line interface for Mem0, available in both Python and Node.js:
|
||||||
|
|
||||||
<Update label="2025-07-09" description="">
|
- **Install:** `pip install mem0-cli` (Python) or `npm install -g @mem0/cli` (Node.js)
|
||||||
|
- **Full command suite:** `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`
|
||||||
**Improvements:**
|
- **Interactive setup:** `mem0 init` with API key entry and user ID configuration
|
||||||
- **Graph:** Graph Optimizations V2 and memory improvements
|
- **Works everywhere:** Platform (Mem0 Cloud) and self-hosted OSS modes
|
||||||
|
- **Scriptable:** `-o json` flag for CI/CD pipelines and automation
|
||||||
</Update>
|
- **Dual SDK:** Same commands, same experience across Python and Node.js
|
||||||
|
- **Shared spec:** Both implementations driven by a single `cli-spec.json` ensuring identical behavior ([#4575](https://github.com/mem0ai/mem0/pull/4575))
|
||||||
<Update label="2025-07-08" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Database:** Added read replica support for improved database performance
|
|
||||||
- **UI:** Implemented UI changes for Users Page
|
|
||||||
- **Feedback:** Enabled feedback functionality
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Serializer:** Fixed GET ALL Serializer
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-05" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **UI:** User Page Revamp and New Users Page
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-04" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Users:** New Users Page implementation
|
|
||||||
- **Tools:** Added script to backfill memory categories
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Filters:** Fixed Filters Get All functionality
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-03" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Graph:** Graph Memory optimization
|
|
||||||
- **Memory:** Fixed exact memories and semantically similar memories retrieval
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-02" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-07-01" description="">
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory:** Fixed old_memory issue in Async memory addition lambda
|
|
||||||
- **Events:** Fixed missing events
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-30" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Graph:** Improvements to graph memory and added user to LTM-STM
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-28" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Graph:** Added support for SQS in graph memory addition
|
|
||||||
- **Testing:** Added Locust load testing script and Grafana Dashboard
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-27" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
|
|
||||||
- **Performance:** Improved Neo4j performance
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-26" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Memory:** Edit Memory From Drawer functionality
|
|
||||||
- **API:** Added Topic Suggestions API Endpoint
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-25" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
|
|
||||||
- **Memory:** Editable Metadata in Memories
|
|
||||||
- **UI:** Memory Actions Badges
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-19" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Rate Limiting:** Implemented comprehensive rate limiting system
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Performance:** Added performance indexes for memory stats query
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Search:** Fixed search events not respecting top-k parameter
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-18" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
|
|
||||||
- **Playground:** Added Claude 4 support on Playground
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Memory:** Added ability to update memory metadata
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-17" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **UI:** New Memories Page UI design
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-16" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-13" description="">
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-11" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **OMM:** Added OMM Script and UI functionality
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **API:** Added filters validation to semantic_search_v2 endpoint
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-09" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Intercom:** Set Intercom events for ADD and SEARCH operations
|
|
||||||
- **OpenMemory:** Added Posthog integration and feedback functionality
|
|
||||||
- **MCP:** New JavaScript MCP Server with feedback support
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Structured Data:** Enhanced structured data handling in memory management
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-06" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **OAuth:** Added Mem0 OAuth integration
|
|
||||||
- **OMM:** Added OMM-Mem0 sync for deleted memories
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-05" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-06-02" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **OpenMemory Cloud:** Added OpenMemory Cloud support
|
|
||||||
- **Structured Data:** Added 'structured_attributes' field to Memory model
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-05-30" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Projects:** Added version and enable_graph to project views
|
|
||||||
- **OpenMemory:** Added Postgres support for OpenMemory
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-05-19" description="">
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
|
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="Vercel AI SDK">
|
<Tab title="Plugins">
|
||||||
|
|
||||||
<Update label="2025-12-26" description="v2.0.5">
|
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
|
||||||
|
|
||||||
|
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
||||||
|
|
||||||
|
The unified Mem0 plugin for AI development environments:
|
||||||
|
|
||||||
|
- **9 MCP memory tools:** `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities` — all via `mcp.mem0.ai`
|
||||||
|
- **Lifecycle hooks:** Automatic memory capture at session start, context compaction, task completion, and session end
|
||||||
|
- **Cloud MCP server:** Managed endpoint replaces local MCP and Smithery setup
|
||||||
|
- **Streamable HTTP transport:** New MCP transport protocol for real-time streaming
|
||||||
|
- **Codex-specific skill:** Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
|
||||||
|
- **Supported editors:** Claude Code, Claude Cowork, Cursor, Codex
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2025-12-26" description="Vercel AI SDK v2.0.5">
|
||||||
**Bug Fix:**
|
**Bug Fix:**
|
||||||
- **Vercel AI SDK:** Removed unnecessary dependencies to make the package lighter.
|
- Removed unnecessary dependencies to make the package lighter.
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-09-25" description="v2.0.4">
|
<Update label="2025-09-25" description="Vercel AI SDK v2.0.3 – v2.0.4">
|
||||||
|
**New Features:**
|
||||||
|
- Added file support for multimodal capabilities with memory context (v2.0.3)
|
||||||
|
|
||||||
**Bug Fix:**
|
**Bug Fix:**
|
||||||
- **Vercel AI SDK:** Fixed version parameter in the AI SDK to use V2 for addition.
|
- Fixed version parameter to use V2 for addition (v2.0.4)
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-09-25" description="v2.0.3">
|
<Update label="2025-09-03" description="Vercel AI SDK v2.0.2">
|
||||||
**New Features:**
|
|
||||||
- **Vercel AI SDK:** Added file support for multimodal capabilities with memory context
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-09-03" description="v2.0.2">
|
|
||||||
**Bug Fix:**
|
**Bug Fix:**
|
||||||
- **Vercel AI SDK:** Fixed streaming response in the AI SDK.
|
- Fixed streaming response in the AI SDK.
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-08-05" description="v2.0.1">
|
<Update label="2025-08-05" description="Vercel AI SDK v2.0.0 – v2.0.1">
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Vercel AI SDK:** Added a new param `host` to the config.
|
- Migration to AI SDK V5 (v2.0.0)
|
||||||
|
- Added `host` param to the config (v2.0.1)
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-08-05" description="v2.0.0">
|
<Update label="2025-06-15" description="Vercel AI SDK v1.0.6">
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Vercel AI SDK:** Migration to AI SDK V5.
|
- Added `filter_memories` param.
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-06-15" description="v1.0.6">
|
<Update label="2025-05-23" description="Vercel AI SDK v1.0.5">
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Vercel AI SDK:** Added param `filter_memories`.
|
- Added support for Google provider.
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-05-23" description="v1.0.5">
|
<Update label="2025-05-10" description="Vercel AI SDK v1.0.3 – v1.0.4">
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Vercel AI SDK:** Added support for Google provider.
|
- Added support for `output_format` param (v1.0.4)
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-05-10" description="v1.0.4">
|
|
||||||
**New Features:**
|
|
||||||
- **Vercel AI SDK:** Added support for new param `output_format`.
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2025-05-08" description="v1.0.3">
|
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Vercel AI SDK:** Added support for graceful failure in cases services are down.
|
- Added graceful failure handling when services are down (v1.0.3)
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
<Update label="2025-05-01" description="v1.0.1">
|
<Update label="2025-05-01" description="Vercel AI SDK v1.0.1">
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Vercel AI SDK:** Added support for graph memories
|
- Added support for graph memories.
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ config = {
|
|||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gemini-2.0-flash-001",
|
"model": "gemini-2.0-flash-001",
|
||||||
|
"api_key": "your-gemini-api-key",
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
"top_p": 1.0
|
"top_p": 1.0
|
||||||
@@ -52,6 +53,7 @@ const config = {
|
|||||||
provider: "gemini",
|
provider: "gemini",
|
||||||
config: {
|
config: {
|
||||||
model: "gemini-2.0-flash-001",
|
model: "gemini-2.0-flash-001",
|
||||||
|
apiKey: process.env.GOOGLE_API_KEY || '',
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Mem0 supports LangChain as a provider for vector store integration. LangChain pr
|
|||||||
```python Python
|
```python Python
|
||||||
import os
|
import os
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
from langchain_community.vectorstores import Chroma
|
from langchain_chroma import Chroma
|
||||||
from langchain_openai import OpenAIEmbeddings
|
from langchain_openai import OpenAIEmbeddings
|
||||||
|
|
||||||
# Initialize a LangChain vector store
|
# Initialize a LangChain vector store
|
||||||
|
|||||||
@@ -50,4 +50,25 @@ Here are the parameters available for configuring Valkey:
|
|||||||
| `hnsw_m` | Number of bi-directional links for HNSW | `16` |
|
| `hnsw_m` | Number of bi-directional links for HNSW | `16` |
|
||||||
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
|
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
|
||||||
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
|
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
|
||||||
|
| `cluster_mode` | Enable cluster mode for Valkey cluster (CME) deployments | `false` |
|
||||||
| `distance_metric` | Distance metric for vector similarity | `cosine` |
|
| `distance_metric` | Distance metric for vector similarity | `cosine` |
|
||||||
|
|
||||||
|
## Cluster Mode
|
||||||
|
|
||||||
|
To use Valkey with cluster mode enabled (CME), set `cluster_mode` to `true`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
config = {
|
||||||
|
"vector_store": {
|
||||||
|
"provider": "valkey",
|
||||||
|
"config": {
|
||||||
|
"collection_name": "memories",
|
||||||
|
"valkey_url": "valkey://cluster-endpoint:6379",
|
||||||
|
"embedding_model_dims": 1536,
|
||||||
|
"cluster_mode": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When cluster mode is enabled, the connector uses `ValkeyCluster` instead of the standalone client, which handles `MOVED`/`ASK` redirections automatically. Search queries are coordinated across all shards by the valkey-search module's built-in coordinator. See the [valkey-search documentation](https://github.com/valkey-io/valkey-search) for details on cluster mode behavior.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ Before you begin, follow these steps to set up the demo application:
|
|||||||
OPENAI_API_KEY=your_openai_api_key
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
MEM0_API_KEY=your_mem0_api_key
|
MEM0_API_KEY=your_mem0_api_key
|
||||||
```
|
```
|
||||||
You can obtain your `MEM0_API_KEY` by signing up at [Mem0 API Dashboard](https://app.mem0.ai/dashboard/api-keys).
|
You can obtain your `MEM0_API_KEY` by signing up at <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Dashboard</a>.
|
||||||
|
|
||||||
5. Start the development server:
|
5. Start the development server:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ client = MemoryClient(api_key="your-api-key")
|
|||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Replace `your-api-key` with your actual Mem0 API key from the [dashboard](https://app.mem0.ai). Without proper API authentication, memory operations will fail.
|
Replace `your-api-key` with your actual Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a>. Without proper API authentication, memory operations will fail.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from mem0 import MemoryClient
|
|||||||
client = MemoryClient(api_key="m0-...")
|
client = MemoryClient(api_key="m0-...")
|
||||||
```
|
```
|
||||||
|
|
||||||
Grab an API key from the <Link href="https://app.mem0.ai/">Mem0 dashboard</Link> to get started.
|
Grab an API key from the <a href="https://app.mem0.ai/" rel="nofollow">Mem0 dashboard</a> to get started.
|
||||||
|
|
||||||
## Store and Retrieve Scoped Memories
|
## Store and Retrieve Scoped Memories
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ client = MemoryClient(api_key="your-api-key")
|
|||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Your API key needs export permissions to download memory data. Check your project settings on the [dashboard](https://app.mem0.ai) if export operations fail with authentication errors.
|
Your API key needs export permissions to download memory data. Check your project settings on the <a href="https://app.mem0.ai" rel="nofollow">dashboard</a> if export operations fail with authentication errors.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
Let's add some sample memories to work with:
|
Let's add some sample memories to work with:
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
title: Browser Extension Memory
|
|
||||||
description: "Add Mem0's universal memory layer to Chrome chat surfaces."
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
Enhance your AI interactions with Mem0, a Chrome extension that introduces a universal memory layer across platforms like ChatGPT, Claude, and Perplexity. Mem0 ensures seamless context sharing, making your AI experiences more personalized and efficient.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
We now support Grok! The Mem0 Chrome Extension has been updated to work with Grok, bringing the same powerful memory capabilities to your Grok conversations.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Universal Memory Layer**: Share context seamlessly across ChatGPT, Claude, Perplexity, and Grok.
|
|
||||||
- **Smart Context Detection**: Automatically captures relevant information from your conversations.
|
|
||||||
- **Intelligent Memory Retrieval**: Surfaces pertinent memories at the right time.
|
|
||||||
- **One-Click Sync**: Easily synchronize with existing ChatGPT memories.
|
|
||||||
- **Memory Dashboard**: Manage all your memories in one centralized location.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
You can install the Mem0 Chrome Extension using one of the following methods:
|
|
||||||
|
|
||||||
### Method 1: Chrome Web Store Installation
|
|
||||||
|
|
||||||
1. **Download the Extension**: Open Google Chrome and navigate to the [Mem0 Chrome Extension page](https://chromewebstore.google.com/detail/mem0/onihkkbipkfeijkadecaafbgagkhglop?hl=en).
|
|
||||||
2. **Add to Chrome**: Click on the "Add to Chrome" button.
|
|
||||||
3. **Confirm Installation**: In the pop-up dialog, click "Add extension" to confirm. The Mem0 icon should now appear in your Chrome toolbar.
|
|
||||||
|
|
||||||
### Method 2: Manual Installation
|
|
||||||
|
|
||||||
1. **Download the Extension**: Clone or download the extension files from the [Mem0 Chrome Extension GitHub repository](https://github.com/mem0ai/mem0-chrome-extension).
|
|
||||||
2. **Access Chrome Extensions**: Open Google Chrome and navigate to `chrome://extensions`.
|
|
||||||
3. **Enable Developer Mode**: Toggle the "Developer mode" switch in the top right corner.
|
|
||||||
4. **Load Unpacked Extension**: Click "Load unpacked" and select the directory containing the extension files.
|
|
||||||
5. **Confirm Installation**: The Mem0 Chrome Extension should now appear in your Chrome toolbar.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. **Locate the Mem0 Icon**: After installation, find the Mem0 icon in your Chrome toolbar.
|
|
||||||
2. **Sign In**: Click the icon and sign in with your Google account.
|
|
||||||
3. **Interact with AI Assistants**:
|
|
||||||
- **ChatGPT and Perplexity**: Continue your conversations as usual; Mem0 operates seamlessly in the background.
|
|
||||||
- **Claude**: Click the Mem0 button or use the shortcut `Ctrl + M` to activate memory functions.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- **API Key**: Obtain your API key from the Mem0 Dashboard to connect the extension to the Mem0 API.
|
|
||||||
- **User ID**: This is your unique identifier in the Mem0 system. If not provided, it defaults to `chrome-extension-user`.
|
|
||||||
|
|
||||||
## Demo Video
|
|
||||||
|
|
||||||
<iframe width="700" height="400" src="https://www.youtube.com/embed/dqenCMMlfwQ?si=zhGVrkq6IS_0Jwyj" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
||||||
|
|
||||||
## Privacy and Data Security
|
|
||||||
|
|
||||||
Your messages are sent to the Mem0 API for extracting and retrieving memories. Mem0 is committed to ensuring your data's privacy and security.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
|
||||||
<Card title="Build a Mem0 Companion" icon="users" href="/cookbooks/essentials/building-ai-companion">
|
|
||||||
Learn the foundations of memory-powered assistants that work across platforms.
|
|
||||||
</Card>
|
|
||||||
<Card title="Multimodal Support" icon="image" href="/platform/features/multimodal-support">
|
|
||||||
Extend your browser interactions with vision and audio memory.
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
@@ -55,7 +55,7 @@ GEMINI_API_KEY=your-gemini-api-key-here
|
|||||||
```
|
```
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Ensure you have your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai) and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys).
|
Ensure you have your Mem0 API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Dashboard</a> and your Gemini API key from the [Google AI Studio](https://ai.studio/app/api-keys).
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Gemini Memory Agent
|
## Gemini Memory Agent
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Set up your environment variables:
|
|||||||
- `MEM0_API_KEY`: Your Mem0 Platform API key
|
- `MEM0_API_KEY`: Your Mem0 Platform API key
|
||||||
- `OPENAI_API_KEY`: Your OpenAI API key
|
- `OPENAI_API_KEY`: Your OpenAI API key
|
||||||
|
|
||||||
You can obtain your Mem0 Platform API key from the [Mem0 Platform](https://app.mem0.ai).
|
You can obtain your Mem0 Platform API key from the <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.
|
||||||
|
|
||||||
## Complete Implementation
|
## Complete Implementation
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ Based on our previous session, I remember we covered Vision Language Models and
|
|||||||
## Help & Resources
|
## Help & Resources
|
||||||
|
|
||||||
- [LlamaIndex Agent Workflows](https://docs.llamaindex.ai/en/stable/use_cases/agents/)
|
- [LlamaIndex Agent Workflows](https://docs.llamaindex.ai/en/stable/use_cases/agents/)
|
||||||
- [Mem0 Platform](https://app.mem0.ai/)
|
- <a href="https://app.mem0.ai/" rel="nofollow">Mem0 Platform</a>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ os.environ["OPENAI_API_KEY"] = "<your-openai-api-key>"
|
|||||||
llm = OpenAI(model="gpt-4.1-nano-2025-04-14")
|
llm = OpenAI(model="gpt-4.1-nano-2025-04-14")
|
||||||
```
|
```
|
||||||
|
|
||||||
Initialize the Mem0 client. You can find your API key [here](https://app.mem0.ai/dashboard/api-keys). Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview).
|
Initialize the Mem0 client. You can find your API key <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">here</a>. Read about Mem0 [Open Source](https://docs.mem0.ai/open-source/overview).
|
||||||
```python
|
```python
|
||||||
os.environ["MEM0_API_KEY"] = "<your-mem0-api-key>"
|
os.environ["MEM0_API_KEY"] = "<your-mem0-api-key>"
|
||||||
|
|
||||||
|
|||||||
@@ -754,7 +754,7 @@ Exact output varies as Mem0 automatically extracts and deduplicates entities. Th
|
|||||||
- [MiroFish GitHub](https://github.com/666ghj/MiroFish) — Source code and setup guide
|
- [MiroFish GitHub](https://github.com/666ghj/MiroFish) — Source code and setup guide
|
||||||
- [MiroFish Documentation](https://deepwiki.com/666ghj/MiroFish) — Full framework docs
|
- [MiroFish Documentation](https://deepwiki.com/666ghj/MiroFish) — Full framework docs
|
||||||
- [Mem0 Graph Memory](/open-source/features/graph-memory) — Graph Memory documentation
|
- [Mem0 Graph Memory](/open-source/features/graph-memory) — Graph Memory documentation
|
||||||
- [Mem0 Documentation](https://docs.mem0.ai/) — Full API reference
|
- [Mem0 Documentation](https://docs.mem0.ai/introduction) — Full API reference
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
<Card title="Graph Memory" icon="network-wired" href="/open-source/features/graph-memory">
|
<Card title="Graph Memory" icon="network-wired" href="/open-source/features/graph-memory">
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ context = Mem0Context(user_id="user123")
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
- [Mem0 Documentation](https://docs.mem0.ai/introduction)
|
||||||
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
|
- <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
|
||||||
- [API Reference](https://docs.mem0.ai/api-reference)
|
- [API Reference](https://docs.mem0.ai/api-reference)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ With Mem0 and AWS services like Bedrock, OpenSearch, and Neptune Analytics, you
|
|||||||
<Card title="Neptune Analytics with Mem0" icon="database" href="/cookbooks/integrations/neptune-analytics">
|
<Card title="Neptune Analytics with Mem0" icon="database" href="/cookbooks/integrations/neptune-analytics">
|
||||||
Explore graph-based memory storage with AWS Neptune Analytics.
|
Explore graph-based memory storage with AWS Neptune Analytics.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Graph Memory Features" icon="sitemap" href="/platform/features/graph-memory">
|
<Card title="Graph Memory Features" icon="sitemap" href="/open-source/features/graph-memory">
|
||||||
Learn how to leverage knowledge graphs for entity relationships.
|
Learn how to leverage knowledge graphs for entity relationships.
|
||||||
</Card>
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ MEM0_API_KEY=your_mem0_api_key
|
|||||||
OPENAI_API_KEY=your_openai_api_key
|
OPENAI_API_KEY=your_openai_api_key
|
||||||
```
|
```
|
||||||
|
|
||||||
Get your Mem0 API key from the [Mem0 Dashboard](https://app.mem0.ai/dashboard/api-keys).
|
Get your Mem0 API key from the <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 Dashboard</a>.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
@@ -308,8 +308,8 @@ run().catch(console.error);
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
- [Mem0 Documentation](https://docs.mem0.ai/introduction)
|
||||||
- [Mem0 Dashboard](https://app.mem0.ai/dashboard)
|
- <a href="https://app.mem0.ai/dashboard" rel="nofollow">Mem0 Dashboard</a>
|
||||||
- [API Reference](https://docs.mem0.ai/api-reference)
|
- [API Reference](https://docs.mem0.ai/api-reference)
|
||||||
- [OpenAI Documentation](https://platform.openai.com/docs)
|
- [OpenAI Documentation](https://platform.openai.com/docs)
|
||||||
|
|
||||||
|
|||||||
@@ -201,10 +201,7 @@ Here are some examples of how Mem0 can be integrated into various applications:
|
|||||||
>
|
>
|
||||||
Persistent personality for Eliza agents.
|
Persistent personality for Eliza agents.
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="Browser Extension Memory" icon="globe" href="/cookbooks/frameworks/chrome-extension">
|
</CardGroup>
|
||||||
Universal memory layer for Chrome.
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
|
|||||||
- **Messages** – The ordered list of user/assistant turns you send to `add`.
|
- **Messages** – The ordered list of user/assistant turns you send to `add`.
|
||||||
- **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
|
- **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
|
||||||
- **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
|
- **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
|
||||||
- **User / Session identifiers** – `user_id`, `session_id`, or `run_id` that scope the memory for future searches.
|
- **User / Session identifiers** – `user_id`, `agent_id`, or `run_id` that scope the memory for future searches.
|
||||||
|
|
||||||
## How does it work?
|
## How does it work?
|
||||||
|
|
||||||
@@ -85,7 +85,6 @@ const messages = [
|
|||||||
|
|
||||||
await client.add(messages, {
|
await client.add(messages, {
|
||||||
user_id: "alice",
|
user_id: "alice",
|
||||||
version: "v2",
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
@@ -173,7 +172,6 @@ For full list of supported fields, required formats, and advanced options, see t
|
|||||||
| Capability | Mem0 Platform | Mem0 OSS |
|
| Capability | Mem0 Platform | Mem0 OSS |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage |
|
| Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage |
|
||||||
| Graph writes | Toggle per request (`enable_graph=True`) | Requires configuring a graph provider |
|
|
||||||
| Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs |
|
| Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs |
|
||||||
| Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI |
|
| Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI |
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ import MemoryClient from 'mem0ai';
|
|||||||
|
|
||||||
const client = new MemoryClient({ apiKey: "your-api-key" });
|
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||||
|
|
||||||
client.deleteAll({ user_id: "alice" })
|
client.deleteAll({ userId: "alice" })
|
||||||
.then(result => console.log(result))
|
.then(result => console.log(result))
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
```
|
```
|
||||||
@@ -162,12 +162,12 @@ import MemoryClient from 'mem0ai';
|
|||||||
const client = new MemoryClient({ apiKey: "your-api-key" });
|
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||||
|
|
||||||
// Delete all memories across every user in the project
|
// Delete all memories across every user in the project
|
||||||
client.deleteAll({ user_id: "*" })
|
client.deleteAll({ userId: "*" })
|
||||||
.then(result => console.log(result))
|
.then(result => console.log(result))
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
|
|
||||||
// Full project wipe — all four filters must be explicitly set to "*"
|
// Full project wipe — all four filters must be explicitly set to "*"
|
||||||
client.deleteAll({ user_id: "*", agent_id: "*", app_id: "*", run_id: "*" })
|
client.deleteAll({ userId: "*", agentId: "*", appId: "*", runId: "*" })
|
||||||
.then(result => console.log(result))
|
.then(result => console.log(result))
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Mem0 maps these classic categories onto its layered storage so you can decide wh
|
|||||||
Mem0 stores each layer separately and merges them when you query:
|
Mem0 stores each layer separately and merges them when you query:
|
||||||
|
|
||||||
1. **Capture** – Messages enter the conversation layer while the turn is active.
|
1. **Capture** – Messages enter the conversation layer while the turn is active.
|
||||||
2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `session_id`, and metadata.
|
2. **Promote** – Relevant details persist to session or user memory based on your `user_id`, `run_id`, and metadata.
|
||||||
3. **Retrieve** – The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
|
3. **Retrieve** – The search pipeline pulls from all layers, ranking user memories first, then session notes, then raw history.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -66,19 +66,19 @@ memory = Memory(api_key=os.environ["MEM0_API_KEY"])
|
|||||||
memory.add(
|
memory.add(
|
||||||
["I'm Alex and I prefer boutique hotels."],
|
["I'm Alex and I prefer boutique hotels."],
|
||||||
user_id="alex",
|
user_id="alex",
|
||||||
session_id="trip-planning-2025",
|
run_id="trip-planning-2025",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Later in the session, pull long-term + session context
|
# Later in the session, pull long-term + session context
|
||||||
results = memory.search(
|
results = memory.search(
|
||||||
"Any hotel preferences?",
|
"Any hotel preferences?",
|
||||||
user_id="alex",
|
user_id="alex",
|
||||||
session_id="trip-planning-2025",
|
run_id="trip-planning-2025",
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
Use `session_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
|
Use `run_id` when you want short-term context to expire automatically; rely on `user_id` for lasting personalization.
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
## When should you use each layer?
|
## When should you use each layer?
|
||||||
|
|||||||
+80
-16
@@ -12,7 +12,7 @@
|
|||||||
"logo": {
|
"logo": {
|
||||||
"light": "/logo/light.svg",
|
"light": "/logo/light.svg",
|
||||||
"dark": "/logo/dark.svg",
|
"dark": "/logo/dark.svg",
|
||||||
"href": "https://app.mem0.ai/"
|
"href": "https://mem0.ai"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"anchors": [
|
"anchors": [
|
||||||
@@ -70,7 +70,6 @@
|
|||||||
"platform/features/v2-memory-filters",
|
"platform/features/v2-memory-filters",
|
||||||
"platform/features/entity-scoped-memory",
|
"platform/features/entity-scoped-memory",
|
||||||
"platform/features/async-client",
|
"platform/features/async-client",
|
||||||
"platform/features/async-mode-default-change",
|
|
||||||
"platform/features/multimodal-support",
|
"platform/features/multimodal-support",
|
||||||
"platform/features/custom-categories"
|
"platform/features/custom-categories"
|
||||||
]
|
]
|
||||||
@@ -79,7 +78,6 @@
|
|||||||
"group": "Advanced Features",
|
"group": "Advanced Features",
|
||||||
"icon": "bolt",
|
"icon": "bolt",
|
||||||
"pages": [
|
"pages": [
|
||||||
"platform/features/graph-memory",
|
|
||||||
"platform/features/graph-threshold",
|
"platform/features/graph-threshold",
|
||||||
"platform/features/advanced-retrieval",
|
"platform/features/advanced-retrieval",
|
||||||
"platform/advanced-memory-operations",
|
"platform/advanced-memory-operations",
|
||||||
@@ -94,8 +92,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"platform/features/direct-import",
|
"platform/features/direct-import",
|
||||||
"platform/features/memory-export",
|
"platform/features/memory-export",
|
||||||
"platform/features/timestamp",
|
"platform/features/timestamp"
|
||||||
"platform/features/expiration-date"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -122,8 +119,6 @@
|
|||||||
"icon": "arrow-right",
|
"icon": "arrow-right",
|
||||||
"pages": [
|
"pages": [
|
||||||
"migration/oss-to-platform",
|
"migration/oss-to-platform",
|
||||||
"migration/v0-to-v1",
|
|
||||||
"migration/breaking-changes",
|
|
||||||
"migration/api-changes"
|
"migration/api-changes"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -136,6 +131,19 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tab": "OpenClaw",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Agent Harness",
|
||||||
|
"icon": "robot",
|
||||||
|
"pages": [
|
||||||
|
"integrations/openclaw",
|
||||||
|
"integrations/hermes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tab": "Open Source",
|
"tab": "Open Source",
|
||||||
"groups": [
|
"groups": [
|
||||||
@@ -159,7 +167,7 @@
|
|||||||
"open-source/features/reranker-search",
|
"open-source/features/reranker-search",
|
||||||
"open-source/features/async-memory",
|
"open-source/features/async-memory",
|
||||||
"open-source/features/multimodal-support",
|
"open-source/features/multimodal-support",
|
||||||
"open-source/features/custom-fact-extraction-prompt",
|
"open-source/features/custom-instructions",
|
||||||
"open-source/features/custom-update-memory-prompt",
|
"open-source/features/custom-update-memory-prompt",
|
||||||
"open-source/features/rest-api",
|
"open-source/features/rest-api",
|
||||||
"open-source/features/openai_compatibility"
|
"open-source/features/openai_compatibility"
|
||||||
@@ -365,7 +373,6 @@
|
|||||||
"cookbooks/frameworks/llamaindex-multiagent",
|
"cookbooks/frameworks/llamaindex-multiagent",
|
||||||
"cookbooks/frameworks/multimodal-retrieval",
|
"cookbooks/frameworks/multimodal-retrieval",
|
||||||
"cookbooks/frameworks/eliza-os-character",
|
"cookbooks/frameworks/eliza-os-character",
|
||||||
"cookbooks/frameworks/chrome-extension",
|
|
||||||
"cookbooks/frameworks/gemini-3-with-mem0-mcp",
|
"cookbooks/frameworks/gemini-3-with-mem0-mcp",
|
||||||
"cookbooks/frameworks/mirofish-swarm-memory"
|
"cookbooks/frameworks/mirofish-swarm-memory"
|
||||||
]
|
]
|
||||||
@@ -393,11 +400,11 @@
|
|||||||
"integrations/autogen",
|
"integrations/autogen",
|
||||||
"integrations/agno",
|
"integrations/agno",
|
||||||
"integrations/camel-ai",
|
"integrations/camel-ai",
|
||||||
"integrations/openclaw",
|
|
||||||
"integrations/openai-agents-sdk",
|
"integrations/openai-agents-sdk",
|
||||||
"integrations/google-ai-adk",
|
"integrations/google-ai-adk",
|
||||||
"integrations/mastra",
|
"integrations/mastra",
|
||||||
"integrations/vercel-ai-sdk"
|
"integrations/vercel-ai-sdk",
|
||||||
|
"integrations/chatdev"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -430,6 +437,28 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"tab": "Agent Plugins",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"group": "Coding Agents",
|
||||||
|
"icon": "terminal",
|
||||||
|
"pages": [
|
||||||
|
"integrations/claude-code",
|
||||||
|
"integrations/cursor",
|
||||||
|
"integrations/codex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Agent Harness",
|
||||||
|
"icon": "robot",
|
||||||
|
"pages": [
|
||||||
|
"integrations/openclaw",
|
||||||
|
"integrations/hermes"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"tab": "API Reference",
|
"tab": "API Reference",
|
||||||
"groups": [
|
"groups": [
|
||||||
@@ -522,10 +551,13 @@
|
|||||||
"tab": "Release Notes",
|
"tab": "Release Notes",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
{
|
||||||
"group": "Changelog",
|
"group": "Release Notes",
|
||||||
"icon": "rocket",
|
"icon": "rocket",
|
||||||
"pages": [
|
"pages": [
|
||||||
"changelog"
|
"changelog/highlights",
|
||||||
|
"changelog/sdk",
|
||||||
|
"changelog/platform",
|
||||||
|
"changelog/openclaw"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -579,6 +611,34 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"redirects": [
|
"redirects": [
|
||||||
|
{
|
||||||
|
"source": "/migration/breaking-changes",
|
||||||
|
"destination": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/migration/v0-to-v1",
|
||||||
|
"destination": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/platform/features/expiration-date",
|
||||||
|
"destination": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/platform/features/async-mode-default-change",
|
||||||
|
"destination": "/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/open-source/features/custom-fact-extraction-prompt",
|
||||||
|
"destination": "/open-source/features/custom-instructions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/platform/features/graph-memory",
|
||||||
|
"destination": "/open-source/features/graph-memory"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/changelog",
|
||||||
|
"destination": "/changelog/highlights"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/api-reference/memory/v2-search-memories",
|
"source": "/api-reference/memory/v2-search-memories",
|
||||||
"destination": "/api-reference/memory/search-memories"
|
"destination": "/api-reference/memory/search-memories"
|
||||||
@@ -737,7 +797,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/examples/chrome-extension",
|
"source": "/examples/chrome-extension",
|
||||||
"destination": "/cookbooks/frameworks/chrome-extension"
|
"destination": "/cookbooks/overview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/cookbooks/frameworks/chrome-extension",
|
||||||
|
"destination": "/cookbooks/overview"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/examples",
|
"source": "/examples",
|
||||||
@@ -777,7 +841,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/v0x/examples/chrome-extension",
|
"source": "/v0x/examples/chrome-extension",
|
||||||
"destination": "/cookbooks/frameworks/chrome-extension"
|
"destination": "/cookbooks/overview"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/v0x/examples/youtube-assistant",
|
"source": "/v0x/examples/youtube-assistant",
|
||||||
@@ -929,7 +993,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/features/graph-memory",
|
"source": "/features/graph-memory",
|
||||||
"destination": "/platform/features/graph-memory"
|
"destination": "/open-source/features/graph-memory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/features/:slug",
|
"source": "/features/:slug",
|
||||||
|
|||||||
@@ -381,4 +381,39 @@ Here are the available integrations for Mem0:
|
|||||||
>
|
>
|
||||||
Build AI agents with persistent memory using Mastra's framework and tools.
|
Build AI agents with persistent memory using Mastra's framework and tools.
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="OpenAI Agents SDK"
|
||||||
|
icon="robot"
|
||||||
|
href="/integrations/openai-agents-sdk"
|
||||||
|
>
|
||||||
|
Integrate Mem0 with the OpenAI Agents SDK for persistent memory across multi-agent workflows.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Google ADK"
|
||||||
|
icon="google"
|
||||||
|
href="/integrations/google-ai-adk"
|
||||||
|
>
|
||||||
|
Integrate Mem0 with Google Agent Development Kit for persistent memory across multi-agent workflows.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="Flowise"
|
||||||
|
icon="diagram-project"
|
||||||
|
href="/integrations/flowise"
|
||||||
|
>
|
||||||
|
Add persistent Mem0 memory to Flowise chatflows for context-aware conversations in the low-code builder.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="AWS Bedrock"
|
||||||
|
icon="cloud"
|
||||||
|
href="/integrations/aws-bedrock"
|
||||||
|
>
|
||||||
|
Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage.
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
title="ChatDev"
|
||||||
|
icon="comments"
|
||||||
|
href="/integrations/chatdev"
|
||||||
|
>
|
||||||
|
Add persistent cloud-managed memory to ChatDev multi-agent workflows with zero-code YAML configuration.
|
||||||
|
</Card>
|
||||||
</CardGroup>
|
</CardGroup>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pip install mem0ai agentops python-dotenv
|
|||||||
2. Valid API keys:
|
2. Valid API keys:
|
||||||
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
|
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
|
||||||
- OpenAI API Key (for LLM operations)
|
- OpenAI API Key (for LLM operations)
|
||||||
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) (optional, for cloud operations)
|
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a> (optional, for cloud operations)
|
||||||
|
|
||||||
## Basic Integration Example
|
## Basic Integration Example
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ pip install agno mem0ai python-dotenv
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. Valid API keys:
|
2. Valid API keys:
|
||||||
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys)
|
- <a href="https://app.mem0.ai/dashboard/api-keys" rel="nofollow">Mem0 API Key</a>
|
||||||
- OpenAI API Key (for the agent model)
|
- OpenAI API Key (for the agent model)
|
||||||
|
|
||||||
## Quick Integration (Using `Mem0Tools`)
|
## Quick Integration (Using `Mem0Tools`)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pip install autogen mem0ai openai python-dotenv
|
|||||||
|
|
||||||
First, we'll import the necessary libraries and set up our configurations.
|
First, we'll import the necessary libraries and set up our configurations.
|
||||||
|
|
||||||
<Note>Remember to get the Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
|
<Note>Remember to get the Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import os
|
import os
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
---
|
||||||
|
title: ChatDev
|
||||||
|
description: "Add persistent, cloud-managed memory to ChatDev multi-agent workflows with Mem0 — no code required, just YAML configuration."
|
||||||
|
---
|
||||||
|
|
||||||
|
Build multi-agent workflows in [ChatDev](https://github.com/OpenBMB/ChatDev) with persistent memory powered by Mem0. ChatDev is a zero-code multi-agent platform where agents, tools, and workflows are defined entirely in YAML. Mem0 integrates as a built-in memory store (`type: mem0`), giving your agents cloud-managed semantic search and cross-session persistence — all without writing any code.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In this guide, you'll:
|
||||||
|
1. Set up ChatDev with the Mem0 memory store
|
||||||
|
2. Configure agents with persistent memory using YAML
|
||||||
|
3. Enable automatic memory retrieval and storage across conversations
|
||||||
|
4. Leverage cross-session persistence for personalized multi-agent interactions
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Python 3.12+**
|
||||||
|
- **[uv](https://docs.astral.sh/uv/)** — Python package manager
|
||||||
|
- **Node.js 18+** and **npm** — only needed if using the web console
|
||||||
|
- A **Mem0 API key** from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>
|
||||||
|
- An **OpenAI API key** (or another LLM provider supported by ChatDev)
|
||||||
|
|
||||||
|
## Setup and Configuration
|
||||||
|
|
||||||
|
Install ChatDev and its dependencies (includes `mem0ai`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/OpenBMB/ChatDev.git
|
||||||
|
cd ChatDev
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
If you plan to use the web console, also install the frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && npm install && cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
Set up your environment variables in a `.env` file:
|
||||||
|
|
||||||
|
<Note>Get your Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a>.</Note>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
MEM0_API_KEY=your-mem0-api-key
|
||||||
|
API_KEY=your-openai-api-key
|
||||||
|
BASE_URL=https://api.openai.com/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure Mem0 Memory Store
|
||||||
|
|
||||||
|
In your ChatDev workflow YAML, add a Mem0 memory store in the `memory` section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
memory:
|
||||||
|
- name: mem0_store
|
||||||
|
type: mem0
|
||||||
|
config:
|
||||||
|
api_key: ${MEM0_API_KEY}
|
||||||
|
user_id: my-user-123 # optional: scope memories to a user
|
||||||
|
agent_id: my-agent # optional: scope memories to an agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Mem0 handles all storage, embeddings, and search server-side — no local vector databases or embedding models are needed.
|
||||||
|
|
||||||
|
## Attach Memory to an Agent
|
||||||
|
|
||||||
|
Reference the memory store in your agent node's `memories` list:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
nodes:
|
||||||
|
- id: writer
|
||||||
|
type: agent
|
||||||
|
config:
|
||||||
|
role: |
|
||||||
|
You are a knowledgeable writer. Use your memories to build
|
||||||
|
on past interactions.
|
||||||
|
memories:
|
||||||
|
- name: mem0_store
|
||||||
|
top_k: 5
|
||||||
|
similarity_threshold: 0.5 # minimum relevance score (0.0–1.0); set to -1.0 to disable
|
||||||
|
retrieve_stage:
|
||||||
|
- gen
|
||||||
|
read: true
|
||||||
|
write: true
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`read: true`** — Agent retrieves relevant memories before generating a response
|
||||||
|
- **`write: true`** — Agent stores new memories from user input after each interaction
|
||||||
|
- **`top_k`** — Number of memories to retrieve per query
|
||||||
|
- **`similarity_threshold`** — Minimum relevance score for retrieved memories. Set to `-1.0` to return all results regardless of score
|
||||||
|
- **`retrieve_stage`** — When to retrieve memories. Options: `pre_gen_thinking` (before generation), `gen` (during generation), `post_gen_thinking` (after generation), `finished` (after completion)
|
||||||
|
|
||||||
|
## Full Example Workflow
|
||||||
|
|
||||||
|
Here's a complete workflow YAML that creates a memory-backed conversational agent:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 0.4.0
|
||||||
|
graph:
|
||||||
|
description: Memory-backed conversation using Mem0
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
- id: writer
|
||||||
|
type: agent
|
||||||
|
config:
|
||||||
|
base_url: ${BASE_URL}
|
||||||
|
api_key: ${API_KEY}
|
||||||
|
provider: openai
|
||||||
|
name: gpt-5.4
|
||||||
|
role: |
|
||||||
|
You are a knowledgeable writer. Use your memories to build
|
||||||
|
on past interactions. If memory sections are provided
|
||||||
|
(wrapped by ===== Related Memories =====), incorporate
|
||||||
|
relevant context from those memories into your response.
|
||||||
|
params:
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 2000
|
||||||
|
memories:
|
||||||
|
- name: mem0_store
|
||||||
|
top_k: 5
|
||||||
|
retrieve_stage:
|
||||||
|
- gen
|
||||||
|
read: true
|
||||||
|
write: true
|
||||||
|
|
||||||
|
memory:
|
||||||
|
- name: mem0_store
|
||||||
|
type: mem0
|
||||||
|
config:
|
||||||
|
api_key: ${MEM0_API_KEY}
|
||||||
|
user_id: project-user-123
|
||||||
|
agent_id: writer-agent
|
||||||
|
|
||||||
|
start:
|
||||||
|
- writer
|
||||||
|
end: []
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the workflow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option 1: CLI (recommended for quick testing)
|
||||||
|
uv run python run.py --path yaml_instance/demo_mem0_memory.yaml --name my_project
|
||||||
|
|
||||||
|
# Option 2: Web console
|
||||||
|
make dev
|
||||||
|
# Backend starts at http://localhost:6400, frontend at http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
To use the web console, open `http://localhost:5173`, create a new workflow, and paste your YAML configuration into the editor. The web console provides a visual chat interface for interacting with your memory-backed agents.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
When an agent with Mem0 memory receives input, the following cycle runs automatically:
|
||||||
|
|
||||||
|
**1. Retrieve** — Before generating a response, ChatDev queries Mem0 with the user's input using semantic search. Relevant memories are injected into the agent's context in this format:
|
||||||
|
|
||||||
|
```
|
||||||
|
===== Related Memories =====
|
||||||
|
--- mem0_store ---
|
||||||
|
1. User's favorite language is Rust
|
||||||
|
2. User lives in San Francisco
|
||||||
|
===== End of Memory =====
|
||||||
|
```
|
||||||
|
|
||||||
|
This is why the role prompt in the example references `===== Related Memories =====` — the agent needs to know how to use this injected context.
|
||||||
|
|
||||||
|
**2. Generate** — The agent produces a response using the retrieved memories as additional context.
|
||||||
|
|
||||||
|
**3. Store** — After generation, the user's input is sent to Mem0 via `client.add()`. Mem0's extraction model automatically identifies and stores facts, preferences, and key information. Only user input is stored — agent output is excluded to keep memories clean.
|
||||||
|
|
||||||
|
Memories persist in Mem0's cloud across all sessions. The next time the same `user_id` or `agent_id` is used, previous memories are automatically retrieved.
|
||||||
|
|
||||||
|
## Dual-Scope Memory (User + Agent)
|
||||||
|
|
||||||
|
When both `user_id` and `agent_id` are configured, Mem0 uses an OR filter to search across both scopes in a single query:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
memory:
|
||||||
|
- name: shared_store
|
||||||
|
type: mem0
|
||||||
|
config:
|
||||||
|
api_key: ${MEM0_API_KEY}
|
||||||
|
user_id: alice # stores user preferences ("Alice prefers dark mode")
|
||||||
|
agent_id: support-bot # stores agent-learned context ("Resolved Alice's billing issue")
|
||||||
|
```
|
||||||
|
|
||||||
|
This means retrieval returns memories from **both** the user's scope and the agent's scope. Writes include both IDs, so each memory is accessible from either dimension. Use this when you want an agent to remember both what the user told it *and* what the agent learned across sessions.
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
### Memory Store Config
|
||||||
|
|
||||||
|
| Field | Required | Description |
|
||||||
|
|-------|----------|-------------|
|
||||||
|
| `api_key` | Yes | Mem0 API key from <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> |
|
||||||
|
| `user_id` | No | Scope memories to a specific user |
|
||||||
|
| `agent_id` | No | Scope memories to a specific agent |
|
||||||
|
|
||||||
|
### Memory Attachment Config
|
||||||
|
|
||||||
|
| Field | Default | Description |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| `top_k` | `3` | Number of memories to retrieve |
|
||||||
|
| `similarity_threshold` | `-1.0` (disabled) | Minimum relevance score. Set a value between `0.0` and `1.0` to filter low-relevance results. Default (`-1.0`) returns all matches without filtering |
|
||||||
|
| `retrieve_stage` | `["gen"]` | When to retrieve: `pre_gen_thinking`, `gen`, `post_gen_thinking`, or `finished` |
|
||||||
|
| `read` | `true` | Whether the agent retrieves memories |
|
||||||
|
| `write` | `true` | Whether the agent stores new memories |
|
||||||
|
|
||||||
|
## Tips and Common Pitfalls
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
**Indexing delay** — Freshly stored memories may take a few seconds to become searchable. If a memory isn't retrieved immediately after being stored, wait a moment and try again.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
- **No memories returned on first run** — This is expected. Memories are stored *after* the agent responds, so the first interaction has no prior context. Memories appear starting from the second interaction onward.
|
||||||
|
- **`mem0ai` not installed** — If you see `ImportError: mem0ai is required for Mem0Memory`, run `uv add mem0ai` or `pip install mem0ai` to add the dependency.
|
||||||
|
- **Invalid API key** — A wrong or expired `MEM0_API_KEY` will log errors like `Mem0 search failed` or `Mem0 add failed` but won't crash the agent. Check your key at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a>.
|
||||||
|
- **Pipeline headers in memories** — ChatDev automatically strips internal pipeline headers (e.g., `=== INPUT FROM TASK (user) ===`) before sending text to Mem0, so your memories stay clean.
|
||||||
|
- **Clearing test memories** — To delete memories created during testing, use the Mem0 dashboard at <a href="https://app.mem0.ai" rel="nofollow">app.mem0.ai</a> or the Python SDK: `MemoryClient().delete_all(user_id="your-test-user")`.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
1. **Zero-Code Integration** — Configure Mem0 entirely through YAML, no Python code required
|
||||||
|
2. **Cloud-Managed Storage** — Mem0 handles embeddings, persistence, and search server-side
|
||||||
|
3. **Semantic Search** — Retrieve contextually relevant memories, not just keyword matches
|
||||||
|
4. **Cross-Session Persistence** — Memories survive across runs, sessions, and restarts
|
||||||
|
5. **Multi-Agent Memory Sharing** — Multiple agents can share memories through common `user_id` or `agent_id` scopes
|
||||||
|
6. **Intelligent Input Processing** — Only user input is stored; agent output is excluded to prevent noisy memories
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
By adding Mem0 as a memory store in ChatDev, your multi-agent workflows gain persistent, intelligent memory with zero code changes. Agents automatically remember past interactions and use that context to provide personalized, coherent responses across sessions.
|
||||||
|
|
||||||
|
<CardGroup cols={2}>
|
||||||
|
<Card title="CrewAI Integration" icon="users" href="/integrations/crewai">
|
||||||
|
Build multi-agent systems with CrewAI and Mem0
|
||||||
|
</Card>
|
||||||
|
<Card title="AutoGen Integration" icon="robot" href="/integrations/autogen">
|
||||||
|
Build conversational agents with AutoGen and Mem0
|
||||||
|
</Card>
|
||||||
|
</CardGroup>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user