Compare commits
134
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b6790bafb | ||
|
|
93da5ef8f7 | ||
|
|
c1c5bd62f6 | ||
|
|
2ec3c4ab20 | ||
|
|
3fbc1c9aef | ||
|
|
0b14f75c05 | ||
|
|
fb224083e4 | ||
|
|
30469aec17 | ||
|
|
50db9e428d | ||
|
|
fb87349664 | ||
|
|
8827553576 | ||
|
|
c8e20a9bb5 | ||
|
|
93a51f4763 | ||
|
|
86fe275f53 | ||
|
|
e6d6276bb9 | ||
|
|
9692726db4 | ||
|
|
d8d776636f | ||
|
|
a5a688295e | ||
|
|
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 | ||
|
|
25e25aaa2f | ||
|
|
5d30af9560 | ||
|
|
47969aaa6b | ||
|
|
7213e1b1a6 | ||
|
|
97291aa119 | ||
|
|
b8a5ca1b70 | ||
|
|
431cba20e9 | ||
|
|
4482228383 | ||
|
|
ae49edec44 | ||
|
|
4b7f51d194 | ||
|
|
41abb571a2 | ||
|
|
e280665578 | ||
|
|
27a6e7863e | ||
|
|
ae8e03c6b7 | ||
|
|
376be3b6d4 | ||
|
|
cd2dd7cc54 | ||
|
|
ece654811a | ||
|
|
13d42a99e9 | ||
|
|
3225e30859 | ||
|
|
88fd0e77d0 | ||
|
|
c2698f2b71 | ||
|
|
e87240d4d9 | ||
|
|
68cf4e118d | ||
|
|
12624555b4 | ||
|
|
e8004b93db | ||
|
|
4e414e4015 | ||
|
|
16455789d4 | ||
|
|
3ac4e047de | ||
|
|
a2ffca3266 | ||
|
|
3a9fcdbec2 | ||
|
|
515f87b6bd | ||
|
|
5edc0cc99f | ||
|
|
7fff26f374 | ||
|
|
1aecfadf45 | ||
|
|
7e06aeeada | ||
|
|
2a59c9fd99 | ||
|
|
669ed184e4 |
@@ -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,45 @@
|
|||||||
|
name: docs - llms.txt check
|
||||||
|
|
||||||
|
# Blocks PRs that introduce new .mdx pages without a matching entry in
|
||||||
|
# docs/llms.txt, or that link to pages that no longer exist. Contributors
|
||||||
|
# must update docs/llms.txt in the same PR. Run locally with:
|
||||||
|
# python scripts/check-llms-txt-coverage.py # read-only
|
||||||
|
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'docs/**/*.mdx'
|
||||||
|
- 'docs/llms.txt'
|
||||||
|
- 'scripts/check-llms-txt-coverage.py'
|
||||||
|
- 'scripts/llms-txt-ignore.txt'
|
||||||
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-llms-txt:
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 2
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Verify docs/llms.txt coverage
|
||||||
|
run: |
|
||||||
|
if ! python3 scripts/check-llms-txt-coverage.py; then
|
||||||
|
echo ""
|
||||||
|
echo "::error title=llms.txt out of sync::docs/llms.txt does not match docs/**/*.mdx."
|
||||||
|
echo ""
|
||||||
|
echo "To fix:"
|
||||||
|
echo " 1. Run locally: python scripts/check-llms-txt-coverage.py --write"
|
||||||
|
echo " This appends placeholder entries under '## Unclassified - needs triage'."
|
||||||
|
echo " 2. For each placeholder:"
|
||||||
|
echo " - replace [TODO: Platform|OSS|Both] with the correct scope tag"
|
||||||
|
echo " - rewrite the description as 'Use when ...'"
|
||||||
|
echo " - move the entry into the appropriate section"
|
||||||
|
echo " - delete the '## Unclassified - needs triage' heading once empty"
|
||||||
|
echo " 3. Resolve any stale URLs listed above by updating or removing the link."
|
||||||
|
echo " 4. Commit the updated docs/llms.txt to this PR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -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,583 @@
|
|||||||
|
# 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 |
|
||||||
|
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
|
||||||
|
|
||||||
|
## 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
|
||||||
|
5. **llms.txt**: Any new `.mdx` page under `docs/` must be linked in `docs/llms.txt` with a scope tag (`[Platform]` / `[OSS]` / `[Both]`) and a `Use when ...` description. The `docs-llms-txt-check.yml` workflow runs on every PR that touches docs and **fails the check** if the index is out of sync. To fix: run `python scripts/check-llms-txt-coverage.py --write` locally to scaffold placeholders under `## Unclassified - needs triage`, then replace the `[TODO: ...]` tags, rewrite descriptions as `Use when ...`, move entries into the right section, and delete the triage heading when empty.
|
||||||
|
|
||||||
|
### 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)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -41,16 +41,30 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://mem0.ai/research"><strong>📄 Building Production-Ready AI Agents with Scalable Long-Term Memory →</strong></a>
|
<a href="https://mem0.ai/research"><strong>📄 Building Production-Ready AI Agents with Scalable Long-Term Memory →</strong></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
|
||||||
<strong>⚡ +26% Accuracy vs. OpenAI Memory • 🚀 91% Faster • 💰 90% Fewer Tokens</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
> **🎉 mem0ai v1.0.0 is now available!** This major release includes API modernization, improved vector store support, and enhanced GCP integration. [See migration guide →](MIGRATION_GUIDE_v1.0.md)
|
## New Memory Algorithm (April 2026)
|
||||||
|
|
||||||
## 🔥 Research Highlights
|
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||||
- **+26% Accuracy** over OpenAI Memory on the LOCOMO benchmark
|
| --- | --- | --- | --- | --- |
|
||||||
- **91% Faster Responses** than full-context, ensuring low-latency at scale
|
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||||
- **90% Lower Token Usage** than full-context, cutting costs without compromise
|
| **LongMemEval** | 67.8 | **93.4** | 6.8K | 1.09s |
|
||||||
|
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||||
|
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||||
|
|
||||||
|
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops).
|
||||||
|
|
||||||
|
**What changed:**
|
||||||
|
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
|
||||||
|
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
|
||||||
|
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
|
||||||
|
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
|
||||||
|
|
||||||
|
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||||
|
|
||||||
|
## Research Highlights
|
||||||
|
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||||
|
- **93.4 on LongMemEval** -- +26 points, with +53.6 on assistant memory recall
|
||||||
|
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||||
- [Read the full paper](https://mem0.ai/research)
|
- [Read the full paper](https://mem0.ai/research)
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
@@ -88,14 +102,37 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
Manage memories from your terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @mem0/cli # or: pip install mem0-cli
|
||||||
|
|
||||||
|
mem0 init
|
||||||
|
mem0 add "Prefers dark mode and vim keybindings" --user-id alice
|
||||||
|
mem0 search "What does Alice prefer?" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
|
||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
Mem0 requires an LLM to function, with `gpt-5-mini` 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:
|
||||||
|
|
||||||
@@ -108,13 +145,13 @@ memory = Memory()
|
|||||||
|
|
||||||
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
|
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
|
||||||
# Retrieve relevant memories
|
# Retrieve relevant memories
|
||||||
relevant_memories = memory.search(query=message, user_id=user_id, limit=3)
|
relevant_memories = memory.search(query=message, filters={"user_id": user_id}, top_k=3)
|
||||||
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
|
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
|
||||||
|
|
||||||
# Generate Assistant response
|
# Generate Assistant response
|
||||||
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
|
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
|
||||||
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
|
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
|
||||||
response = openai_client.chat.completions.create(model="gpt-4.1-nano-2025-04-14", messages=messages)
|
response = openai_client.chat.completions.create(model="gpt-5-mini", messages=messages)
|
||||||
assistant_response = response.choices[0].message.content
|
assistant_response = response.choices[0].message.content
|
||||||
|
|
||||||
# Create new memories from the conversation
|
# Create new memories from the conversation
|
||||||
@@ -148,7 +185,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
|
|||||||
## 📚 Documentation & Support
|
## 📚 Documentation & Support
|
||||||
|
|
||||||
- Full docs: https://docs.mem0.ai
|
- Full docs: https://docs.mem0.ai
|
||||||
- Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
|
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
|
||||||
- Contact: founders@mem0.ai
|
- Contact: founders@mem0.ai
|
||||||
|
|
||||||
## Citation
|
## Citation
|
||||||
@@ -166,4 +203,4 @@ We now have a paper you can cite:
|
|||||||
|
|
||||||
## ⚖️ License
|
## ⚖️ License
|
||||||
|
|
||||||
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
|
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# mem0 CLI
|
||||||
|
|
||||||
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
|
||||||
|
|
||||||
|
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @mem0/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mem0-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Both packages install a `mem0` binary with identical behavior.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email (get a new API key)
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `mem0 init` | Setup wizard — login via email or configure API key manually |
|
||||||
|
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
|
||||||
|
| `mem0 search` | Search memories using natural language |
|
||||||
|
| `mem0 list` | List memories with optional filters and pagination |
|
||||||
|
| `mem0 get` | Retrieve a specific memory by ID |
|
||||||
|
| `mem0 update` | Update the text or metadata of a memory |
|
||||||
|
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
|
||||||
|
| `mem0 import` | Bulk import memories from a JSON file |
|
||||||
|
| `mem0 config` | View or modify CLI configuration |
|
||||||
|
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||||
|
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||||
|
| `mem0 status` | Verify API connection and display current project |
|
||||||
|
| `mem0 version` | Print the CLI version |
|
||||||
|
|
||||||
|
Run `mem0 <command> --help` for detailed usage on any command.
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
## Implementations
|
||||||
|
|
||||||
|
| Language | Directory | Package | Docs |
|
||||||
|
|----------|-----------|---------|------|
|
||||||
|
| TypeScript | [`node/`](./node/) | `@mem0/cli` | [README](./node/README.md) |
|
||||||
|
| Python | [`python/`](./python/) | `mem0-cli` | [README](./python/README.md) |
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
{
|
||||||
|
"specVersion": 1,
|
||||||
|
"cli": {
|
||||||
|
"name": "mem0",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "The Memory Layer for AI Agents",
|
||||||
|
"helpText": "\u25c6 mem0 CLI \u2014 The Memory Layer for AI Agents"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"logoMini": "\u25c6 mem0",
|
||||||
|
"tagline": "The Memory Layer for AI Agents",
|
||||||
|
"colors": {
|
||||||
|
"brand": "#8b5cf6",
|
||||||
|
"accent": "#a78bfa",
|
||||||
|
"success": "#22c55e",
|
||||||
|
"error": "#ef4444",
|
||||||
|
"warning": "#f59e0b",
|
||||||
|
"dim": "#6b7280"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"success": "\u2713",
|
||||||
|
"error": "\u2717",
|
||||||
|
"warning": "\u26a0",
|
||||||
|
"info": "\u25c6",
|
||||||
|
"pending": "\u29d7",
|
||||||
|
"add": "+",
|
||||||
|
"update": "~",
|
||||||
|
"delete": "-",
|
||||||
|
"noop": "\u00b7",
|
||||||
|
"connected": "\u25cf",
|
||||||
|
"disconnected": "\u25cf"
|
||||||
|
},
|
||||||
|
"logo": "███\u2557 ███\u2557███████\u2557███\u2557 ███\u2557 ██████\u2557 ██████\u2557██\u2557 ██\u2557\n████\u2557 ████\u2551██\u2554\u2550\u2550\u2550\u2550\u255d████\u2557 ████\u2551██\u2554\u2550████\u2557 ██\u2554\u2550\u2550\u2550\u2550\u255d██\u2551 ██\u2551\n██\u2554████\u2554██\u2551█████\u2557 ██\u2554████\u2554██\u2551██\u2551██\u2554██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551\u255a██\u2554\u255d██\u2551██\u2554\u2550\u2550\u255d ██\u2551\u255a██\u2554\u255d██\u2551████\u2554\u255d██\u2551 ██\u2551 ██\u2551 ██\u2551\n██\u2551 \u255a\u2550\u255d ██\u2551███████\u2557██\u2551 \u255a\u2550\u255d ██\u2551\u255a██████\u2554\u255d \u255a██████\u2557███████\u2557██\u2551\n\u255a\u2550\u255d \u255a\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d \u255a\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d \u255a\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u255d\u255a\u2550\u255d"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"configDir": "~/.mem0",
|
||||||
|
"configFile": "config.json",
|
||||||
|
"version": 1,
|
||||||
|
"defaultBaseUrl": "https://api.mem0.ai",
|
||||||
|
"sections": {
|
||||||
|
"platform": {
|
||||||
|
"fields": {
|
||||||
|
"api_key": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_API_KEY",
|
||||||
|
"redact": true
|
||||||
|
},
|
||||||
|
"base_url": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "https://api.mem0.ai",
|
||||||
|
"envVar": "MEM0_BASE_URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"fields": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_USER_ID"
|
||||||
|
},
|
||||||
|
"agent_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_AGENT_ID"
|
||||||
|
},
|
||||||
|
"app_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_APP_ID"
|
||||||
|
},
|
||||||
|
"run_id": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "",
|
||||||
|
"envVar": "MEM0_RUN_ID"
|
||||||
|
},
|
||||||
|
"enable_graph": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"envVar": "MEM0_ENABLE_GRAPH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"endpoints": {
|
||||||
|
"add": { "method": "POST", "path": "/v1/memories/" },
|
||||||
|
"search": { "method": "POST", "path": "/v2/memories/search/" },
|
||||||
|
"get": { "method": "GET", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"list": { "method": "POST", "path": "/v2/memories/" },
|
||||||
|
"update": { "method": "PUT", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"delete": { "method": "DELETE", "path": "/v1/memories/{memory_id}/" },
|
||||||
|
"deleteAll": { "method": "DELETE", "path": "/v1/memories/" },
|
||||||
|
"entities": { "method": "GET", "path": "/v1/entities/" },
|
||||||
|
"deleteEntities": { "method": "DELETE", "path": "/v1/entities/" }
|
||||||
|
},
|
||||||
|
"authHeader": "Authorization",
|
||||||
|
"authScheme": "Token",
|
||||||
|
"timeout": 30,
|
||||||
|
"entityTypeMap": {
|
||||||
|
"users": "user",
|
||||||
|
"agents": "agent",
|
||||||
|
"apps": "app",
|
||||||
|
"runs": "run"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"AuthError": {
|
||||||
|
"httpStatus": 401,
|
||||||
|
"message": "Authentication failed. Your API key may be invalid or expired."
|
||||||
|
},
|
||||||
|
"NotFoundError": {
|
||||||
|
"httpStatus": 404,
|
||||||
|
"messageTemplate": "Resource not found: {path}"
|
||||||
|
},
|
||||||
|
"APIError": {
|
||||||
|
"httpStatus": 400,
|
||||||
|
"messageTemplate": "Bad request to {path}: {detail}"
|
||||||
|
},
|
||||||
|
"noApiKey": {
|
||||||
|
"message": "No API key configured.",
|
||||||
|
"hint": "Run 'mem0 init' or set MEM0_API_KEY environment variable."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"optionGroups": {
|
||||||
|
"scope": {
|
||||||
|
"label": "Scope",
|
||||||
|
"options": ["user_id", "agent_id", "app_id", "run_id"]
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"label": "Search",
|
||||||
|
"options": ["top_k", "threshold", "rerank", "keyword", "filter_json", "fields", "graph", "no_graph"]
|
||||||
|
},
|
||||||
|
"pagination": {
|
||||||
|
"label": "Pagination",
|
||||||
|
"options": ["page", "page_size"]
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"label": "Filters",
|
||||||
|
"options": ["category", "after", "before", "graph", "no_graph"]
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"label": "Output",
|
||||||
|
"options": ["output"]
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"label": "Connection",
|
||||||
|
"options": ["api_key", "base_url"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globalOptions": [
|
||||||
|
{
|
||||||
|
"name": "api_key",
|
||||||
|
"flags": ["--api-key"],
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"envVar": "MEM0_API_KEY",
|
||||||
|
"help": "Override API key.",
|
||||||
|
"panel": "Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "base_url",
|
||||||
|
"flags": ["--base-url"],
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Override API base URL.",
|
||||||
|
"panel": "Connection"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "version",
|
||||||
|
"flags": ["--version"],
|
||||||
|
"type": "boolean",
|
||||||
|
"required": false,
|
||||||
|
"help": "Show version and exit."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "add",
|
||||||
|
"description": "Add a memory from text, messages, file, or stdin.",
|
||||||
|
"usage": "mem0 add <text> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Text content to add as a memory."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||||
|
{ "name": "messages", "flags": ["--messages"], "type": "string", "help": "Conversation messages as JSON." },
|
||||||
|
{ "name": "file", "flags": ["--file", "-f"], "type": "path", "help": "Read messages from JSON file." },
|
||||||
|
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Custom metadata as JSON." },
|
||||||
|
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
|
||||||
|
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
|
||||||
|
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||||
|
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "search",
|
||||||
|
"description": "Search memories by semantic query.",
|
||||||
|
"usage": "mem0 search <query> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "table"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "query",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Search query."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||||
|
{ "name": "top_k", "flags": ["--top-k", "-k", "--limit"], "type": "integer", "default": 10, "help": "Number of results.", "panel": "Search" },
|
||||||
|
{ "name": "threshold", "flags": ["--threshold"], "type": "float", "default": 0.3, "help": "Minimum similarity score.", "panel": "Search" },
|
||||||
|
{ "name": "rerank", "flags": ["--rerank"], "type": "boolean", "default": false, "help": "Enable reranking (Platform only).", "panel": "Search" },
|
||||||
|
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
|
||||||
|
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
|
||||||
|
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get",
|
||||||
|
"description": "Get a specific memory by ID.",
|
||||||
|
"usage": "mem0 get <memory_id> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Memory ID to retrieve."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "get"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "List memories with optional filters.",
|
||||||
|
"usage": "mem0 list [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": true,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "table"],
|
||||||
|
"defaultOutput": "table",
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Filter by user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Filter by agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Filter by app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Filter by run.", "panel": "Scope" },
|
||||||
|
{ "name": "page", "flags": ["--page"], "type": "integer", "default": 1, "help": "Page number.", "panel": "Pagination" },
|
||||||
|
{ "name": "page_size", "flags": ["--page-size"], "type": "integer", "default": 100, "help": "Results per page.", "panel": "Pagination" },
|
||||||
|
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
|
||||||
|
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
|
||||||
|
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
|
||||||
|
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
|
||||||
|
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "update",
|
||||||
|
"description": "Update a memory's text or metadata.",
|
||||||
|
"usage": "mem0 update <memory_id> [text] [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Memory ID to update."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "New memory text."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "update"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete",
|
||||||
|
"description": "Delete a memory, all memories matching a scope, or an entity.",
|
||||||
|
"usage": "mem0 delete [memory_id] [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": true,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "memory_id",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"help": "Memory ID to delete (omit when using --all or --entity)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
|
||||||
|
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
|
||||||
|
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
|
||||||
|
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Scope to agent.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "Scope to app.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Scope to run.", "panel": "Scope" },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "delete",
|
||||||
|
"notes": "Mutually exclusive modes: (1) mem0 delete <id> -- single memory, (2) mem0 delete --all [scope] -- bulk delete, (3) mem0 delete --entity [scope] -- entity cascade delete. Cannot combine <memoryId> with --all or --entity, and cannot combine --all with --entity."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "import",
|
||||||
|
"description": "Import memories from a JSON file.",
|
||||||
|
"usage": "mem0 import <file_path> [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": true,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["text"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "file_path",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "JSON file to import."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Override user ID.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Override agent ID.", "panel": "Scope" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "config",
|
||||||
|
"description": "Manage mem0 configuration.",
|
||||||
|
"isGroup": true,
|
||||||
|
"subcommands": [
|
||||||
|
{
|
||||||
|
"name": "show",
|
||||||
|
"description": "Display current configuration (secrets redacted).",
|
||||||
|
"usage": "mem0 config show",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "get",
|
||||||
|
"description": "Get a configuration value.",
|
||||||
|
"usage": "mem0 config get <key>",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Config key (e.g. platform.api_key)."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "set",
|
||||||
|
"description": "Set a configuration value.",
|
||||||
|
"usage": "mem0 config set <key> <value>",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Config key (e.g. platform.api_key)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "value",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Value to set."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entity",
|
||||||
|
"description": "Manage entities.",
|
||||||
|
"isGroup": true,
|
||||||
|
"subcommands": [
|
||||||
|
{
|
||||||
|
"name": "list",
|
||||||
|
"description": "List all entities of a given type.",
|
||||||
|
"usage": "mem0 entity list <entity_type>",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"outputFormats": ["table", "json"],
|
||||||
|
"defaultOutput": "table",
|
||||||
|
"arguments": [
|
||||||
|
{
|
||||||
|
"name": "entity_type",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"help": "Entity type: users, agents, apps, runs.",
|
||||||
|
"choices": ["users", "agents", "apps", "runs"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"options": [
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: table, json.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "entities"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "delete",
|
||||||
|
"description": "Delete an entity and ALL its memories (cascade).",
|
||||||
|
"usage": "mem0 entity delete [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": true,
|
||||||
|
"outputFormats": ["text", "json", "quiet"],
|
||||||
|
"defaultOutput": "text",
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "User ID.", "panel": "Scope" },
|
||||||
|
{ "name": "agent_id", "flags": ["--agent-id"], "type": "string", "help": "Agent ID.", "panel": "Scope" },
|
||||||
|
{ "name": "app_id", "flags": ["--app-id"], "type": "string", "help": "App ID.", "panel": "Scope" },
|
||||||
|
{ "name": "run_id", "flags": ["--run-id"], "type": "string", "help": "Run ID.", "panel": "Scope" },
|
||||||
|
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||||
|
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||||
|
],
|
||||||
|
"apiEndpoint": "deleteEntities"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "init",
|
||||||
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
|
"usage": "mem0 init [OPTIONS]",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
|
||||||
|
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||||
|
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
||||||
|
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
|
||||||
|
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status",
|
||||||
|
"description": "Check connectivity and authentication.",
|
||||||
|
"usage": "mem0 status [OPTIONS]",
|
||||||
|
"needsBackend": true,
|
||||||
|
"needsConfig": true,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "help",
|
||||||
|
"description": "Show help. Use --json for machine-readable output (for LLM agents).",
|
||||||
|
"usage": "mem0 help [OPTIONS]",
|
||||||
|
"needsBackend": false,
|
||||||
|
"needsConfig": false,
|
||||||
|
"resolveIds": false,
|
||||||
|
"resolveGraph": false,
|
||||||
|
"confirmDangerous": false,
|
||||||
|
"arguments": [],
|
||||||
|
"options": [
|
||||||
|
{ "name": "json", "flags": ["--json"], "type": "boolean", "default": false, "help": "Output machine-readable JSON for LLM agents." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,337 @@
|
|||||||
|
# mem0 CLI (Node.js)
|
||||||
|
|
||||||
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js **18+**
|
||||||
|
- pnpm (`npm install -g pnpm`) — for development only
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @mem0/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/node
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Development mode (runs TypeScript directly, no build needed)
|
||||||
|
pnpm dev --help
|
||||||
|
pnpm dev add "test memory" --user-id alice
|
||||||
|
pnpm dev search "test" --user-id alice
|
||||||
|
|
||||||
|
# Or build first, then run the compiled JS
|
||||||
|
pnpm build
|
||||||
|
node dist/index.js --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Node.js **18+**
|
||||||
|
- pnpm (`npm install -g pnpm`)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
From the `node/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the CLI
|
||||||
|
|
||||||
|
There are two ways to run the CLI during development:
|
||||||
|
|
||||||
|
### Option 1: Development mode (no build needed)
|
||||||
|
|
||||||
|
Uses `tsx` to run TypeScript directly. Pass CLI arguments after `pnpm dev`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev --help
|
||||||
|
pnpm dev version
|
||||||
|
pnpm dev add "test memory" --user-id alice
|
||||||
|
pnpm dev search "test" --user-id alice
|
||||||
|
pnpm dev config show
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Do NOT use `pnpm dev -- --help`. With pnpm, arguments pass through directly — adding `--` inserts a literal `--` that breaks the CLI parser.
|
||||||
|
|
||||||
|
### Option 2: Build and run compiled JS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build first
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# Run the compiled CLI
|
||||||
|
node dist/index.js --help
|
||||||
|
node dist/index.js version
|
||||||
|
node dist/index.js add "test memory" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Link globally (makes `mem0` available system-wide)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
pnpm link --global
|
||||||
|
|
||||||
|
# Now use it like a normal CLI
|
||||||
|
mem0 --help
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled output is in `dist/`.
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
pnpm test
|
||||||
|
|
||||||
|
# Watch mode
|
||||||
|
pnpm test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Auto-fix
|
||||||
|
pnpm lint:fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm typecheck
|
||||||
|
```
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "@mem0/cli",
|
||||||
|
"version": "0.2.3",
|
||||||
|
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"mem0": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest",
|
||||||
|
"lint": "biome check src/",
|
||||||
|
"lint:fix": "biome check --write src/",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"author": "mem0.ai <founders@mem0.ai>",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mem0ai/mem0",
|
||||||
|
"directory": "cli/node"
|
||||||
|
},
|
||||||
|
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^12.0.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-table3": "^0.6.4",
|
||||||
|
"ora": "^8.0.0",
|
||||||
|
"boxen": "^7.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.4.0",
|
||||||
|
"tsup": "^8.0.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
|
"vitest": "^1.5.0",
|
||||||
|
"@biomejs/biome": "^1.7.0",
|
||||||
|
"@types/node": "^20.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2066
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Abstract backend interface and factory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Mem0Config } from "../config.js";
|
||||||
|
import { PlatformBackend } from "./platform.js";
|
||||||
|
|
||||||
|
export interface AddOptions {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
immutable?: boolean;
|
||||||
|
infer?: boolean;
|
||||||
|
expires?: string;
|
||||||
|
categories?: string[];
|
||||||
|
enableGraph?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchOptions {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
topK?: number;
|
||||||
|
threshold?: number;
|
||||||
|
rerank?: boolean;
|
||||||
|
keyword?: boolean;
|
||||||
|
filters?: Record<string, unknown>;
|
||||||
|
fields?: string[];
|
||||||
|
enableGraph?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListOptions {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
category?: string;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
enableGraph?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteOptions {
|
||||||
|
all?: boolean;
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityIds {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Backend {
|
||||||
|
add(
|
||||||
|
content?: string,
|
||||||
|
messages?: Record<string, unknown>[],
|
||||||
|
opts?: AddOptions,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
search(
|
||||||
|
query: string,
|
||||||
|
opts?: SearchOptions,
|
||||||
|
): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
|
get(memoryId: string): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
|
update(
|
||||||
|
memoryId: string,
|
||||||
|
content?: string,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts?: DeleteOptions,
|
||||||
|
): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
ping(): Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
|
status(opts?: { userId?: string; agentId?: string }): Promise<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
|
||||||
|
entities(entityType: string): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
|
listEvents(): Promise<Record<string, unknown>[]>;
|
||||||
|
|
||||||
|
getEvent(eventId: string): Promise<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthError extends Error {
|
||||||
|
constructor(
|
||||||
|
message = "Authentication failed. Your API key may be invalid or expired.",
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AuthError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends Error {
|
||||||
|
constructor(path: string) {
|
||||||
|
super(`Resource not found: ${path}`);
|
||||||
|
this.name = "NotFoundError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class APIError extends Error {
|
||||||
|
constructor(path: string, detail: string) {
|
||||||
|
super(`Bad request to ${path}: ${detail}`);
|
||||||
|
this.name = "APIError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBackend(config: Mem0Config): Backend {
|
||||||
|
return new PlatformBackend(config.platform);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Backend factory re-export.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { getBackend } from "./base.js";
|
||||||
|
export type {
|
||||||
|
Backend,
|
||||||
|
AddOptions,
|
||||||
|
SearchOptions,
|
||||||
|
ListOptions,
|
||||||
|
DeleteOptions,
|
||||||
|
EntityIds,
|
||||||
|
} from "./base.js";
|
||||||
|
export { AuthError, NotFoundError, APIError } from "./base.js";
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
/**
|
||||||
|
* Platform (SaaS) backend — communicates with api.mem0.ai.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { PlatformConfig } from "../config.js";
|
||||||
|
import { isAgentMode } from "../state.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
|
import {
|
||||||
|
APIError,
|
||||||
|
type AddOptions,
|
||||||
|
AuthError,
|
||||||
|
type Backend,
|
||||||
|
type DeleteOptions,
|
||||||
|
type EntityIds,
|
||||||
|
type ListOptions,
|
||||||
|
NotFoundError,
|
||||||
|
type SearchOptions,
|
||||||
|
} from "./base.js";
|
||||||
|
|
||||||
|
export class PlatformBackend implements Backend {
|
||||||
|
private baseUrl: string;
|
||||||
|
private headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(config: PlatformConfig) {
|
||||||
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||||
|
this.headers = {
|
||||||
|
Authorization: `Token ${config.apiKey}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "node",
|
||||||
|
"X-Mem0-Client-Version": CLI_VERSION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _request(
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
opts?: { json?: unknown; params?: Record<string, string> },
|
||||||
|
): Promise<unknown> {
|
||||||
|
let url = `${this.baseUrl}${path}`;
|
||||||
|
if (opts?.params) {
|
||||||
|
const qs = new URLSearchParams(opts.params).toString();
|
||||||
|
url += `?${qs}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...this.headers,
|
||||||
|
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchOpts: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
|
};
|
||||||
|
if (opts?.json) {
|
||||||
|
fetchOpts.body = JSON.stringify(opts.json);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url, fetchOpts);
|
||||||
|
|
||||||
|
if (resp.status === 401) {
|
||||||
|
throw new AuthError();
|
||||||
|
}
|
||||||
|
if (resp.status === 404) {
|
||||||
|
throw new NotFoundError(path);
|
||||||
|
}
|
||||||
|
if (resp.status === 400) {
|
||||||
|
let detail: string;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail =
|
||||||
|
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
|
||||||
|
resp.statusText;
|
||||||
|
} catch {
|
||||||
|
detail = resp.statusText;
|
||||||
|
}
|
||||||
|
throw new APIError(path, detail);
|
||||||
|
}
|
||||||
|
if (!resp.ok) {
|
||||||
|
let detail: string = resp.statusText;
|
||||||
|
try {
|
||||||
|
const body = (await resp.json()) as Record<string, unknown>;
|
||||||
|
detail = (body.detail ?? body.message ?? resp.statusText) as string;
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${detail}`);
|
||||||
|
}
|
||||||
|
if (resp.status === 204) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(
|
||||||
|
content?: string,
|
||||||
|
messages?: Record<string, unknown>[],
|
||||||
|
opts: AddOptions = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (messages) {
|
||||||
|
payload.messages = messages;
|
||||||
|
} else if (content) {
|
||||||
|
payload.messages = [{ role: "user", content }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.userId) payload.user_id = opts.userId;
|
||||||
|
if (opts.agentId) payload.agent_id = opts.agentId;
|
||||||
|
if (opts.appId) payload.app_id = opts.appId;
|
||||||
|
if (opts.runId) payload.run_id = opts.runId;
|
||||||
|
if (opts.metadata) payload.metadata = opts.metadata;
|
||||||
|
if (opts.immutable) payload.immutable = true;
|
||||||
|
if (opts.infer === false) payload.infer = false;
|
||||||
|
if (opts.expires) payload.expiration_date = opts.expires;
|
||||||
|
if (opts.categories) payload.categories = opts.categories;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
|
return (await this._request("POST", "/v1/memories/", {
|
||||||
|
json: payload,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _buildFilters(opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
extraFilters?: Record<string, unknown>;
|
||||||
|
}): Record<string, unknown> | undefined {
|
||||||
|
// If caller passed a pre-built filter structure, use it directly
|
||||||
|
if (
|
||||||
|
opts.extraFilters &&
|
||||||
|
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
|
||||||
|
) {
|
||||||
|
return opts.extraFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const andConditions: Record<string, unknown>[] = [];
|
||||||
|
if (opts.userId) andConditions.push({ user_id: opts.userId });
|
||||||
|
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
|
||||||
|
if (opts.appId) andConditions.push({ app_id: opts.appId });
|
||||||
|
if (opts.runId) andConditions.push({ run_id: opts.runId });
|
||||||
|
|
||||||
|
if (opts.extraFilters) {
|
||||||
|
for (const [k, v] of Object.entries(opts.extraFilters)) {
|
||||||
|
andConditions.push({ [k]: v });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (andConditions.length === 1) return andConditions[0];
|
||||||
|
if (andConditions.length > 1) return { AND: andConditions };
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(
|
||||||
|
query: string,
|
||||||
|
opts: SearchOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
query,
|
||||||
|
top_k: opts.topK ?? 10,
|
||||||
|
threshold: opts.threshold ?? 0.3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiFilters = this._buildFilters({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
extraFilters: opts.filters,
|
||||||
|
});
|
||||||
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
|
if (opts.rerank) payload.rerank = true;
|
||||||
|
if (opts.keyword) payload.keyword_search = true;
|
||||||
|
if (opts.fields) payload.fields = opts.fields;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
|
const result = (await this._request("POST", "/v2/memories/search/", {
|
||||||
|
json: payload,
|
||||||
|
})) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
const obj = result as Record<string, unknown>;
|
||||||
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||||
|
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||||
|
params: { source: "CLI" },
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listMemories(
|
||||||
|
opts: ListOptions = {},
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
page: String(opts.page ?? 1),
|
||||||
|
page_size: String(opts.pageSize ?? 100),
|
||||||
|
};
|
||||||
|
|
||||||
|
const extra: Record<string, unknown> = {};
|
||||||
|
if (opts.category) {
|
||||||
|
extra.categories = { contains: opts.category };
|
||||||
|
}
|
||||||
|
if (opts.after) {
|
||||||
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
gte: opts.after,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.before) {
|
||||||
|
extra.created_at = {
|
||||||
|
...(extra.created_at as Record<string, unknown> | undefined),
|
||||||
|
lte: opts.before,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiFilters = this._buildFilters({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
||||||
|
});
|
||||||
|
if (apiFilters) payload.filters = apiFilters;
|
||||||
|
if (opts.enableGraph) payload.enable_graph = true;
|
||||||
|
payload.source = "CLI";
|
||||||
|
|
||||||
|
const result = (await this._request("POST", "/v2/memories/", {
|
||||||
|
json: payload,
|
||||||
|
params,
|
||||||
|
})) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
const obj = result as Record<string, unknown>;
|
||||||
|
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
memoryId: string,
|
||||||
|
content?: string,
|
||||||
|
metadata?: Record<string, unknown>,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
if (content) payload.text = content;
|
||||||
|
if (metadata) payload.metadata = metadata;
|
||||||
|
payload.source = "CLI";
|
||||||
|
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||||
|
json: payload,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(
|
||||||
|
memoryId?: string,
|
||||||
|
opts: DeleteOptions = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
if (opts.all) {
|
||||||
|
const params: Record<string, string> = { source: "CLI" };
|
||||||
|
if (opts.userId) params.user_id = opts.userId;
|
||||||
|
if (opts.agentId) params.agent_id = opts.agentId;
|
||||||
|
if (opts.appId) params.app_id = opts.appId;
|
||||||
|
if (opts.runId) params.run_id = opts.runId;
|
||||||
|
return (await this._request("DELETE", "/v1/memories/", {
|
||||||
|
params,
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
if (memoryId) {
|
||||||
|
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||||
|
params: { source: "CLI" },
|
||||||
|
})) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
throw new Error("Either memoryId or --all is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
|
||||||
|
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
|
const typeMap: [string, string | undefined][] = [
|
||||||
|
["user", opts.userId],
|
||||||
|
["agent", opts.agentId],
|
||||||
|
["app", opts.appId],
|
||||||
|
["run", opts.runId],
|
||||||
|
];
|
||||||
|
const entities = typeMap.filter(([, v]) => v) as [string, string][];
|
||||||
|
if (entities.length === 0) {
|
||||||
|
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||||
|
}
|
||||||
|
// Delete each provided entity via the v2 path-based endpoint
|
||||||
|
let result: Record<string, unknown> = {};
|
||||||
|
for (const [entityType, entityId] of entities) {
|
||||||
|
result = (await this._request(
|
||||||
|
"DELETE",
|
||||||
|
`/v2/entities/${entityType}/${entityId}/`,
|
||||||
|
{ params: { source: "CLI" } },
|
||||||
|
)) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ping(): Promise<Record<string, unknown>> {
|
||||||
|
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async status(
|
||||||
|
opts: { userId?: string; agentId?: string } = {},
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
try {
|
||||||
|
await this.ping();
|
||||||
|
return { connected: true, backend: "platform", base_url: this.baseUrl };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
backend: "platform",
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async entities(entityType: string): Promise<Record<string, unknown>[]> {
|
||||||
|
const result = (await this._request("GET", "/v1/entities/")) as unknown;
|
||||||
|
let items: Record<string, unknown>[];
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
items = result;
|
||||||
|
} else {
|
||||||
|
items = ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
users: "user",
|
||||||
|
agents: "agent",
|
||||||
|
apps: "app",
|
||||||
|
runs: "run",
|
||||||
|
};
|
||||||
|
const targetType = typeMap[entityType];
|
||||||
|
if (targetType) {
|
||||||
|
items = items.filter(
|
||||||
|
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listEvents(): Promise<Record<string, unknown>[]> {
|
||||||
|
const result = (await this._request("GET", "/v1/events/")) as unknown;
|
||||||
|
if (Array.isArray(result)) return result;
|
||||||
|
return ((result as Record<string, unknown>).results ?? []) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||||
|
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* Branding and ASCII art for mem0 CLI.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from "chalk";
|
||||||
|
import ora, { type Ora } from "ora";
|
||||||
|
import { getCurrentCommand, isAgentMode } from "./state.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
|
export const LOGO = `
|
||||||
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
|
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
|
||||||
|
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
|
||||||
|
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
|
||||||
|
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
|
||||||
|
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const LOGO_MINI = "◆ mem0";
|
||||||
|
export const TAGLINE = "The Memory Layer for AI Agents";
|
||||||
|
|
||||||
|
export const BRAND_COLOR = "#8b5cf6";
|
||||||
|
export const ACCENT_COLOR = "#a78bfa";
|
||||||
|
export const SUCCESS_COLOR = "#22c55e";
|
||||||
|
export const ERROR_COLOR = "#ef4444";
|
||||||
|
export const WARNING_COLOR = "#f59e0b";
|
||||||
|
export const DIM_COLOR = "#6b7280";
|
||||||
|
|
||||||
|
const brand = chalk.hex(BRAND_COLOR);
|
||||||
|
const accent = chalk.hex(ACCENT_COLOR);
|
||||||
|
const success = chalk.hex(SUCCESS_COLOR);
|
||||||
|
const error = chalk.hex(ERROR_COLOR);
|
||||||
|
const warning = chalk.hex(WARNING_COLOR);
|
||||||
|
const dim = chalk.hex(DIM_COLOR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choose a symbol based on TTY/NO_COLOR. Fancy for interactive terminals,
|
||||||
|
* plain-text for piped/non-TTY or NO_COLOR environments.
|
||||||
|
*/
|
||||||
|
export function sym(fancy: string, plain: string): string {
|
||||||
|
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
|
||||||
|
return fancy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printBanner(): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
|
||||||
|
const logoLines = LOGO.trimEnd().split("\n");
|
||||||
|
const tagline = ` ${TAGLINE}`;
|
||||||
|
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
|
||||||
|
const contentLines = ["", ...logoLines, "", tagline, ""];
|
||||||
|
|
||||||
|
// Compute inner width from longest content line + padding both sides
|
||||||
|
const maxContent = Math.max(...contentLines.map((l) => l.length));
|
||||||
|
const innerWidth = maxContent + pad * 2;
|
||||||
|
const totalWidth = innerWidth + 2; // + 2 for │ borders
|
||||||
|
|
||||||
|
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
|
||||||
|
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
|
||||||
|
const bottomBorder = brand(
|
||||||
|
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const body = contentLines.map((line) => {
|
||||||
|
const rightPad = innerWidth - pad - line.length;
|
||||||
|
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
|
||||||
|
});
|
||||||
|
// Re-color tagline line with accent instead of brand.bold
|
||||||
|
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
|
||||||
|
const taglineRightPad = innerWidth - pad - tagline.length;
|
||||||
|
body[taglineIdx] =
|
||||||
|
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
|
||||||
|
|
||||||
|
console.log(topBorder);
|
||||||
|
for (const line of body) console.log(line);
|
||||||
|
console.log(bottomBorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printSuccess(message: string): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printError(message: string, hint?: string): void {
|
||||||
|
if (isAgentMode()) {
|
||||||
|
const envelope = {
|
||||||
|
status: "error",
|
||||||
|
command: getCurrentCommand(),
|
||||||
|
error: message,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
console.log(JSON.stringify(envelope));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
|
||||||
|
const resolvedHint =
|
||||||
|
hint ??
|
||||||
|
(message.includes("Authentication failed")
|
||||||
|
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys`
|
||||||
|
: undefined);
|
||||||
|
if (resolvedHint) {
|
||||||
|
console.error(` ${dim(resolvedHint)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printWarning(message: string): void {
|
||||||
|
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printInfo(message: string): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
console.error(`${brand(sym("◆", "*"))} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printScope(ids: Record<string, string | undefined>): void {
|
||||||
|
if (isAgentMode()) return;
|
||||||
|
const parts: string[] = [];
|
||||||
|
for (const [key, val] of Object.entries(ids)) {
|
||||||
|
if (val) {
|
||||||
|
parts.push(`${key}=${val}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parts.length > 0) {
|
||||||
|
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimedStatusContext {
|
||||||
|
successMsg: string;
|
||||||
|
errorMsg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run an async function with a spinner, timing the operation.
|
||||||
|
* Equivalent to Python's timed_status context manager.
|
||||||
|
*/
|
||||||
|
export async function timedStatus<T>(
|
||||||
|
message: string,
|
||||||
|
fn: (ctx: TimedStatusContext) => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
if (isAgentMode()) {
|
||||||
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
|
return fn(ctx);
|
||||||
|
}
|
||||||
|
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
|
||||||
|
const spinner = ora({
|
||||||
|
text: dim(message),
|
||||||
|
color: "yellow",
|
||||||
|
stream: process.stderr,
|
||||||
|
}).start();
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fn(ctx);
|
||||||
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
|
spinner.stop();
|
||||||
|
if (ctx.successMsg) {
|
||||||
|
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
|
||||||
|
spinner.stop();
|
||||||
|
if (ctx.errorMsg) {
|
||||||
|
printError(`${ctx.errorMsg} (${elapsed}s)`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format helpers using brand colors for external use. */
|
||||||
|
export const colors = { brand, accent, success, error, warning, dim };
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Config management commands: show, set, get.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import { colors, printError, printSuccess } from "../branding.js";
|
||||||
|
import {
|
||||||
|
getNestedValue,
|
||||||
|
loadConfig,
|
||||||
|
redactKey,
|
||||||
|
saveConfig,
|
||||||
|
setNestedValue,
|
||||||
|
} from "../config.js";
|
||||||
|
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||||
|
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, dim } = colors;
|
||||||
|
|
||||||
|
export function cmdConfigShow(opts: { output?: string } = {}): void {
|
||||||
|
setCurrentCommand("config show");
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config show",
|
||||||
|
data: {
|
||||||
|
defaults: {
|
||||||
|
user_id: config.defaults.userId || null,
|
||||||
|
agent_id: config.defaults.agentId || null,
|
||||||
|
app_id: config.defaults.appId || null,
|
||||||
|
run_id: config.defaults.runId || null,
|
||||||
|
enable_graph: config.defaults.enableGraph,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
api_key: redactKey(config.platform.apiKey),
|
||||||
|
base_url: config.platform.baseUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(` ${brand("◆ mem0 Configuration")}\n`);
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: [accent("Key"), accent("Value")],
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Defaults
|
||||||
|
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
|
||||||
|
table.push([
|
||||||
|
"defaults.agent_id",
|
||||||
|
config.defaults.agentId || dim("(not set)"),
|
||||||
|
]);
|
||||||
|
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
|
||||||
|
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
|
||||||
|
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
|
||||||
|
table.push(["", ""]);
|
||||||
|
|
||||||
|
// Platform
|
||||||
|
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
|
||||||
|
table.push(["platform.base_url", config.platform.baseUrl]);
|
||||||
|
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdConfigGet(key: string): void {
|
||||||
|
setCurrentCommand("config get");
|
||||||
|
const config = loadConfig();
|
||||||
|
const value = getNestedValue(config, key);
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
printError(`Unknown config key: ${key}`);
|
||||||
|
} else {
|
||||||
|
// Redact secrets
|
||||||
|
const displayValue =
|
||||||
|
key.includes("api_key") || key.split(".").pop() === "key"
|
||||||
|
? redactKey(String(value))
|
||||||
|
: String(value);
|
||||||
|
if (isAgentMode()) {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config get",
|
||||||
|
data: { key, value: displayValue },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(displayValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdConfigSet(key: string, value: string): void {
|
||||||
|
setCurrentCommand("config set");
|
||||||
|
const config = loadConfig();
|
||||||
|
if (setNestedValue(config, key, value)) {
|
||||||
|
saveConfig(config);
|
||||||
|
const display = key.includes("key") ? redactKey(value) : value;
|
||||||
|
if (isAgentMode()) {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "config set",
|
||||||
|
data: { key, value: display },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
printSuccess(`${key} = ${display}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printError(`Unknown config key: ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/**
|
||||||
|
* Entity management commands.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import readline from "node:readline";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, dim } = colors;
|
||||||
|
|
||||||
|
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
|
||||||
|
|
||||||
|
export async function cmdEntitiesList(
|
||||||
|
backend: Backend,
|
||||||
|
entityType: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("entity list");
|
||||||
|
if (!VALID_TYPES.has(entityType)) {
|
||||||
|
printError(
|
||||||
|
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus(`Fetching ${entityType}...`, async () => {
|
||||||
|
return backend.entities(entityType);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(
|
||||||
|
e instanceof Error ? e.message : String(e),
|
||||||
|
"This feature may require the mem0 Platform.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "entity list",
|
||||||
|
data: results,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
printInfo(`No ${entityType} found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: [accent("Name / ID"), accent("Created")],
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entity of results) {
|
||||||
|
const name = String(entity.name ?? entity.id ?? "—");
|
||||||
|
const created = String(entity.created_at ?? "—").slice(0, 10);
|
||||||
|
table.push([name, created]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log(
|
||||||
|
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdEntitiesDelete(
|
||||||
|
backend: Backend,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
dryRun?: boolean;
|
||||||
|
force: boolean;
|
||||||
|
output: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("entity delete");
|
||||||
|
const { isAgentMode } = await import("../state.js");
|
||||||
|
if (isAgentMode() && !opts.force) {
|
||||||
|
printError("Destructive operation requires --force in agent mode.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
|
||||||
|
printError(
|
||||||
|
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeParts: string[] = [];
|
||||||
|
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||||
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
|
const scope = scopeParts.join(", ");
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
printInfo(`Would delete entity ${scope} and all its memories.`);
|
||||||
|
printInfo("No changes made.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.force) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Deleting entity...", async () => {
|
||||||
|
return backend.deleteEntities({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "entity delete",
|
||||||
|
data: { deleted: true },
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Event commands: list and status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printInfo, timedStatus } from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJson } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
const { brand, accent, success, error: errorColor, warning, dim } = colors;
|
||||||
|
|
||||||
|
function statusStyled(status: string): string {
|
||||||
|
switch (status.toUpperCase()) {
|
||||||
|
case "SUCCEEDED":
|
||||||
|
return success("SUCCEEDED");
|
||||||
|
case "PENDING":
|
||||||
|
return accent("PENDING");
|
||||||
|
case "FAILED":
|
||||||
|
return errorColor("FAILED");
|
||||||
|
case "PROCESSING":
|
||||||
|
return warning("PROCESSING");
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdEventList(
|
||||||
|
backend: Backend,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("event list");
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Fetching events...", async () => {
|
||||||
|
return backend.listEvents();
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "event list",
|
||||||
|
data: results,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
console.log();
|
||||||
|
printInfo("No events found.");
|
||||||
|
console.log();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = new Table({
|
||||||
|
head: [
|
||||||
|
accent("Event ID"),
|
||||||
|
accent("Type"),
|
||||||
|
accent("Status"),
|
||||||
|
accent("Latency"),
|
||||||
|
accent("Created"),
|
||||||
|
],
|
||||||
|
colWidths: [12, 14, 14, 10, 22],
|
||||||
|
wordWrap: true,
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const ev of results) {
|
||||||
|
const evId = String(ev.id ?? "").slice(0, 8);
|
||||||
|
const evType = String(ev.event_type ?? "—");
|
||||||
|
const status = String(ev.status ?? "—");
|
||||||
|
const latency = ev.latency as number | undefined;
|
||||||
|
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||||
|
const created = String(ev.created_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log(
|
||||||
|
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdEventStatus(
|
||||||
|
backend: Backend,
|
||||||
|
eventId: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("event status");
|
||||||
|
const start = performance.now();
|
||||||
|
let ev: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
ev = await timedStatus("Fetching event...", async () => {
|
||||||
|
return backend.getEvent(eventId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "event status",
|
||||||
|
data: ev,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = String(ev.status ?? "—");
|
||||||
|
const evType = String(ev.event_type ?? "—");
|
||||||
|
const latency = ev.latency as number | undefined;
|
||||||
|
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
|
||||||
|
const created = String(ev.created_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
const updated = String(ev.updated_at ?? "—")
|
||||||
|
.slice(0, 19)
|
||||||
|
.replace("T", " ");
|
||||||
|
const results = ev.results as Record<string, unknown>[] | undefined;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(` ${dim("Event ID:")} ${eventId}`);
|
||||||
|
lines.push(` ${dim("Type:")} ${evType}`);
|
||||||
|
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
|
||||||
|
lines.push(` ${dim("Latency:")} ${latencyStr}`);
|
||||||
|
lines.push(` ${dim("Created:")} ${created}`);
|
||||||
|
lines.push(` ${dim("Updated:")} ${updated}`);
|
||||||
|
|
||||||
|
if (results && results.length > 0) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(` ${dim(`Results (${results.length}):`)}`);
|
||||||
|
for (const r of results) {
|
||||||
|
const memId = String(r.id ?? "").slice(0, 8);
|
||||||
|
const data = r.data as Record<string, unknown> | undefined;
|
||||||
|
const memory = data?.memory ? String(data.memory) : "";
|
||||||
|
const evName = String(r.event ?? "");
|
||||||
|
const user = String(r.user_id ?? "");
|
||||||
|
let detail = `${evName} ${memory}`;
|
||||||
|
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
|
||||||
|
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Event Status"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
/**
|
||||||
|
* mem0 init — interactive setup wizard.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import readline from "node:readline";
|
||||||
|
import { PlatformBackend } from "../backend/platform.js";
|
||||||
|
import {
|
||||||
|
colors,
|
||||||
|
printBanner,
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printSuccess,
|
||||||
|
} from "../branding.js";
|
||||||
|
import {
|
||||||
|
CONFIG_FILE,
|
||||||
|
DEFAULT_BASE_URL,
|
||||||
|
type Mem0Config,
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
redactKey,
|
||||||
|
saveConfig,
|
||||||
|
} from "../config.js";
|
||||||
|
|
||||||
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
|
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||||
|
|
||||||
|
function validateEmail(email: string): void {
|
||||||
|
if (!EMAIL_RE.test(email)) {
|
||||||
|
printError(`Invalid email address: ${JSON.stringify(email)}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function emailLogin(
|
||||||
|
email: string,
|
||||||
|
code: string | undefined,
|
||||||
|
baseUrl: string,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const url = baseUrl.replace(/\/+$/, "");
|
||||||
|
let codeValue = code;
|
||||||
|
|
||||||
|
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> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
process.stdout.write(label);
|
||||||
|
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(true);
|
||||||
|
}
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.setEncoding("utf-8");
|
||||||
|
|
||||||
|
const chars: string[] = [];
|
||||||
|
|
||||||
|
const onData = (key: string) => {
|
||||||
|
for (const ch of key) {
|
||||||
|
if (ch === "\r" || ch === "\n") {
|
||||||
|
cleanup();
|
||||||
|
process.stdout.write("\n");
|
||||||
|
resolve(chars.join(""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ch === "\x03") {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error("Interrupted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ch === "\x7f" || ch === "\x08") {
|
||||||
|
// backspace
|
||||||
|
if (chars.length > 0) {
|
||||||
|
chars.pop();
|
||||||
|
process.stdout.write("\b \b");
|
||||||
|
}
|
||||||
|
} else if (ch === "\x15") {
|
||||||
|
// Ctrl+U — clear line
|
||||||
|
process.stdout.write("\b \b".repeat(chars.length));
|
||||||
|
chars.length = 0;
|
||||||
|
} else if (ch >= " ") {
|
||||||
|
chars.push(ch);
|
||||||
|
process.stdout.write("*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
process.stdin.removeListener("data", onData);
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
process.stdin.setRawMode(false);
|
||||||
|
}
|
||||||
|
process.stdin.pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
process.stdin.on("data", onData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptLine(label: string, defaultValue?: string): Promise<string> {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(prompt, (answer) => {
|
||||||
|
rl.close();
|
||||||
|
resolve(answer.trim() || defaultValue || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
process.stdout.write(` ${brand("API Key")}: `);
|
||||||
|
const apiKey = await promptSecret("");
|
||||||
|
if (!apiKey) {
|
||||||
|
printError("API key is required.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
config.platform.apiKey = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
printInfo("Set default entity IDs (press Enter to skip).\n");
|
||||||
|
|
||||||
|
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
const userId = await promptLine(
|
||||||
|
` ${brand("Default User ID")} ${dim("(recommended)")}`,
|
||||||
|
_systemUser,
|
||||||
|
);
|
||||||
|
if (userId) config.defaults.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validatePlatform(config: Mem0Config): Promise<void> {
|
||||||
|
console.log();
|
||||||
|
printInfo("Validating connection...");
|
||||||
|
try {
|
||||||
|
const backend = new PlatformBackend(config.platform);
|
||||||
|
const status = await backend.status({
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
});
|
||||||
|
if (status.connected) {
|
||||||
|
printSuccess("Connected to mem0 Platform!");
|
||||||
|
// Cache user_email from ping response for telemetry distinct_id
|
||||||
|
try {
|
||||||
|
const pingData = (await backend.ping()) as Record<string, unknown>;
|
||||||
|
const userEmail = pingData?.user_email as string | undefined;
|
||||||
|
if (userEmail) {
|
||||||
|
config.platform.userEmail = userEmail;
|
||||||
|
}
|
||||||
|
} 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;
|
||||||
|
email?: string;
|
||||||
|
code?: string;
|
||||||
|
force?: boolean;
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
const savedConfig = loadConfig();
|
||||||
|
const baseUrl =
|
||||||
|
process.env.MEM0_BASE_URL ||
|
||||||
|
savedConfig.platform.baseUrl ||
|
||||||
|
DEFAULT_BASE_URL;
|
||||||
|
|
||||||
|
// Guards
|
||||||
|
if (opts.code && !opts.email) {
|
||||||
|
printError("--code requires --email.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.email && opts.apiKey) {
|
||||||
|
printError("Cannot use both --api-key and --email.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if an existing config with an API key would be overwritten
|
||||||
|
if (
|
||||||
|
!opts.force &&
|
||||||
|
fs.existsSync(CONFIG_FILE) &&
|
||||||
|
savedConfig.platform.apiKey
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
|
||||||
|
);
|
||||||
|
if (process.stdin.isTTY) {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
" Overwrite existing config? This cannot be undone. [y/N] ",
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled. Use --force to skip this check.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printError(
|
||||||
|
"Existing config would be overwritten.",
|
||||||
|
"Use --force to overwrite.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Email login flow ──────────────────────────────────────────────────────
|
||||||
|
if (opts.email) {
|
||||||
|
const email = opts.email.trim().toLowerCase();
|
||||||
|
validateEmail(email);
|
||||||
|
|
||||||
|
printBanner();
|
||||||
|
console.log();
|
||||||
|
printInfo(`Logging in as ${email}...\n`);
|
||||||
|
|
||||||
|
const result = await emailLogin(email, opts.code, baseUrl);
|
||||||
|
|
||||||
|
const apiKeyVal = result.api_key as string | undefined;
|
||||||
|
if (!apiKeyVal) {
|
||||||
|
printError(
|
||||||
|
"Auth succeeded but no API key was returned. Contact support.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.platform.apiKey = apiKeyVal;
|
||||||
|
config.platform.baseUrl = baseUrl;
|
||||||
|
config.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
/**
|
||||||
|
* Memory CRUD commands: add, search, get, list, update, delete.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import {
|
||||||
|
printError,
|
||||||
|
printInfo,
|
||||||
|
printScope,
|
||||||
|
printSuccess,
|
||||||
|
timedStatus,
|
||||||
|
} from "../branding.js";
|
||||||
|
import {
|
||||||
|
formatAddResult,
|
||||||
|
formatAgentEnvelope,
|
||||||
|
formatJson,
|
||||||
|
formatJsonEnvelope,
|
||||||
|
formatMemoriesTable,
|
||||||
|
formatMemoriesText,
|
||||||
|
formatSingleMemory,
|
||||||
|
printResultSummary,
|
||||||
|
} from "../output.js";
|
||||||
|
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||||
|
|
||||||
|
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||||
|
function _stdinIsPiped(): boolean {
|
||||||
|
if (isAgentMode()) return false;
|
||||||
|
try {
|
||||||
|
const stat = fs.fstatSync(0);
|
||||||
|
return stat.isFIFO() || stat.isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdAdd(
|
||||||
|
backend: Backend,
|
||||||
|
text: string | undefined,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
messages?: string;
|
||||||
|
file?: string;
|
||||||
|
metadata?: string;
|
||||||
|
immutable: boolean;
|
||||||
|
noInfer: boolean;
|
||||||
|
expires?: string;
|
||||||
|
categories?: string;
|
||||||
|
enableGraph: boolean;
|
||||||
|
output: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("add");
|
||||||
|
let msgs: Record<string, unknown>[] | undefined;
|
||||||
|
let content = text;
|
||||||
|
|
||||||
|
// Read from file
|
||||||
|
if (opts.file) {
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(opts.file, "utf-8");
|
||||||
|
msgs = JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Parse messages JSON
|
||||||
|
else if (opts.messages) {
|
||||||
|
try {
|
||||||
|
msgs = JSON.parse(opts.messages);
|
||||||
|
} catch (e) {
|
||||||
|
printError(
|
||||||
|
`Invalid JSON in --messages: ${e instanceof Error ? e.message : e}`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Read from stdin only if stdin is an actual pipe or file redirect
|
||||||
|
else if (!content && _stdinIsPiped()) {
|
||||||
|
content = fs.readFileSync(0, "utf-8").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content !== undefined && content.trim() === "") {
|
||||||
|
printError("Content cannot be empty.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!content && !msgs) {
|
||||||
|
printError(
|
||||||
|
"No content provided. Pass text, --messages, --file, or pipe via stdin.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate --expires
|
||||||
|
if (opts.expires) {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||||
|
printError(
|
||||||
|
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (new Date(opts.expires) <= new Date()) {
|
||||||
|
printError("--expires date must be in the future.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let meta: Record<string, unknown> | undefined;
|
||||||
|
if (opts.metadata) {
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(opts.metadata);
|
||||||
|
} catch {
|
||||||
|
printError("Invalid JSON in --metadata.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cats: string[] | undefined;
|
||||||
|
if (opts.categories) {
|
||||||
|
try {
|
||||||
|
cats = JSON.parse(opts.categories);
|
||||||
|
} catch {
|
||||||
|
cats = opts.categories.split(",").map((c) => c.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Adding memory...", async () => {
|
||||||
|
return backend.add(content ?? undefined, msgs, {
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
metadata: meta,
|
||||||
|
immutable: opts.immutable,
|
||||||
|
infer: !opts.noInfer,
|
||||||
|
expires: opts.expires,
|
||||||
|
categories: cats,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
// Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||||
|
const rawResults: Record<string, unknown>[] = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||||
|
const seenEvents = new Set<string>();
|
||||||
|
const deduped: Record<string, unknown>[] = [];
|
||||||
|
for (const r of rawResults) {
|
||||||
|
if (r.status === "PENDING") {
|
||||||
|
const eid = (r.event_id as string) ?? "";
|
||||||
|
if (eid && seenEvents.has(eid)) continue;
|
||||||
|
if (eid) seenEvents.add(eid);
|
||||||
|
}
|
||||||
|
deduped.push(r);
|
||||||
|
}
|
||||||
|
// Write back so downstream formatters see deduplicated data
|
||||||
|
const dedupedResult: Record<string, unknown> = Array.isArray(result)
|
||||||
|
? (deduped as unknown as Record<string, unknown>)
|
||||||
|
: { ...result, results: deduped };
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "add",
|
||||||
|
data: deduped,
|
||||||
|
scope,
|
||||||
|
count: deduped.length,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "json") {
|
||||||
|
formatAddResult(dedupedResult, opts.output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
printScope({
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
});
|
||||||
|
const count = deduped.length;
|
||||||
|
const allPending = count > 0 && deduped.every((r) => r.status === "PENDING");
|
||||||
|
if (allPending) {
|
||||||
|
printSuccess(
|
||||||
|
`Memory queued — ${count} event${count !== 1 ? "s" : ""} pending`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printSuccess(
|
||||||
|
`Memory processed — ${count} memor${count === 1 ? "y" : "ies"} extracted`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
formatAddResult(dedupedResult, opts.output);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdSearch(
|
||||||
|
backend: Backend,
|
||||||
|
query: string | undefined,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
topK: number;
|
||||||
|
threshold: number;
|
||||||
|
rerank: boolean;
|
||||||
|
keyword: boolean;
|
||||||
|
filterJson?: string;
|
||||||
|
fields?: string;
|
||||||
|
enableGraph: boolean;
|
||||||
|
output: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("search");
|
||||||
|
if (!query) {
|
||||||
|
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let filters: Record<string, unknown> | undefined;
|
||||||
|
if (opts.filterJson) {
|
||||||
|
try {
|
||||||
|
filters = JSON.parse(opts.filterJson);
|
||||||
|
} catch {
|
||||||
|
printError("Invalid JSON in --filter.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldList = opts.fields
|
||||||
|
? opts.fields.split(",").map((f) => f.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (opts.topK < 1) {
|
||||||
|
printError("--top-k must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.threshold < 0 || opts.threshold > 1) {
|
||||||
|
printError("--threshold must be between 0.0 and 1.0.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Searching memories...", async () => {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: guarded by process.exit above
|
||||||
|
return backend.search(query!, {
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
topK: opts.topK,
|
||||||
|
threshold: opts.threshold,
|
||||||
|
rerank: opts.rerank,
|
||||||
|
keyword: opts.keyword,
|
||||||
|
filters,
|
||||||
|
fields: fieldList,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "search",
|
||||||
|
data: results,
|
||||||
|
scope,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "json") {
|
||||||
|
formatJson(results);
|
||||||
|
} else if (opts.output === "table") {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesTable(results, { showScore: true });
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found matching your query.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesText(results);
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found matching your query.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdGet(
|
||||||
|
backend: Backend,
|
||||||
|
memoryId: string,
|
||||||
|
opts: { output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("get");
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Fetching memory...", async () => {
|
||||||
|
return backend.get(memoryId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({ command: "get", data: result });
|
||||||
|
} else {
|
||||||
|
formatSingleMemory(result, opts.output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdList(
|
||||||
|
backend: Backend,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
category?: string;
|
||||||
|
after?: string;
|
||||||
|
before?: string;
|
||||||
|
enableGraph: boolean;
|
||||||
|
output: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("list");
|
||||||
|
if (opts.pageSize < 1) {
|
||||||
|
printError("--page-size must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.page < 1) {
|
||||||
|
printError("--page must be >= 1.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let results: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
results = await timedStatus("Listing memories...", async () => {
|
||||||
|
return backend.listMemories({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
page: opts.page,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
category: opts.category,
|
||||||
|
after: opts.after,
|
||||||
|
before: opts.before,
|
||||||
|
enableGraph: opts.enableGraph,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "quiet") return;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
const scope: Record<string, string | undefined> = {
|
||||||
|
user_id: opts.userId,
|
||||||
|
agent_id: opts.agentId,
|
||||||
|
app_id: opts.appId,
|
||||||
|
run_id: opts.runId,
|
||||||
|
};
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "list",
|
||||||
|
data: results,
|
||||||
|
scope,
|
||||||
|
count: results.length,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "table") {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesTable(results);
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (results.length > 0) {
|
||||||
|
formatMemoriesText(results, "memories");
|
||||||
|
printResultSummary({
|
||||||
|
count: results.length,
|
||||||
|
durationSecs: elapsed,
|
||||||
|
page: opts.page,
|
||||||
|
scopeIds: { user_id: opts.userId, agent_id: opts.agentId },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log();
|
||||||
|
printInfo("No memories found.");
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdUpdate(
|
||||||
|
backend: Backend,
|
||||||
|
memoryId: string,
|
||||||
|
text: string | undefined,
|
||||||
|
opts: { metadata?: string; output: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("update");
|
||||||
|
let meta: Record<string, unknown> | undefined;
|
||||||
|
if (opts.metadata) {
|
||||||
|
try {
|
||||||
|
meta = JSON.parse(opts.metadata);
|
||||||
|
} catch {
|
||||||
|
printError("Invalid JSON in --metadata.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Updating memory...", async () => {
|
||||||
|
return backend.update(memoryId, text, meta);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "update",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} updated (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdDelete(
|
||||||
|
backend: Backend,
|
||||||
|
memoryId: string,
|
||||||
|
opts: { output: string; dryRun?: boolean; force?: boolean },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("delete");
|
||||||
|
if (opts.dryRun) {
|
||||||
|
let mem: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
mem = await backend.get(memoryId);
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const text = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
printInfo(`Would delete memory ${memoryId.slice(0, 8)}: ${text}`);
|
||||||
|
printInfo("No changes made.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Deleting...", async () => {
|
||||||
|
return backend.delete(memoryId);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete",
|
||||||
|
data: { id: memoryId, deleted: true },
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
printSuccess(
|
||||||
|
`Memory ${memoryId.slice(0, 8)} deleted (${elapsed.toFixed(2)}s)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdDeleteAll(
|
||||||
|
backend: Backend,
|
||||||
|
opts: {
|
||||||
|
force: boolean;
|
||||||
|
dryRun?: boolean;
|
||||||
|
all?: boolean;
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
output: string;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("delete-all");
|
||||||
|
const { isAgentMode } = await import("../state.js");
|
||||||
|
if (isAgentMode() && !opts.force) {
|
||||||
|
printError("Destructive operation requires --force in agent mode.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.all) {
|
||||||
|
// Project-wide wipe using wildcard entity IDs
|
||||||
|
// Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||||
|
|
||||||
|
if (!opts.force) {
|
||||||
|
const readline = await import("node:readline");
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
"\n \u26a0 Delete ALL memories across the ENTIRE project? This cannot be undone. [y/N] ",
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus(
|
||||||
|
"Deleting all memories project-wide...",
|
||||||
|
async () => {
|
||||||
|
return backend.delete(undefined, {
|
||||||
|
all: true,
|
||||||
|
userId: "*",
|
||||||
|
agentId: "*",
|
||||||
|
appId: "*",
|
||||||
|
runId: "*",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete-all",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
if (result.message) {
|
||||||
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printSuccess(`All project memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.dryRun) {
|
||||||
|
let memories: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
memories = await backend.listMemories({
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
printInfo(`Would delete ${memories.length} memories.`);
|
||||||
|
printInfo("No changes made.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts.force) {
|
||||||
|
const scopeParts: string[] = [];
|
||||||
|
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
|
||||||
|
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
|
||||||
|
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
|
||||||
|
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
|
||||||
|
const scope =
|
||||||
|
scopeParts.length > 0 ? scopeParts.join(", ") : "ALL entities";
|
||||||
|
|
||||||
|
const readline = await import("node:readline");
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
const answer = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
`\n \u26a0 Delete ALL memories for ${scope}? This cannot be undone. [y/N] `,
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
rl.close();
|
||||||
|
if (answer.toLowerCase() !== "y") {
|
||||||
|
printInfo("Cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Deleting all memories...", async () => {
|
||||||
|
return backend.delete(undefined, {
|
||||||
|
all: true,
|
||||||
|
userId: opts.userId,
|
||||||
|
agentId: opts.agentId,
|
||||||
|
appId: opts.appId,
|
||||||
|
runId: opts.runId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
printError(e instanceof Error ? e.message : String(e));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "delete-all",
|
||||||
|
data: result,
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
} else if (opts.output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
} else if (opts.output !== "quiet") {
|
||||||
|
if (result.message) {
|
||||||
|
printInfo(
|
||||||
|
"Deletion started. Memories will be removed in the background.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
printSuccess(`All matching memories deleted (${elapsed.toFixed(2)}s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* Utility commands: status, version, import.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import boxen from "boxen";
|
||||||
|
import type { Backend } from "../backend/base.js";
|
||||||
|
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||||
|
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||||
|
import { setCurrentCommand } from "../state.js";
|
||||||
|
import { CLI_VERSION } from "../version.js";
|
||||||
|
|
||||||
|
const { brand, dim, success, error: errorColor } = colors;
|
||||||
|
|
||||||
|
export async function cmdStatus(
|
||||||
|
backend: Backend,
|
||||||
|
opts: { userId?: string; agentId?: string; output?: string } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("status");
|
||||||
|
const start = performance.now();
|
||||||
|
let result: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
result = await timedStatus("Checking connection...", async () => {
|
||||||
|
return backend.status({ userId: opts.userId, agentId: opts.agentId });
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
result = {
|
||||||
|
connected: false,
|
||||||
|
error: e instanceof Error ? e.message : String(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "status",
|
||||||
|
data: {
|
||||||
|
connected: result.connected,
|
||||||
|
backend: result.backend ?? null,
|
||||||
|
base_url: result.base_url ?? null,
|
||||||
|
},
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (result.connected) {
|
||||||
|
lines.push(` ${success("\u25cf")} Connected`);
|
||||||
|
} else {
|
||||||
|
lines.push(` ${errorColor("\u25cf")} Disconnected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
|
||||||
|
if (result.base_url) {
|
||||||
|
lines.push(` ${dim("API URL:")} ${result.base_url}`);
|
||||||
|
}
|
||||||
|
if (result.error) {
|
||||||
|
lines.push(` ${errorColor("Error:")} ${result.error}`);
|
||||||
|
if (String(result.error).includes("Authentication failed")) {
|
||||||
|
lines.push("");
|
||||||
|
lines.push(
|
||||||
|
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
|
||||||
|
);
|
||||||
|
lines.push(
|
||||||
|
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Connection Status"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cmdVersion(): void {
|
||||||
|
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cmdImport(
|
||||||
|
backend: Backend,
|
||||||
|
filePath: string,
|
||||||
|
opts: { userId?: string; agentId?: string; output?: string },
|
||||||
|
): Promise<void> {
|
||||||
|
setCurrentCommand("import");
|
||||||
|
let data: Record<string, unknown>[];
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(filePath, "utf-8");
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
data = Array.isArray(parsed) ? parsed : [parsed];
|
||||||
|
} catch (e) {
|
||||||
|
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = 0;
|
||||||
|
let failed = 0;
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const item = data[i];
|
||||||
|
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
|
||||||
|
if (!content) {
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await backend.add(content, undefined, {
|
||||||
|
userId: opts.userId ?? (item.user_id as string | undefined),
|
||||||
|
agentId: opts.agentId ?? (item.agent_id as string | undefined),
|
||||||
|
metadata: item.metadata as Record<string, unknown> | undefined,
|
||||||
|
});
|
||||||
|
added++;
|
||||||
|
} catch {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple progress indicator
|
||||||
|
if ((i + 1) % 10 === 0 || i === data.length - 1) {
|
||||||
|
process.stdout.write(
|
||||||
|
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = (performance.now() - start) / 1000;
|
||||||
|
console.log(); // Clear progress line
|
||||||
|
|
||||||
|
if (opts.output === "agent" || opts.output === "json") {
|
||||||
|
formatAgentEnvelope({
|
||||||
|
command: "import",
|
||||||
|
data: {
|
||||||
|
added,
|
||||||
|
failed,
|
||||||
|
},
|
||||||
|
durationMs: Math.round(elapsed * 1000),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
|
||||||
|
if (failed > 0) {
|
||||||
|
printError(`${failed} memories failed to import.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* Configuration management for mem0 CLI.
|
||||||
|
*
|
||||||
|
* Config precedence (highest to lowest):
|
||||||
|
* 1. CLI flags (--api-key, --base-url, etc.)
|
||||||
|
* 2. Environment variables (MEM0_API_KEY, etc.)
|
||||||
|
* 3. Config file (~/.mem0/config.json)
|
||||||
|
* 4. Defaults
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const CONFIG_DIR = path.join(os.homedir(), ".mem0");
|
||||||
|
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||||||
|
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
||||||
|
export const CONFIG_VERSION = 1;
|
||||||
|
|
||||||
|
export interface PlatformConfig {
|
||||||
|
apiKey: string;
|
||||||
|
baseUrl: string;
|
||||||
|
userEmail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultsConfig {
|
||||||
|
userId: string;
|
||||||
|
agentId: string;
|
||||||
|
appId: string;
|
||||||
|
runId: string;
|
||||||
|
enableGraph: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TelemetryConfig {
|
||||||
|
anonymousId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Mem0Config {
|
||||||
|
version: number;
|
||||||
|
defaults: DefaultsConfig;
|
||||||
|
platform: PlatformConfig;
|
||||||
|
telemetry: TelemetryConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDefaultConfig(): Mem0Config {
|
||||||
|
return {
|
||||||
|
version: CONFIG_VERSION,
|
||||||
|
defaults: {
|
||||||
|
userId: "",
|
||||||
|
agentId: "",
|
||||||
|
appId: "",
|
||||||
|
runId: "",
|
||||||
|
enableGraph: false,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
apiKey: "",
|
||||||
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
|
userEmail: "",
|
||||||
|
},
|
||||||
|
telemetry: {
|
||||||
|
anonymousId: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureConfigDir(): string {
|
||||||
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
||||||
|
return CONFIG_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(): Mem0Config {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
||||||
|
const data = JSON.parse(raw);
|
||||||
|
|
||||||
|
config.version = data.version ?? CONFIG_VERSION;
|
||||||
|
|
||||||
|
const plat = data.platform ?? {};
|
||||||
|
config.platform.apiKey = plat.api_key ?? "";
|
||||||
|
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||||
|
config.platform.userEmail = plat.user_email ?? "";
|
||||||
|
|
||||||
|
const defaults = data.defaults ?? {};
|
||||||
|
config.defaults.userId = defaults.user_id ?? "";
|
||||||
|
config.defaults.agentId = defaults.agent_id ?? "";
|
||||||
|
config.defaults.appId = defaults.app_id ?? "";
|
||||||
|
config.defaults.runId = defaults.run_id ?? "";
|
||||||
|
config.defaults.enableGraph = defaults.enable_graph ?? false;
|
||||||
|
|
||||||
|
const telemetry = data.telemetry ?? {};
|
||||||
|
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variable overrides
|
||||||
|
if (process.env.MEM0_API_KEY)
|
||||||
|
config.platform.apiKey = process.env.MEM0_API_KEY;
|
||||||
|
if (process.env.MEM0_BASE_URL)
|
||||||
|
config.platform.baseUrl = process.env.MEM0_BASE_URL;
|
||||||
|
if (process.env.MEM0_USER_ID)
|
||||||
|
config.defaults.userId = process.env.MEM0_USER_ID;
|
||||||
|
if (process.env.MEM0_AGENT_ID)
|
||||||
|
config.defaults.agentId = process.env.MEM0_AGENT_ID;
|
||||||
|
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
|
||||||
|
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
|
||||||
|
if (process.env.MEM0_ENABLE_GRAPH) {
|
||||||
|
config.defaults.enableGraph = ["true", "1", "yes"].includes(
|
||||||
|
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConfig(config: Mem0Config): void {
|
||||||
|
ensureConfigDir();
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
version: config.version,
|
||||||
|
defaults: {
|
||||||
|
user_id: config.defaults.userId,
|
||||||
|
agent_id: config.defaults.agentId,
|
||||||
|
app_id: config.defaults.appId,
|
||||||
|
run_id: config.defaults.runId,
|
||||||
|
enable_graph: config.defaults.enableGraph,
|
||||||
|
},
|
||||||
|
platform: {
|
||||||
|
api_key: config.platform.apiKey,
|
||||||
|
base_url: config.platform.baseUrl,
|
||||||
|
user_email: config.platform.userEmail,
|
||||||
|
},
|
||||||
|
telemetry: {
|
||||||
|
anonymous_id: config.telemetry.anonymousId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||||
|
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function redactKey(key: string): string {
|
||||||
|
if (!key) return "(not set)";
|
||||||
|
if (key.length <= 8) return `${key.slice(0, 2)}***`;
|
||||||
|
return `${key.slice(0, 4)}...${key.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Key map from dotted config path to the config object fields. */
|
||||||
|
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
|
||||||
|
"platform.api_key": ["platform", "apiKey"],
|
||||||
|
"platform.base_url": ["platform", "baseUrl"],
|
||||||
|
"platform.user_email": ["platform", "userEmail"],
|
||||||
|
"defaults.user_id": ["defaults", "userId"],
|
||||||
|
"defaults.agent_id": ["defaults", "agentId"],
|
||||||
|
"defaults.app_id": ["defaults", "appId"],
|
||||||
|
"defaults.run_id": ["defaults", "runId"],
|
||||||
|
"defaults.enable_graph": ["defaults", "enableGraph"],
|
||||||
|
// Short-form aliases
|
||||||
|
api_key: ["platform", "apiKey"],
|
||||||
|
base_url: ["platform", "baseUrl"],
|
||||||
|
user_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 {
|
||||||
|
const mapping = KEY_MAP[dottedKey];
|
||||||
|
if (!mapping) return undefined;
|
||||||
|
const [section, field] = mapping;
|
||||||
|
return (config[section] as unknown as Record<string, unknown>)[field];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNestedValue(
|
||||||
|
config: Mem0Config,
|
||||||
|
dottedKey: string,
|
||||||
|
value: string,
|
||||||
|
): boolean {
|
||||||
|
const mapping = KEY_MAP[dottedKey];
|
||||||
|
if (!mapping) return false;
|
||||||
|
const [section, field] = mapping;
|
||||||
|
const obj = config[section] as unknown as Record<string, unknown>;
|
||||||
|
|
||||||
|
const current = obj[field];
|
||||||
|
if (typeof current === "boolean") {
|
||||||
|
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
|
||||||
|
} else if (typeof current === "number") {
|
||||||
|
obj[field] = Number.parseInt(value, 10);
|
||||||
|
} else {
|
||||||
|
obj[field] = value;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
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;
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
/**
|
||||||
|
* Rich-style help formatter for Commander.js that matches the Python CLI's
|
||||||
|
* Typer + Rich output (rounded box panels, brand purple, grouped options).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import chalk from "chalk";
|
||||||
|
import type { Argument, Command, Help, Option } from "commander";
|
||||||
|
// Colors imported from chalk directly to match Typer/Rich defaults
|
||||||
|
|
||||||
|
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
|
||||||
|
|
||||||
|
const cyanBold = chalk.cyan.bold; // option flags, command names
|
||||||
|
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
|
||||||
|
const yellowBold = chalk.yellow.bold; // metavar <value>
|
||||||
|
const yellow = chalk.yellow; // "Usage:" label
|
||||||
|
const bold = chalk.bold; // command name in usage
|
||||||
|
const dim = chalk.dim; // defaults, descriptions
|
||||||
|
const dimBorder = chalk.dim; // panel borders
|
||||||
|
|
||||||
|
// ── Strip ANSI ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
|
||||||
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
||||||
|
|
||||||
|
function stripAnsi(str: string): number {
|
||||||
|
return str.replace(ANSI_RE, "").length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command display order (matches Python CLI) ──────────────────────────
|
||||||
|
|
||||||
|
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
|
||||||
|
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
|
||||||
|
{
|
||||||
|
panel: "Memory",
|
||||||
|
commands: ["add", "search", "get", "list", "update", "delete"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
panel: "Management",
|
||||||
|
commands: ["init", "status", "import", "help", "entity", "event", "config"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Flat order derived from COMMAND_GROUPS. */
|
||||||
|
const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
|
||||||
|
|
||||||
|
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
|
||||||
|
|
||||||
|
const OPTION_PANELS: Record<string, Record<string, string>> = {
|
||||||
|
add: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--top-k": "Search",
|
||||||
|
"--threshold": "Search",
|
||||||
|
"--rerank": "Search",
|
||||||
|
"--keyword": "Search",
|
||||||
|
"--filter": "Search",
|
||||||
|
"--fields": "Search",
|
||||||
|
"--graph": "Search",
|
||||||
|
"--no-graph": "Search",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
get: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--page": "Pagination",
|
||||||
|
"--page-size": "Pagination",
|
||||||
|
"--category": "Filters",
|
||||||
|
"--after": "Filters",
|
||||||
|
"--before": "Filters",
|
||||||
|
"--graph": "Filters",
|
||||||
|
"--no-graph": "Filters",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--app-id": "Scope",
|
||||||
|
"--run-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
import: {
|
||||||
|
"--user-id": "Scope",
|
||||||
|
"--agent-id": "Scope",
|
||||||
|
"--output": "Output",
|
||||||
|
"--api-key": "Connection",
|
||||||
|
"--base-url": "Connection",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PANEL_ORDER: string[] = [
|
||||||
|
"Scope",
|
||||||
|
"Search",
|
||||||
|
"Pagination",
|
||||||
|
"Filters",
|
||||||
|
"Output",
|
||||||
|
"Connection",
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Panel rendering ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a Rich-style ROUNDED box panel.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* ╭─ Title ────────────────────────╮
|
||||||
|
* │ row content padded │
|
||||||
|
* ╰────────────────────────────────╯
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
function renderPanel(title: string, rows: string[], width: number): string {
|
||||||
|
if (rows.length === 0) return "";
|
||||||
|
|
||||||
|
// Inner width is total width minus the two border chars
|
||||||
|
const inner = width - 2;
|
||||||
|
|
||||||
|
// Top border: ╭─ Title ─...─╮
|
||||||
|
const titleStr = ` ${title} `;
|
||||||
|
const fillLen = Math.max(0, inner - 1 - titleStr.length);
|
||||||
|
const topLine =
|
||||||
|
dimBorder("╭─") +
|
||||||
|
dimBorder(titleStr) +
|
||||||
|
dimBorder("─".repeat(fillLen)) +
|
||||||
|
dimBorder("╮");
|
||||||
|
|
||||||
|
// Bottom border: ╰─...─╯
|
||||||
|
const bottomLine =
|
||||||
|
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
|
||||||
|
|
||||||
|
// Content rows
|
||||||
|
const contentLines = rows.map((row) => {
|
||||||
|
const visLen = stripAnsi(row);
|
||||||
|
const pad = Math.max(0, inner - 1 - visLen);
|
||||||
|
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return [topLine, ...contentLines, bottomLine].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format an option term (short + long) ────────────────────────────────
|
||||||
|
|
||||||
|
function formatOptionTerm(opt: Option): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (opt.short) parts.push(opt.short);
|
||||||
|
if (opt.long) parts.push(opt.long);
|
||||||
|
let term = parts.join(", ");
|
||||||
|
|
||||||
|
// Append value placeholder for non-boolean options
|
||||||
|
if (opt.flags) {
|
||||||
|
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
|
||||||
|
if (match) {
|
||||||
|
term += ` ${match[0]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return term;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Get the long flag name for panel lookup ─────────────────────────────
|
||||||
|
|
||||||
|
function getLongFlag(opt: Option): string {
|
||||||
|
if (opt.long) return opt.long;
|
||||||
|
return opt.short || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format a default value ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
function formatDefault(opt: Option): string {
|
||||||
|
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
|
||||||
|
return dim(` [default: ${opt.defaultValue}]`);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── The main help formatter ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function richFormatHelp(cmd: Command, helper: Help): string {
|
||||||
|
const width = process.stdout.columns || 80;
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
const isRoot = !cmd.parent;
|
||||||
|
|
||||||
|
// ── Usage line ──
|
||||||
|
const usage = helper.commandUsage(cmd);
|
||||||
|
lines.push("");
|
||||||
|
if (isRoot) {
|
||||||
|
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
|
||||||
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Subcommands: split into command path (bold) and args (yellow)
|
||||||
|
const usageParts = usage.split(" ");
|
||||||
|
const cmdPath: string[] = [];
|
||||||
|
const argParts: string[] = [];
|
||||||
|
let pastCmd = false;
|
||||||
|
for (const part of usageParts) {
|
||||||
|
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
|
||||||
|
cmdPath.push(part);
|
||||||
|
} else {
|
||||||
|
pastCmd = true;
|
||||||
|
argParts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(
|
||||||
|
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
// ── Description ──
|
||||||
|
const desc = helper.commandDescription(cmd);
|
||||||
|
if (desc) {
|
||||||
|
// Split multi-line descriptions (e.g., title + tagline)
|
||||||
|
const descLines = desc.split("\n");
|
||||||
|
for (let i = 0; i < descLines.length; i++) {
|
||||||
|
const dLine = descLines[i];
|
||||||
|
// First line is the title, subsequent non-empty lines are tagline (dimmed)
|
||||||
|
if (i === 0 || dLine.trim() === "") {
|
||||||
|
lines.push(` ${dLine}`);
|
||||||
|
} else {
|
||||||
|
lines.push(` ${dim(dLine)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arguments panel (subcommands only) ──
|
||||||
|
if (!isRoot) {
|
||||||
|
const visibleArgs = helper.visibleArguments(cmd);
|
||||||
|
if (visibleArgs.length > 0) {
|
||||||
|
const maxLen = Math.max(
|
||||||
|
...visibleArgs.map((a: Argument) => a.name().length),
|
||||||
|
);
|
||||||
|
const argRows = visibleArgs.map((a: Argument) => {
|
||||||
|
const name = cyanBold(a.name().padEnd(maxLen));
|
||||||
|
const description = helper.argumentDescription(a);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel("Arguments", argRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collect options (grouped into panels for subcommands) ──
|
||||||
|
const visibleOpts = helper.visibleOptions(cmd);
|
||||||
|
const cmdName = cmd.name();
|
||||||
|
const panelMap =
|
||||||
|
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
|
||||||
|
|
||||||
|
const grouped: Record<string, Option[]> = { Options: [] };
|
||||||
|
for (const panelName of PANEL_ORDER) {
|
||||||
|
grouped[panelName] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const opt of visibleOpts) {
|
||||||
|
const flag = getLongFlag(opt);
|
||||||
|
const panel = panelMap[flag];
|
||||||
|
if (panel && PANEL_ORDER.includes(panel)) {
|
||||||
|
grouped[panel].push(opt);
|
||||||
|
} else {
|
||||||
|
grouped.Options.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collect commands ──
|
||||||
|
const visibleCmds = helper.visibleCommands(cmd);
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
// ROOT: Options first, then command groups (matches Python/Typer ordering)
|
||||||
|
if (grouped.Options.length > 0) {
|
||||||
|
const optRows = formatOptionRows(grouped.Options);
|
||||||
|
const panel = renderPanel("Options", optRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
if (visibleCmds.length > 0) {
|
||||||
|
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
|
||||||
|
for (const group of COMMAND_GROUPS) {
|
||||||
|
const groupCmds = group.commands
|
||||||
|
.map((name) => cmdMap.get(name))
|
||||||
|
.filter((c): c is Command => c !== undefined);
|
||||||
|
if (groupCmds.length === 0) continue;
|
||||||
|
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
|
||||||
|
const cmdRows = groupCmds.map((c) => {
|
||||||
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
|
const description = helper.subcommandDescription(c);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel(group.panel, cmdRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SUBCOMMANDS: Options/panels first, then sub-subcommands
|
||||||
|
const panelSequence = ["Options", ...PANEL_ORDER];
|
||||||
|
for (const panelName of panelSequence) {
|
||||||
|
const opts = grouped[panelName];
|
||||||
|
if (opts && opts.length > 0) {
|
||||||
|
const optRows = formatOptionRows(opts);
|
||||||
|
const panel = renderPanel(panelName, optRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
|
||||||
|
if (visibleCmds.length > 0) {
|
||||||
|
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
|
||||||
|
const cmdRows = visibleCmds.map((c) => {
|
||||||
|
const name = cyanBold(c.name().padEnd(maxLen));
|
||||||
|
const description = helper.subcommandDescription(c);
|
||||||
|
return ` ${name} ${description}`;
|
||||||
|
});
|
||||||
|
const panel = renderPanel("Commands", cmdRows, width);
|
||||||
|
if (panel) lines.push(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push("");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format option rows with aligned columns ─────────────────────────────
|
||||||
|
|
||||||
|
function formatOptionRows(opts: Option[]): string[] {
|
||||||
|
const terms = opts.map((o) => formatOptionTerm(o));
|
||||||
|
const maxTermLen = Math.max(...terms.map((t) => t.length));
|
||||||
|
|
||||||
|
return opts.map((opt, i) => {
|
||||||
|
const term = cyanBold(terms[i].padEnd(maxTermLen));
|
||||||
|
const desc = opt.description || "";
|
||||||
|
const def = formatDefault(opt);
|
||||||
|
return ` ${term} ${desc}${def}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
|
||||||
|
|
||||||
|
function sortCommands(cmds: Command[]): Command[] {
|
||||||
|
return [...cmds].sort((a, b) => {
|
||||||
|
const ai = COMMAND_ORDER.indexOf(a.name());
|
||||||
|
const bi = COMMAND_ORDER.indexOf(b.name());
|
||||||
|
// Unknown commands go to end, preserving original order
|
||||||
|
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
|
||||||
|
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
|
||||||
|
return aIdx - bIdx;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,795 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main CLI application — the entrypoint for `mem0`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { Command } from "commander";
|
||||||
|
import { AuthError, type Backend, getBackend } from "./backend/index.js";
|
||||||
|
import { colors, printError, printWarning } from "./branding.js";
|
||||||
|
import type { Mem0Config } from "./config.js";
|
||||||
|
import { loadConfig, saveConfig } from "./config.js";
|
||||||
|
import { richFormatHelp } from "./help.js";
|
||||||
|
import { setAgentMode } from "./state.js";
|
||||||
|
import { captureEvent } from "./telemetry.js";
|
||||||
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
// ── Validated user identity (set by getBackendAndConfig) ─────────────────
|
||||||
|
|
||||||
|
let _validatedUserEmail: string | undefined;
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function getBackendAndConfig(
|
||||||
|
apiKey?: string,
|
||||||
|
baseUrl?: string,
|
||||||
|
): Promise<{ backend: Backend; config: Mem0Config }> {
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
if (apiKey) config.platform.apiKey = apiKey;
|
||||||
|
if (baseUrl) config.platform.baseUrl = baseUrl;
|
||||||
|
|
||||||
|
if (!config.platform.apiKey) {
|
||||||
|
printError(
|
||||||
|
"No API key configured.",
|
||||||
|
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = getBackend(config);
|
||||||
|
|
||||||
|
// Validate the API key upfront with a fast timeout
|
||||||
|
try {
|
||||||
|
const pingData = (await Promise.race([
|
||||||
|
backend.ping(),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("timeout")), 5000),
|
||||||
|
),
|
||||||
|
])) as Record<string, unknown>;
|
||||||
|
|
||||||
|
const email = pingData?.user_email as string | undefined;
|
||||||
|
if (email) {
|
||||||
|
_validatedUserEmail = email;
|
||||||
|
if (config.platform.userEmail !== email) {
|
||||||
|
config.platform.userEmail = email;
|
||||||
|
try {
|
||||||
|
saveConfig(config);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof AuthError) {
|
||||||
|
printError(
|
||||||
|
"Invalid or expired API key.",
|
||||||
|
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
// Network error / timeout — warn but proceed
|
||||||
|
printWarning(
|
||||||
|
"Could not validate API key (network issue). Proceeding anyway.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { backend, config };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getBackendOnly(
|
||||||
|
apiKey?: string,
|
||||||
|
baseUrl?: string,
|
||||||
|
): Promise<Backend> {
|
||||||
|
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAgentMode(): boolean {
|
||||||
|
const rootOpts = program.opts();
|
||||||
|
const isAgent = !!(rootOpts.json || rootOpts.agent);
|
||||||
|
if (isAgent) setAgentMode(true);
|
||||||
|
return isAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve entity IDs: CLI flag > config default > undefined.
|
||||||
|
*
|
||||||
|
* If any explicit ID is provided, only use explicit IDs (don't mix
|
||||||
|
* in defaults for other entity types which would over-filter).
|
||||||
|
* If no explicit IDs, fall back to all configured defaults.
|
||||||
|
*/
|
||||||
|
function resolveIds(
|
||||||
|
config: Mem0Config,
|
||||||
|
opts: {
|
||||||
|
userId?: string;
|
||||||
|
agentId?: string;
|
||||||
|
appId?: string;
|
||||||
|
runId?: string;
|
||||||
|
},
|
||||||
|
): { userId?: string; agentId?: string; appId?: string; runId?: string } {
|
||||||
|
const hasExplicit = !!(
|
||||||
|
opts.userId ||
|
||||||
|
opts.agentId ||
|
||||||
|
opts.appId ||
|
||||||
|
opts.runId
|
||||||
|
);
|
||||||
|
if (hasExplicit) {
|
||||||
|
return {
|
||||||
|
userId: opts.userId || undefined,
|
||||||
|
agentId: opts.agentId || undefined,
|
||||||
|
appId: opts.appId || undefined,
|
||||||
|
runId: opts.runId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
appId: config.defaults.appId || undefined,
|
||||||
|
runId: config.defaults.runId || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve graph tri-state: --no-graph > --graph > config default.
|
||||||
|
*/
|
||||||
|
function resolveGraph(
|
||||||
|
config: Mem0Config,
|
||||||
|
opts: { graph?: boolean; noGraph?: boolean },
|
||||||
|
): boolean {
|
||||||
|
if (opts.noGraph) return false;
|
||||||
|
if (opts.graph) return true;
|
||||||
|
return config.defaults.enableGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main program ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("mem0")
|
||||||
|
.description(
|
||||||
|
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||||
|
)
|
||||||
|
.option("--version", "Show version and exit.")
|
||||||
|
.on("option:version", () => {
|
||||||
|
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||||
|
.option(
|
||||||
|
"--agent",
|
||||||
|
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||||
|
)
|
||||||
|
.usage("<command> [options]")
|
||||||
|
.helpOption("--help", "Show this message and exit.")
|
||||||
|
.addHelpCommand(false)
|
||||||
|
.configureHelp({ formatHelp: richFormatHelp });
|
||||||
|
|
||||||
|
// ── Telemetry hook ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
||||||
|
try {
|
||||||
|
const commandName = actionCommand.name();
|
||||||
|
const parentName = actionCommand.parent?.name();
|
||||||
|
const fullCommand =
|
||||||
|
parentName && parentName !== "mem0"
|
||||||
|
? `${parentName}.${commandName}`
|
||||||
|
: commandName;
|
||||||
|
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||||
|
captureEvent(
|
||||||
|
`cli.${fullCommand}`,
|
||||||
|
{
|
||||||
|
command: fullCommand,
|
||||||
|
is_agent: isAgent,
|
||||||
|
},
|
||||||
|
_validatedUserEmail,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
/* silently swallow */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Init ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("init")
|
||||||
|
.description("Interactive setup wizard for mem0 CLI.")
|
||||||
|
.option("--api-key <key>", "API key (skip prompt).")
|
||||||
|
.option("-u, --user-id <id>", "Default user ID (skip prompt).")
|
||||||
|
.option("--email <email>", "Login via email verification code.")
|
||||||
|
.option(
|
||||||
|
"--code <code>",
|
||||||
|
"Verification code (use with --email for non-interactive login).",
|
||||||
|
)
|
||||||
|
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { runInit } = await import("./commands/init.js");
|
||||||
|
await runInit({
|
||||||
|
apiKey: opts.apiKey,
|
||||||
|
userId: opts.userId,
|
||||||
|
email: opts.email,
|
||||||
|
code: opts.code,
|
||||||
|
force: opts.force,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("add [text]")
|
||||||
|
.description("Add a memory from text, messages, file, or stdin.")
|
||||||
|
.option("-u, --user-id <id>", "Scope to user.")
|
||||||
|
.option("--agent-id <id>", "Scope to agent.")
|
||||||
|
.option("--app-id <id>", "Scope to app.")
|
||||||
|
.option("--run-id <id>", "Scope to run.")
|
||||||
|
.option("--messages <json>", "Conversation messages as JSON.")
|
||||||
|
.option("-f, --file <path>", "Read messages from JSON file.")
|
||||||
|
.option("-m, --metadata <json>", "Custom metadata as JSON.")
|
||||||
|
.option("--immutable", "Prevent future updates.", false)
|
||||||
|
.option("--no-infer", "Skip inference, store raw.")
|
||||||
|
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||||
|
.option("--categories <value>", "Categories (JSON array or comma-separated).")
|
||||||
|
.option("--graph", "Enable graph memory extraction.", false)
|
||||||
|
.option("--no-graph", "Disable graph memory extraction.")
|
||||||
|
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
'\nExamples:\n $ mem0 add "I prefer dark mode" --user-id alice\n $ echo "text" | mem0 add -u alice\n $ mem0 add --file msgs.json -u alice -o json',
|
||||||
|
)
|
||||||
|
.action(async (text, opts) => {
|
||||||
|
const { cmdAdd } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdAdd(backend, text, { ...ids, ...opts, enableGraph, output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: search ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("search [query]")
|
||||||
|
.description(
|
||||||
|
"Query your memory store — semantic, keyword, or hybrid retrieval.",
|
||||||
|
)
|
||||||
|
.option("-u, --user-id <id>", "Filter by user.")
|
||||||
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
|
.option("--app-id <id>", "Filter by app.")
|
||||||
|
.option("--run-id <id>", "Filter by run.")
|
||||||
|
.option(
|
||||||
|
"-k, --top-k <n>",
|
||||||
|
"Number of results.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
.option(
|
||||||
|
"--threshold <n>",
|
||||||
|
"Minimum similarity score.",
|
||||||
|
(v) => Number.parseFloat(v),
|
||||||
|
0.3,
|
||||||
|
)
|
||||||
|
.option("--rerank", "Enable reranking (Platform only).", false)
|
||||||
|
.option("--keyword", "Use keyword search.", false)
|
||||||
|
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||||
|
.option("--fields <list>", "Specific fields to return (comma-separated).")
|
||||||
|
.option("--graph", "Enable graph in search.", false)
|
||||||
|
.option("--no-graph", "Disable graph in search.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
'\nExamples:\n $ mem0 search "preferences" --user-id alice\n $ mem0 search "tools" -u alice -o json -k 5\n $ echo "preferences" | mem0 search -u alice',
|
||||||
|
)
|
||||||
|
.action(async (query, opts) => {
|
||||||
|
let resolvedQuery = query;
|
||||||
|
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||||
|
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
|
||||||
|
}
|
||||||
|
if (!resolvedQuery) {
|
||||||
|
printError("No query provided. Pass a query argument or pipe via stdin.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const { cmdSearch } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdSearch(backend, resolvedQuery, {
|
||||||
|
...ids,
|
||||||
|
topK: opts.topK,
|
||||||
|
threshold: opts.threshold,
|
||||||
|
rerank: opts.rerank,
|
||||||
|
keyword: opts.keyword,
|
||||||
|
filterJson: opts.filter,
|
||||||
|
fields: opts.fields,
|
||||||
|
enableGraph,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: get ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("get <memoryId>")
|
||||||
|
.description("Get a specific memory by ID.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 get abc-123-def-456\n $ mem0 get abc-123-def-456 -o json",
|
||||||
|
)
|
||||||
|
.action(async (memoryId, opts) => {
|
||||||
|
const { cmdGet } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdGet(backend, memoryId, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: list ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("list")
|
||||||
|
.description("List memories with optional filters.")
|
||||||
|
.option("-u, --user-id <id>", "Filter by user.")
|
||||||
|
.option("--agent-id <id>", "Filter by agent.")
|
||||||
|
.option("--app-id <id>", "Filter by app.")
|
||||||
|
.option("--run-id <id>", "Filter by run.")
|
||||||
|
.option("--page <n>", "Page number.", (v) => Number.parseInt(v), 1)
|
||||||
|
.option(
|
||||||
|
"--page-size <n>",
|
||||||
|
"Results per page.",
|
||||||
|
(v) => Number.parseInt(v),
|
||||||
|
100,
|
||||||
|
)
|
||||||
|
.option("--category <name>", "Filter by category.")
|
||||||
|
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||||
|
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||||
|
.option("--graph", "Enable graph in listing.", false)
|
||||||
|
.option("--no-graph", "Disable graph in listing.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 list -u alice\n $ mem0 list --category prefs --after 2024-01-01 -o json",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdList } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const enableGraph = resolveGraph(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdList(backend, {
|
||||||
|
...ids,
|
||||||
|
page: opts.page,
|
||||||
|
pageSize: opts.pageSize,
|
||||||
|
category: opts.category,
|
||||||
|
after: opts.after,
|
||||||
|
before: opts.before,
|
||||||
|
enableGraph,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: update ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("update <memoryId> [text]")
|
||||||
|
.description("Update a memory's text or metadata.")
|
||||||
|
.option("-m, --metadata <json>", "Update metadata (JSON).")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
`\nExamples:\n $ mem0 update abc-123 "new text"\n $ mem0 update abc-123 --metadata '{"key":"val"}'\n $ echo "new text" | mem0 update abc-123`,
|
||||||
|
)
|
||||||
|
.action(async (memoryId, text, opts) => {
|
||||||
|
let resolvedText = text;
|
||||||
|
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||||
|
resolvedText = fs.readFileSync(0, "utf-8").trim();
|
||||||
|
}
|
||||||
|
const { cmdUpdate } = await import("./commands/memory.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||||
|
metadata: opts.metadata,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Memory: delete (consolidated) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("delete [memoryId]")
|
||||||
|
.description("Delete a memory, all memories matching a scope, or an entity.")
|
||||||
|
.option("--all", "Delete all memories matching scope filters.", false)
|
||||||
|
.option(
|
||||||
|
"--entity",
|
||||||
|
"Delete the entity itself and all its memories (cascade).",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||||
|
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||||
|
.option("--force", "Skip confirmation.", false)
|
||||||
|
.option("-u, --user-id <id>", "Scope to user.")
|
||||||
|
.option("--agent-id <id>", "Scope to agent.")
|
||||||
|
.option("--app-id <id>", "Scope to app.")
|
||||||
|
.option("--run-id <id>", "Scope to run.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
[
|
||||||
|
"\nExamples:",
|
||||||
|
" $ mem0 delete abc-123-def-456 # single memory",
|
||||||
|
" $ mem0 delete --all -u alice --force # all memories for user",
|
||||||
|
" $ mem0 delete --all --project --force # project-wide wipe",
|
||||||
|
" $ mem0 delete --entity -u alice --force # entity + all its memories",
|
||||||
|
].join("\n"),
|
||||||
|
)
|
||||||
|
.action(async (memoryId, opts) => {
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
// ── Mutual-exclusion checks ──
|
||||||
|
if (memoryId && opts.all) {
|
||||||
|
printError("Cannot combine <memoryId> with --all. Use one or the other.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (memoryId && opts.entity) {
|
||||||
|
printError(
|
||||||
|
"Cannot combine <memoryId> with --entity. Use one or the other.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (opts.all && opts.entity) {
|
||||||
|
printError("Cannot combine --all with --entity. Use one or the other.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!memoryId && !opts.all && !opts.entity) {
|
||||||
|
printError(
|
||||||
|
"Specify a memory ID, --all, or --entity.\n" +
|
||||||
|
" mem0 delete <id> Delete a single memory\n" +
|
||||||
|
" mem0 delete --all [scope] Delete all memories matching scope\n" +
|
||||||
|
" mem0 delete --entity [scope] Delete an entity and all its memories",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch: single memory ──
|
||||||
|
if (memoryId) {
|
||||||
|
const { cmdDelete } = await import("./commands/memory.js");
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
await cmdDelete(backend, memoryId, {
|
||||||
|
output,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
force: opts.force,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch: --all ──
|
||||||
|
if (opts.all) {
|
||||||
|
const { cmdDeleteAll } = await import("./commands/memory.js");
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const ids = opts.project
|
||||||
|
? {
|
||||||
|
userId: undefined,
|
||||||
|
agentId: undefined,
|
||||||
|
appId: undefined,
|
||||||
|
runId: undefined,
|
||||||
|
}
|
||||||
|
: resolveIds(config, opts);
|
||||||
|
await cmdDeleteAll(backend, {
|
||||||
|
force: opts.force,
|
||||||
|
dryRun: opts.dryRun,
|
||||||
|
all: opts.project,
|
||||||
|
...ids,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dispatch: --entity ──
|
||||||
|
if (opts.entity) {
|
||||||
|
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Config subcommands ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const configCmd = program
|
||||||
|
.command("config")
|
||||||
|
.description("Manage mem0 configuration.")
|
||||||
|
.addHelpCommand(false);
|
||||||
|
|
||||||
|
configCmd
|
||||||
|
.command("show")
|
||||||
|
.description("Display current configuration (secrets redacted).")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config show\n $ mem0 config show -o json",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdConfigShow } = await import("./commands/config.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
cmdConfigShow({ output });
|
||||||
|
});
|
||||||
|
|
||||||
|
configCmd
|
||||||
|
.command("get <key>")
|
||||||
|
.description("Get a configuration value.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config get platform.api_key\n $ mem0 config get defaults.user_id",
|
||||||
|
)
|
||||||
|
.action(async (key) => {
|
||||||
|
const { cmdConfigGet } = await import("./commands/config.js");
|
||||||
|
checkAgentMode();
|
||||||
|
cmdConfigGet(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
configCmd
|
||||||
|
.command("set <key> <value>")
|
||||||
|
.description("Set a configuration value.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 config set defaults.user_id alice\n $ mem0 config set platform.base_url https://api.mem0.ai",
|
||||||
|
)
|
||||||
|
.action(async (key, value) => {
|
||||||
|
const { cmdConfigSet } = await import("./commands/config.js");
|
||||||
|
checkAgentMode();
|
||||||
|
cmdConfigSet(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Entity subcommand group ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
const entityCmd = program
|
||||||
|
.command("entity")
|
||||||
|
.description("Manage entities.")
|
||||||
|
.addHelpCommand(false)
|
||||||
|
.configureHelp({ formatHelp: richFormatHelp });
|
||||||
|
|
||||||
|
entityCmd
|
||||||
|
.command("list <entityType>")
|
||||||
|
.description("List all entities of a given type.")
|
||||||
|
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity list users\n $ mem0 entity list agents -o json",
|
||||||
|
)
|
||||||
|
.action(async (entityType, opts) => {
|
||||||
|
const { cmdEntitiesList } = await import("./commands/entities.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEntitiesList(backend, entityType, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
entityCmd
|
||||||
|
.command("delete")
|
||||||
|
.description("Delete an entity and ALL its memories (cascade).")
|
||||||
|
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||||
|
.option("-u, --user-id <id>", "Scope to user.")
|
||||||
|
.option("--agent-id <id>", "Scope to agent.")
|
||||||
|
.option("--app-id <id>", "Scope to app.")
|
||||||
|
.option("--run-id <id>", "Scope to run.")
|
||||||
|
.option("--force", "Skip confirmation.", false)
|
||||||
|
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 entity delete --user-id alice --force\n $ mem0 entity delete --user-id alice --dry-run",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdEntitiesDelete } = await import("./commands/entities.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEntitiesDelete(backend, { ...opts, output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Event subcommands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const eventCmd = program
|
||||||
|
.command("event")
|
||||||
|
.description("Inspect background processing events.")
|
||||||
|
.addHelpCommand(false)
|
||||||
|
.configureHelp({ formatHelp: richFormatHelp });
|
||||||
|
|
||||||
|
eventCmd
|
||||||
|
.command("list")
|
||||||
|
.description("List recent background processing events.")
|
||||||
|
.option("-o, --output <format>", "Output: table, json.", "table")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 event list\n $ mem0 event list -o json",
|
||||||
|
)
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdEventList } = await import("./commands/events.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEventList(backend, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
eventCmd
|
||||||
|
.command("status <eventId>")
|
||||||
|
.description("Check the status of a specific background event.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 event status <event-id>\n $ mem0 event status <event-id> -o json",
|
||||||
|
)
|
||||||
|
.action(async (eventId, opts) => {
|
||||||
|
const { cmdEventStatus } = await import("./commands/events.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdEventStatus(backend, eventId, { output });
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Utility commands ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("status")
|
||||||
|
.description("Check connectivity and authentication.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText("after", "\nExamples:\n $ mem0 status\n $ mem0 status -o json")
|
||||||
|
.action(async (opts) => {
|
||||||
|
const { cmdStatus } = await import("./commands/utils.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdStatus(backend, {
|
||||||
|
userId: config.defaults.userId || undefined,
|
||||||
|
agentId: config.defaults.agentId || undefined,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("import <filePath>")
|
||||||
|
.description("Import memories from a JSON file.")
|
||||||
|
.option("-u, --user-id <id>", "Override user ID.")
|
||||||
|
.option("--agent-id <id>", "Override agent ID.")
|
||||||
|
.option("-o, --output <format>", "Output: text, json.", "text")
|
||||||
|
.option("--api-key <key>", "Override API key.")
|
||||||
|
.option("--base-url <url>", "Override API base URL.")
|
||||||
|
.addHelpText(
|
||||||
|
"after",
|
||||||
|
"\nExamples:\n $ mem0 import data.json --user-id alice\n $ mem0 import data.json -u alice -o json",
|
||||||
|
)
|
||||||
|
.action(async (filePath, opts) => {
|
||||||
|
const { cmdImport } = await import("./commands/utils.js");
|
||||||
|
const isAgent = checkAgentMode();
|
||||||
|
const { backend, config } = await getBackendAndConfig(
|
||||||
|
opts.apiKey,
|
||||||
|
opts.baseUrl,
|
||||||
|
);
|
||||||
|
const ids = resolveIds(config, opts);
|
||||||
|
const output = isAgent ? "agent" : opts.output;
|
||||||
|
await cmdImport(backend, filePath, {
|
||||||
|
userId: ids.userId,
|
||||||
|
agentId: ids.agentId,
|
||||||
|
output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Help (machine-readable) ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("help")
|
||||||
|
.description(
|
||||||
|
"Show help. Use --json for machine-readable output (for LLM agents).",
|
||||||
|
)
|
||||||
|
.option("--json", "Output machine-readable JSON for LLM agents.", false)
|
||||||
|
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||||
|
.action((opts) => {
|
||||||
|
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||||
|
// program.opts().json is set when the root --json global flag was used first.
|
||||||
|
if (opts.json || program.opts().json) {
|
||||||
|
// Load spec from parent directory
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||||
|
if (fs.existsSync(specPath)) {
|
||||||
|
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
|
||||||
|
console.log(JSON.stringify(spec, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "mem0",
|
||||||
|
version: CLI_VERSION,
|
||||||
|
description: "The Memory Layer for AI Agents",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { brand: b } = colors;
|
||||||
|
console.log(
|
||||||
|
`${b("◆ Mem0 CLI")} v${CLI_VERSION} · Node.js SDK\n The Memory Layer for AI Agents\n`,
|
||||||
|
);
|
||||||
|
console.log("Usage: mem0 <command> [OPTIONS]\n");
|
||||||
|
console.log("Commands:");
|
||||||
|
console.log(
|
||||||
|
" add Add a memory from text, messages, file, or stdin",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
" search Query your memory store (semantic, keyword, hybrid)",
|
||||||
|
);
|
||||||
|
console.log(" get Get a specific memory by ID");
|
||||||
|
console.log(" list List memories with optional filters");
|
||||||
|
console.log(" update Update a memory's text or metadata");
|
||||||
|
console.log(
|
||||||
|
" delete Delete a memory, all memories, or an entity",
|
||||||
|
);
|
||||||
|
console.log(" import Import memories from a JSON file");
|
||||||
|
console.log(" config Manage configuration (show, get, set)");
|
||||||
|
console.log(" entity Manage entities (list, delete)");
|
||||||
|
console.log(
|
||||||
|
" event Inspect background events (list, status)",
|
||||||
|
);
|
||||||
|
console.log(" init Interactive setup wizard");
|
||||||
|
console.log(" status Check connectivity and authentication");
|
||||||
|
console.log();
|
||||||
|
console.log(" mem0 <command> --help Get help for a command");
|
||||||
|
console.log(
|
||||||
|
" mem0 help --json Machine-readable help (for LLM agents)",
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
program.parse();
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
/**
|
||||||
|
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import boxen from "boxen";
|
||||||
|
import Table from "cli-table3";
|
||||||
|
import { colors, sym } from "./branding.js";
|
||||||
|
|
||||||
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
|
|
||||||
|
function formatDate(dtStr?: string): string | undefined {
|
||||||
|
if (!dtStr) return undefined;
|
||||||
|
try {
|
||||||
|
const dt = new Date(dtStr.replace("Z", "+00:00"));
|
||||||
|
return dt.toISOString().slice(0, 10);
|
||||||
|
} catch {
|
||||||
|
return dtStr?.slice(0, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMemoriesText(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
title = "memories",
|
||||||
|
): void {
|
||||||
|
const count = memories.length;
|
||||||
|
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
|
||||||
|
|
||||||
|
for (let i = 0; i < memories.length; i++) {
|
||||||
|
const mem = memories[i];
|
||||||
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
const memId = ((mem.id as string) ?? "").slice(0, 8);
|
||||||
|
const score = mem.score as number | undefined;
|
||||||
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
|
let category: string | undefined;
|
||||||
|
const cats = mem.categories;
|
||||||
|
if (Array.isArray(cats)) {
|
||||||
|
category = cats[0] as string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ${i + 1}. ${memoryText}`);
|
||||||
|
|
||||||
|
const details: string[] = [];
|
||||||
|
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
|
||||||
|
if (memId) details.push(`ID: ${memId}`);
|
||||||
|
if (created) details.push(`Created: ${created}`);
|
||||||
|
if (category) details.push(`Category: ${category}`);
|
||||||
|
|
||||||
|
if (details.length > 0) {
|
||||||
|
console.log(` ${dim(details.join(" · "))}`);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMemoriesTable(
|
||||||
|
memories: Record<string, unknown>[],
|
||||||
|
opts: { showScore?: boolean } = {},
|
||||||
|
): void {
|
||||||
|
const head = opts.showScore
|
||||||
|
? [
|
||||||
|
accent("ID"),
|
||||||
|
accent("Score"),
|
||||||
|
accent("Memory"),
|
||||||
|
accent("Category"),
|
||||||
|
accent("Created"),
|
||||||
|
]
|
||||||
|
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
|
||||||
|
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
|
||||||
|
const table = new Table({
|
||||||
|
head,
|
||||||
|
colWidths,
|
||||||
|
wordWrap: true,
|
||||||
|
style: { head: [], border: [] },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const mem of memories) {
|
||||||
|
const memId = (mem.id as string) ?? "";
|
||||||
|
let memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
if (memoryText.length > 60) {
|
||||||
|
memoryText = `${memoryText.slice(0, 57)}...`;
|
||||||
|
}
|
||||||
|
const categories = mem.categories;
|
||||||
|
const cat =
|
||||||
|
Array.isArray(categories) && categories.length > 0
|
||||||
|
? categories.length > 1
|
||||||
|
? `${categories[0]} (+${categories.length - 1})`
|
||||||
|
: (categories[0] as string)
|
||||||
|
: "—";
|
||||||
|
const created = formatDate(mem.created_at as string | undefined) ?? "—";
|
||||||
|
if (opts.showScore) {
|
||||||
|
const score = mem.score as number | undefined;
|
||||||
|
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
|
||||||
|
table.push([dim(memId), scoreStr, memoryText, cat, created]);
|
||||||
|
} else {
|
||||||
|
table.push([dim(memId), memoryText, cat, created]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(table.toString());
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatJson(data: unknown): void {
|
||||||
|
console.log(JSON.stringify(data, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSingleMemory(
|
||||||
|
mem: Record<string, unknown>,
|
||||||
|
output = "text",
|
||||||
|
): void {
|
||||||
|
if (output === "json") {
|
||||||
|
formatJson(mem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoryText = (mem.memory ?? mem.text ?? "") as string;
|
||||||
|
const memId = (mem.id ?? "") as string;
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push(` ${memoryText}`);
|
||||||
|
lines.push("");
|
||||||
|
|
||||||
|
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
|
||||||
|
const created = formatDate(mem.created_at as string | undefined);
|
||||||
|
if (created) lines.push(` ${dim("Created:")} ${created}`);
|
||||||
|
const updated = formatDate(mem.updated_at as string | undefined);
|
||||||
|
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
|
||||||
|
const meta = mem.metadata;
|
||||||
|
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
|
||||||
|
const categories = mem.categories;
|
||||||
|
if (categories) {
|
||||||
|
const catStr = Array.isArray(categories)
|
||||||
|
? categories.join(", ")
|
||||||
|
: String(categories);
|
||||||
|
lines.push(` ${dim("Categories:")} ${catStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = lines.join("\n");
|
||||||
|
console.log();
|
||||||
|
console.log(
|
||||||
|
boxen(content, {
|
||||||
|
title: brand("Memory"),
|
||||||
|
titleAlignment: "left",
|
||||||
|
borderColor: "magenta",
|
||||||
|
padding: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAddResult(
|
||||||
|
result: Record<string, unknown> | Record<string, unknown>[],
|
||||||
|
output = "text",
|
||||||
|
): void {
|
||||||
|
if (output === "json") {
|
||||||
|
formatJson(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (output === "quiet") return;
|
||||||
|
|
||||||
|
const results: Record<string, unknown>[] = Array.isArray(result)
|
||||||
|
? result
|
||||||
|
: ((result.results as Record<string, unknown>[]) ?? [result]);
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
console.log(` ${dim("No memories extracted.")}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
const seenPendingEvents = new Set<string>();
|
||||||
|
for (const r of results) {
|
||||||
|
// Detect async PENDING response
|
||||||
|
if (r.status === "PENDING") {
|
||||||
|
const eventId = (r.event_id as string) ?? "";
|
||||||
|
// Deduplicate PENDING entries with the same event_id
|
||||||
|
if (eventId && seenPendingEvents.has(eventId)) continue;
|
||||||
|
if (eventId) seenPendingEvents.add(eventId);
|
||||||
|
const icon = accent(sym("⧗", "..."));
|
||||||
|
const parts = [
|
||||||
|
` ${icon} ${dim("Queued".padEnd(10))}`,
|
||||||
|
"Processing in background",
|
||||||
|
];
|
||||||
|
console.log(parts.join(" "));
|
||||||
|
if (eventId) {
|
||||||
|
console.log(` ${dim(` event_id: ${eventId}`)}`);
|
||||||
|
console.log(
|
||||||
|
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = (r.event ?? "ADD") as string;
|
||||||
|
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
|
||||||
|
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
|
let icon: string;
|
||||||
|
let label: string;
|
||||||
|
if (event === "ADD") {
|
||||||
|
icon = success("+");
|
||||||
|
label = "Added";
|
||||||
|
} else if (event === "UPDATE") {
|
||||||
|
icon = accent("~");
|
||||||
|
label = "Updated";
|
||||||
|
} else if (event === "DELETE") {
|
||||||
|
icon = errorColor("-");
|
||||||
|
label = "Deleted";
|
||||||
|
} else if (event === "NOOP") {
|
||||||
|
icon = dim("·");
|
||||||
|
label = "No change";
|
||||||
|
} else {
|
||||||
|
icon = dim("?");
|
||||||
|
label = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
|
||||||
|
if (memory) parts.push(memory);
|
||||||
|
if (memId) parts.push(dim(`(${memId})`));
|
||||||
|
console.log(parts.join(" "));
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatJsonEnvelope(opts: {
|
||||||
|
command: string;
|
||||||
|
data: unknown;
|
||||||
|
durationMs?: number;
|
||||||
|
scope?: Record<string, string | undefined>;
|
||||||
|
count?: number;
|
||||||
|
status?: string;
|
||||||
|
error?: string;
|
||||||
|
}): void {
|
||||||
|
const envelope: Record<string, unknown> = {
|
||||||
|
status: opts.status ?? "success",
|
||||||
|
command: opts.command,
|
||||||
|
};
|
||||||
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||||
|
if (opts.scope !== undefined) envelope.scope = opts.scope;
|
||||||
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
|
if (opts.error) envelope.error = opts.error;
|
||||||
|
envelope.data = opts.data;
|
||||||
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pick(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key in obj) result[key] = obj[key];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||||
|
if (data === null || data === undefined) return data;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "add": {
|
||||||
|
const items = Array.isArray(data) ? data : [data];
|
||||||
|
return items.map((item) => {
|
||||||
|
const r = item as Record<string, unknown>;
|
||||||
|
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
|
||||||
|
return pick(r, ["id", "memory", "event"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case "search":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||||
|
);
|
||||||
|
case "get": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, [
|
||||||
|
"id",
|
||||||
|
"memory",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"categories",
|
||||||
|
"metadata",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
case "update": {
|
||||||
|
const r = data as Record<string, unknown>;
|
||||||
|
return pick(r, ["id", "memory"]);
|
||||||
|
}
|
||||||
|
case "delete":
|
||||||
|
case "delete-all":
|
||||||
|
case "entity delete":
|
||||||
|
return data;
|
||||||
|
case "entity list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) => ({
|
||||||
|
name: (r.name ?? r.id) as string,
|
||||||
|
...pick(r, ["type", "count"]),
|
||||||
|
}));
|
||||||
|
case "event list":
|
||||||
|
return (data as Record<string, unknown>[]).map((r) =>
|
||||||
|
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
|
||||||
|
);
|
||||||
|
case "event status": {
|
||||||
|
const ev = data as Record<string, unknown>;
|
||||||
|
const rawResults =
|
||||||
|
(ev.results as Record<string, unknown>[] | undefined) ?? [];
|
||||||
|
const sanitizedResults = rawResults.map((r) => {
|
||||||
|
const nested = r.data as Record<string, unknown> | undefined;
|
||||||
|
return {
|
||||||
|
id: r.id,
|
||||||
|
event: r.event,
|
||||||
|
user_id: r.user_id,
|
||||||
|
memory: nested?.memory ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...pick(ev, [
|
||||||
|
"id",
|
||||||
|
"event_type",
|
||||||
|
"status",
|
||||||
|
"latency",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
]),
|
||||||
|
results: sanitizedResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatAgentEnvelope(opts: {
|
||||||
|
command: string;
|
||||||
|
data: unknown;
|
||||||
|
durationMs?: number;
|
||||||
|
scope?: Record<string, string | undefined>;
|
||||||
|
count?: number;
|
||||||
|
}): void {
|
||||||
|
const envelope: Record<string, unknown> = {
|
||||||
|
status: "success",
|
||||||
|
command: opts.command,
|
||||||
|
};
|
||||||
|
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
|
||||||
|
if (opts.scope) {
|
||||||
|
const filtered = Object.fromEntries(
|
||||||
|
Object.entries(opts.scope).filter(([, v]) => v),
|
||||||
|
);
|
||||||
|
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
|
||||||
|
}
|
||||||
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
|
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||||
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printResultSummary(opts: {
|
||||||
|
count: number;
|
||||||
|
durationSecs?: number;
|
||||||
|
page?: number;
|
||||||
|
scopeIds?: Record<string, string | undefined>;
|
||||||
|
}): void {
|
||||||
|
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
|
||||||
|
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
|
||||||
|
if (opts.scopeIds) {
|
||||||
|
const scopeParts = Object.entries(opts.scopeIds)
|
||||||
|
.filter(([, v]) => v)
|
||||||
|
.map(([k, v]) => `${k}=${v}`);
|
||||||
|
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
|
||||||
|
}
|
||||||
|
if (opts.durationSecs !== undefined)
|
||||||
|
parts.push(`${opts.durationSecs.toFixed(2)}s`);
|
||||||
|
|
||||||
|
console.log(` ${dim(parts.join(" · "))}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Agent mode state — set by the root program option handler,
|
||||||
|
* read by commands and branding functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _agentMode = false;
|
||||||
|
let _currentCommand = "";
|
||||||
|
|
||||||
|
export function isAgentMode(): boolean {
|
||||||
|
return _agentMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAgentMode(val: boolean): void {
|
||||||
|
_agentMode = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentCommand(): string {
|
||||||
|
return _currentCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentCommand(name: string): void {
|
||||||
|
_currentCommand = name;
|
||||||
|
}
|
||||||
@@ -0,0 +1,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(() => {});
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Tests for branding utilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
BRAND_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
TAGLINE,
|
||||||
|
LOGO_MINI,
|
||||||
|
printSuccess,
|
||||||
|
printError,
|
||||||
|
printWarning,
|
||||||
|
printInfo,
|
||||||
|
printScope,
|
||||||
|
} from "../src/branding.js";
|
||||||
|
|
||||||
|
let output: string;
|
||||||
|
let errOutput: string;
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
output = "";
|
||||||
|
errOutput = "";
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
output += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
errOutput += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = originalLog;
|
||||||
|
console.error = originalError;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("branding constants", () => {
|
||||||
|
it("has correct brand color", () => {
|
||||||
|
expect(BRAND_COLOR).toBe("#8b5cf6");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has correct tagline", () => {
|
||||||
|
expect(TAGLINE).toBe("The Memory Layer for AI Agents");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has correct logo mini", () => {
|
||||||
|
expect(LOGO_MINI).toBe("◆ mem0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printSuccess", () => {
|
||||||
|
it("prints success message", () => {
|
||||||
|
printSuccess("Operation completed");
|
||||||
|
expect(output).toContain("Operation completed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printError", () => {
|
||||||
|
it("prints error message to stderr", () => {
|
||||||
|
printError("Something failed");
|
||||||
|
expect(errOutput).toContain("Something failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints hint when provided to stderr", () => {
|
||||||
|
printError("Failed", "Try again");
|
||||||
|
expect(errOutput).toContain("Try again");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printWarning", () => {
|
||||||
|
it("prints warning message to stderr", () => {
|
||||||
|
printWarning("Be careful");
|
||||||
|
expect(errOutput).toContain("Be careful");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printInfo", () => {
|
||||||
|
it("prints info message", () => {
|
||||||
|
printInfo("Important note");
|
||||||
|
expect(errOutput).toContain("Important note");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printScope", () => {
|
||||||
|
it("prints scope when IDs present", () => {
|
||||||
|
printScope({ user_id: "alice", agent_id: "bot" });
|
||||||
|
expect(errOutput).toContain("alice");
|
||||||
|
expect(errOutput).toContain("bot");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prints nothing when no IDs", () => {
|
||||||
|
printScope({});
|
||||||
|
expect(errOutput).toBe("");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests — invoke CLI as subprocess to test end-to-end.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
function run(
|
||||||
|
args: string[],
|
||||||
|
opts: { home?: string; env?: Record<string, string> } = {},
|
||||||
|
): { stdout: string; stderr: string; exitCode: number } {
|
||||||
|
const env = { ...process.env };
|
||||||
|
// Strip MEM0_ env vars
|
||||||
|
for (const key of Object.keys(env)) {
|
||||||
|
if (key.startsWith("MEM0_")) delete env[key];
|
||||||
|
}
|
||||||
|
if (opts.home) env.HOME = opts.home;
|
||||||
|
if (opts.env) Object.assign(env, opts.env);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stdout = execSync(
|
||||||
|
`npx tsx src/index.ts ${args.join(" ")}`,
|
||||||
|
{ cwd: path.join(__dirname, ".."), env, encoding: "utf-8", timeout: 15000 },
|
||||||
|
);
|
||||||
|
return { stdout, stderr: "", exitCode: 0 };
|
||||||
|
} catch (e: any) {
|
||||||
|
return {
|
||||||
|
stdout: e.stdout ?? "",
|
||||||
|
stderr: e.stderr ?? "",
|
||||||
|
exitCode: e.status ?? 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CLI Integration — help and version", () => {
|
||||||
|
it("shows help with --help", () => {
|
||||||
|
const result = run(["--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("mem0");
|
||||||
|
expect(result.stdout).toContain("add");
|
||||||
|
expect(result.stdout).toContain("search");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("help --json produces valid JSON", () => {
|
||||||
|
const result = run(["help", "--json"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const parsed = JSON.parse(result.stdout);
|
||||||
|
// spec may have cli.name or top-level name
|
||||||
|
const name = parsed.name ?? parsed.cli?.name;
|
||||||
|
expect(name).toBe("mem0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows add help", () => {
|
||||||
|
const result = run(["add", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("user-id");
|
||||||
|
expect(result.stdout).toContain("messages");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows search help", () => {
|
||||||
|
const result = run(["search", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("top-k");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows list help", () => {
|
||||||
|
const result = run(["list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("page-size");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows delete help with --all, --entity, --project", () => {
|
||||||
|
const result = run(["delete", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--all");
|
||||||
|
expect(result.stdout).toContain("--entity");
|
||||||
|
expect(result.stdout).toContain("--project");
|
||||||
|
expect(result.stdout).toContain("--force");
|
||||||
|
expect(result.stdout.toLowerCase()).toContain("memory");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delete with no args errors", () => {
|
||||||
|
const result = run(["delete"]);
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
const combined = result.stdout + result.stderr;
|
||||||
|
expect(combined).toContain("--all");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows entity list help", () => {
|
||||||
|
const result = run(["entity", "list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout.toLowerCase()).toContain("entitytype");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows entity delete help", () => {
|
||||||
|
const result = run(["entity", "delete", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--user-id");
|
||||||
|
expect(result.stdout).toContain("--force");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows import help", () => {
|
||||||
|
const result = run(["import", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add help has --graph flag", () => {
|
||||||
|
const result = run(["add", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("search help has --graph flag", () => {
|
||||||
|
const result = run(["search", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list help has --graph flag", () => {
|
||||||
|
const result = run(["list", "--help"]);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout).toContain("--graph");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("CLI Integration — isolated (clean home)", () => {
|
||||||
|
function cleanHome(): string {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
it("add without API key errors", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["add", "test", "--user-id", "alice"], { home });
|
||||||
|
expect(result.exitCode).not.toBe(0);
|
||||||
|
const combined = result.stdout + result.stderr;
|
||||||
|
expect(combined.toLowerCase()).toMatch(/api.key|error/i);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("config show works with clean home", () => {
|
||||||
|
const home = cleanHome();
|
||||||
|
const result = run(["config", "show"], { home });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
fs.rmSync(home, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/**
|
||||||
|
* Tests for CLI commands using mock backend.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { createMockBackend } from "./setup.js";
|
||||||
|
import type { Backend } from "../src/backend/base.js";
|
||||||
|
import { setAgentMode } from "../src/state.js";
|
||||||
|
|
||||||
|
let mockBackend: Backend;
|
||||||
|
|
||||||
|
// Capture console.log and console.error output
|
||||||
|
let output: string;
|
||||||
|
let errOutput: string;
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalError = console.error;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockBackend = createMockBackend();
|
||||||
|
output = "";
|
||||||
|
errOutput = "";
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
output += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
console.error = (...args: unknown[]) => {
|
||||||
|
errOutput += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore after each test
|
||||||
|
import { afterEach } from "vitest";
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = originalLog;
|
||||||
|
console.error = originalError;
|
||||||
|
setAgentMode(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdAdd", () => {
|
||||||
|
it("adds text memory", async () => {
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "I prefer dark mode", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds from messages JSON", async () => {
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, undefined, {
|
||||||
|
userId: "alice",
|
||||||
|
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs json format", async () => {
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
expect(output).toContain("results");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("quiet mode produces no memory content", async () => {
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "quiet",
|
||||||
|
});
|
||||||
|
expect(output).not.toContain("dark mode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdAdd deduplicates PENDING", () => {
|
||||||
|
const DUPLICATE_PENDING = {
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it("text shows one pending block", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("json shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
|
||||||
|
expect(pending).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("agent shows one pending entry", async () => {
|
||||||
|
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const data = JSON.parse(output);
|
||||||
|
expect(data.count).toBe(1);
|
||||||
|
expect(data.data).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdSearch", () => {
|
||||||
|
it("searches and shows results in text mode", async () => {
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output).toContain("Found 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs json format", async () => {
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "json",
|
||||||
|
});
|
||||||
|
expect(output).toContain("memory");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows no results message", async () => {
|
||||||
|
(mockBackend.search as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "nonexistent", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(errOutput).toContain("No memories found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdGet", () => {
|
||||||
|
it("gets memory in text mode", async () => {
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "text" });
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets memory in json mode", async () => {
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "json" });
|
||||||
|
expect(output).toContain("memory");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdList", () => {
|
||||||
|
it("lists in table mode", async () => {
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "table",
|
||||||
|
});
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty message", async () => {
|
||||||
|
(mockBackend.listMemories as ReturnType<typeof vi.fn>).mockResolvedValue([]);
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(errOutput).toContain("No memories found");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdUpdate", () => {
|
||||||
|
it("updates memory", async () => {
|
||||||
|
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||||
|
await cmdUpdate(mockBackend, "abc-123", "New text", { output: "text" });
|
||||||
|
expect(output.toLowerCase()).toContain("updated");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdDelete", () => {
|
||||||
|
it("deletes memory", async () => {
|
||||||
|
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDelete(mockBackend, "abc-123", { output: "text" });
|
||||||
|
expect(output.toLowerCase()).toContain("deleted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdDeleteAll", () => {
|
||||||
|
it("deletes all with force", async () => {
|
||||||
|
const { cmdDeleteAll } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDeleteAll(mockBackend, {
|
||||||
|
force: true,
|
||||||
|
userId: "alice",
|
||||||
|
output: "text",
|
||||||
|
});
|
||||||
|
expect(output.toLowerCase()).toContain("deleted");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe("cmdEntitiesList", () => {
|
||||||
|
it("lists users in table mode", async () => {
|
||||||
|
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
||||||
|
await cmdEntitiesList(mockBackend, "users", { output: "table" });
|
||||||
|
expect(output).toContain("alice");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists in json mode", async () => {
|
||||||
|
const { cmdEntitiesList } = await import("../src/commands/entities.js");
|
||||||
|
await cmdEntitiesList(mockBackend, "users", { output: "json" });
|
||||||
|
expect(output).toContain("alice");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdEventList", () => {
|
||||||
|
it("lists events in table mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect(output).toContain("evt-abc-");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lists events in json mode", async () => {
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("evt-def-456-ghi-789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows empty message when no events", async () => {
|
||||||
|
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "table" });
|
||||||
|
expect((output + errOutput).toLowerCase()).toContain("no events");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cmdEventStatus", () => {
|
||||||
|
it("shows event details in text mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("SUCCEEDED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows event details in json mode", async () => {
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
|
||||||
|
expect(output).toContain("evt-abc-123-def-456");
|
||||||
|
expect(output).toContain("ADD");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agent mode", () => {
|
||||||
|
it("cmdAdd outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||||
|
await cmdAdd(mockBackend, "test preference", {
|
||||||
|
userId: "alice",
|
||||||
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("add");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.scope).toMatchObject({ user_id: "alice" });
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdSearch outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdSearch } = await import("../src/commands/memory.js");
|
||||||
|
await cmdSearch(mockBackend, "preferences", {
|
||||||
|
userId: "alice",
|
||||||
|
topK: 10,
|
||||||
|
threshold: 0.3,
|
||||||
|
rerank: false,
|
||||||
|
keyword: false,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("search");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
const keys = Object.keys(parsed.data[0]);
|
||||||
|
expect(keys).toContain("id");
|
||||||
|
expect(keys).toContain("memory");
|
||||||
|
expect(keys).toContain("score");
|
||||||
|
expect(keys).toContain("created_at");
|
||||||
|
expect(keys).toContain("categories");
|
||||||
|
expect(keys).not.toContain("user_id");
|
||||||
|
expect(keys).not.toContain("agent_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdList } = await import("../src/commands/memory.js");
|
||||||
|
await cmdList(mockBackend, {
|
||||||
|
userId: "alice",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enableGraph: false,
|
||||||
|
output: "agent",
|
||||||
|
});
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdGet outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdGet } = await import("../src/commands/memory.js");
|
||||||
|
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("get");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
|
||||||
|
expect(Object.keys(parsed.data)).not.toContain("user_id");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdUpdate outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdUpdate } = await import("../src/commands/memory.js");
|
||||||
|
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("update");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdDelete outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdDelete } = await import("../src/commands/memory.js");
|
||||||
|
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("delete");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventList outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventList } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventList(mockBackend, { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event list");
|
||||||
|
expect(Array.isArray(parsed.data)).toBe(true);
|
||||||
|
expect(parsed.count).toBe(2);
|
||||||
|
expect(Object.keys(parsed.data[0]).sort()).toEqual(
|
||||||
|
["created_at", "event_type", "id", "latency", "status"],
|
||||||
|
);
|
||||||
|
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cmdEventStatus outputs JSON envelope", async () => {
|
||||||
|
setAgentMode(true);
|
||||||
|
const { cmdEventStatus } = await import("../src/commands/events.js");
|
||||||
|
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
|
||||||
|
const parsed = JSON.parse(output.trim());
|
||||||
|
expect(parsed.status).toBe("success");
|
||||||
|
expect(parsed.command).toBe("event status");
|
||||||
|
expect(parsed.data).toBeDefined();
|
||||||
|
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
|
||||||
|
expect(parsed.data.results[0]).toHaveProperty("memory");
|
||||||
|
expect(parsed.data.results[0]).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Tests for configuration management.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
createDefaultConfig,
|
||||||
|
loadConfig,
|
||||||
|
saveConfig,
|
||||||
|
redactKey,
|
||||||
|
getNestedValue,
|
||||||
|
setNestedValue,
|
||||||
|
CONFIG_DIR,
|
||||||
|
CONFIG_FILE,
|
||||||
|
} from "../src/config.js";
|
||||||
|
|
||||||
|
// Use a temp directory for config during tests
|
||||||
|
let origConfigDir: string;
|
||||||
|
let origConfigFile: string;
|
||||||
|
let tmpDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||||
|
// Monkey-patch the module-level constants
|
||||||
|
// We'll use env vars and direct file manipulation instead
|
||||||
|
// Clear MEM0_ env vars
|
||||||
|
for (const key of Object.keys(process.env)) {
|
||||||
|
if (key.startsWith("MEM0_")) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("redactKey", () => {
|
||||||
|
it("returns '(not set)' for empty key", () => {
|
||||||
|
expect(redactKey("")).toBe("(not set)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts short key", () => {
|
||||||
|
expect(redactKey("abc")).toBe("ab***");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts normal key", () => {
|
||||||
|
const result = redactKey("m0-abcdefgh12345678");
|
||||||
|
expect(result).toBe("m0-a...5678");
|
||||||
|
expect(result).not.toContain("abcdefgh");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redacts exactly 8-char key as short", () => {
|
||||||
|
expect(redactKey("12345678")).toBe("12***");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createDefaultConfig", () => {
|
||||||
|
it("has correct defaults", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
|
||||||
|
expect(config.platform.apiKey).toBe("");
|
||||||
|
expect(config.defaults.userId).toBe("");
|
||||||
|
expect(config.defaults.enableGraph).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getNestedValue", () => {
|
||||||
|
it("gets platform.api_key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
config.platform.apiKey = "test-key";
|
||||||
|
expect(getNestedValue(config, "platform.api_key")).toBe("test-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined for nonexistent key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(getNestedValue(config, "nonexistent.key")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets defaults.user_id", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
config.defaults.userId = "alice";
|
||||||
|
expect(getNestedValue(config, "defaults.user_id")).toBe("alice");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setNestedValue", () => {
|
||||||
|
it("sets platform.api_key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "platform.api_key", "new-key")).toBe(true);
|
||||||
|
expect(config.platform.apiKey).toBe("new-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for nonexistent key", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "nonexistent.key", "val")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets defaults.user_id", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "defaults.user_id", "bob")).toBe(true);
|
||||||
|
expect(config.defaults.userId).toBe("bob");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("coerces boolean for enable_graph", () => {
|
||||||
|
const config = createDefaultConfig();
|
||||||
|
expect(setNestedValue(config, "defaults.enable_graph", "true")).toBe(true);
|
||||||
|
expect(config.defaults.enableGraph).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Tests for output formatting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import {
|
||||||
|
formatMemoriesText,
|
||||||
|
formatMemoriesTable,
|
||||||
|
formatJson,
|
||||||
|
formatSingleMemory,
|
||||||
|
formatAddResult,
|
||||||
|
printResultSummary,
|
||||||
|
sanitizeAgentData,
|
||||||
|
} from "../src/output.js";
|
||||||
|
|
||||||
|
let output: string;
|
||||||
|
const originalLog = console.log;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
output = "";
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
output += args.map(String).join(" ") + "\n";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = originalLog;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleMemories = [
|
||||||
|
{
|
||||||
|
id: "abc-123-def-456",
|
||||||
|
memory: "User prefers dark mode",
|
||||||
|
score: 0.92,
|
||||||
|
created_at: "2026-02-15T10:30:00Z",
|
||||||
|
categories: ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ghi-789-jkl-012",
|
||||||
|
memory: "User uses vim keybindings",
|
||||||
|
score: 0.78,
|
||||||
|
created_at: "2026-03-01T14:00:00Z",
|
||||||
|
categories: ["tools"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("formatMemoriesText", () => {
|
||||||
|
it("shows count and memory content", () => {
|
||||||
|
formatMemoriesText(sampleMemories);
|
||||||
|
expect(output).toContain("Found 2");
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
expect(output).toContain("vim keybindings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows scores and IDs", () => {
|
||||||
|
formatMemoriesText(sampleMemories);
|
||||||
|
expect(output).toContain("0.92");
|
||||||
|
expect(output).toContain("abc-123-");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatMemoriesTable", () => {
|
||||||
|
it("renders a table with memory content", () => {
|
||||||
|
formatMemoriesTable(sampleMemories);
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatJson", () => {
|
||||||
|
it("outputs valid JSON", () => {
|
||||||
|
formatJson({ key: "value" });
|
||||||
|
expect(JSON.parse(output)).toEqual({ key: "value" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatSingleMemory", () => {
|
||||||
|
it("shows memory text in text mode", () => {
|
||||||
|
formatSingleMemory(sampleMemories[0], "text");
|
||||||
|
expect(output).toContain("dark mode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("outputs JSON in json mode", () => {
|
||||||
|
formatSingleMemory(sampleMemories[0], "json");
|
||||||
|
expect(output).toContain("memory");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatAddResult", () => {
|
||||||
|
it("shows ADD event", () => {
|
||||||
|
formatAddResult({
|
||||||
|
results: [{ id: "abc-123", memory: "Test", event: "ADD" }],
|
||||||
|
});
|
||||||
|
expect(output).toContain("Added");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows PENDING event", () => {
|
||||||
|
formatAddResult({
|
||||||
|
results: [{ status: "PENDING", event_id: "evt-12345678" }],
|
||||||
|
});
|
||||||
|
expect(output).toContain("Queued");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deduplicates PENDING entries with same event_id", () => {
|
||||||
|
formatAddResult({
|
||||||
|
results: [
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
{ status: "PENDING", event_id: "evt-dup" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Should show only one PENDING block despite two entries with same event_id
|
||||||
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
|
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("printResultSummary", () => {
|
||||||
|
it("shows count and duration", () => {
|
||||||
|
printResultSummary({ count: 5, durationSecs: 1.23 });
|
||||||
|
expect(output).toContain("5 results");
|
||||||
|
expect(output).toContain("1.23s");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles singular", () => {
|
||||||
|
printResultSummary({ count: 1 });
|
||||||
|
expect(output).toContain("1 result");
|
||||||
|
expect(output).not.toContain("results");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sanitizeAgentData", () => {
|
||||||
|
it("projects add results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through PENDING add items", () => {
|
||||||
|
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
|
||||||
|
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
|
||||||
|
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects search results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("user_id");
|
||||||
|
expect(result[0]).toHaveProperty("score");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects list results", () => {
|
||||||
|
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
|
||||||
|
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
|
||||||
|
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects get result", () => {
|
||||||
|
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
|
||||||
|
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
|
||||||
|
expect(result).not.toHaveProperty("user_id");
|
||||||
|
expect(result).toHaveProperty("metadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects update result", () => {
|
||||||
|
const raw = { id: "abc", memory: "updated", extra: "noise" };
|
||||||
|
const result = sanitizeAgentData("update", raw);
|
||||||
|
expect(result).toEqual({ id: "abc", memory: "updated" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects event list results", () => {
|
||||||
|
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
|
||||||
|
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
|
||||||
|
expect(result[0]).not.toHaveProperty("updated_at");
|
||||||
|
expect(result[0]).not.toHaveProperty("graph_status");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("flattens event status results", () => {
|
||||||
|
const raw = {
|
||||||
|
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
|
||||||
|
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
|
||||||
|
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
|
||||||
|
};
|
||||||
|
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
|
||||||
|
const firstResult = (result.results as Record<string, unknown>[])[0];
|
||||||
|
expect(firstResult).toHaveProperty("memory", "dark mode");
|
||||||
|
expect(firstResult).not.toHaveProperty("data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes through status/config/import commands unchanged", () => {
|
||||||
|
const data = { key: "value", other: "stuff" };
|
||||||
|
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
|
||||||
|
expect(sanitizeAgentData(cmd, data)).toEqual(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null data", () => {
|
||||||
|
expect(sanitizeAgentData("add", null)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Shared test helpers and mock factories for mem0 CLI tests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { vi } from "vitest";
|
||||||
|
import type { Backend } from "../src/backend/base.js";
|
||||||
|
|
||||||
|
/** Create a mock backend with all methods stubbed with sensible defaults. */
|
||||||
|
export function createMockBackend(): Backend {
|
||||||
|
return {
|
||||||
|
add: vi.fn().mockResolvedValue({
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: "abc-123-def-456",
|
||||||
|
memory: "User prefers dark mode",
|
||||||
|
event: "ADD",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
search: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "abc-123-def-456",
|
||||||
|
memory: "User prefers dark mode",
|
||||||
|
score: 0.92,
|
||||||
|
created_at: "2026-02-15T10:30:00Z",
|
||||||
|
categories: ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ghi-789-jkl-012",
|
||||||
|
memory: "User uses vim keybindings",
|
||||||
|
score: 0.78,
|
||||||
|
created_at: "2026-03-01T14:00:00Z",
|
||||||
|
categories: ["tools"],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
get: vi.fn().mockResolvedValue({
|
||||||
|
id: "abc-123-def-456",
|
||||||
|
memory: "User prefers dark mode",
|
||||||
|
created_at: "2026-02-15T10:30:00Z",
|
||||||
|
updated_at: "2026-02-20T08:00:00Z",
|
||||||
|
metadata: { source: "onboarding" },
|
||||||
|
categories: ["preferences"],
|
||||||
|
}),
|
||||||
|
|
||||||
|
listMemories: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "abc-123-def-456",
|
||||||
|
memory: "User prefers dark mode",
|
||||||
|
created_at: "2026-02-15T10:30:00Z",
|
||||||
|
categories: ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ghi-789-jkl-012",
|
||||||
|
memory: "User uses vim keybindings",
|
||||||
|
created_at: "2026-03-01T14:00:00Z",
|
||||||
|
categories: ["tools"],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
|
update: vi.fn().mockResolvedValue({ id: "abc-123-def-456", memory: "Updated memory" }),
|
||||||
|
delete: vi.fn().mockResolvedValue({ status: "deleted" }),
|
||||||
|
status: vi.fn().mockResolvedValue({
|
||||||
|
connected: true,
|
||||||
|
backend: "platform",
|
||||||
|
base_url: "https://api.mem0.ai",
|
||||||
|
}),
|
||||||
|
deleteEntities: vi.fn().mockResolvedValue({ message: "Entity deleted" }),
|
||||||
|
entities: vi.fn().mockResolvedValue([
|
||||||
|
{ name: "alice", count: 5 },
|
||||||
|
{ name: "bob", count: 3 },
|
||||||
|
]),
|
||||||
|
listEvents: vi.fn().mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: null,
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "evt-def-456-ghi-789",
|
||||||
|
event_type: "SEARCH",
|
||||||
|
status: "PENDING",
|
||||||
|
graph_status: null,
|
||||||
|
latency: null,
|
||||||
|
created_at: "2026-04-01T10:01:00Z",
|
||||||
|
updated_at: "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
getEvent: vi.fn().mockResolvedValue({
|
||||||
|
id: "evt-abc-123-def-456",
|
||||||
|
event_type: "ADD",
|
||||||
|
status: "SUCCEEDED",
|
||||||
|
graph_status: "SUCCEEDED",
|
||||||
|
latency: 1234.5,
|
||||||
|
created_at: "2026-04-01T10:00:00Z",
|
||||||
|
updated_at: "2026-04-01T10:00:01Z",
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: "mem-abc-123",
|
||||||
|
event: "ADD",
|
||||||
|
user_id: "alice",
|
||||||
|
data: { memory: "User prefers dark mode" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declaration": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { defineConfig } from 'tsup';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
const pkg = _require('./package.json');
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/index.ts'],
|
||||||
|
format: ['esm'],
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
define: {
|
||||||
|
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
const _require = createRequire(import.meta.url);
|
||||||
|
const pkg = _require("./package.json") as { version: string };
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
VENV := .venv
|
||||||
|
PYTHON := $(VENV)/bin/python
|
||||||
|
PIP := $(VENV)/bin/pip
|
||||||
|
|
||||||
|
.PHONY: install dev lint format test build clean publish publish-test shell
|
||||||
|
|
||||||
|
$(VENV)/bin/activate:
|
||||||
|
python3 -m venv $(VENV)
|
||||||
|
$(PIP) install -U pip
|
||||||
|
|
||||||
|
install: $(VENV)/bin/activate
|
||||||
|
$(PIP) install -e .
|
||||||
|
|
||||||
|
dev: $(VENV)/bin/activate
|
||||||
|
$(PIP) install -e ".[dev]"
|
||||||
|
|
||||||
|
lint: dev
|
||||||
|
$(VENV)/bin/ruff check .
|
||||||
|
$(VENV)/bin/ruff format --check .
|
||||||
|
|
||||||
|
format: dev
|
||||||
|
$(VENV)/bin/ruff check --fix .
|
||||||
|
$(VENV)/bin/ruff format .
|
||||||
|
|
||||||
|
test: dev
|
||||||
|
$(VENV)/bin/pytest
|
||||||
|
|
||||||
|
build: clean $(VENV)/bin/activate
|
||||||
|
$(PIP) install hatch
|
||||||
|
$(VENV)/bin/hatch build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist/
|
||||||
|
|
||||||
|
publish: build
|
||||||
|
$(VENV)/bin/hatch publish
|
||||||
|
|
||||||
|
publish-test: build
|
||||||
|
$(VENV)/bin/hatch publish --repo test
|
||||||
|
|
||||||
|
shell: $(VENV)/bin/activate
|
||||||
|
@echo "Spawning a new shell with the virtual environment activated..."
|
||||||
|
@VIRTUAL_ENV=$(CURDIR)/$(VENV) PATH=$(CURDIR)/$(VENV)/bin:$$PATH exec $(SHELL)
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
# mem0 CLI (Python)
|
||||||
|
|
||||||
|
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
|
||||||
|
|
||||||
|
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python **3.10+**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Using pipx (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install mem0-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using pip
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mem0-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Interactive setup wizard
|
||||||
|
mem0 init
|
||||||
|
|
||||||
|
# Or login via email
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
|
||||||
|
# Or authenticate with an existing API key
|
||||||
|
mem0 init --api-key m0-xxx
|
||||||
|
|
||||||
|
# Add a memory
|
||||||
|
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
|
||||||
|
|
||||||
|
# Search memories
|
||||||
|
mem0 search "What are Alice's preferences?" --user-id alice
|
||||||
|
|
||||||
|
# List all memories for a user
|
||||||
|
mem0 list --user-id alice
|
||||||
|
|
||||||
|
# Get a specific memory
|
||||||
|
mem0 get <memory-id>
|
||||||
|
|
||||||
|
# Update a memory
|
||||||
|
mem0 update <memory-id> "I switched to light mode"
|
||||||
|
|
||||||
|
# Delete a memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### `mem0 init`
|
||||||
|
|
||||||
|
Interactive setup wizard. Prompts for your API key and default user ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
|
mem0 init --email alice@company.com
|
||||||
|
```
|
||||||
|
|
||||||
|
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 init --api-key m0-xxx --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--api-key` | API key (skip prompt) |
|
||||||
|
| `-u, --user-id` | Default user ID (skip prompt) |
|
||||||
|
| `--email` | Login via email verification code |
|
||||||
|
| `--code` | Verification code (use with `--email` for non-interactive login) |
|
||||||
|
| `--force` | Overwrite existing config without confirmation |
|
||||||
|
|
||||||
|
### `mem0 add`
|
||||||
|
|
||||||
|
Add a memory from text, a JSON messages array, a file, or stdin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 add "I prefer dark mode" --user-id alice
|
||||||
|
mem0 add --file conversation.json --user-id alice
|
||||||
|
echo "Loves hiking on weekends" | mem0 add --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Scope to a user |
|
||||||
|
| `--agent-id` | Scope to an agent |
|
||||||
|
| `--messages` | Conversation messages as JSON |
|
||||||
|
| `-f, --file` | Read messages from a JSON file |
|
||||||
|
| `-m, --metadata` | Custom metadata as JSON |
|
||||||
|
| `--categories` | Categories (JSON array or comma-separated) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph memory extraction |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `quiet` |
|
||||||
|
|
||||||
|
### `mem0 search`
|
||||||
|
|
||||||
|
Search memories using natural language.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 search "dietary restrictions" --user-id alice
|
||||||
|
mem0 search "preferred tools" --user-id alice --output json --top-k 5
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `-k, --top-k` | Number of results (default: 10) |
|
||||||
|
| `--threshold` | Minimum similarity score (default: 0.3) |
|
||||||
|
| `--rerank` | Enable reranking |
|
||||||
|
| `--keyword` | Use keyword search instead of semantic |
|
||||||
|
| `--filter` | Advanced filter expression (JSON) |
|
||||||
|
| `--graph / --no-graph` | Enable or disable graph in search |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 list`
|
||||||
|
|
||||||
|
List memories with optional filters and pagination.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 list --user-id alice
|
||||||
|
mem0 list --user-id alice --category preferences --output json
|
||||||
|
mem0 list --user-id alice --after 2024-01-01 --page-size 50
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-u, --user-id` | Filter by user |
|
||||||
|
| `--page` | Page number (default: 1) |
|
||||||
|
| `--page-size` | Results per page (default: 100) |
|
||||||
|
| `--category` | Filter by category |
|
||||||
|
| `--after` | Created after date (YYYY-MM-DD) |
|
||||||
|
| `--before` | Created before date (YYYY-MM-DD) |
|
||||||
|
| `-o, --output` | Output format: `text`, `json`, `table` |
|
||||||
|
|
||||||
|
### `mem0 get`
|
||||||
|
|
||||||
|
Retrieve a specific memory by ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
|
||||||
|
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 update`
|
||||||
|
|
||||||
|
Update the text or metadata of an existing memory.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 update <memory-id> "Updated preference text"
|
||||||
|
mem0 update <memory-id> --metadata '{"priority": "high"}'
|
||||||
|
echo "new text" | mem0 update <memory-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 delete`
|
||||||
|
|
||||||
|
Delete a single memory, all memories for a scope, or an entire entity.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Delete a single memory
|
||||||
|
mem0 delete <memory-id>
|
||||||
|
|
||||||
|
# Delete all memories for a user
|
||||||
|
mem0 delete --all --user-id alice --force
|
||||||
|
|
||||||
|
# Delete all memories project-wide
|
||||||
|
mem0 delete --all --project --force
|
||||||
|
|
||||||
|
# Preview what would be deleted
|
||||||
|
mem0 delete --all --user-id alice --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--all` | Delete all memories matching scope filters |
|
||||||
|
| `--entity` | Delete the entity and all its memories |
|
||||||
|
| `--project` | With `--all`: delete all memories project-wide |
|
||||||
|
| `--dry-run` | Preview without deleting |
|
||||||
|
| `--force` | Skip confirmation prompt |
|
||||||
|
|
||||||
|
### `mem0 import`
|
||||||
|
|
||||||
|
Bulk import memories from a JSON file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 import data.json --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
|
||||||
|
|
||||||
|
### `mem0 config`
|
||||||
|
|
||||||
|
View or modify the local CLI configuration.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 config show # Display current config (secrets redacted)
|
||||||
|
mem0 config get api_key # Get a specific value
|
||||||
|
mem0 config set user_id bob # Set a value
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 entity`
|
||||||
|
|
||||||
|
List or delete entities (users, agents, apps, runs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 entity list users
|
||||||
|
mem0 entity list agents --output json
|
||||||
|
mem0 entity delete --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 event`
|
||||||
|
|
||||||
|
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent events
|
||||||
|
mem0 event list
|
||||||
|
|
||||||
|
# Check the status of a specific event
|
||||||
|
mem0 event status <event-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `-o, --output` | Output format: `text`, `json` |
|
||||||
|
|
||||||
|
### `mem0 status`
|
||||||
|
|
||||||
|
Verify your API connection and display the current project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 status
|
||||||
|
```
|
||||||
|
|
||||||
|
### `mem0 version`
|
||||||
|
|
||||||
|
Print the CLI version.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent mode
|
||||||
|
|
||||||
|
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mem0 --agent search "user preferences" --user-id alice
|
||||||
|
mem0 --agent add "User prefers dark mode" --user-id alice
|
||||||
|
mem0 --agent list --user-id alice
|
||||||
|
mem0 --agent delete --all --user-id alice --force
|
||||||
|
```
|
||||||
|
|
||||||
|
Every command returns the same envelope shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"command": "search",
|
||||||
|
"duration_ms": 134,
|
||||||
|
"scope": { "user_id": "alice" },
|
||||||
|
"count": 2,
|
||||||
|
"data": [
|
||||||
|
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
What agent mode does differently from `--output json`:
|
||||||
|
|
||||||
|
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
|
||||||
|
- **No human output**: spinners, colors, and banners are suppressed entirely
|
||||||
|
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
|
||||||
|
|
||||||
|
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
|
||||||
|
|
||||||
|
## Output formats
|
||||||
|
|
||||||
|
Control how results are displayed with `--output`:
|
||||||
|
|
||||||
|
| Format | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `text` | Human-readable with colors and formatting (default) |
|
||||||
|
| `json` | Structured JSON for piping to `jq` (raw API response) |
|
||||||
|
| `table` | Tabular format (default for `list`) |
|
||||||
|
| `quiet` | Minimal — just IDs or status codes |
|
||||||
|
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
These flags are available on all commands:
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
|
||||||
|
| `--agent` | Alias for `--json` |
|
||||||
|
| `--api-key` | Override the configured API key for this request |
|
||||||
|
| `--base-url` | Override the configured API base URL for this request |
|
||||||
|
| `-o, --output` | Set the output format |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `MEM0_API_KEY` | API key (overrides config file) |
|
||||||
|
| `MEM0_BASE_URL` | API base URL |
|
||||||
|
| `MEM0_USER_ID` | Default user ID |
|
||||||
|
| `MEM0_AGENT_ID` | Default agent ID |
|
||||||
|
| `MEM0_APP_ID` | Default app ID |
|
||||||
|
| `MEM0_RUN_ID` | Default run ID |
|
||||||
|
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
|
||||||
|
|
||||||
|
Environment variables take precedence over values in the config file, which take precedence over defaults.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cli/python
|
||||||
|
python -m venv .venv && source .venv/bin/activate
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
# Run during development
|
||||||
|
python -m mem0_cli --help
|
||||||
|
mem0 add "test memory" --user-id alice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
1. Update `version` in `pyproject.toml`
|
||||||
|
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
|
||||||
|
|
||||||
|
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python **3.10+**
|
||||||
|
- `make` (optional — you can use plain Python commands instead)
|
||||||
|
|
||||||
|
All commands below should be run from the `python/` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd python
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Using Make (recommended)
|
||||||
|
|
||||||
|
All `make` targets automatically create a virtual environment (`.venv/`) and install the required dependencies — no manual setup needed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install the CLI in editable mode
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Install with dev tools (tests + linting)
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Python directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install -U pip
|
||||||
|
|
||||||
|
# Install in editable mode
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# With dev tools
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Make targets
|
||||||
|
|
||||||
|
| Target | Description |
|
||||||
|
| ------------------- | ------------------------------------------------ |
|
||||||
|
| `make install` | Create venv and install the CLI (editable mode) |
|
||||||
|
| `make dev` | Create venv and install CLI + dev dependencies |
|
||||||
|
| `make test` | Run all tests (installs dev deps if needed) |
|
||||||
|
| `make lint` | Run linter and format check |
|
||||||
|
| `make format` | Auto-fix lint issues and format code |
|
||||||
|
| `make build` | Build distribution packages |
|
||||||
|
| `make clean` | Remove `dist/` |
|
||||||
|
| `make publish` | Build and publish to PyPI |
|
||||||
|
| `make publish-test` | Build and publish to Test PyPI |
|
||||||
|
| `make shell` | Open a new shell with the venv activated |
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Using Python directly
|
||||||
|
pytest
|
||||||
|
|
||||||
|
# Run a specific test file
|
||||||
|
pytest tests/test_cli_integration.py
|
||||||
|
|
||||||
|
# Run a single test
|
||||||
|
pytest -k test_help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make — drop into an activated shell
|
||||||
|
make shell
|
||||||
|
mem0 --help
|
||||||
|
|
||||||
|
# Using Python directly (with venv activated)
|
||||||
|
source .venv/bin/activate
|
||||||
|
mem0 --help
|
||||||
|
mem0 version
|
||||||
|
|
||||||
|
# Or run without activating
|
||||||
|
.venv/bin/mem0 --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using Make
|
||||||
|
make lint # check only
|
||||||
|
make format # auto-fix
|
||||||
|
|
||||||
|
# Using Python directly (with venv activated)
|
||||||
|
ruff check .
|
||||||
|
ruff format .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional extras
|
||||||
|
|
||||||
|
### OSS integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e ".[oss]"
|
||||||
|
```
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "mem0-cli"
|
||||||
|
version = "0.2.3"
|
||||||
|
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||||
|
readme = "README.md"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
authors = [
|
||||||
|
{ name = "mem0.ai", email = "founders@mem0.ai" },
|
||||||
|
]
|
||||||
|
keywords = ["mem0", "memory", "ai", "agents", "cli"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Software Development :: Libraries",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"typer>=0.9.0",
|
||||||
|
"rich>=13.0.0",
|
||||||
|
"httpx>=0.24.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
oss = ["mem0ai>=0.1.0"]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-asyncio>=0.21",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mem0 = "mem0_cli.app:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["src/mem0_cli"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = ["src/mem0_cli"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 100
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort (import sorting)
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"UP", # pyupgrade (modern Python syntax)
|
||||||
|
"B", # flake8-bugbear (common bugs)
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
"RUF", # ruff-specific rules
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long — handled by formatter
|
||||||
|
"B008", # function call in default arg — required by Typer's Option/Argument pattern
|
||||||
|
"SIM108", # ternary operator — sometimes less readable
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["mem0_cli"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
docstring-code-format = true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||||
|
|
||||||
|
__version__ = "0.2.3"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Allow running with `python -m mem0_cli`."""
|
||||||
|
|
||||||
|
from mem0_cli.app import main
|
||||||
|
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
|||||||
|
"""Backend abstraction layer for mem0 CLI."""
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend, get_backend
|
||||||
|
|
||||||
|
__all__ = ["Backend", "get_backend"]
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""Abstract backend interface and factory."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mem0_cli.config import Mem0Config
|
||||||
|
|
||||||
|
|
||||||
|
class Backend(ABC):
|
||||||
|
"""Abstract interface for mem0 backends."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
content: str | None = None,
|
||||||
|
messages: list[dict] | None = None,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
metadata: dict | None = None,
|
||||||
|
immutable: bool = False,
|
||||||
|
infer: bool = True,
|
||||||
|
expires: str | None = None,
|
||||||
|
categories: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> dict: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
top_k: int = 10,
|
||||||
|
threshold: float = 0.3,
|
||||||
|
rerank: bool = False,
|
||||||
|
keyword: bool = False,
|
||||||
|
filters: dict | None = None,
|
||||||
|
fields: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get(self, memory_id: str) -> dict: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_memories(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
category: str | None = None,
|
||||||
|
after: str | None = None,
|
||||||
|
before: str | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def update(
|
||||||
|
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||||
|
) -> dict: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
memory_id: str | None = None,
|
||||||
|
*,
|
||||||
|
all: bool = False,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_entities(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def status(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
) -> dict[str, Any]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def entities(self, entity_type: str) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_events(self) -> list[dict]: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_event(self, event_id: str) -> dict: ...
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend(config: Mem0Config) -> Backend:
|
||||||
|
"""Return the Platform backend."""
|
||||||
|
from mem0_cli.backend.platform import PlatformBackend
|
||||||
|
|
||||||
|
return PlatformBackend(config.platform)
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
"""Platform (SaaS) backend — communicates with api.mem0.ai."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from mem0_cli import __version__
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.config import PlatformConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformBackend(Backend):
|
||||||
|
"""Backend that talks to the mem0 Platform API."""
|
||||||
|
|
||||||
|
def __init__(self, config: PlatformConfig) -> None:
|
||||||
|
self.config = config
|
||||||
|
self.base_url = config.base_url.rstrip("/")
|
||||||
|
self._client = httpx.Client(
|
||||||
|
base_url=self.base_url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Token {config.api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Mem0-Source": "cli",
|
||||||
|
"X-Mem0-Client-Language": "python",
|
||||||
|
"X-Mem0-Client-Version": __version__,
|
||||||
|
},
|
||||||
|
timeout=30.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
if resp.status_code == 401:
|
||||||
|
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
|
||||||
|
if resp.status_code == 404:
|
||||||
|
raise NotFoundError(f"Resource not found: {path}")
|
||||||
|
if resp.status_code == 400:
|
||||||
|
# Extract API error detail when available
|
||||||
|
try:
|
||||||
|
detail = resp.json().get("detail", resp.text)
|
||||||
|
except Exception:
|
||||||
|
detail = resp.text
|
||||||
|
raise APIError(f"Bad request to {path}: {detail}")
|
||||||
|
resp.raise_for_status()
|
||||||
|
if resp.status_code == 204:
|
||||||
|
return {}
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
def add(
|
||||||
|
self,
|
||||||
|
content: str | None = None,
|
||||||
|
messages: list[dict] | None = None,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
metadata: dict | None = None,
|
||||||
|
immutable: bool = False,
|
||||||
|
infer: bool = True,
|
||||||
|
expires: str | None = None,
|
||||||
|
categories: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if messages:
|
||||||
|
payload["messages"] = messages
|
||||||
|
elif content:
|
||||||
|
payload["messages"] = [{"role": "user", "content": content}]
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
payload["user_id"] = user_id
|
||||||
|
if agent_id:
|
||||||
|
payload["agent_id"] = agent_id
|
||||||
|
if app_id:
|
||||||
|
payload["app_id"] = app_id
|
||||||
|
if run_id:
|
||||||
|
payload["run_id"] = run_id
|
||||||
|
if metadata:
|
||||||
|
payload["metadata"] = metadata
|
||||||
|
if immutable:
|
||||||
|
payload["immutable"] = True
|
||||||
|
if not infer:
|
||||||
|
payload["infer"] = False
|
||||||
|
if expires:
|
||||||
|
payload["expiration_date"] = expires
|
||||||
|
if categories:
|
||||||
|
payload["categories"] = categories
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
|
return self._request("POST", "/v1/memories/", json=payload)
|
||||||
|
|
||||||
|
def _build_filters(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
extra_filters: dict | None = None,
|
||||||
|
) -> dict | None:
|
||||||
|
"""Build a filters dict for v2 API endpoints.
|
||||||
|
|
||||||
|
Entity IDs are ANDed (all provided IDs must match).
|
||||||
|
Extra filters (date ranges, categories) are also ANDed.
|
||||||
|
"""
|
||||||
|
# If caller passed a pre-built filter structure (e.g. --filter from CLI), use it directly
|
||||||
|
if extra_filters and ("AND" in extra_filters or "OR" in extra_filters):
|
||||||
|
return extra_filters
|
||||||
|
|
||||||
|
# Build AND conditions for entity IDs
|
||||||
|
and_conditions: list[dict[str, Any]] = []
|
||||||
|
if user_id:
|
||||||
|
and_conditions.append({"user_id": user_id})
|
||||||
|
if agent_id:
|
||||||
|
and_conditions.append({"agent_id": agent_id})
|
||||||
|
if app_id:
|
||||||
|
and_conditions.append({"app_id": app_id})
|
||||||
|
if run_id:
|
||||||
|
and_conditions.append({"run_id": run_id})
|
||||||
|
|
||||||
|
# Append any extra filters (dates, categories)
|
||||||
|
if extra_filters:
|
||||||
|
for k, v in extra_filters.items():
|
||||||
|
and_conditions.append({k: v})
|
||||||
|
|
||||||
|
if len(and_conditions) == 1:
|
||||||
|
return and_conditions[0]
|
||||||
|
elif and_conditions:
|
||||||
|
return {"AND": and_conditions}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
top_k: int = 10,
|
||||||
|
threshold: float = 0.3,
|
||||||
|
rerank: bool = False,
|
||||||
|
keyword: bool = False,
|
||||||
|
filters: dict | None = None,
|
||||||
|
fields: list[str] | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]:
|
||||||
|
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
||||||
|
|
||||||
|
api_filters = self._build_filters(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
extra_filters=filters,
|
||||||
|
)
|
||||||
|
if api_filters:
|
||||||
|
payload["filters"] = api_filters
|
||||||
|
if rerank:
|
||||||
|
payload["rerank"] = True
|
||||||
|
if keyword:
|
||||||
|
payload["keyword_search"] = True
|
||||||
|
if fields:
|
||||||
|
payload["fields"] = fields
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
|
result = self._request("POST", "/v2/memories/search/", json=payload)
|
||||||
|
return (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("results", result.get("memories", []))
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, memory_id: str) -> dict:
|
||||||
|
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||||
|
|
||||||
|
def list_memories(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 100,
|
||||||
|
category: str | None = None,
|
||||||
|
after: str | None = None,
|
||||||
|
before: str | None = None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
) -> list[dict]:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
params = {"page": str(page), "page_size": str(page_size)}
|
||||||
|
|
||||||
|
# Build filters for v2 API — entity IDs and date filters go inside "filters"
|
||||||
|
extra: dict[str, Any] = {}
|
||||||
|
if category:
|
||||||
|
extra["categories"] = {"contains": category}
|
||||||
|
if after:
|
||||||
|
extra["created_at"] = {**(extra.get("created_at", {})), "gte": after}
|
||||||
|
if before:
|
||||||
|
extra["created_at"] = {**(extra.get("created_at", {})), "lte": before}
|
||||||
|
|
||||||
|
api_filters = self._build_filters(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
extra_filters=extra if extra else None,
|
||||||
|
)
|
||||||
|
if api_filters:
|
||||||
|
payload["filters"] = api_filters
|
||||||
|
if enable_graph:
|
||||||
|
payload["enable_graph"] = True
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
|
||||||
|
result = self._request("POST", "/v2/memories/", json=payload, params=params)
|
||||||
|
return (
|
||||||
|
result
|
||||||
|
if isinstance(result, list)
|
||||||
|
else result.get("results", result.get("memories", []))
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||||
|
) -> dict:
|
||||||
|
payload: dict[str, Any] = {}
|
||||||
|
if content:
|
||||||
|
payload["text"] = content
|
||||||
|
if metadata:
|
||||||
|
payload["metadata"] = metadata
|
||||||
|
payload["source"] = "CLI"
|
||||||
|
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||||
|
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
memory_id: str | None = None,
|
||||||
|
*,
|
||||||
|
all: bool = False,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
if all:
|
||||||
|
params: dict[str, str] = {"source": "CLI"}
|
||||||
|
if user_id:
|
||||||
|
params["user_id"] = user_id
|
||||||
|
if agent_id:
|
||||||
|
params["agent_id"] = agent_id
|
||||||
|
if app_id:
|
||||||
|
params["app_id"] = app_id
|
||||||
|
if run_id:
|
||||||
|
params["run_id"] = run_id
|
||||||
|
return self._request("DELETE", "/v1/memories/", params=params)
|
||||||
|
elif memory_id:
|
||||||
|
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||||
|
else:
|
||||||
|
raise ValueError("Either memory_id or --all is required")
|
||||||
|
|
||||||
|
def delete_entities(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
app_id: str | None = None,
|
||||||
|
run_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
|
||||||
|
type_map = {
|
||||||
|
"user": user_id,
|
||||||
|
"agent": agent_id,
|
||||||
|
"app": app_id,
|
||||||
|
"run": run_id,
|
||||||
|
}
|
||||||
|
entities = {t: v for t, v in type_map.items() if v}
|
||||||
|
if not entities:
|
||||||
|
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||||
|
# Delete each provided entity via the v2 path-based endpoint
|
||||||
|
result: dict = {}
|
||||||
|
for entity_type, entity_id in entities.items():
|
||||||
|
result = self._request(
|
||||||
|
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", 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(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Check connectivity using the ping endpoint."""
|
||||||
|
try:
|
||||||
|
self.ping()
|
||||||
|
return {"connected": True, "backend": "platform", "base_url": self.base_url}
|
||||||
|
except Exception as e:
|
||||||
|
return {"connected": False, "backend": "platform", "error": str(e)}
|
||||||
|
|
||||||
|
def entities(self, entity_type: str) -> list[dict]:
|
||||||
|
result = self._request("GET", "/v1/entities/")
|
||||||
|
items = result if isinstance(result, list) else result.get("results", [])
|
||||||
|
# Filter by entity type client-side (API returns all types)
|
||||||
|
type_map = {"users": "user", "agents": "agent", "apps": "app", "runs": "run"}
|
||||||
|
target_type = type_map.get(entity_type)
|
||||||
|
if target_type:
|
||||||
|
items = [e for e in items if e.get("type", "").lower() == target_type]
|
||||||
|
return items
|
||||||
|
|
||||||
|
def list_events(self) -> list[dict]:
|
||||||
|
result = self._request("GET", "/v1/events/")
|
||||||
|
return result if isinstance(result, list) else result.get("results", [])
|
||||||
|
|
||||||
|
def get_event(self, event_id: str) -> dict:
|
||||||
|
return self._request("GET", f"/v1/event/{event_id}/")
|
||||||
|
|
||||||
|
|
||||||
|
class AuthError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Branding and ASCII art for mem0 CLI."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.status import Status
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
# stderr console for spinners, errors, and timing messages
|
||||||
|
_err = Console(stderr=True)
|
||||||
|
|
||||||
|
LOGO = r"""
|
||||||
|
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
|
||||||
|
████╗ ████║██╔════╝████╗ ████║██╔═████╗ ██╔════╝██║ ██║
|
||||||
|
██╔████╔██║█████╗ ██╔████╔██║██║██╔██║ ██║ ██║ ██║
|
||||||
|
██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║████╔╝██║ ██║ ██║ ██║
|
||||||
|
██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝ ╚██████╗███████╗██║
|
||||||
|
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚═╝
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOGO_MINI = "◆ mem0"
|
||||||
|
|
||||||
|
TAGLINE = "The Memory Layer for AI Agents"
|
||||||
|
|
||||||
|
BRAND_COLOR = "#8b5cf6" # Purple
|
||||||
|
ACCENT_COLOR = "#a78bfa"
|
||||||
|
SUCCESS_COLOR = "#22c55e"
|
||||||
|
ERROR_COLOR = "#ef4444"
|
||||||
|
WARNING_COLOR = "#f59e0b"
|
||||||
|
DIM_COLOR = "#6b7280"
|
||||||
|
|
||||||
|
|
||||||
|
def _sym(fancy: str, plain: str) -> str:
|
||||||
|
"""Return *fancy* when stdout is a TTY with colour, else *plain*."""
|
||||||
|
if not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None:
|
||||||
|
return plain
|
||||||
|
return fancy
|
||||||
|
|
||||||
|
|
||||||
|
def print_banner(console: Console) -> None:
|
||||||
|
"""Print the mem0 welcome banner."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
|
||||||
|
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
|
||||||
|
|
||||||
|
content = Text()
|
||||||
|
content.append_text(logo_text)
|
||||||
|
content.append_text(tagline)
|
||||||
|
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(0, 2),
|
||||||
|
subtitle=f"[{DIM_COLOR}]Python SDK · v{_get_version()}[/]",
|
||||||
|
subtitle_align="right",
|
||||||
|
)
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def print_success(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("✓", "[ok]")
|
||||||
|
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_error(console: Console, message: str, hint: str | None = None) -> None:
|
||||||
|
from mem0_cli.state import get_current_command, is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
envelope = {
|
||||||
|
"status": "error",
|
||||||
|
"command": get_current_command(),
|
||||||
|
"error": message,
|
||||||
|
"data": None,
|
||||||
|
}
|
||||||
|
print(_json.dumps(envelope))
|
||||||
|
return
|
||||||
|
sym = _sym("✗", "[error]")
|
||||||
|
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||||
|
if hint:
|
||||||
|
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
||||||
|
|
||||||
|
|
||||||
|
def print_warning(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("⚠", "[warn]")
|
||||||
|
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def print_info(console: Console, message: str) -> None:
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
sym = _sym("◆", "*")
|
||||||
|
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def timed_status(console: Console, message: str):
|
||||||
|
"""Spinner with automatic timing. Yields a context object for setting the final message.
|
||||||
|
|
||||||
|
The spinner and timing output are sent to stderr (via ``_err``) so they
|
||||||
|
never contaminate machine-readable stdout. The *console* parameter is
|
||||||
|
kept for backward compatibility but is not used for spinner output.
|
||||||
|
In agent mode the spinner is suppressed entirely.
|
||||||
|
"""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
class _Ctx:
|
||||||
|
def __init__(self):
|
||||||
|
self.success_msg = ""
|
||||||
|
self.error_msg = ""
|
||||||
|
|
||||||
|
ctx = _Ctx()
|
||||||
|
if is_agent_mode():
|
||||||
|
try:
|
||||||
|
yield ctx
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
return
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
try:
|
||||||
|
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
|
||||||
|
yield ctx
|
||||||
|
except Exception:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
if ctx.error_msg:
|
||||||
|
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
|
||||||
|
if "Authentication failed" in ctx.error_msg:
|
||||||
|
_err.print(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
|
||||||
|
f" · [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
elapsed = time.perf_counter() - start
|
||||||
|
if ctx.success_msg:
|
||||||
|
print_success(_err, f"{ctx.success_msg} ({elapsed:.2f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
def print_scope(console: Console, **ids: str | None) -> None:
|
||||||
|
"""Show active entity scope if any IDs are set."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return
|
||||||
|
parts = []
|
||||||
|
for key, val in ids.items():
|
||||||
|
if val:
|
||||||
|
parts.append(f"{key}={val}")
|
||||||
|
if parts:
|
||||||
|
scope_str = ", ".join(parts)
|
||||||
|
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_version() -> str:
|
||||||
|
from mem0_cli import __version__
|
||||||
|
|
||||||
|
return __version__
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""CLI command modules."""
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""Config management commands: show, set, get."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, print_error, print_success
|
||||||
|
from mem0_cli.config import (
|
||||||
|
get_nested_value,
|
||||||
|
load_config,
|
||||||
|
redact_key,
|
||||||
|
save_config,
|
||||||
|
set_nested_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_show(*, output: str = "text") -> None:
|
||||||
|
"""Display current configuration (secrets redacted)."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config show")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
config = load_config()
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="config show",
|
||||||
|
data={
|
||||||
|
"defaults": {
|
||||||
|
"user_id": config.defaults.user_id or None,
|
||||||
|
"agent_id": config.defaults.agent_id or None,
|
||||||
|
"app_id": config.defaults.app_id or None,
|
||||||
|
"run_id": config.defaults.run_id or None,
|
||||||
|
"enable_graph": config.defaults.enable_graph,
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"api_key": redact_key(config.platform.api_key),
|
||||||
|
"base_url": config.platform.base_url,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(f" [{BRAND_COLOR}]◆ mem0 Configuration[/]\n")
|
||||||
|
|
||||||
|
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 2))
|
||||||
|
table.add_column("Key", style="bold")
|
||||||
|
table.add_column("Value")
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
table.add_row(
|
||||||
|
"defaults.user_id",
|
||||||
|
config.defaults.user_id or f"[{DIM_COLOR}](not set)[/]",
|
||||||
|
)
|
||||||
|
table.add_row(
|
||||||
|
"defaults.agent_id",
|
||||||
|
config.defaults.agent_id or f"[{DIM_COLOR}](not set)[/]",
|
||||||
|
)
|
||||||
|
table.add_row(
|
||||||
|
"defaults.app_id",
|
||||||
|
config.defaults.app_id or f"[{DIM_COLOR}](not set)[/]",
|
||||||
|
)
|
||||||
|
table.add_row(
|
||||||
|
"defaults.run_id",
|
||||||
|
config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
|
||||||
|
)
|
||||||
|
table.add_row(
|
||||||
|
"defaults.enable_graph",
|
||||||
|
str(config.defaults.enable_graph).lower(),
|
||||||
|
)
|
||||||
|
table.add_row("", "")
|
||||||
|
|
||||||
|
# Platform
|
||||||
|
table.add_row("[bold]platform.api_key[/]", redact_key(config.platform.api_key))
|
||||||
|
table.add_row("platform.base_url", config.platform.base_url)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_get(key: str) -> None:
|
||||||
|
"""Get a config value."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config get")
|
||||||
|
config = load_config()
|
||||||
|
value = get_nested_value(config, key)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
|
return
|
||||||
|
|
||||||
|
display_value = (
|
||||||
|
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config get", data={"key": key, "value": display_value}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print(display_value)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_config_set(key: str, value: str) -> None:
|
||||||
|
"""Set a config value."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("config set")
|
||||||
|
config = load_config()
|
||||||
|
if set_nested_value(config, key, value):
|
||||||
|
save_config(config)
|
||||||
|
display = redact_key(value) if "key" in key else value
|
||||||
|
if is_agent_mode():
|
||||||
|
format_agent_envelope(
|
||||||
|
console, command="config set", data={"key": key, "value": display}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(console, f"{key} = {display}")
|
||||||
|
else:
|
||||||
|
print_error(err_console, f"Unknown config key: {key}")
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
"""Entity management commands."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
ACCENT_COLOR,
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
print_error,
|
||||||
|
print_info,
|
||||||
|
print_success,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
from mem0_cli.output import format_agent_envelope, format_json
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
|
||||||
|
"""List entities of a given type."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
valid_types = {"users", "agents", "apps", "runs"}
|
||||||
|
if entity_type not in valid_types:
|
||||||
|
print_error(
|
||||||
|
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, f"Fetching {entity_type}...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.entities(entity_type)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e), hint="This feature may require the mem0 Platform.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
print_info(console, f"No {entity_type} found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(border_style=BRAND_COLOR, header_style=f"bold {ACCENT_COLOR}", padding=(0, 1))
|
||||||
|
table.add_column("Name / ID", style="bold")
|
||||||
|
table.add_column("Created", max_width=12)
|
||||||
|
|
||||||
|
for entity in results:
|
||||||
|
name = entity.get("name", entity.get("id", "—"))
|
||||||
|
created = str(entity.get("created_at", "—"))[:10]
|
||||||
|
table.add_row(str(name), created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print(f" [{DIM_COLOR}]{len(results)} {entity_type} ({_elapsed:.2f}s)[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_entities_delete(
|
||||||
|
backend: Backend,
|
||||||
|
*,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
app_id: str | None,
|
||||||
|
run_id: str | None,
|
||||||
|
force: bool,
|
||||||
|
dry_run: bool = False,
|
||||||
|
output: str,
|
||||||
|
) -> None:
|
||||||
|
"""Delete an entity and all its memories (cascade delete)."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("entity delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if not any([user_id, agent_id, app_id, run_id]):
|
||||||
|
print_error(
|
||||||
|
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
scope_parts = []
|
||||||
|
if user_id:
|
||||||
|
scope_parts.append(f"user={user_id}")
|
||||||
|
if agent_id:
|
||||||
|
scope_parts.append(f"agent={agent_id}")
|
||||||
|
if app_id:
|
||||||
|
scope_parts.append(f"app={app_id}")
|
||||||
|
if run_id:
|
||||||
|
scope_parts.append(f"run={run_id}")
|
||||||
|
scope_str = ", ".join(scope_parts)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print_info(console, f"Would delete entity {scope_str} and all its memories.")
|
||||||
|
print_info(console, "No changes made (dry run).")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
confirm = typer.confirm(
|
||||||
|
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
|
||||||
|
)
|
||||||
|
if not confirm:
|
||||||
|
print_info(console, "Cancelled.")
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Deleting entity...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.delete_entities(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="entity delete",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
"""Event commands: list and status."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
ACCENT_COLOR,
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
WARNING_COLOR,
|
||||||
|
print_info,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
from mem0_cli.output import format_agent_envelope, format_json
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
_STATUS_STYLE = {
|
||||||
|
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
|
||||||
|
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
|
||||||
|
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
|
||||||
|
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _status_styled(status: str) -> str:
|
||||||
|
return _STATUS_STYLE.get(status.upper(), status)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
|
||||||
|
"""List recent background events."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching events...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.list_events()
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event list",
|
||||||
|
data=results,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No events found.")
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
header_style=f"bold {ACCENT_COLOR}",
|
||||||
|
row_styles=["", "dim"],
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
|
||||||
|
table.add_column("Type", max_width=14)
|
||||||
|
table.add_column("Status", max_width=12)
|
||||||
|
table.add_column("Latency", max_width=10, justify="right")
|
||||||
|
table.add_column("Created", max_width=20)
|
||||||
|
|
||||||
|
for ev in results:
|
||||||
|
ev_id = str(ev.get("id", ""))[:8]
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
|
||||||
|
"""Get the status of a specific background event."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("event status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
import time as _time
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Fetching event...") as _ts:
|
||||||
|
try:
|
||||||
|
ev = backend.get_event(event_id)
|
||||||
|
except Exception as e:
|
||||||
|
_ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="event status",
|
||||||
|
data=ev,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, ev)
|
||||||
|
return
|
||||||
|
|
||||||
|
status = str(ev.get("status", "—"))
|
||||||
|
ev_type = str(ev.get("event_type", "—"))
|
||||||
|
latency = ev.get("latency")
|
||||||
|
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
|
||||||
|
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
|
||||||
|
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
|
||||||
|
results = ev.get("results")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||||
|
|
||||||
|
if results:
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
|
||||||
|
for r in results:
|
||||||
|
mem_id = str(r.get("id", ""))[:8]
|
||||||
|
data = r.get("data", {})
|
||||||
|
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
|
||||||
|
ev_name = str(r.get("event", ""))
|
||||||
|
user = str(r.get("user_id", ""))
|
||||||
|
detail = f"{ev_name} {memory}"
|
||||||
|
if user:
|
||||||
|
detail += f" [{DIM_COLOR}](user_id={user})[/]"
|
||||||
|
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Event Status[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
"""mem0 init — interactive setup wizard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
print_banner,
|
||||||
|
print_error,
|
||||||
|
print_info,
|
||||||
|
print_success,
|
||||||
|
)
|
||||||
|
from mem0_cli.config import CONFIG_FILE, DEFAULT_BASE_URL, Mem0Config, load_config, save_config
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _prompt_secret(label: str) -> str:
|
||||||
|
"""Prompt for a secret value, echoing '*' for each character typed."""
|
||||||
|
sys.stdout.write(label)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
chars: list[str] = []
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ch = msvcrt.getwch()
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
if ch == "\x03":
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
if ch in ("\x08", "\x7f"): # backspace
|
||||||
|
if chars:
|
||||||
|
chars.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
chars.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
else:
|
||||||
|
import termios
|
||||||
|
import tty
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
while True:
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
if ch in ("\r", "\n"):
|
||||||
|
sys.stdout.write("\r\n")
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
if ch == "\x03":
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
if ch in ("\x7f", "\x08"): # backspace/delete
|
||||||
|
if chars:
|
||||||
|
chars.pop()
|
||||||
|
sys.stdout.write("\b \b")
|
||||||
|
sys.stdout.flush()
|
||||||
|
elif ch == "\x15": # Ctrl+U — clear line
|
||||||
|
sys.stdout.write("\b \b" * len(chars))
|
||||||
|
sys.stdout.flush()
|
||||||
|
chars = []
|
||||||
|
elif ch >= " ": # ignore other control characters
|
||||||
|
chars.append(ch)
|
||||||
|
sys.stdout.write("*")
|
||||||
|
sys.stdout.flush()
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
|
||||||
|
return "".join(chars)
|
||||||
|
|
||||||
|
|
||||||
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_email(email: str) -> None:
|
||||||
|
"""Exit with an error if *email* doesn't look like a valid address."""
|
||||||
|
if not _EMAIL_RE.match(email):
|
||||||
|
print_error(err_console, f"Invalid email address: {email!r}")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _email_login(
|
||||||
|
email: str,
|
||||||
|
code: str | None,
|
||||||
|
base_url: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Run the email verification code login flow.
|
||||||
|
|
||||||
|
Returns the parsed JSON response from the verify endpoint.
|
||||||
|
The caller expects at minimum an ``api_key`` field.
|
||||||
|
"""
|
||||||
|
url = base_url.rstrip("/")
|
||||||
|
_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.
|
||||||
|
|
||||||
|
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||||
|
(non-interactive mode). When running in a non-TTY without the required
|
||||||
|
flags, an error message is printed.
|
||||||
|
"""
|
||||||
|
config = Mem0Config()
|
||||||
|
|
||||||
|
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||||
|
|
||||||
|
if code and not email:
|
||||||
|
print_error(err_console, "--code requires --email.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# Warn if an existing config with an API key would be overwritten
|
||||||
|
if not force and CONFIG_FILE.exists():
|
||||||
|
existing = load_config()
|
||||||
|
if existing.platform.api_key:
|
||||||
|
from mem0_cli.config import redact_key
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
|
||||||
|
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
|
||||||
|
)
|
||||||
|
if sys.stdin.isatty():
|
||||||
|
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
|
||||||
|
if not confirm:
|
||||||
|
print_info(console, "Cancelled. Use --force to skip this check.")
|
||||||
|
raise typer.Exit(0)
|
||||||
|
else:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
"Existing config would be overwritten.",
|
||||||
|
hint="Use --force to overwrite.",
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
# ── Email login flow ──────────────────────────────────────────────
|
||||||
|
if email:
|
||||||
|
if api_key:
|
||||||
|
print_error(err_console, "Cannot use both --api-key and --email.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
email = email.strip().lower()
|
||||||
|
_validate_email(email)
|
||||||
|
|
||||||
|
print_banner(console)
|
||||||
|
console.print()
|
||||||
|
print_info(console, f"Logging in as {email}...\n")
|
||||||
|
|
||||||
|
result = _email_login(email, code, base_url)
|
||||||
|
|
||||||
|
api_key_val = result.get("api_key")
|
||||||
|
if not api_key_val:
|
||||||
|
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
config.platform.api_key = api_key_val
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
config.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
|
||||||
|
if api_key and user_id:
|
||||||
|
config.platform.api_key = api_key
|
||||||
|
config.defaults.user_id = user_id
|
||||||
|
_validate_platform(config)
|
||||||
|
save_config(config)
|
||||||
|
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_banner(console)
|
||||||
|
console.print()
|
||||||
|
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
|
||||||
|
|
||||||
|
# If no flags at all, ask user how they want to authenticate
|
||||||
|
if not api_key:
|
||||||
|
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
|
||||||
|
console.print()
|
||||||
|
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
|
||||||
|
|
||||||
|
if choice == "1":
|
||||||
|
console.print()
|
||||||
|
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
|
||||||
|
if not email_addr:
|
||||||
|
print_error(err_console, "Email is required.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
email_addr = email_addr.strip().lower()
|
||||||
|
_validate_email(email_addr)
|
||||||
|
print_info(console, f"Logging in as {email_addr}...\n")
|
||||||
|
|
||||||
|
result = _email_login(email_addr, None, base_url)
|
||||||
|
|
||||||
|
api_key_val = result.get("api_key")
|
||||||
|
if not api_key_val:
|
||||||
|
print_error(
|
||||||
|
err_console, "Auth succeeded but no API key was returned. Contact support."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
config.platform.api_key = api_key_val
|
||||||
|
config.platform.base_url = base_url
|
||||||
|
config.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:
|
||||||
|
config.platform.api_key = api_key
|
||||||
|
else:
|
||||||
|
_setup_platform(config)
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
config.defaults.user_id = user_id
|
||||||
|
else:
|
||||||
|
_setup_defaults(config)
|
||||||
|
|
||||||
|
_validate_platform(config)
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
console.print()
|
||||||
|
print_success(console, "Configuration saved to ~/.mem0/config.json")
|
||||||
|
console.print()
|
||||||
|
console.print(f" [{DIM_COLOR}]Get started:[/]")
|
||||||
|
if config.defaults.user_id:
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
|
||||||
|
else:
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode" --user-id alice[/]')
|
||||||
|
console.print(f' [{DIM_COLOR}] mem0 search "preferences" --user-id alice[/]')
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_platform(config: Mem0Config) -> None:
|
||||||
|
"""Platform setup flow."""
|
||||||
|
console.print()
|
||||||
|
console.print(f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
|
||||||
|
api_key = _prompt_secret("")
|
||||||
|
if not api_key:
|
||||||
|
print_error(err_console, "API key is required.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
config.platform.api_key = api_key
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_defaults(config: Mem0Config) -> None:
|
||||||
|
"""Collect default entity IDs."""
|
||||||
|
console.print()
|
||||||
|
print_info(console, "Set default entity IDs (press Enter to skip).\n")
|
||||||
|
|
||||||
|
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
|
user_id = Prompt.ask(
|
||||||
|
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
|
||||||
|
default=_default_user,
|
||||||
|
)
|
||||||
|
if user_id:
|
||||||
|
config.defaults.user_id = user_id
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_platform(config: Mem0Config) -> None:
|
||||||
|
"""Validate platform connection after all inputs are collected."""
|
||||||
|
console.print()
|
||||||
|
print_info(console, "Validating connection...")
|
||||||
|
try:
|
||||||
|
from mem0_cli.backend.platform import PlatformBackend
|
||||||
|
|
||||||
|
backend = PlatformBackend(config.platform)
|
||||||
|
status = backend.status(
|
||||||
|
user_id=config.defaults.user_id or None,
|
||||||
|
agent_id=config.defaults.agent_id or None,
|
||||||
|
)
|
||||||
|
if status.get("connected"):
|
||||||
|
print_success(console, "Connected to mem0 Platform!")
|
||||||
|
# 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:
|
||||||
|
print_error(
|
||||||
|
err_console,
|
||||||
|
f"Could not connect: {status.get('error', 'Unknown error')}",
|
||||||
|
hint="Visit https://app.mem0.ai/dashboard/api-keys to get a new key, then run mem0 init again.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, f"Connection test failed: {e}")
|
||||||
@@ -0,0 +1,677 @@
|
|||||||
|
"""Memory CRUD commands: add, search, get, list, update, delete."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat as _stat_mod
|
||||||
|
import sys
|
||||||
|
import time as _time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
print_error,
|
||||||
|
print_info,
|
||||||
|
print_scope,
|
||||||
|
print_success,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
from mem0_cli.output import (
|
||||||
|
format_add_result,
|
||||||
|
format_agent_envelope,
|
||||||
|
format_json,
|
||||||
|
format_memories_table,
|
||||||
|
format_memories_text,
|
||||||
|
format_single_memory,
|
||||||
|
print_result_summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _stdin_is_piped() -> bool:
|
||||||
|
"""Return True only when stdin is an actual pipe or file redirect."""
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
|
if is_agent_mode():
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
mode = os.fstat(sys.stdin.fileno()).st_mode
|
||||||
|
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(
|
||||||
|
backend: Backend,
|
||||||
|
text: str | None,
|
||||||
|
*,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
app_id: str | None,
|
||||||
|
run_id: str | None,
|
||||||
|
messages: str | None,
|
||||||
|
file: Path | None,
|
||||||
|
metadata: str | None,
|
||||||
|
immutable: bool,
|
||||||
|
no_infer: bool,
|
||||||
|
expires: str | None,
|
||||||
|
categories: str | None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
output: str = "text",
|
||||||
|
) -> None:
|
||||||
|
"""Add a memory."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("add")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
msgs = None
|
||||||
|
content = text
|
||||||
|
|
||||||
|
# Read from file
|
||||||
|
if file:
|
||||||
|
try:
|
||||||
|
raw = Path(file).read_text()
|
||||||
|
msgs = json.loads(raw)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
|
print_error(err_console, f"Failed to read file: {e}")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
# Parse messages JSON
|
||||||
|
elif messages:
|
||||||
|
try:
|
||||||
|
msgs = json.loads(messages)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print_error(err_console, f"Invalid JSON in --messages: {e}")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
# Read from stdin only if stdin is an actual pipe or file redirect
|
||||||
|
elif not content and _stdin_is_piped():
|
||||||
|
content = sys.stdin.read().strip()
|
||||||
|
|
||||||
|
if not content and not msgs:
|
||||||
|
print_error(
|
||||||
|
err_console, "No content provided. Pass text, --messages, --file, or pipe via stdin."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
meta = None
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
meta = json.loads(metadata)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print_error(err_console, "Invalid JSON in --metadata.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
cats = None
|
||||||
|
if categories:
|
||||||
|
try:
|
||||||
|
cats = json.loads(categories)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
cats = [c.strip() for c in categories.split(",")]
|
||||||
|
|
||||||
|
# Validate --expires
|
||||||
|
if expires:
|
||||||
|
import re
|
||||||
|
|
||||||
|
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
|
||||||
|
print_error(
|
||||||
|
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||||
|
)
|
||||||
|
raise typer.Exit(1)
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
if date.fromisoformat(expires) <= date.today():
|
||||||
|
print_error(err_console, "--expires date must be in the future.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
with timed_status(err_console, "Adding memory...") as ts:
|
||||||
|
try:
|
||||||
|
result = backend.add(
|
||||||
|
content=content,
|
||||||
|
messages=msgs,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
metadata=meta,
|
||||||
|
immutable=immutable,
|
||||||
|
infer=not no_infer,
|
||||||
|
expires=expires,
|
||||||
|
categories=cats,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
ts.error_msg = str(e)
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Deduplicate PENDING entries sharing the same event_id across all output modes
|
||||||
|
results_list = result if isinstance(result, list) else result.get("results", [result])
|
||||||
|
seen_events: set[str] = set()
|
||||||
|
deduped: list[dict] = []
|
||||||
|
for r in results_list:
|
||||||
|
if r.get("status") == "PENDING":
|
||||||
|
eid = r.get("event_id", "")
|
||||||
|
if eid and eid in seen_events:
|
||||||
|
continue
|
||||||
|
if eid:
|
||||||
|
seen_events.add(eid)
|
||||||
|
deduped.append(r)
|
||||||
|
# Write back so downstream formatters see deduplicated data
|
||||||
|
if isinstance(result, dict) and "results" in result:
|
||||||
|
result = {**result, "results": deduped}
|
||||||
|
else:
|
||||||
|
result = deduped
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="add",
|
||||||
|
data=deduped,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(deduped),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_add_result(console, result, output)
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
|
||||||
|
count = len(deduped)
|
||||||
|
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
|
||||||
|
if all_pending:
|
||||||
|
print_success(
|
||||||
|
console,
|
||||||
|
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print_success(
|
||||||
|
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
|
||||||
|
)
|
||||||
|
format_add_result(console, result, output)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_search(
|
||||||
|
backend: Backend,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
app_id: str | None,
|
||||||
|
run_id: str | None,
|
||||||
|
top_k: int,
|
||||||
|
threshold: float,
|
||||||
|
rerank: bool,
|
||||||
|
keyword: bool,
|
||||||
|
filter_json: str | None,
|
||||||
|
fields: str | None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
output: str = "text",
|
||||||
|
) -> None:
|
||||||
|
"""Search memories."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("search")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
filters = None
|
||||||
|
if filter_json:
|
||||||
|
try:
|
||||||
|
filters = json.loads(filter_json)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print_error(err_console, "Invalid JSON in --filter.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
field_list = None
|
||||||
|
if fields:
|
||||||
|
field_list = [f.strip() for f in fields.split(",")]
|
||||||
|
|
||||||
|
if top_k < 1:
|
||||||
|
print_error(err_console, "--top-k must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if not (0.0 <= threshold <= 1.0):
|
||||||
|
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Searching memories...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.search(
|
||||||
|
query,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
top_k=top_k,
|
||||||
|
threshold=threshold,
|
||||||
|
rerank=rerank,
|
||||||
|
keyword=keyword,
|
||||||
|
filters=filters,
|
||||||
|
fields=field_list,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="search",
|
||||||
|
data=results,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, results)
|
||||||
|
elif output == "table":
|
||||||
|
if results:
|
||||||
|
format_memories_table(console, results, show_score=True)
|
||||||
|
print_result_summary(
|
||||||
|
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found matching your query.")
|
||||||
|
console.print()
|
||||||
|
else:
|
||||||
|
if results:
|
||||||
|
format_memories_text(console, results)
|
||||||
|
print_result_summary(
|
||||||
|
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found matching your query.")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
|
||||||
|
"""Get a specific memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("get")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
with timed_status(err_console, "Fetching memory...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.get(memory_id)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(console, command="get", data=result)
|
||||||
|
else:
|
||||||
|
format_single_memory(console, result, output)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list(
|
||||||
|
backend: Backend,
|
||||||
|
*,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
app_id: str | None,
|
||||||
|
run_id: str | None,
|
||||||
|
page: int,
|
||||||
|
page_size: int,
|
||||||
|
category: str | None,
|
||||||
|
after: str | None,
|
||||||
|
before: str | None,
|
||||||
|
enable_graph: bool = False,
|
||||||
|
output: str = "table",
|
||||||
|
) -> None:
|
||||||
|
"""List memories."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("list")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if page_size < 1:
|
||||||
|
print_error(err_console, "--page-size must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if page < 1:
|
||||||
|
print_error(err_console, "--page must be >= 1.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Listing memories...") as _ts:
|
||||||
|
try:
|
||||||
|
results = backend.list_memories(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
category=category,
|
||||||
|
after=after,
|
||||||
|
before=before,
|
||||||
|
enable_graph=enable_graph,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="list",
|
||||||
|
data=results,
|
||||||
|
scope=scope or None,
|
||||||
|
count=len(results),
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "table":
|
||||||
|
if results:
|
||||||
|
format_memories_table(console, results)
|
||||||
|
print_result_summary(
|
||||||
|
console,
|
||||||
|
len(results),
|
||||||
|
duration_secs=_elapsed,
|
||||||
|
page=page,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found.")
|
||||||
|
console.print()
|
||||||
|
else:
|
||||||
|
if results:
|
||||||
|
format_memories_text(console, results, title="memories")
|
||||||
|
print_result_summary(
|
||||||
|
console,
|
||||||
|
len(results),
|
||||||
|
duration_secs=_elapsed,
|
||||||
|
page=page,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
console.print()
|
||||||
|
print_info(console, "No memories found.")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_update(
|
||||||
|
backend: Backend,
|
||||||
|
memory_id: str,
|
||||||
|
text: str | None,
|
||||||
|
*,
|
||||||
|
metadata: str | None,
|
||||||
|
output: str,
|
||||||
|
) -> None:
|
||||||
|
"""Update a memory."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("update")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
meta = None
|
||||||
|
if metadata:
|
||||||
|
try:
|
||||||
|
meta = json.loads(metadata)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print_error(err_console, "Invalid JSON in --metadata.")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Updating memory...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.update(memory_id, content=text, metadata=meta)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="update",
|
||||||
|
data=result,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete(
|
||||||
|
backend: Backend,
|
||||||
|
memory_id: str,
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
force: bool = False,
|
||||||
|
output: str,
|
||||||
|
) -> None:
|
||||||
|
"""Delete a single memory by ID."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if dry_run:
|
||||||
|
# Fetch and display what would be deleted
|
||||||
|
try:
|
||||||
|
mem = backend.get(memory_id)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
format_single_memory(console, mem, output)
|
||||||
|
print_info(console, "No changes made (dry run).")
|
||||||
|
return
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Deleting...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.delete(memory_id=memory_id)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete",
|
||||||
|
data={"id": memory_id, "deleted": True},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_delete_all(
|
||||||
|
backend: Backend,
|
||||||
|
*,
|
||||||
|
force: bool,
|
||||||
|
dry_run: bool = False,
|
||||||
|
all_: bool = False,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
app_id: str | None,
|
||||||
|
run_id: str | None,
|
||||||
|
output: str,
|
||||||
|
) -> None:
|
||||||
|
"""Delete all memories matching a scope."""
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("delete-all")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
if not force:
|
||||||
|
print_error(err_console, "Destructive operation requires --force in agent mode.")
|
||||||
|
raise typer.Exit(1)
|
||||||
|
if all_:
|
||||||
|
# Project-wide wipe using wildcard entity IDs
|
||||||
|
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
confirm = typer.confirm(
|
||||||
|
"\n ⚠ Delete ALL memories across the ENTIRE project? This cannot be undone."
|
||||||
|
)
|
||||||
|
if not confirm:
|
||||||
|
print_info(console, "Cancelled.")
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Deleting all memories project-wide...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.delete(
|
||||||
|
all=True,
|
||||||
|
user_id="*",
|
||||||
|
agent_id="*",
|
||||||
|
app_id="*",
|
||||||
|
run_id="*",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True, "scope": "project"},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
if isinstance(result, dict) and "message" in result:
|
||||||
|
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||||
|
else:
|
||||||
|
print_success(console, f"All project memories deleted ({_elapsed:.2f}s)")
|
||||||
|
return
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
# List matching memories and show count
|
||||||
|
try:
|
||||||
|
results = backend.list_memories(
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
count = len(results)
|
||||||
|
print_info(console, f"Would delete {count} memor{'y' if count == 1 else 'ies'}.")
|
||||||
|
print_info(console, "No changes made (dry run).")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not force:
|
||||||
|
scope_parts = []
|
||||||
|
if user_id:
|
||||||
|
scope_parts.append(f"user={user_id}")
|
||||||
|
if agent_id:
|
||||||
|
scope_parts.append(f"agent={agent_id}")
|
||||||
|
if app_id:
|
||||||
|
scope_parts.append(f"app={app_id}")
|
||||||
|
if run_id:
|
||||||
|
scope_parts.append(f"run={run_id}")
|
||||||
|
scope = ", ".join(scope_parts) if scope_parts else "ALL entities"
|
||||||
|
|
||||||
|
confirm = typer.confirm(f"\n ⚠ Delete ALL memories for {scope}? This cannot be undone.")
|
||||||
|
if not confirm:
|
||||||
|
print_info(console, "Cancelled.")
|
||||||
|
raise typer.Exit(0)
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Deleting all memories...") as _ts:
|
||||||
|
try:
|
||||||
|
result = backend.delete(
|
||||||
|
all=True,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_id=agent_id,
|
||||||
|
app_id=app_id,
|
||||||
|
run_id=run_id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print_error(err_console, str(e))
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
scope = {
|
||||||
|
k: v
|
||||||
|
for k, v in {
|
||||||
|
"user_id": user_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"app_id": app_id,
|
||||||
|
"run_id": run_id,
|
||||||
|
}.items()
|
||||||
|
if v
|
||||||
|
}
|
||||||
|
if output == "agent":
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="delete-all",
|
||||||
|
data={"deleted": True},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
elif output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
elif output != "quiet":
|
||||||
|
if isinstance(result, dict) and "message" in result:
|
||||||
|
print_info(console, "Deletion started. Memories will be removed in the background.")
|
||||||
|
else:
|
||||||
|
print_success(console, f"All matching memories deleted ({_elapsed:.2f}s)")
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Utility commands: status, version, import."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import time as _time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.progress import track
|
||||||
|
|
||||||
|
from mem0_cli import __version__
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.branding import (
|
||||||
|
BRAND_COLOR,
|
||||||
|
DIM_COLOR,
|
||||||
|
ERROR_COLOR,
|
||||||
|
SUCCESS_COLOR,
|
||||||
|
print_error,
|
||||||
|
print_success,
|
||||||
|
timed_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
err_console = Console(stderr=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status(
|
||||||
|
backend: Backend,
|
||||||
|
*,
|
||||||
|
user_id: str | None = None,
|
||||||
|
agent_id: str | None = None,
|
||||||
|
output: str = "text",
|
||||||
|
) -> None:
|
||||||
|
"""Check connectivity and auth."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("status")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
with timed_status(err_console, "Checking connection...") as _ts:
|
||||||
|
result = backend.status(user_id=user_id, agent_id=agent_id)
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="status",
|
||||||
|
data={
|
||||||
|
"connected": result.get("connected", False),
|
||||||
|
"backend": result.get("backend", "?"),
|
||||||
|
"base_url": result.get("base_url", ""),
|
||||||
|
},
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
if result.get("connected"):
|
||||||
|
lines.append(f" [{SUCCESS_COLOR}]●[/] Connected")
|
||||||
|
else:
|
||||||
|
lines.append(f" [{ERROR_COLOR}]●[/] Disconnected")
|
||||||
|
|
||||||
|
lines.append(f" [{DIM_COLOR}]Backend:[/] {result.get('backend', '?')}")
|
||||||
|
if result.get("base_url"):
|
||||||
|
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
|
||||||
|
if result.get("error"):
|
||||||
|
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
|
||||||
|
if "Authentication failed" in str(result["error"]):
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys[/bold][/]"
|
||||||
|
)
|
||||||
|
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Connection Status[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_version() -> None:
|
||||||
|
"""Show version."""
|
||||||
|
console.print(f" [{BRAND_COLOR}]◆ Mem0[/] CLI v{__version__}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_import(
|
||||||
|
backend: Backend,
|
||||||
|
file_path: str,
|
||||||
|
*,
|
||||||
|
user_id: str | None,
|
||||||
|
agent_id: str | None,
|
||||||
|
output: str = "text",
|
||||||
|
) -> None:
|
||||||
|
"""Import memories from a JSON file."""
|
||||||
|
from mem0_cli.output import format_agent_envelope
|
||||||
|
from mem0_cli.state import is_agent_mode, set_current_command
|
||||||
|
|
||||||
|
set_current_command("import")
|
||||||
|
if is_agent_mode():
|
||||||
|
output = "agent"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(Path(file_path).read_text())
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
|
print_error(err_console, f"Failed to read file: {e}")
|
||||||
|
raise typer.Exit(1) from None
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
data = [data]
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
failed = 0
|
||||||
|
_start = _time.perf_counter()
|
||||||
|
for item in track(
|
||||||
|
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
|
||||||
|
):
|
||||||
|
content = item.get("memory", item.get("text", item.get("content", "")))
|
||||||
|
if not content:
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
backend.add(
|
||||||
|
content=content,
|
||||||
|
user_id=user_id or item.get("user_id"),
|
||||||
|
agent_id=agent_id or item.get("agent_id"),
|
||||||
|
metadata=item.get("metadata"),
|
||||||
|
)
|
||||||
|
added += 1
|
||||||
|
except Exception:
|
||||||
|
failed += 1
|
||||||
|
_elapsed = _time.perf_counter() - _start
|
||||||
|
|
||||||
|
if output in ("json", "agent"):
|
||||||
|
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
|
||||||
|
format_agent_envelope(
|
||||||
|
console,
|
||||||
|
command="import",
|
||||||
|
data={"added": added, "failed": failed},
|
||||||
|
scope=scope or None,
|
||||||
|
duration_ms=int(_elapsed * 1000),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print_success(err_console, f"Imported {added} memories ({_elapsed:.2f}s)")
|
||||||
|
if failed:
|
||||||
|
print_error(err_console, f"{failed} memories failed to import.")
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""Configuration management for mem0 CLI.
|
||||||
|
|
||||||
|
Config precedence (highest to lowest):
|
||||||
|
1. CLI flags (--api-key, --base-url, etc.)
|
||||||
|
2. Environment variables (MEM0_API_KEY, etc.)
|
||||||
|
3. Config file (~/.mem0/config.json)
|
||||||
|
4. Defaults
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
CONFIG_DIR = Path.home() / ".mem0"
|
||||||
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
||||||
|
|
||||||
|
DEFAULT_BASE_URL = "https://api.mem0.ai"
|
||||||
|
CONFIG_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlatformConfig:
|
||||||
|
api_key: str = ""
|
||||||
|
base_url: str = DEFAULT_BASE_URL
|
||||||
|
user_email: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DefaultsConfig:
|
||||||
|
user_id: str = ""
|
||||||
|
agent_id: str = ""
|
||||||
|
app_id: str = ""
|
||||||
|
run_id: str = ""
|
||||||
|
enable_graph: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TelemetryConfig:
|
||||||
|
anonymous_id: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mem0Config:
|
||||||
|
version: int = CONFIG_VERSION
|
||||||
|
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||||
|
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:
|
||||||
|
"""Create ~/.mem0 directory with secure permissions if it doesn't exist."""
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700
|
||||||
|
return CONFIG_DIR
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> Mem0Config:
|
||||||
|
"""Load config from file, applying env var overrides."""
|
||||||
|
config = Mem0Config()
|
||||||
|
|
||||||
|
if CONFIG_FILE.exists():
|
||||||
|
with open(CONFIG_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
config.version = data.get("version", CONFIG_VERSION)
|
||||||
|
|
||||||
|
plat = data.get("platform", {})
|
||||||
|
config.platform.api_key = plat.get("api_key", "")
|
||||||
|
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||||
|
config.platform.user_email = plat.get("user_email", "")
|
||||||
|
|
||||||
|
defaults = data.get("defaults", {})
|
||||||
|
config.defaults.user_id = defaults.get("user_id", "")
|
||||||
|
config.defaults.agent_id = defaults.get("agent_id", "")
|
||||||
|
config.defaults.app_id = defaults.get("app_id", "")
|
||||||
|
config.defaults.run_id = defaults.get("run_id", "")
|
||||||
|
config.defaults.enable_graph = defaults.get("enable_graph", False)
|
||||||
|
|
||||||
|
telemetry = data.get("telemetry", {})
|
||||||
|
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
||||||
|
|
||||||
|
# Environment variable overrides
|
||||||
|
env_key = os.environ.get("MEM0_API_KEY")
|
||||||
|
if env_key:
|
||||||
|
config.platform.api_key = env_key
|
||||||
|
|
||||||
|
env_base = os.environ.get("MEM0_BASE_URL")
|
||||||
|
if env_base:
|
||||||
|
config.platform.base_url = env_base
|
||||||
|
|
||||||
|
env_user_id = os.environ.get("MEM0_USER_ID")
|
||||||
|
if env_user_id:
|
||||||
|
config.defaults.user_id = env_user_id
|
||||||
|
|
||||||
|
env_agent_id = os.environ.get("MEM0_AGENT_ID")
|
||||||
|
if env_agent_id:
|
||||||
|
config.defaults.agent_id = env_agent_id
|
||||||
|
|
||||||
|
env_app_id = os.environ.get("MEM0_APP_ID")
|
||||||
|
if env_app_id:
|
||||||
|
config.defaults.app_id = env_app_id
|
||||||
|
|
||||||
|
env_run_id = os.environ.get("MEM0_RUN_ID")
|
||||||
|
if env_run_id:
|
||||||
|
config.defaults.run_id = env_run_id
|
||||||
|
|
||||||
|
env_graph = os.environ.get("MEM0_ENABLE_GRAPH")
|
||||||
|
if env_graph:
|
||||||
|
config.defaults.enable_graph = env_graph.lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def save_config(config: Mem0Config) -> None:
|
||||||
|
"""Write config to disk with secure permissions."""
|
||||||
|
ensure_config_dir()
|
||||||
|
|
||||||
|
data: dict[str, Any] = {
|
||||||
|
"version": config.version,
|
||||||
|
"defaults": {
|
||||||
|
"user_id": config.defaults.user_id,
|
||||||
|
"agent_id": config.defaults.agent_id,
|
||||||
|
"app_id": config.defaults.app_id,
|
||||||
|
"run_id": config.defaults.run_id,
|
||||||
|
"enable_graph": config.defaults.enable_graph,
|
||||||
|
},
|
||||||
|
"platform": {
|
||||||
|
"api_key": config.platform.api_key,
|
||||||
|
"base_url": config.platform.base_url,
|
||||||
|
"user_email": config.platform.user_email,
|
||||||
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"anonymous_id": config.telemetry.anonymous_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||||
|
|
||||||
|
|
||||||
|
def redact_key(key: str) -> str:
|
||||||
|
"""Redact an API key for display: m0-xxx...xxx"""
|
||||||
|
if not key:
|
||||||
|
return "(not set)"
|
||||||
|
if len(key) <= 8:
|
||||||
|
return key[:2] + "***"
|
||||||
|
return key[:4] + "..." + key[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
|
||||||
|
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
|
||||||
|
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
obj: Any = config
|
||||||
|
for part in parts:
|
||||||
|
if hasattr(obj, part):
|
||||||
|
obj = getattr(obj, part)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||||
|
"""Set a config value by dotted path. Returns True on success."""
|
||||||
|
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
|
||||||
|
parts = dotted_key.split(".")
|
||||||
|
obj: Any = config
|
||||||
|
for part in parts[:-1]:
|
||||||
|
if hasattr(obj, part):
|
||||||
|
obj = getattr(obj, part)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
final_key = parts[-1]
|
||||||
|
if not hasattr(obj, final_key):
|
||||||
|
return False
|
||||||
|
|
||||||
|
current = getattr(obj, final_key)
|
||||||
|
# Type coercion
|
||||||
|
if isinstance(current, bool):
|
||||||
|
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||||
|
elif isinstance(current, int):
|
||||||
|
value = int(value) # type: ignore[assignment]
|
||||||
|
|
||||||
|
setattr(obj, final_key, value)
|
||||||
|
return True
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
"""Output formatting for mem0 CLI — text, JSON, table, quiet modes."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from mem0_cli.branding import ACCENT_COLOR, BRAND_COLOR, DIM_COLOR, SUCCESS_COLOR, _sym
|
||||||
|
|
||||||
|
|
||||||
|
def format_memories_text(console: Console, memories: list[dict], title: str = "memories") -> None:
|
||||||
|
"""Render memories in human-friendly text mode."""
|
||||||
|
count = len(memories)
|
||||||
|
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
|
||||||
|
|
||||||
|
for i, mem in enumerate(memories, 1):
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
mem_id = mem.get("id", "")[:8]
|
||||||
|
score = mem.get("score")
|
||||||
|
created = _format_date(mem.get("created_at"))
|
||||||
|
category = mem.get("categories", [None])
|
||||||
|
if isinstance(category, list):
|
||||||
|
category = category[0] if category else None
|
||||||
|
|
||||||
|
line = Text()
|
||||||
|
line.append(f" {i}. ", style="bold")
|
||||||
|
line.append(memory_text, style="white")
|
||||||
|
console.print(line)
|
||||||
|
|
||||||
|
details = []
|
||||||
|
if score is not None:
|
||||||
|
details.append(f"Score: {score:.2f}")
|
||||||
|
if mem_id:
|
||||||
|
details.append(f"ID: {mem_id}")
|
||||||
|
if created:
|
||||||
|
details.append(f"Created: {created}")
|
||||||
|
if category:
|
||||||
|
details.append(f"Category: {category}")
|
||||||
|
|
||||||
|
if details:
|
||||||
|
detail_str = " · ".join(details)
|
||||||
|
console.print(f" [{DIM_COLOR}]{detail_str}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_memories_table(
|
||||||
|
console: Console, memories: list[dict], *, show_score: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""Render memories in a rich table."""
|
||||||
|
table = Table(
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
header_style=f"bold {ACCENT_COLOR}",
|
||||||
|
row_styles=["", "dim"],
|
||||||
|
padding=(0, 1),
|
||||||
|
)
|
||||||
|
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
|
||||||
|
if show_score:
|
||||||
|
table.add_column("Score", max_width=7, justify="right")
|
||||||
|
table.add_column("Memory", max_width=50, no_wrap=False)
|
||||||
|
table.add_column("Category", max_width=14)
|
||||||
|
table.add_column("Created", max_width=12)
|
||||||
|
|
||||||
|
for mem in memories:
|
||||||
|
mem_id = mem.get("id", "")
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
if len(memory_text) > 60:
|
||||||
|
memory_text = memory_text[:57] + "..."
|
||||||
|
categories = mem.get("categories", [])
|
||||||
|
if isinstance(categories, list) and categories:
|
||||||
|
cat = (
|
||||||
|
categories[0]
|
||||||
|
if len(categories) == 1
|
||||||
|
else f"{categories[0]} (+{len(categories) - 1})"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cat = "—"
|
||||||
|
created = _format_date(mem.get("created_at")) or "—"
|
||||||
|
if show_score:
|
||||||
|
score = mem.get("score")
|
||||||
|
score_str = f"{score:.2f}" if score is not None else "—"
|
||||||
|
table.add_row(mem_id, score_str, memory_text, cat, created)
|
||||||
|
else:
|
||||||
|
table.add_row(mem_id, memory_text, cat, created)
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
console.print(table)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_json(console: Console, data: Any) -> None:
|
||||||
|
"""Output data as pretty-printed JSON."""
|
||||||
|
console.print_json(json.dumps(data, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def format_single_memory(console: Console, mem: dict, output: str = "text") -> None:
|
||||||
|
"""Format a single memory for display."""
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, mem)
|
||||||
|
return
|
||||||
|
|
||||||
|
memory_text = mem.get("memory", mem.get("text", ""))
|
||||||
|
mem_id = mem.get("id", "")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
lines.append(f" [white bold]{memory_text}[/]")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if mem_id:
|
||||||
|
lines.append(f" [{DIM_COLOR}]ID:[/] {mem_id}")
|
||||||
|
created = _format_date(mem.get("created_at"))
|
||||||
|
if created:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
|
||||||
|
updated = _format_date(mem.get("updated_at"))
|
||||||
|
if updated:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
|
||||||
|
meta = mem.get("metadata")
|
||||||
|
if meta:
|
||||||
|
lines.append(f" [{DIM_COLOR}]Metadata:[/] {json.dumps(meta)}")
|
||||||
|
categories = mem.get("categories")
|
||||||
|
if categories:
|
||||||
|
cat_str = ", ".join(categories) if isinstance(categories, list) else categories
|
||||||
|
lines.append(f" [{DIM_COLOR}]Categories:[/] {cat_str}")
|
||||||
|
|
||||||
|
content = "\n".join(lines)
|
||||||
|
panel = Panel(
|
||||||
|
content,
|
||||||
|
title=f"[{BRAND_COLOR}]Memory[/]",
|
||||||
|
title_align="left",
|
||||||
|
border_style=BRAND_COLOR,
|
||||||
|
padding=(1, 1),
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_add_result(console: Console, result: dict | list, output: str = "text") -> None:
|
||||||
|
"""Format the result of an add operation."""
|
||||||
|
if output == "json":
|
||||||
|
format_json(console, result)
|
||||||
|
return
|
||||||
|
if output == "quiet":
|
||||||
|
return
|
||||||
|
|
||||||
|
# result from API is typically {"results": [...]}
|
||||||
|
results = result if isinstance(result, list) else result.get("results", [result])
|
||||||
|
if not results:
|
||||||
|
console.print(f" [{DIM_COLOR}]No memories extracted.[/]")
|
||||||
|
return
|
||||||
|
|
||||||
|
console.print()
|
||||||
|
seen_pending_events: set[str] = set()
|
||||||
|
for r in results:
|
||||||
|
# Detect async PENDING response from Platform API
|
||||||
|
if r.get("status") == "PENDING":
|
||||||
|
event_id = r.get("event_id", "")
|
||||||
|
# Deduplicate PENDING entries with the same event_id
|
||||||
|
if event_id and event_id in seen_pending_events:
|
||||||
|
continue
|
||||||
|
if event_id:
|
||||||
|
seen_pending_events.add(event_id)
|
||||||
|
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
|
||||||
|
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
|
||||||
|
parts.append("[white]Processing in background[/]")
|
||||||
|
console.print(" ".join(parts))
|
||||||
|
if event_id:
|
||||||
|
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
|
||||||
|
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
|
||||||
|
continue
|
||||||
|
|
||||||
|
event = r.get("event", "ADD")
|
||||||
|
memory = r.get("memory") or r.get("text") or r.get("content") or r.get("data") or ""
|
||||||
|
mem_id = (r.get("id") or r.get("memory_id") or "")[:8]
|
||||||
|
|
||||||
|
if event == "ADD":
|
||||||
|
icon = f"[{SUCCESS_COLOR}]+[/]"
|
||||||
|
label = "Added"
|
||||||
|
elif event == "UPDATE":
|
||||||
|
icon = f"[{ACCENT_COLOR}]~[/]"
|
||||||
|
label = "Updated"
|
||||||
|
elif event == "DELETE":
|
||||||
|
icon = "[red]-[/]"
|
||||||
|
label = "Deleted"
|
||||||
|
elif event == "NOOP":
|
||||||
|
icon = f"[{DIM_COLOR}]·[/]"
|
||||||
|
label = "No change"
|
||||||
|
else:
|
||||||
|
icon = f"[{DIM_COLOR}]?[/]"
|
||||||
|
label = event
|
||||||
|
|
||||||
|
# Build the display line
|
||||||
|
parts = [f" {icon} [{DIM_COLOR}]{label:<10}[/]"]
|
||||||
|
if memory:
|
||||||
|
parts.append(f"[white]{memory}[/]")
|
||||||
|
if mem_id:
|
||||||
|
parts.append(f"[{DIM_COLOR}]({mem_id})[/]")
|
||||||
|
console.print(" ".join(parts))
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def format_json_envelope(
|
||||||
|
console: Console,
|
||||||
|
*,
|
||||||
|
command: str,
|
||||||
|
data: Any,
|
||||||
|
duration_ms: int | None = None,
|
||||||
|
scope: dict | None = None,
|
||||||
|
count: int | None = None,
|
||||||
|
status: str = "success",
|
||||||
|
error: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Output structured JSON envelope for AI agent consumption."""
|
||||||
|
envelope: dict[str, Any] = {
|
||||||
|
"status": status,
|
||||||
|
"command": command,
|
||||||
|
}
|
||||||
|
if duration_ms is not None:
|
||||||
|
envelope["duration_ms"] = duration_ms
|
||||||
|
if scope is not None:
|
||||||
|
envelope["scope"] = scope
|
||||||
|
if count is not None:
|
||||||
|
envelope["count"] = count
|
||||||
|
if error:
|
||||||
|
envelope["error"] = error
|
||||||
|
envelope["data"] = data
|
||||||
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||||
|
"""Project API response data to minimal relevant fields for agent consumption."""
|
||||||
|
|
||||||
|
def pick(obj: dict, keys: list) -> dict:
|
||||||
|
return {k: obj[k] for k in keys if k in obj}
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "add":
|
||||||
|
items = data if isinstance(data, list) else [data]
|
||||||
|
result = []
|
||||||
|
for item in items:
|
||||||
|
if item.get("status") == "PENDING":
|
||||||
|
result.append(pick(item, ["status", "event_id"]))
|
||||||
|
else:
|
||||||
|
result.append(pick(item, ["id", "memory", "event"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "search":
|
||||||
|
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "list":
|
||||||
|
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||||
|
|
||||||
|
if command == "get":
|
||||||
|
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||||
|
|
||||||
|
if command == "update":
|
||||||
|
return pick(data, ["id", "memory"])
|
||||||
|
|
||||||
|
if command in ("delete", "delete-all", "entity delete"):
|
||||||
|
return data
|
||||||
|
|
||||||
|
if command == "entity list":
|
||||||
|
result = []
|
||||||
|
for r in data:
|
||||||
|
item = pick(r, ["type", "count"])
|
||||||
|
item["name"] = r.get("name") or r.get("id", "")
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if command == "event list":
|
||||||
|
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
|
||||||
|
|
||||||
|
if command == "event status":
|
||||||
|
ev = data
|
||||||
|
raw_results = ev.get("results") or []
|
||||||
|
sanitized_results = []
|
||||||
|
for r in raw_results:
|
||||||
|
nested = r.get("data") or {}
|
||||||
|
memory = nested.get("memory") if isinstance(nested, dict) else None
|
||||||
|
sanitized_results.append(
|
||||||
|
{
|
||||||
|
"id": r.get("id"),
|
||||||
|
"event": r.get("event"),
|
||||||
|
"user_id": r.get("user_id"),
|
||||||
|
"memory": memory,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
|
||||||
|
result["results"] = sanitized_results
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Pass-through: status, import, config show/get/set
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def format_agent_envelope(
|
||||||
|
console: Console,
|
||||||
|
*,
|
||||||
|
command: str,
|
||||||
|
data: Any,
|
||||||
|
duration_ms: int | None = None,
|
||||||
|
scope: dict | None = None,
|
||||||
|
count: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
|
||||||
|
envelope: dict[str, Any] = {
|
||||||
|
"status": "success",
|
||||||
|
"command": command,
|
||||||
|
}
|
||||||
|
if duration_ms is not None:
|
||||||
|
envelope["duration_ms"] = duration_ms
|
||||||
|
if scope:
|
||||||
|
filtered = {k: v for k, v in scope.items() if v}
|
||||||
|
if filtered:
|
||||||
|
envelope["scope"] = filtered
|
||||||
|
if count is not None:
|
||||||
|
envelope["count"] = count
|
||||||
|
envelope["data"] = sanitize_agent_data(command, data)
|
||||||
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
def print_result_summary(
|
||||||
|
console: Console,
|
||||||
|
count: int,
|
||||||
|
*,
|
||||||
|
duration_secs: float | None = None,
|
||||||
|
page: int | None = None,
|
||||||
|
**scope_ids: str | None,
|
||||||
|
) -> None:
|
||||||
|
"""Print a summary footer after result lists."""
|
||||||
|
parts = [f"{count} result{'s' if count != 1 else ''}"]
|
||||||
|
if page is not None:
|
||||||
|
parts.append(f"page {page}")
|
||||||
|
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
|
||||||
|
if scope_parts:
|
||||||
|
parts.append(", ".join(scope_parts))
|
||||||
|
if duration_secs is not None:
|
||||||
|
parts.append(f"{duration_secs:.2f}s")
|
||||||
|
|
||||||
|
summary = " · ".join(parts)
|
||||||
|
console.print(f" [{DIM_COLOR}]{summary}[/]")
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
|
||||||
|
def _format_date(dt_str: str | None) -> str | None:
|
||||||
|
if not dt_str:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
||||||
|
return dt.strftime("%Y-%m-%d")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return str(dt_str)[:10] if dt_str else None
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Agent mode state — set by the root callback, read by commands and branding."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
_agent_mode: bool = False
|
||||||
|
_current_command: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def is_agent_mode() -> bool:
|
||||||
|
return _agent_mode
|
||||||
|
|
||||||
|
|
||||||
|
def set_agent_mode(val: bool) -> None:
|
||||||
|
global _agent_mode
|
||||||
|
_agent_mode = val
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_command() -> str:
|
||||||
|
return _current_command
|
||||||
|
|
||||||
|
|
||||||
|
def set_current_command(name: str) -> None:
|
||||||
|
global _current_command
|
||||||
|
_current_command = name
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""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()
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
"""Shared fixtures for mem0 CLI tests."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mem0_cli.backend.base import Backend
|
||||||
|
from mem0_cli.config import Mem0Config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def isolate_config(tmp_path, monkeypatch):
|
||||||
|
"""Redirect config to a temp directory so tests don't touch real config."""
|
||||||
|
fake_config_dir = tmp_path / ".mem0"
|
||||||
|
fake_config_file = fake_config_dir / "config.json"
|
||||||
|
monkeypatch.setattr("mem0_cli.config.CONFIG_DIR", fake_config_dir)
|
||||||
|
monkeypatch.setattr("mem0_cli.config.CONFIG_FILE", fake_config_file)
|
||||||
|
# Also patch the commands that import config
|
||||||
|
monkeypatch.setattr("mem0_cli.commands.config_cmd.CONFIG_DIR", fake_config_dir, raising=False)
|
||||||
|
# Clear any MEM0 env vars
|
||||||
|
for key in list(os.environ.keys()):
|
||||||
|
if key.startswith("MEM0_"):
|
||||||
|
monkeypatch.delenv(key, raising=False)
|
||||||
|
return fake_config_dir
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_backend():
|
||||||
|
"""Return a mock backend with all methods stubbed."""
|
||||||
|
backend = MagicMock(spec=Backend)
|
||||||
|
|
||||||
|
# Default return values
|
||||||
|
backend.add.return_value = {
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"memory": "User prefers dark mode",
|
||||||
|
"event": "ADD",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
backend.search.return_value = [
|
||||||
|
{
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"memory": "User prefers dark mode",
|
||||||
|
"score": 0.92,
|
||||||
|
"created_at": "2026-02-15T10:30:00Z",
|
||||||
|
"categories": ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ghi-789-jkl-012",
|
||||||
|
"memory": "User uses vim keybindings",
|
||||||
|
"score": 0.78,
|
||||||
|
"created_at": "2026-03-01T14:00:00Z",
|
||||||
|
"categories": ["tools"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
backend.get.return_value = {
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"memory": "User prefers dark mode",
|
||||||
|
"created_at": "2026-02-15T10:30:00Z",
|
||||||
|
"updated_at": "2026-02-20T08:00:00Z",
|
||||||
|
"metadata": {"source": "onboarding"},
|
||||||
|
"categories": ["preferences"],
|
||||||
|
}
|
||||||
|
|
||||||
|
backend.list_memories.return_value = [
|
||||||
|
{
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"memory": "User prefers dark mode",
|
||||||
|
"created_at": "2026-02-15T10:30:00Z",
|
||||||
|
"categories": ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ghi-789-jkl-012",
|
||||||
|
"memory": "User uses vim keybindings",
|
||||||
|
"created_at": "2026-03-01T14:00:00Z",
|
||||||
|
"categories": ["tools"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
backend.update.return_value = {"id": "abc-123-def-456", "memory": "Updated memory"}
|
||||||
|
backend.delete.return_value = {"status": "deleted"}
|
||||||
|
backend.status.return_value = {
|
||||||
|
"connected": True,
|
||||||
|
"backend": "platform",
|
||||||
|
"base_url": "https://api.mem0.ai",
|
||||||
|
}
|
||||||
|
backend.delete_entities.return_value = {"message": "Entity deleted"}
|
||||||
|
backend.entities.return_value = [
|
||||||
|
{"name": "alice", "count": 5},
|
||||||
|
{"name": "bob", "count": 3},
|
||||||
|
]
|
||||||
|
backend.list_events.return_value = [
|
||||||
|
{
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "evt-def-456-ghi-789",
|
||||||
|
"event_type": "SEARCH",
|
||||||
|
"status": "PENDING",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": None,
|
||||||
|
"created_at": "2026-04-01T10:01:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:01:00Z",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
backend.get_event.return_value = {
|
||||||
|
"id": "evt-abc-123-def-456",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": "SUCCEEDED",
|
||||||
|
"latency": 1234.5,
|
||||||
|
"created_at": "2026-04-01T10:00:00Z",
|
||||||
|
"updated_at": "2026-04-01T10:00:01Z",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"id": "mem-abc-123",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"data": {"memory": "User prefers dark mode"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return backend
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_config():
|
||||||
|
"""Return a sample config object."""
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "m0-test-key-12345678"
|
||||||
|
config.platform.base_url = "https://api.mem0.ai"
|
||||||
|
return config
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Tests for branding and output helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from mem0_cli.branding import print_banner, print_error, print_info, print_success, print_warning
|
||||||
|
|
||||||
|
|
||||||
|
def _make_console() -> tuple[Console, StringIO]:
|
||||||
|
buf = StringIO()
|
||||||
|
return Console(file=buf, force_terminal=False, no_color=True, width=80), buf
|
||||||
|
|
||||||
|
|
||||||
|
class TestBranding:
|
||||||
|
def test_print_banner(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_banner(console)
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Banner contains the mem0 ASCII art and tagline
|
||||||
|
assert "Memory Layer" in output or "mem" in output.lower()
|
||||||
|
|
||||||
|
def test_print_success(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_success(console, "It worked!")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "It worked!" in output
|
||||||
|
|
||||||
|
def test_print_error(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_error(console, "Something failed", hint="Try this fix")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Something failed" in output
|
||||||
|
assert "Try this fix" in output
|
||||||
|
|
||||||
|
def test_print_error_no_hint(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_error(console, "Failed")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Failed" in output
|
||||||
|
|
||||||
|
def test_print_warning(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_warning(console, "Watch out")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Watch out" in output
|
||||||
|
|
||||||
|
def test_print_info(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
print_info(console, "FYI")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "FYI" in output
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
"""Integration tests — invoke CLI as subprocess to test end-to-end.
|
||||||
|
|
||||||
|
These tests launch the CLI as a real subprocess, so they must manage
|
||||||
|
environment isolation themselves (monkeypatch doesn't cross process
|
||||||
|
boundaries).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_ansi(text: str) -> str:
|
||||||
|
"""Remove ANSI escape codes so substring checks work regardless of color mode."""
|
||||||
|
return _ANSI_RE.sub("", text)
|
||||||
|
|
||||||
|
|
||||||
|
def _run(
|
||||||
|
args: list[str],
|
||||||
|
env_override: dict | None = None,
|
||||||
|
home_dir: str | None = None,
|
||||||
|
) -> subprocess.CompletedProcess:
|
||||||
|
"""Run mem0 CLI command and capture output.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: CLI arguments.
|
||||||
|
env_override: Extra env vars to set.
|
||||||
|
home_dir: If provided, set HOME to this path so the subprocess
|
||||||
|
reads config from ``<home_dir>/.mem0/config.json`` instead
|
||||||
|
of the user's real config. This is critical for tests that
|
||||||
|
depend on a clean (no API key) or custom config state.
|
||||||
|
|
||||||
|
Returns a CompletedProcess whose stdout/stderr have ANSI escape codes
|
||||||
|
stripped. GitHub Actions sets FORCE_COLOR=1 which causes Rich/Typer to
|
||||||
|
fragment option names like --user-id into separately-styled ANSI segments,
|
||||||
|
making plain ``in`` checks fail. Stripping here is version-agnostic and
|
||||||
|
ensures all assertions see the same plain text regardless of terminal env.
|
||||||
|
"""
|
||||||
|
env = os.environ.copy()
|
||||||
|
# Strip all MEM0_ env vars so tests start clean
|
||||||
|
for key in list(env.keys()):
|
||||||
|
if key.startswith("MEM0_"):
|
||||||
|
del env[key]
|
||||||
|
env.pop("FORCE_COLOR", None)
|
||||||
|
if home_dir:
|
||||||
|
env["HOME"] = home_dir
|
||||||
|
if env_override:
|
||||||
|
env.update(env_override)
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
return subprocess.CompletedProcess(
|
||||||
|
args=result.args,
|
||||||
|
returncode=result.returncode,
|
||||||
|
stdout=_strip_ansi(result.stdout),
|
||||||
|
stderr=_strip_ansi(result.stderr),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def clean_home(tmp_path):
|
||||||
|
"""Return a temp directory to use as HOME, ensuring no ~/.mem0 exists."""
|
||||||
|
return str(tmp_path)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIIntegration:
|
||||||
|
"""Tests that only inspect help text / version — no config needed."""
|
||||||
|
|
||||||
|
def test_help(self):
|
||||||
|
result = _run(["--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "mem0" in result.stdout
|
||||||
|
assert "add" in result.stdout
|
||||||
|
assert "search" in result.stdout
|
||||||
|
|
||||||
|
def test_add_help(self):
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "user-id" in result.stdout
|
||||||
|
assert "messages" in result.stdout
|
||||||
|
|
||||||
|
def test_add_help_has_scope_panel(self):
|
||||||
|
"""Verify rich_help_panel grouping shows in help output."""
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "Scope" in result.stdout
|
||||||
|
|
||||||
|
def test_search_help(self):
|
||||||
|
result = _run(["search", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "top-k" in result.stdout
|
||||||
|
|
||||||
|
def test_list_help(self):
|
||||||
|
result = _run(["list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "page-size" in result.stdout
|
||||||
|
|
||||||
|
def test_delete_help(self):
|
||||||
|
result = _run(["delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--all" in result.stdout
|
||||||
|
assert "--entity" in result.stdout
|
||||||
|
assert "--project" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
assert "--dry-run" in result.stdout
|
||||||
|
|
||||||
|
def test_entity_list_help(self):
|
||||||
|
result = _run(["entity", "list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "entity-type" in result.stdout.lower() or "entity_type" in result.stdout.lower()
|
||||||
|
|
||||||
|
def test_entity_delete_help(self):
|
||||||
|
result = _run(["entity", "delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--user-id" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
|
||||||
|
def test_import_help(self):
|
||||||
|
result = _run(["import", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
def test_no_args_shows_help(self):
|
||||||
|
"""no_args_is_help=True makes Typer print help and exit with code 2."""
|
||||||
|
result = _run([])
|
||||||
|
# Typer returns exit code 2 for "no command given" — this is standard
|
||||||
|
# Click/Typer behaviour and not an error.
|
||||||
|
assert result.returncode in (0, 2)
|
||||||
|
assert "Usage" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLIIsolated:
|
||||||
|
"""Tests that need a clean HOME to avoid reading the user's real config."""
|
||||||
|
|
||||||
|
def test_add_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 add` must fail with a helpful message."""
|
||||||
|
result = _run(
|
||||||
|
["add", "test", "--user-id", "alice"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "api" in combined.lower() or "Error" in combined
|
||||||
|
|
||||||
|
def test_search_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 search` must fail."""
|
||||||
|
result = _run(
|
||||||
|
["search", "preferences", "--user-id", "alice"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "Error" in combined
|
||||||
|
|
||||||
|
def test_list_no_key_errors(self, clean_home):
|
||||||
|
"""Without an API key, `mem0 list` must fail."""
|
||||||
|
result = _run(["list"], home_dir=clean_home)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "API key" in combined or "Error" in combined
|
||||||
|
|
||||||
|
def test_delete_no_id_no_all_errors(self, clean_home):
|
||||||
|
"""Delete without memory_id, --all, or --entity must fail."""
|
||||||
|
result = _run(
|
||||||
|
["delete", "--api-key", "m0-fake-key"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert (
|
||||||
|
"memory ID" in combined.lower()
|
||||||
|
or "--all" in combined
|
||||||
|
or "--entity" in combined
|
||||||
|
or "Error" in combined
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_show_clean(self, clean_home):
|
||||||
|
"""config show with no config should still work."""
|
||||||
|
result = _run(["config", "show"], home_dir=clean_home)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "backend" in result.stdout.lower() or "platform" in result.stdout.lower()
|
||||||
|
|
||||||
|
def test_config_set_and_get_roundtrip(self, clean_home):
|
||||||
|
"""config set then config get should return the set value."""
|
||||||
|
_run(
|
||||||
|
["config", "set", "defaults.user_id", "integration-test-user"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
result = _run(
|
||||||
|
["config", "get", "defaults.user_id"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "integration-test-user" in result.stdout
|
||||||
|
|
||||||
|
def test_import_nonexistent_file(self, clean_home):
|
||||||
|
"""Importing a nonexistent file should fail gracefully."""
|
||||||
|
result = _run(
|
||||||
|
["import", "/nonexistent/file.json", "--api-key", "m0-fake"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "Failed" in combined or "Error" in combined or "error" in combined
|
||||||
|
|
||||||
|
def test_add_no_content_errors(self, clean_home):
|
||||||
|
"""add with no text/messages/file should fail."""
|
||||||
|
result = _run(
|
||||||
|
["add", "--user-id", "alice", "--api-key", "m0-fake"],
|
||||||
|
home_dir=clean_home,
|
||||||
|
)
|
||||||
|
assert result.returncode != 0
|
||||||
|
combined = result.stderr + result.stdout
|
||||||
|
assert "No content" in combined or "Error" in combined
|
||||||
|
|
||||||
|
|
||||||
|
class TestCLINewFeatures:
|
||||||
|
"""Tests for MCP parity features: --graph, --limit, entities delete."""
|
||||||
|
|
||||||
|
def test_add_help_has_graph(self):
|
||||||
|
result = _run(["add", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
|
||||||
|
def test_search_help_has_graph_and_limit(self):
|
||||||
|
result = _run(["search", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
assert "--limit" in result.stdout
|
||||||
|
|
||||||
|
def test_list_help_has_graph(self):
|
||||||
|
result = _run(["list", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--graph" in result.stdout
|
||||||
|
|
||||||
|
def test_delete_entity_via_delete_flag(self):
|
||||||
|
"""delete --entity should appear in help output."""
|
||||||
|
result = _run(["delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--entity" in result.stdout
|
||||||
|
|
||||||
|
def test_entity_delete_has_scope_options(self):
|
||||||
|
"""entity delete should expose scope options."""
|
||||||
|
result = _run(["entity", "delete", "--help"])
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--user-id" in result.stdout
|
||||||
|
assert "--force" in result.stdout
|
||||||
|
assert "--app-id" in result.stdout
|
||||||
|
assert "--run-id" in result.stdout
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,240 @@
|
|||||||
|
"""Tests for configuration management."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from mem0_cli.config import (
|
||||||
|
Mem0Config,
|
||||||
|
get_nested_value,
|
||||||
|
load_config,
|
||||||
|
redact_key,
|
||||||
|
save_config,
|
||||||
|
set_nested_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedactKey:
|
||||||
|
def test_empty_key(self):
|
||||||
|
assert redact_key("") == "(not set)"
|
||||||
|
|
||||||
|
def test_short_key(self):
|
||||||
|
assert redact_key("abc") == "ab***"
|
||||||
|
|
||||||
|
def test_normal_key(self):
|
||||||
|
result = redact_key("m0-abcdefgh12345678")
|
||||||
|
assert result == "m0-a...5678"
|
||||||
|
assert "abcdefgh" not in result
|
||||||
|
|
||||||
|
def test_exact_8_chars(self):
|
||||||
|
# 8 chars is <= 8, so it gets the short redaction
|
||||||
|
assert redact_key("12345678") == "12***"
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig:
|
||||||
|
def test_default_config(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert config.platform.base_url == "https://api.mem0.ai"
|
||||||
|
assert config.platform.api_key == ""
|
||||||
|
|
||||||
|
def test_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "m0-test-key"
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "m0-test-key"
|
||||||
|
|
||||||
|
def test_env_var_override(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "file-key"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
monkeypatch.setenv("MEM0_API_KEY", "env-key")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "env-key"
|
||||||
|
|
||||||
|
def test_load_nonexistent_config(self, isolate_config):
|
||||||
|
config = load_config()
|
||||||
|
assert config.platform.api_key == ""
|
||||||
|
|
||||||
|
def test_config_file_permissions(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "secret"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE
|
||||||
|
|
||||||
|
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||||
|
assert mode == 0o600
|
||||||
|
|
||||||
|
def test_defaults_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "alice"
|
||||||
|
config.defaults.agent_id = "support-bot"
|
||||||
|
config.defaults.app_id = "my-app"
|
||||||
|
config.defaults.run_id = "run-001"
|
||||||
|
|
||||||
|
save_config(config)
|
||||||
|
loaded = load_config()
|
||||||
|
|
||||||
|
assert loaded.defaults.user_id == "alice"
|
||||||
|
assert loaded.defaults.agent_id == "support-bot"
|
||||||
|
assert loaded.defaults.app_id == "my-app"
|
||||||
|
assert loaded.defaults.run_id == "run-001"
|
||||||
|
|
||||||
|
def test_defaults_env_var_override(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "file-user"
|
||||||
|
save_config(config)
|
||||||
|
|
||||||
|
monkeypatch.setenv("MEM0_USER_ID", "env-user")
|
||||||
|
monkeypatch.setenv("MEM0_AGENT_ID", "env-agent")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.user_id == "env-user"
|
||||||
|
assert loaded.defaults.agent_id == "env-agent"
|
||||||
|
|
||||||
|
def test_backward_compat_no_defaults_key(self, isolate_config):
|
||||||
|
"""Old config files without 'defaults' key should load fine."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
|
||||||
|
|
||||||
|
ensure_config_dir()
|
||||||
|
# Write a config without the "defaults" key
|
||||||
|
data = {
|
||||||
|
"version": 1,
|
||||||
|
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
|
||||||
|
}
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.platform.api_key == "m0-test"
|
||||||
|
assert loaded.defaults.user_id == ""
|
||||||
|
assert loaded.defaults.agent_id == ""
|
||||||
|
|
||||||
|
def test_default_config_has_empty_defaults(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert config.defaults.user_id == ""
|
||||||
|
assert config.defaults.agent_id == ""
|
||||||
|
assert config.defaults.app_id == ""
|
||||||
|
assert config.defaults.run_id == ""
|
||||||
|
assert config.defaults.enable_graph is False
|
||||||
|
|
||||||
|
def test_enable_graph_save_and_load(self, isolate_config):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.enable_graph = True
|
||||||
|
save_config(config)
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is True
|
||||||
|
|
||||||
|
def test_enable_graph_env_var_true(self, isolate_config, monkeypatch):
|
||||||
|
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "true")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is True
|
||||||
|
|
||||||
|
def test_enable_graph_env_var_false(self, isolate_config, monkeypatch):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.enable_graph = True
|
||||||
|
save_config(config)
|
||||||
|
monkeypatch.setenv("MEM0_ENABLE_GRAPH", "false")
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is False
|
||||||
|
|
||||||
|
def test_backward_compat_no_enable_graph_key(self, isolate_config):
|
||||||
|
"""Old config files without 'enable_graph' key should default to False."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
from mem0_cli.config import CONFIG_FILE, ensure_config_dir
|
||||||
|
|
||||||
|
ensure_config_dir()
|
||||||
|
data = {
|
||||||
|
"version": 1,
|
||||||
|
"defaults": {"user_id": "alice"},
|
||||||
|
"platform": {"api_key": "m0-test", "base_url": "https://api.mem0.ai"},
|
||||||
|
}
|
||||||
|
with open(CONFIG_FILE, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
loaded = load_config()
|
||||||
|
assert loaded.defaults.enable_graph is False
|
||||||
|
assert loaded.defaults.user_id == "alice"
|
||||||
|
|
||||||
|
|
||||||
|
class TestNestedAccess:
|
||||||
|
def test_get_nested_value(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.platform.api_key = "test-key"
|
||||||
|
assert get_nested_value(config, "platform.api_key") == "test-key"
|
||||||
|
|
||||||
|
def test_get_nonexistent_key(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert get_nested_value(config, "nonexistent.key") is None
|
||||||
|
|
||||||
|
def test_set_nested_value(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "platform.api_key", "new-key")
|
||||||
|
assert config.platform.api_key == "new-key"
|
||||||
|
|
||||||
|
def test_set_nonexistent_key(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "nonexistent.key", "val") is False
|
||||||
|
|
||||||
|
def test_get_defaults_user_id(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "alice"
|
||||||
|
assert get_nested_value(config, "defaults.user_id") == "alice"
|
||||||
|
|
||||||
|
def test_set_defaults_user_id(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "defaults.user_id", "bob")
|
||||||
|
assert config.defaults.user_id == "bob"
|
||||||
|
|
||||||
|
def test_set_defaults_enable_graph(self):
|
||||||
|
config = Mem0Config()
|
||||||
|
assert set_nested_value(config, "defaults.enable_graph", "true")
|
||||||
|
assert config.defaults.enable_graph is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestResolveIds:
|
||||||
|
def test_cli_flag_overrides_default(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "default-user"
|
||||||
|
ids = _resolve_ids(
|
||||||
|
config,
|
||||||
|
user_id="cli-user",
|
||||||
|
agent_id=None,
|
||||||
|
)
|
||||||
|
assert ids["user_id"] == "cli-user"
|
||||||
|
|
||||||
|
def test_default_used_when_flag_is_none(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = "default-user"
|
||||||
|
config.defaults.agent_id = "default-agent"
|
||||||
|
ids = _resolve_ids(config, user_id=None, agent_id=None)
|
||||||
|
assert ids["user_id"] == "default-user"
|
||||||
|
assert ids["agent_id"] == "default-agent"
|
||||||
|
|
||||||
|
def test_none_when_neither_set(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
ids = _resolve_ids(config, user_id=None, agent_id=None)
|
||||||
|
assert ids["user_id"] is None
|
||||||
|
assert ids["agent_id"] is None
|
||||||
|
assert ids["app_id"] is None
|
||||||
|
assert ids["run_id"] is None
|
||||||
|
|
||||||
|
def test_empty_string_treated_as_unset(self):
|
||||||
|
from mem0_cli.app import _resolve_ids
|
||||||
|
|
||||||
|
config = Mem0Config()
|
||||||
|
config.defaults.user_id = ""
|
||||||
|
ids = _resolve_ids(config, user_id=None)
|
||||||
|
assert ids["user_id"] is None
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
"""Tests for output formatting."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
from mem0_cli.output import (
|
||||||
|
format_add_result,
|
||||||
|
format_memories_table,
|
||||||
|
format_memories_text,
|
||||||
|
format_single_memory,
|
||||||
|
sanitize_agent_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_console() -> tuple[Console, StringIO]:
|
||||||
|
buf = StringIO()
|
||||||
|
return Console(file=buf, force_terminal=False, no_color=True, width=120, highlight=False), buf
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_MEMORIES = [
|
||||||
|
{
|
||||||
|
"id": "abc-123-def-456",
|
||||||
|
"memory": "User prefers dark mode",
|
||||||
|
"score": 0.92,
|
||||||
|
"created_at": "2026-02-15T10:30:00Z",
|
||||||
|
"categories": ["preferences"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ghi-789-jkl-012",
|
||||||
|
"memory": "User uses vim keybindings",
|
||||||
|
"score": 0.78,
|
||||||
|
"created_at": "2026-03-01T14:00:00Z",
|
||||||
|
"categories": ["tools"],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestTextFormat:
|
||||||
|
def test_format_memories_text(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_memories_text(console, SAMPLE_MEMORIES)
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Found 2 memories" in output
|
||||||
|
assert "dark mode" in output
|
||||||
|
assert "vim keybindings" in output
|
||||||
|
assert "0.92" in output
|
||||||
|
|
||||||
|
def test_format_memories_text_empty(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_memories_text(console, [])
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Found 0" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableFormat:
|
||||||
|
def test_format_memories_table(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_memories_table(console, SAMPLE_MEMORIES)
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "dark mode" in output
|
||||||
|
assert "abc-123-" in output
|
||||||
|
|
||||||
|
def test_format_memories_table_empty(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_memories_table(console, [])
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Should still render (empty table)
|
||||||
|
assert "ID" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestSingleMemory:
|
||||||
|
def test_format_single_memory_text(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
mem = SAMPLE_MEMORIES[0]
|
||||||
|
format_single_memory(console, mem, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "dark mode" in output
|
||||||
|
assert "abc-123-def-456" in output
|
||||||
|
|
||||||
|
def test_format_single_memory_json(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
mem = SAMPLE_MEMORIES[0]
|
||||||
|
format_single_memory(console, mem, "json")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert '"memory"' in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestAddResult:
|
||||||
|
def test_format_add_result_text(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"id": "abc-123-def-456", "memory": "User prefers dark mode", "event": "ADD"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "dark mode" in output
|
||||||
|
assert "Added" in output
|
||||||
|
|
||||||
|
def test_format_add_result_update_event(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"id": "abc-123", "memory": "Updated pref", "event": "UPDATE"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "Updated" in output
|
||||||
|
|
||||||
|
def test_format_add_result_noop(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"id": "abc-123", "memory": "Same thing", "event": "NOOP"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "No change" in output
|
||||||
|
|
||||||
|
def test_format_add_result_quiet(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {"results": [{"id": "abc-123", "memory": "Quiet", "event": "ADD"}]}
|
||||||
|
format_add_result(console, result, "quiet")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert output.strip() == ""
|
||||||
|
|
||||||
|
def test_format_add_result_deduplicates_pending_by_event_id(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
result = {
|
||||||
|
"results": [
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
{"status": "PENDING", "event_id": "evt-dup"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
format_add_result(console, result, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
# Should show only one PENDING block despite two entries with same event_id
|
||||||
|
assert output.count("evt-dup") == 2 # event_id line + status hint line
|
||||||
|
assert output.count("Queued") == 1
|
||||||
|
|
||||||
|
def test_format_add_result_empty(self):
|
||||||
|
console, buf = _make_console()
|
||||||
|
format_add_result(console, {"results": []}, "text")
|
||||||
|
output = buf.getvalue()
|
||||||
|
assert "No memories extracted" in output
|
||||||
|
|
||||||
|
|
||||||
|
class TestSanitizeAgentData:
|
||||||
|
def test_add_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"event": "ADD",
|
||||||
|
"metadata": {"x": 1},
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"id": "abc", "memory": "test", "event": "ADD"}]
|
||||||
|
|
||||||
|
def test_add_pending_passthrough(self):
|
||||||
|
raw = [{"status": "PENDING", "event_id": "evt-123", "metadata": "noise"}]
|
||||||
|
result = sanitize_agent_data("add", raw)
|
||||||
|
assert result == [{"status": "PENDING", "event_id": "evt-123"}]
|
||||||
|
|
||||||
|
def test_search_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
"agent_id": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("search", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"score": 0.9,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"categories": ["a"],
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("list", raw)
|
||||||
|
assert result == [
|
||||||
|
{"id": "abc", "memory": "test", "created_at": "2026-01-01", "categories": ["a"]}
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_get_projects_fields(self):
|
||||||
|
raw = {
|
||||||
|
"id": "abc",
|
||||||
|
"memory": "test",
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"categories": ["a"],
|
||||||
|
"metadata": {"k": "v"},
|
||||||
|
"user_id": "u1",
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("get", raw)
|
||||||
|
assert "user_id" not in result
|
||||||
|
assert "id" in result and "memory" in result
|
||||||
|
|
||||||
|
def test_update_projects_fields(self):
|
||||||
|
raw = {"id": "abc", "memory": "updated", "extra": "noise"}
|
||||||
|
result = sanitize_agent_data("update", raw)
|
||||||
|
assert result == {"id": "abc", "memory": "updated"}
|
||||||
|
|
||||||
|
def test_event_list_projects_fields(self):
|
||||||
|
raw = [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"graph_status": None,
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = sanitize_agent_data("event list", raw)
|
||||||
|
assert result == [
|
||||||
|
{
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
assert "updated_at" not in result[0]
|
||||||
|
assert "graph_status" not in result[0]
|
||||||
|
|
||||||
|
def test_event_status_flattens_results(self):
|
||||||
|
raw = {
|
||||||
|
"id": "evt-1",
|
||||||
|
"event_type": "ADD",
|
||||||
|
"status": "SUCCEEDED",
|
||||||
|
"latency": 100.0,
|
||||||
|
"created_at": "2026-01-01",
|
||||||
|
"updated_at": "2026-01-02",
|
||||||
|
"results": [
|
||||||
|
{"id": "mem-1", "event": "ADD", "user_id": "alice", "data": {"memory": "dark mode"}}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = sanitize_agent_data("event status", raw)
|
||||||
|
assert result["results"][0] == {
|
||||||
|
"id": "mem-1",
|
||||||
|
"event": "ADD",
|
||||||
|
"user_id": "alice",
|
||||||
|
"memory": "dark mode",
|
||||||
|
}
|
||||||
|
assert "data" not in result["results"][0]
|
||||||
|
|
||||||
|
def test_passthrough_commands(self):
|
||||||
|
for cmd in ("status", "import", "config show", "config get", "config set"):
|
||||||
|
data = {"key": "value", "other": "stuff"}
|
||||||
|
assert sanitize_agent_data(cmd, data) == data
|
||||||
|
|
||||||
|
def test_none_data(self):
|
||||||
|
assert sanitize_agent_data("add", None) is None
|
||||||
@@ -56,7 +56,7 @@ class Mem0Teachability(AgentCapability):
|
|||||||
|
|
||||||
def process_last_received_message(self, text: Union[Dict, str]):
|
def process_last_received_message(self, text: Union[Dict, str]):
|
||||||
expanded_text = text
|
expanded_text = text
|
||||||
if self.memory.get_all(agent_id=self.agent_id):
|
if self.memory.get_all(filters={"agent_id": self.agent_id}):
|
||||||
expanded_text = self._consider_memo_retrieval(text)
|
expanded_text = self._consider_memo_retrieval(text)
|
||||||
self._consider_memo_storage(text)
|
self._consider_memo_storage(text)
|
||||||
return expanded_text
|
return expanded_text
|
||||||
@@ -139,7 +139,7 @@ class Mem0Teachability(AgentCapability):
|
|||||||
return comment + self._concatenate_memo_texts(memo_list)
|
return comment + self._concatenate_memo_texts(memo_list)
|
||||||
|
|
||||||
def _retrieve_relevant_memos(self, input_text: str) -> list:
|
def _retrieve_relevant_memos(self, input_text: str) -> list:
|
||||||
search_results = self.memory.search(input_text, agent_id=self.agent_id, limit=self.max_num_retrievals)
|
search_results = self.memory.search(input_text, filters={"agent_id": self.agent_id}, top_k=self.max_num_retrievals)
|
||||||
memo_list = [result["memory"] for result in search_results if result["score"] <= self.recall_threshold]
|
memo_list = [result["memory"] for result in search_results if result["score"] <= self.recall_threshold]
|
||||||
|
|
||||||
if self.verbosity >= 1 and not memo_list:
|
if self.verbosity >= 1 and not memo_list:
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<Note type="info">
|
|
||||||
<strong>🎉 Mem0 1.0.0 is here!</strong> Enhanced filtering, reranking, and smarter memory management.
|
|
||||||
</Note>
|
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -10,5 +10,4 @@ List recent events for your organization and project.
|
|||||||
|
|
||||||
- **Dashboards**: Summarize adds/searches over time by paging through events.
|
- **Dashboards**: Summarize adds/searches over time by paging through events.
|
||||||
- **Alerting**: Poll for `FAILED` events and trigger follow-up workflows.
|
- **Alerting**: Poll for `FAILED` events and trigger follow-up workflows.
|
||||||
- **Audit**: Store the returned payload/metadata for compliance logs.
|
- **Audit**: Store the returned payload/metadata for compliance logs.
|
||||||
|
|
||||||
@@ -47,11 +47,13 @@ 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.
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Need more details? See [all request parameters](#body-messages) below for complete field descriptions, types, and constraints.
|
||||||
|
</Tip>
|
||||||
|
|
||||||
## Response
|
## Response
|
||||||
|
|
||||||
Successful requests return an array of events queued for processing. Each event includes the generated memory text and an identifier you can persist for auditing.
|
Successful requests return an array of events queued for processing. Each event includes the generated memory text and an identifier you can persist for auditing.
|
||||||
@@ -79,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.
|
|
||||||
|
|||||||
@@ -53,48 +53,3 @@ memories = client.get_all(
|
|||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
## Graph Memory
|
|
||||||
|
|
||||||
To retrieve graph memory relationships between entities, pass `output_format="v1.1"` in your request. This will return memories with entity and relationship information from the knowledge graph.
|
|
||||||
|
|
||||||
<CodeGroup>
|
|
||||||
```python Code
|
|
||||||
memories = client.get_all(
|
|
||||||
filters={
|
|
||||||
"user_id": "alex"
|
|
||||||
},
|
|
||||||
output_format="v1.1"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
```python Output
|
|
||||||
{
|
|
||||||
"results": [
|
|
||||||
{
|
|
||||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
|
||||||
"memory": "Alex is planning a trip to San Francisco",
|
|
||||||
"entities": [
|
|
||||||
{
|
|
||||||
"id": "entity-1",
|
|
||||||
"name": "Alex",
|
|
||||||
"type": "person"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "entity-2",
|
|
||||||
"name": "San Francisco",
|
|
||||||
"type": "location"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"relations": [
|
|
||||||
{
|
|
||||||
"source": "entity-1",
|
|
||||||
"target": "entity-2",
|
|
||||||
"relationship": "traveling_to"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</CodeGroup>
|
|
||||||
|
|||||||
@@ -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,102 @@
|
|||||||
|
---
|
||||||
|
title: "Highlights"
|
||||||
|
description: "Major product launches, headline features, and milestones for Mem0."
|
||||||
|
mode: "wide"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label="2026-04-14" description="Mem0 SDK v2.0.0 / v3.0.0">
|
||||||
|
|
||||||
|
**New Memory Algorithm — State-of-the-Art Accuracy at ~3-4x Lower Cost**
|
||||||
|
|
||||||
|
Ground-up rewrite of the memory pipeline with 20+ point benchmark improvements:
|
||||||
|
|
||||||
|
- **LoCoMo:** 71.4 → **91.6** (+20) — multi-turn conversation recall
|
||||||
|
- **LongMemEval:** 67.8 → **93.4** (+26) — long-term memory across sessions
|
||||||
|
- **BEAM (1M tokens):** **64.1** — production-scale memory evaluation
|
||||||
|
- **Agent memories are first-class** — Previous algorithm: 46% on assistant recall. New: **100%**
|
||||||
|
- **Temporal reasoning works** — "Where did I live before SF?" Previous: 51%. New: **93%**
|
||||||
|
- **~3-4x fewer tokens** — Under 7K tokens per retrieval vs 25K+ for full-context approaches
|
||||||
|
- **ADD-only extraction** — Memories accumulate; nothing is overwritten or deleted
|
||||||
|
- **Hybrid retrieval** — Semantic + BM25 keyword + entity boost, scored in parallel
|
||||||
|
- **Entity linking** — Entities extracted, embedded, and linked across memories
|
||||||
|
|
||||||
|
Breaking changes: Graph memory removed from OSS, `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<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-06" 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,297 @@
|
|||||||
|
---
|
||||||
|
title: "Platform"
|
||||||
|
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
|
||||||
|
mode: "wide"
|
||||||
|
---
|
||||||
|
|
||||||
|
<Update label="2026-04-16" description="">
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **UI:** Removed Graph Memory tab, page, and all references from dashboard, sidebar, project settings, playground, and billing
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<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,27 +1,158 @@
|
|||||||
---
|
---
|
||||||
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-14" description="v2.0.0">
|
||||||
|
|
||||||
|
**Major Release** — Python SDK with V3 memory pipeline, ADD-only extraction, and cleaned-up API surface.
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **Single-Pass Extraction:** Replaced 2-LLM-call pipeline with additive extraction using `ADDITIVE_EXTRACTION_PROMPT`. Memories accumulate via `linked_memory_ids` — no more UPDATE/DELETE events ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Hybrid Search:** Combined semantic + BM25 keyword matching + entity boost with additive scoring. Native `keyword_search()` added to 15 vector store adapters (Qdrant, Elasticsearch, OpenSearch, Azure AI Search, Weaviate, Redis, PGVector, Pinecone, Databricks, MongoDB, Milvus, Baidu, Upstash, Azure MySQL, Vertex AI) ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Entity Extraction & Linking:** spaCy-based entity extraction with second vector collection (`{collection}_entities`) for cross-memory relationship retrieval. Optional dependency: `pip install mem0ai[nlp]` ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Batch Operations:** Batch embedding, batch persist, and batch entity linking (8-phase pipeline) for both sync `Memory` and async `AsyncMemory` at full parity ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Message Persistence:** SQLite-based rolling window (10 messages per session scope) for LLM context ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Valkey Cluster Mode:** Added `cluster_mode` parameter for Valkey Cluster Mode Enabled (CME) deployments ([#4759](https://github.com/mem0ai/mem0/pull/4759))
|
||||||
|
- **V3 API Endpoints:** `MemoryClient.add()` now posts to `/v3/memories/add/`; `MemoryClient.get_all()` posts to `/v3/memories/` and returns a paginated envelope `{"count": int, "next": str | None, "previous": str | None, "results": [...]}` ([#4856](https://github.com/mem0ai/mem0/pull/4856))
|
||||||
|
- **Default model:** `gpt-5-mini` is now the default across `OpenAILLM`, `OpenAIStructuredLLM`, `AzureOpenAILLM`, `AzureOpenAIStructuredLLM`, and `LiteLLM` fallback ([#4829](https://github.com/mem0ai/mem0/pull/4829))
|
||||||
|
|
||||||
|
**Breaking Changes:**
|
||||||
|
- **`add()` returns ADD-only events** — No more `"UPDATE"` or `"DELETE"` events. Memories accumulate; nothing is overwritten ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **`search()` default `threshold` is now `0.1`** — Pass `threshold=0.0` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **`search()` `score` is now a combined multi-signal score** — The top-level `score` fuses semantic similarity, BM25 keyword match, and entity boost into one value. Absolute numbers shift versus the old raw cosine score; retune any hard thresholds against representative queries. Per-signal scores are not exposed on the response ([#4805](https://github.com/mem0ai/mem0/pull/4805), [#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||||
|
- **`search()` default `rerank` is now `False`** — Pass `rerank=True` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **`top_k` default changed 100 → 20** in `Memory.get_all()` and `Memory.search()` (sync + async). Pass `top_k=100` explicitly to restore the old behavior ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **Entity ID validation:** `user_id` / `agent_id` / `run_id` are trimmed; empty-string and whitespace-only values now raise `ValueError` ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **Search params validation:** `threshold` must be a number in `[0, 1]`; `top_k` must be a non-negative integer — invalid inputs raise `ValueError` ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **`messages` in `Memory.add()` rejects invalid types:** Passing `None` or non-`(str | dict | list)` values raises `Mem0ValidationError` (`error_code="VALIDATION_003"`) ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **`qdrant-client>=1.12.0` required** — Upgrade from `>=1.9.1` ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **`org_id` and `project_id` removed** — Removed from `MemoryClient` constructor and all method signatures ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **Graph Memory Removed (OSS):** `mem0/memory/graph_memory.py`, `memgraph_memory.py`, `kuzu_memory.py`, `apache_age_memory.py`, and `mem0/graphs/` (Neo4j / Memgraph / Kuzu / Apache AGE / Neptune drivers) deleted — ~4,000 lines. Graph memory is no longer supported in the OSS SDK; graph drivers (neo4j, memgraph, kuzu, etc.) can be uninstalled. Use the Platform API for graph features. Remove `enable_graph` and `graph_store` from your config ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **`enable_graph` removed from Client SDK** — Graph memory is now a project-level setting on the Platform. Remove `enable_graph` from `MemoryClient.add()` / `search()` / `get_all()` / `update_project()` calls ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
||||||
|
- **`custom_fact_extraction_prompt` renamed to `custom_instructions`** — Update config and memory module references ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **Typed option classes** — Added Pydantic v2 typed classes: `AddMemoryOptions`, `SearchMemoryOptions`, `GetAllMemoryOptions`, `DeleteAllMemoryOptions`, `UpdateMemoryOptions`, `ProjectUpdateOptions` ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- **FAISS:** Prevent arbitrary code execution via pickle deserialization in `FAISS` vector store ([#4833](https://github.com/mem0ai/mem0/pull/4833))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **V3 migration crashes:** Fixed crashes in the v3 migration path; entity linking on OSS is now functional across Qdrant and Milvus backends ([#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||||
|
- **Qdrant entity store:** Entity store now shares the existing Qdrant client when using embedded mode (`path=...`), eliminating RocksDB lock contention between the main and entity collections ([#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||||
|
- **Reranker:** Fixed incorrect use of SentenceTransformer for cross-encoder reranker models — switched to CrossEncoder API for proper scoring ([#4806](https://github.com/mem0ai/mem0/pull/4806))
|
||||||
|
- **S3 Vectors:** Handle `vector=None` in `update()` to prevent boto3 validation error when `event=NONE` ([#4594](https://github.com/mem0ai/mem0/pull/4594))
|
||||||
|
- **LLMs:** Made OpenAI `store` parameter opt-in to prevent leaking to non-OpenAI backends like Google Gemini ([#4757](https://github.com/mem0ai/mem0/pull/4757))
|
||||||
|
- **LLMs:** Forward `response_format` to Azure OpenAI API to prevent JSON parsing failures ([#4689](https://github.com/mem0ai/mem0/pull/4689))
|
||||||
|
- **Core:** Guard `temp_uuid_mapping` lookups against LLM-hallucinated IDs with safe `.get()` and warnings ([#4674](https://github.com/mem0ai/mem0/pull/4674))
|
||||||
|
- **Client:** Prevent `MemoryClient.feedback()` telemetry TypeError by merging feedback data into single payload ([#4795](https://github.com/mem0ai/mem0/pull/4795))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
||||||
|
|
||||||
|
See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-v2) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-06" 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">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **LLMs:** Added `reasoning_effort` parameter support for reasoning models ([#4461](https://github.com/mem0ai/mem0/pull/4461))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Core:** Preserved original `actor_id` during memory update ([#4570](https://github.com/mem0ai/mem0/pull/4570))
|
||||||
|
- **Core:** Set `updated_at` on creation and preserve pre-existing `created_at` ([#4499](https://github.com/mem0ai/mem0/pull/4499))
|
||||||
|
- **Core:** Centralized entity cleanup and skip malformed LLM relation dicts ([#4515](https://github.com/mem0ai/mem0/pull/4515))
|
||||||
|
- **Core:** Removed `README.md` from wheel shared-data ([#4052](https://github.com/mem0ai/mem0/pull/4052))
|
||||||
|
- **Vector Stores:** Handled `vector=None` in Milvus and Qdrant update methods ([#4568](https://github.com/mem0ai/mem0/pull/4568))
|
||||||
|
- **Vector Stores:** Rebuilt FAISS index on vector deletion ([#4178](https://github.com/mem0ai/mem0/pull/4178))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Embeddings:** Updated default Gemini and Vertex AI embedder model to `gemini-embedding-001` ([#4571](https://github.com/mem0ai/mem0/pull/4571))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-26" description="v1.0.8">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **Vector Stores:** Integrated Turbopuffer as a vector database provider ([#4428](https://github.com/mem0ai/mem0/pull/4428))
|
||||||
|
- **LLMs:** Added MiniMax LLM provider ([#4431](https://github.com/mem0ai/mem0/pull/4431))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **Core:** Fixed merging of multiple filter operators for the same key ([#4559](https://github.com/mem0ai/mem0/pull/4559))
|
||||||
|
- **Core:** Prevented in-place mutation of metadata in `_create_memory` ([#4529](https://github.com/mem0ai/mem0/pull/4529))
|
||||||
|
- **Core:** Preserved custom metadata when updating memory ([#4495](https://github.com/mem0ai/mem0/pull/4495))
|
||||||
|
- **Core:** Handled chatty LLM responses in JSON parsing ([#4525](https://github.com/mem0ai/mem0/pull/4525))
|
||||||
|
- **Core:** Prevented double embedding in `mem0.add` ([#3996](https://github.com/mem0ai/mem0/pull/3996))
|
||||||
|
- **Core:** Raised `ValueError` when deleting nonexistent memory ([#4455](https://github.com/mem0ai/mem0/pull/4455))
|
||||||
|
- **Core:** Cleaned up graph store data on `Memory.delete()` ([#4505](https://github.com/mem0ai/mem0/pull/4505))
|
||||||
|
- **Vector Stores:** Prevented SQL injection in Databricks vector store ([#4558](https://github.com/mem0ai/mem0/pull/4558))
|
||||||
|
- **Vector Stores:** Upgraded MongoDB vector store from deprecated `knnVector` to GA `vectorSearch` ([#3995](https://github.com/mem0ai/mem0/pull/3995))
|
||||||
|
- **Vector Stores:** Prevented embedding corruption in Valkey and Redis when vector is `None` ([#4362](https://github.com/mem0ai/mem0/pull/4362))
|
||||||
|
- **Vector Stores:** Accepted default `/tmp/chroma` path in `ChromaDbConfig` validator ([#4179](https://github.com/mem0ai/mem0/pull/4179))
|
||||||
|
- **Vector Stores:** Wrapped vector and payload in lists for `Langchain.update` ([#4446](https://github.com/mem0ai/mem0/pull/4446))
|
||||||
|
- **Graph:** Soft-delete graph relationships instead of hard `DELETE` ([#4188](https://github.com/mem0ai/mem0/pull/4188))
|
||||||
|
- **Graph:** Sanitized hyphens in Neo4j Cypher relationship names ([#4154](https://github.com/mem0ai/mem0/pull/4154))
|
||||||
|
- **Graph:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
|
||||||
|
- **Qdrant:** Fixed `do not remove local path on init` ([#4475](https://github.com/mem0ai/mem0/pull/4475))
|
||||||
|
- **Qdrant:** Implemented enhanced metadata filtering operators ([#4127](https://github.com/mem0ai/mem0/pull/4127))
|
||||||
|
- **Embeddings:** Fixed OpenAI embedding dimensions ([#4481](https://github.com/mem0ai/mem0/pull/4481))
|
||||||
|
- **LLMs:** Omitted `topP` for Anthropic Converse in Bedrock; used `AWSBedrockConfig` in `LlmFactory` ([#4469](https://github.com/mem0ai/mem0/pull/4469))
|
||||||
|
- **LLMs:** Avoided sending both `temperature` and `top_p` to Anthropic API ([#4471](https://github.com/mem0ai/mem0/pull/4471))
|
||||||
|
- **LLMs:** Handled `None` content and empty candidates in `GeminiLLM` parsing ([#4462](https://github.com/mem0ai/mem0/pull/4462))
|
||||||
|
- **LLMs:** Added missing `_parse_response` to `AzureOpenAIStructuredLLM` ([#4434](https://github.com/mem0ai/mem0/pull/4434))
|
||||||
|
- **History:** Added timestamps for `DELETE` operations in history ([#4492](https://github.com/mem0ai/mem0/pull/4492))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Vector Stores:** Added vector validation to OpenSearchDB to ensure non-null, non-empty, and correct-dimension vectors ([#4472](https://github.com/mem0ai/mem0/pull/4472))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-03-19" description="v1.0.7">
|
<Update label="2026-03-19" description="v1.0.7">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **Core:** Fixed control characters in LLM JSON responses causing parse failures (#4420)
|
- **Core:** Fixed control characters in LLM JSON responses causing parse failures ([#4420](https://github.com/mem0ai/mem0/pull/4420))
|
||||||
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` (#4404)
|
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` ([#4404](https://github.com/mem0ai/mem0/pull/4404))
|
||||||
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch (#4418)
|
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch ([#4418](https://github.com/mem0ai/mem0/pull/4418))
|
||||||
- **Core:** Normalized malformed LLM fact output before embedding (#4224)
|
- **Core:** Normalized malformed LLM fact output before embedding ([#4224](https://github.com/mem0ai/mem0/pull/4224))
|
||||||
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility (#4058)
|
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility ([#4058](https://github.com/mem0ai/mem0/pull/4058))
|
||||||
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response (#4176)
|
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response ([#4176](https://github.com/mem0ai/mem0/pull/4176))
|
||||||
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers (#4405)
|
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers ([#4405](https://github.com/mem0ai/mem0/pull/4405))
|
||||||
- **Vector Stores:** Cast `vector_distance` to float in Redis search (#4377)
|
- **Vector Stores:** Cast `vector_distance` to float in Redis search ([#4377](https://github.com/mem0ai/mem0/pull/4377))
|
||||||
|
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403)
|
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling ([#4403](https://github.com/mem0ai/mem0/pull/4403))
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
@@ -762,6 +893,94 @@ mode: "wide"
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
|
<Update label="2026-04-14" description="v3.0.0">
|
||||||
|
|
||||||
|
**Major Release** — TypeScript SDK with V3 memory pipeline, camelCase parameters, and cleaned-up API surface.
|
||||||
|
|
||||||
|
**V3 Memory Pipeline (OSS):**
|
||||||
|
- **Single-Pass Extraction:** Additive extraction pipeline aligned with Python SDK — memories accumulate, no UPDATE/DELETE events ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Entity Extraction & Linking:** New `entity_extraction.ts` module (720+ lines) with cross-memory relationship retrieval ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Message Persistence:** SQLite-based message history via new `SQLiteManager.ts` with rolling window for LLM context ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Batch Embeddings:** `embedBatch()` support in OpenAI and Azure embedding providers ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **Scoring & Lemmatization:** New `scoring.ts` and `lemmatization.ts` utilities for hybrid search ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **New Prompts:** `prompts/index.ts` (592+ lines) with additive extraction prompt aligned with Python SDK ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **V3 API Endpoints:** `MemoryClient.add()` now posts to `/v3/memories/add/`; `MemoryClient.getAll()` posts to `/v3/memories/` with paginated envelope `{ count, next, previous, results }` ([#4856](https://github.com/mem0ai/mem0/pull/4856))
|
||||||
|
- **Default model:** `gpt-5-mini` is now the default in `OpenAI`, `OpenAIStructured`, and `Azure` LLM providers ([#4829](https://github.com/mem0ai/mem0/pull/4829))
|
||||||
|
|
||||||
|
**Breaking Changes:**
|
||||||
|
- **Graph Memory Removed (OSS):** `graph_memory.ts` (675 lines), `graphs/tools.ts` (267 lines), `graphs/utils.ts` (116 lines), `graphs/configs.ts` (30 lines) deleted. Graph memory is no longer supported in the OSS SDK — use Platform API for graph features ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
|
- **camelCase Parameters (Client SDK):** All user-facing parameters converted from snake_case to camelCase. Mapping is transparent at API boundary via `camelToSnakeKeys()` / `snakeToCamelKeys()` ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
client.add(messages, { user_id: "alice", top_k: 5 });
|
||||||
|
// After
|
||||||
|
client.add(messages, { userId: "alice", topK: 5 });
|
||||||
|
```
|
||||||
|
- **Per-Method Option Types:** Replaced monolithic `MemoryOptions` with typed interfaces: `AddMemoryOptions`, `SearchMemoryOptions`, `GetAllMemoryOptions`, `DeleteAllMemoryOptions` ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **Removed Deprecated Parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `limit` removed from client method signatures. `ClientOptions` reduced to `{ apiKey, host }` only ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **`limit` renamed to `topK` (OSS):** Update all search calls ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **`topK` default changed 100 → 20** in `Memory.getAll()` and `Memory.search()`. Pass `topK: 100` explicitly to restore the old behavior ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **Entity ID validation:** `userId` / `agentId` / `runId` are trimmed; empty-string and whitespace-only values now throw ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **Search params validation:** `threshold` must be in `[0, 1]`; `topK` must be a non-negative integer — invalid inputs throw ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **`messages` in `Memory.add()` is required:** Passing `undefined` or `null` now throws ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
|
- **`customPrompt` renamed to `customInstructions` (OSS):** Update memory and vector store configurations ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
- **`enableGraph` removed (OSS):** Config option removed — graph memory no longer available in OSS ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
||||||
|
|
||||||
|
**New Features:**
|
||||||
|
- **LLMs:** Added DeepSeek LLM provider with OpenAI-compatible integration using custom baseURL to `api.deepseek.com` ([#4613](https://github.com/mem0ai/mem0/pull/4613))
|
||||||
|
- **Entity store isolation:** `MemoryVectorStore` now uses a dedicated `_entities.db` file, preventing entity/memory store collisions ([#4829](https://github.com/mem0ai/mem0/pull/4829), [#4841](https://github.com/mem0ai/mem0/pull/4841))
|
||||||
|
- **Payload backward compatibility:** Legacy camelCase payload keys normalized to snake_case on read ([#4841](https://github.com/mem0ai/mem0/pull/4841))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **V3 migration:** Fixed crashes in the OSS migration path; entity linking works end-to-end ([#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||||
|
- **PGVector init race:** `PGVector.initialize()` now memoises the in-flight init promise ([#4841](https://github.com/mem0ai/mem0/pull/4841))
|
||||||
|
- **Redis module detection:** Handles both node-redis v4+ and legacy `moduleList` response shapes ([#4841](https://github.com/mem0ai/mem0/pull/4841))
|
||||||
|
- **Config:** Fixed `ConfigManager.mergeConfig()` to only include `graphStore` when explicitly provided by user, preventing default Neo4j connection attempts ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
||||||
|
- **LLMs:** Config manager now falls back to `userConf.url` for `baseURL` — prevents custom LLM providers (Ollama, LMStudio) from silently connecting to OpenAI ([#4761](https://github.com/mem0ai/mem0/pull/4761))
|
||||||
|
|
||||||
|
**Improvements:**
|
||||||
|
- **Telemetry:** Sample OSS hot-path events at 10% to reduce PostHog event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
||||||
|
|
||||||
|
See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to-v3) for upgrade instructions.
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-04-06" 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">
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **OSS:** Fixed Qdrant Cloud "Illegal host" error by defaulting to port 6333 when URL has no explicit port ([#4565](https://github.com/mem0ai/mem0/pull/4565))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
|
<Update label="2026-03-26" description="v2.4.3">
|
||||||
|
|
||||||
|
**New Features & Updates:**
|
||||||
|
- **OSS:** Added pgvector support to NodeJS OSS `VectorStoreFactory` ([#3997](https://github.com/mem0ai/mem0/pull/3997))
|
||||||
|
|
||||||
|
**Bug Fixes:**
|
||||||
|
- **OSS:** Made pgvector `pg` import compatible with ESM ([#4544](https://github.com/mem0ai/mem0/pull/4544))
|
||||||
|
- **OSS:** Registered pgvector in `VectorStoreFactory` ([#4502](https://github.com/mem0ai/mem0/pull/4502))
|
||||||
|
- **OSS:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
|
||||||
|
- **OSS:** Fixed `toCamelCase` in Redis `get` method for the payload ([#3172](https://github.com/mem0ai/mem0/pull/3172))
|
||||||
|
- **Client:** Fixed Zod Schema incompatibility with OpenAI Structured Outputs API ([#3462](https://github.com/mem0ai/mem0/pull/3462))
|
||||||
|
|
||||||
|
</Update>
|
||||||
|
|
||||||
<Update label="2026-03-19" description="v2.4.2">
|
<Update label="2026-03-19" description="v2.4.2">
|
||||||
|
|
||||||
@@ -1041,352 +1260,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>
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ config = {
|
|||||||
"embedder": {
|
"embedder": {
|
||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "models/text-embedding-004",
|
"model": "models/gemini-embedding-001",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ Here are the parameters available for configuring Gemini embedder:
|
|||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
| Parameter | Description | Default Value |
|
| Parameter | Description | Default Value |
|
||||||
| ---------------- | ------------------------------------ | ----------------------- |
|
| ---------------- | ------------------------------------ | ----------------------- |
|
||||||
| `model` | The name of the embedding model to use| `models/text-embedding-004` |
|
| `model` | The name of the embedding model to use| `models/gemini-embedding-001` |
|
||||||
| `embedding_dims` | Dimensions of the embedding model | `1536` |
|
| `embedding_dims` | Dimensions of the embedding model | `1536` |
|
||||||
| `api_key` | The Google API key | `None` |
|
| `api_key` | The Google API key | `None` |
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ config = {
|
|||||||
"embedder": {
|
"embedder": {
|
||||||
"provider": "vertexai",
|
"provider": "vertexai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "text-embedding-004",
|
"model": "gemini-embedding-001",
|
||||||
"memory_add_embedding_type": "RETRIEVAL_DOCUMENT",
|
"memory_add_embedding_type": "RETRIEVAL_DOCUMENT",
|
||||||
"memory_update_embedding_type": "RETRIEVAL_DOCUMENT",
|
"memory_update_embedding_type": "RETRIEVAL_DOCUMENT",
|
||||||
"memory_search_embedding_type": "RETRIEVAL_QUERY"
|
"memory_search_embedding_type": "RETRIEVAL_QUERY"
|
||||||
@@ -51,7 +51,7 @@ Here are the parameters available for configuring the Vertex AI embedder:
|
|||||||
|
|
||||||
| Parameter | Description | Default Value |
|
| Parameter | Description | Default Value |
|
||||||
| ------------------------- | ------------------------------------------------ | -------------------- |
|
| ------------------------- | ------------------------------------------------ | -------------------- |
|
||||||
| `model` | The name of the Vertex AI embedding model to use | `text-embedding-004` |
|
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
|
||||||
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
||||||
| `embedding_dims` | Dimensions of the embedding model | `256` |
|
| `embedding_dims` | Dimensions of the embedding model | `256` |
|
||||||
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ config = {
|
|||||||
"provider": "gemini",
|
"provider": "gemini",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gemini-2.0-flash-001",
|
"model": "gemini-2.0-flash-001",
|
||||||
|
"api_key": "your-gemini-api-key",
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
"top_p": 1.0
|
"top_p": 1.0
|
||||||
@@ -52,6 +53,7 @@ const config = {
|
|||||||
provider: "gemini",
|
provider: "gemini",
|
||||||
config: {
|
config: {
|
||||||
model: "gemini-2.0-flash-001",
|
model: "gemini-2.0-flash-001",
|
||||||
|
apiKey: process.env.GOOGLE_API_KEY || '',
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ os.environ["OPENAI_API_KEY"] = "your-api-key"
|
|||||||
|
|
||||||
# Initialize a LangChain model directly
|
# Initialize a LangChain model directly
|
||||||
openai_model = ChatOpenAI(
|
openai_model = ChatOpenAI(
|
||||||
model="gpt-4.1-nano-2025-04-14",
|
model="gpt-5-mini",
|
||||||
temperature=0.2,
|
temperature=0.2,
|
||||||
max_tokens=2000
|
max_tokens=2000
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "litellm",
|
"provider": "litellm",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4.1-nano-2025-04-14",
|
"model": "gpt-5-mini",
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4.1-nano-2025-04-14",
|
"model": "gpt-5-mini",
|
||||||
"temperature": 0.2,
|
"temperature": 0.2,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "openai_structured",
|
"provider": "openai_structured",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4.1-nano-2025-04-14",
|
"model": "gpt-5-mini",
|
||||||
"temperature": 0.0,
|
"temperature": 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4.1-nano-2025-04-14"
|
"model": "gpt-5-mini"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reranker": {
|
"reranker": {
|
||||||
|
|||||||
@@ -7,58 +7,51 @@ When using LLM rerankers, you can customize the prompts used for ranking to bett
|
|||||||
|
|
||||||
## Default Prompt
|
## Default Prompt
|
||||||
|
|
||||||
The default LLM reranker prompt is designed to be general-purpose:
|
The default LLM reranker prompt scores each memory individually on a 0.0-1.0 scale:
|
||||||
|
|
||||||
```
|
```
|
||||||
Given a query and a list of memory entries, rank the memory entries based on their relevance to the query.
|
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||||
Rate each memory on a scale of 1-10 where 10 is most relevant.
|
|
||||||
|
|
||||||
Query: {query}
|
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||||
|
- 1.0 = Perfectly relevant and directly answers the query
|
||||||
|
- 0.8-0.9 = Highly relevant with good information
|
||||||
|
- 0.6-0.7 = Moderately relevant with some useful information
|
||||||
|
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||||
|
- 0.0-0.3 = Not relevant or no useful information
|
||||||
|
|
||||||
Memory entries:
|
Query: "{query}"
|
||||||
{memories}
|
Document: "{document}"
|
||||||
|
|
||||||
Provide your ranking as a JSON array with scores for each memory.
|
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Prompt Configuration
|
## Custom Prompt Configuration
|
||||||
|
|
||||||
You can provide a custom prompt template when configuring the LLM reranker:
|
You can provide a custom prompt template using the `scoring_prompt` parameter:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
|
||||||
custom_prompt = """
|
custom_prompt = """
|
||||||
You are an expert at ranking memories for a personal AI assistant.
|
You are an expert at evaluating memories for a personal AI assistant.
|
||||||
Given a user query and a list of memory entries, rank each memory based on:
|
Given a user query and a memory entry, score how relevant the memory is.
|
||||||
1. Direct relevance to the query
|
Consider direct relevance, temporal relevance, and actionability.
|
||||||
2. Temporal relevance (recent memories may be more important)
|
|
||||||
3. Emotional significance
|
|
||||||
4. Actionability
|
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
User Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memory entries:
|
Provide only a single numerical score between 0.0 and 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Rate each memory from 1-10 and provide reasoning.
|
|
||||||
Return as JSON: {{"rankings": [{{"index": 0, "score": 8, "reason": "..."}}]}}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"reranker": {
|
"reranker": {
|
||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"llm": {
|
"provider": "openai",
|
||||||
"provider": "openai",
|
"model": "gpt-4o-mini",
|
||||||
"config": {
|
"api_key": "your-openai-key",
|
||||||
"model": "gpt-4.1-nano-2025-04-14",
|
"scoring_prompt": custom_prompt,
|
||||||
"api_key": "your-openai-key"
|
"top_k": 5
|
||||||
}
|
|
||||||
},
|
|
||||||
"custom_prompt": custom_prompt,
|
|
||||||
"top_n": 5
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +63,14 @@ memory = Memory.from_config(config)
|
|||||||
|
|
||||||
Your custom prompt can use the following variables:
|
Your custom prompt can use the following variables:
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
| ---------------- | ------------------------------------- |
|
| ------------ | ----------------------------- |
|
||||||
| `{query}` | The search query |
|
| `{query}` | The search query |
|
||||||
| `{memories}` | The list of memory entries to rank |
|
| `{document}` | The memory entry being scored |
|
||||||
| `{user_id}` | The user ID (if available) |
|
|
||||||
| `{user_context}` | Additional user context (if provided) |
|
<Note>
|
||||||
|
Both `{query}` and `{document}` are required in your custom prompt. The LLM reranker scores each memory individually against the query, so the prompt is called once per candidate memory.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## Domain-Specific Examples
|
## Domain-Specific Examples
|
||||||
|
|
||||||
@@ -89,13 +84,10 @@ Prioritize memories that:
|
|||||||
- Show previous resolution patterns
|
- Show previous resolution patterns
|
||||||
- Indicate customer preferences or constraints
|
- Indicate customer preferences or constraints
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Customer Context: Previous interactions with this customer
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories:
|
Score relevance from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Rank each memory 1-10 based on support relevance.
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,19 +95,16 @@ Rank each memory 1-10 based on support relevance.
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
educational_prompt = """
|
educational_prompt = """
|
||||||
Rank these learning memories for a student query.
|
Score this learning memory for relevance to a student query.
|
||||||
Consider:
|
Consider:
|
||||||
- Prerequisite knowledge requirements
|
- Prerequisite knowledge requirements
|
||||||
- Learning progression and difficulty
|
- Learning progression and difficulty
|
||||||
- Relevance to current learning objectives
|
- Relevance to current learning objectives
|
||||||
|
|
||||||
Student Query: {query}
|
Student Query: "{query}"
|
||||||
Learning Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Available memories:
|
Score educational relevance from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Score each memory for educational value (1-10).
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -123,73 +112,64 @@ Score each memory for educational value (1-10).
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
personal_assistant_prompt = """
|
personal_assistant_prompt = """
|
||||||
Rank personal memories for relevance to the user's query.
|
Score this personal memory for relevance to the user's query.
|
||||||
Consider:
|
Consider:
|
||||||
- Recent vs. historical importance
|
- Recent vs. historical importance
|
||||||
- Personal preferences and habits
|
- Personal preferences and habits
|
||||||
- Contextual relationships between memories
|
- Contextual relationships
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Personal context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories to rank:
|
Provide relevance score from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Provide relevance scores (1-10) with brief explanations.
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
## Advanced Prompt Techniques
|
## Advanced Prompt Techniques
|
||||||
|
|
||||||
### Multi-Criteria Ranking
|
### Multi-Criteria Scoring
|
||||||
|
|
||||||
```python
|
```python
|
||||||
multi_criteria_prompt = """
|
multi_criteria_prompt = """
|
||||||
Evaluate memories using multiple criteria:
|
Evaluate this memory using multiple criteria:
|
||||||
|
|
||||||
1. RELEVANCE (40%): How directly related to the query
|
1. RELEVANCE (40%): How directly related to the query
|
||||||
2. RECENCY (20%): How recent the memory is
|
2. RECENCY (20%): How recent the memory appears to be
|
||||||
3. IMPORTANCE (25%): Personal or business significance
|
3. IMPORTANCE (25%): Personal or business significance
|
||||||
4. ACTIONABILITY (15%): How useful for next steps
|
4. ACTIONABILITY (15%): How useful for next steps
|
||||||
|
|
||||||
Query: {query}
|
Query: "{query}"
|
||||||
Context: {user_context}
|
Memory: "{document}"
|
||||||
|
|
||||||
Memories:
|
Compute a weighted score from 0.0 to 1.0 based on these criteria.
|
||||||
{memories}
|
Provide only the final numerical score.
|
||||||
|
|
||||||
For each memory, provide:
|
|
||||||
- Overall score (1-10)
|
|
||||||
- Breakdown by criteria
|
|
||||||
- Final ranking recommendation
|
|
||||||
|
|
||||||
Format: JSON with detailed scoring
|
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contextual Ranking
|
### Chain-of-Thought Scoring
|
||||||
|
|
||||||
```python
|
```python
|
||||||
contextual_prompt = """
|
reasoning_prompt = """
|
||||||
Consider the following context when ranking memories:
|
Evaluate this memory's relevance step by step:
|
||||||
- Current user situation: {user_context}
|
|
||||||
- Time of day: {current_time}
|
|
||||||
- Recent activities: {recent_activities}
|
|
||||||
|
|
||||||
Query: {query}
|
1. What is the main intent of the query?
|
||||||
|
2. What key information does the memory contain?
|
||||||
|
3. How directly does the memory address the query?
|
||||||
|
|
||||||
Rank these memories considering both direct relevance and contextual appropriateness:
|
Based on this analysis, provide a single relevance score from 0.0 to 1.0.
|
||||||
{memories}
|
|
||||||
|
|
||||||
Provide contextually-aware relevance scores (1-10).
|
Query: "{query}"
|
||||||
|
Memory: "{document}"
|
||||||
|
|
||||||
|
Score:
|
||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
||||||
2. **Use Examples**: Include examples in your prompt for better model understanding
|
2. **Use 0.0-1.0 Scale**: The score extractor expects values between 0.0 and 1.0
|
||||||
3. **Structure Output**: Specify the exact JSON format you want returned
|
3. **Request Only the Score**: Ask for just the numerical score to improve extraction reliability
|
||||||
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
||||||
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
||||||
|
|
||||||
@@ -206,16 +186,16 @@ prompts = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
for i, prompt in enumerate(prompts):
|
for i, prompt in enumerate(prompts):
|
||||||
config["reranker"]["config"]["custom_prompt"] = prompt
|
config["reranker"]["config"]["scoring_prompt"] = prompt
|
||||||
memory = Memory.from_config(config)
|
memory = Memory.from_config(config)
|
||||||
|
|
||||||
results = memory.search("test query", user_id="test_user")
|
results = memory.search("test query", filters={"user_id": "test_user"})
|
||||||
print(f"Prompt {i+1} results: {results}")
|
print(f"Prompt {i+1} results: {results}")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Common Issues
|
## Common Issues
|
||||||
|
|
||||||
- **Too Long**: Keep prompts under token limits for your chosen LLM
|
- **Too Long**: Keep prompts under token limits for your chosen LLM
|
||||||
- **Too Vague**: Be specific about ranking criteria
|
- **Too Vague**: Be specific about scoring criteria
|
||||||
- **Inconsistent Format**: Ensure JSON output format is clearly specified
|
- **Wrong Scale**: Use 0.0-1.0 scale to match the default score extractor
|
||||||
- **Missing Context**: Include relevant variables for your use case
|
- **Extra Output**: Ask for only the numeric score — extra text can confuse score extraction
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4.1-nano-2025-04-14"
|
"model": "gpt-5-mini"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reranker": {
|
"reranker": {
|
||||||
@@ -95,7 +95,7 @@ messages = [
|
|||||||
memory.add(messages, user_id="bob")
|
memory.add(messages, user_id="bob")
|
||||||
|
|
||||||
# Search with reranking
|
# Search with reranking
|
||||||
results = memory.search("What is the user's profession?", user_id="bob")
|
results = memory.search("What is the user's profession?", filters={"user_id": "bob"})
|
||||||
|
|
||||||
for result in results['results']:
|
for result in results['results']:
|
||||||
print(f"Memory: {result['memory']}")
|
print(f"Memory: {result['memory']}")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user