Compare commits
195 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ddf1669f4 | |||
| d3d2e89fd5 | |||
| 43175d85f2 | |||
| 661ecb9f0f | |||
| 09f181c577 | |||
| 3497f26a00 | |||
| e9c0547423 | |||
| 25bc1b7426 | |||
| fee344db85 | |||
| b611f69381 | |||
| c2862831db | |||
| 1678e682ee | |||
| ced4af681f | |||
| 565db27121 | |||
| c0ac9f81fa | |||
| 879c68555c | |||
| c2e723352e | |||
| 716f021df8 | |||
| 7fa996261d | |||
| 87bd2d91e0 | |||
| 15a930dac2 | |||
| fa9abc77a6 | |||
| 4e448269bc | |||
| 29d131f7aa | |||
| 42fe129330 | |||
| bd5996f41e | |||
| 299c423213 | |||
| ce0531a13e | |||
| 513b56159f | |||
| 1676b3168d | |||
| 8a786bf72d | |||
| a48f34cf77 | |||
| 650b734b1b | |||
| 871a1de7d2 | |||
| e615cc66de | |||
| ca86a164bd | |||
| 2ac3f3956a | |||
| 7a9f03af3f | |||
| 48f1d6f010 | |||
| f0ccd99924 | |||
| c5971193a2 | |||
| ff53fd60b7 | |||
| ffa334537a | |||
| bd7ce2c13c | |||
| 5d767219ff | |||
| 6b744845c3 | |||
| 5b4478458b | |||
| 1751e7bff9 | |||
| 7ae6a8c36a | |||
| 1dcee153b9 | |||
| 4065e846f6 | |||
| 466249113c | |||
| 3e2ae734e7 | |||
| 96b31c4bc0 | |||
| 0117d5838b | |||
| 42a3b4043c | |||
| 158e9111cb | |||
| 9ed1983b85 | |||
| 703e8a035d | |||
| 7ed2faab84 | |||
| 0d66d3d127 | |||
| 137b7519f7 | |||
| e34f5835bd | |||
| f122eb7c65 | |||
| a5123b8a5e | |||
| 6aa9bffa55 | |||
| d772f9a961 | |||
| 7c841a2bce | |||
| 6a6dfb4935 | |||
| 8b370def80 | |||
| d46464282c | |||
| bb4a239cb1 | |||
| e30f0d91fe | |||
| 9f34e858c7 | |||
| 94bbc13de0 | |||
| a2f01a8fcc | |||
| 30d172e826 | |||
| bb69b036b5 | |||
| b55c51e004 | |||
| 4492e75d04 | |||
| 4d949022f2 | |||
| 3ef034a9e4 | |||
| b90e3c0b76 | |||
| a8eeddde64 | |||
| 09a9e34382 | |||
| 66c4394b40 | |||
| 32575a65fc | |||
| 66901d7393 | |||
| a1eefc31bc | |||
| de471799d1 | |||
| 3951ad4705 | |||
| 9315e3036f | |||
| b3ede5b7c0 | |||
| 3553fc79dd | |||
| f322cf82b9 | |||
| 73c975ba68 | |||
| 931d579ba5 | |||
| f4773a0baf | |||
| 06d33f6cc4 | |||
| 8f3b60f3e1 | |||
| a6e27dcc9c | |||
| 4f10c986b5 | |||
| 821152bd14 | |||
| f48b133101 | |||
| 1d56f85705 | |||
| ced852033b | |||
| b9ad8fa8b2 | |||
| e3f5ce7b41 | |||
| f681889b14 | |||
| b5ec46be5b | |||
| 168ad358d5 | |||
| 2c796d144f | |||
| c676c2c458 | |||
| b36847622d | |||
| 32c8849044 | |||
| 2dd2872c08 | |||
| cf268da19d | |||
| 7a5df64746 | |||
| 4c41f6deeb | |||
| f84aa1eb31 | |||
| 9226ee2229 | |||
| 8399b088a5 | |||
| 437f0b5495 | |||
| 0ffaffa88c | |||
| 433ff494f1 | |||
| de03c52ed3 | |||
| b4a50e3dc8 | |||
| b819d95d18 | |||
| 3ac1c9452c | |||
| d6347f6660 | |||
| e769502baa | |||
| 652193d599 | |||
| a86c87236d | |||
| 2274b5acad | |||
| 9b0705c345 | |||
| d31fa168eb | |||
| f32eb4406b | |||
| 366945965d | |||
| 6702fa3e3e | |||
| a44855af9e | |||
| d817aa9c12 | |||
| 7ac8ab154b | |||
| b00a1a1065 | |||
| 2e90ed4f78 | |||
| 069ea0887c | |||
| ae7f406265 | |||
| 90f2d24e83 | |||
| 64b9646e7d | |||
| 866888df41 | |||
| 95b6f95f7b | |||
| 74771b4e76 | |||
| 8e65ce915d | |||
| a3154d59e5 | |||
| 1019f0e17c | |||
| 9328c36a46 | |||
| eb4afc6ef7 | |||
| fea748d7e6 | |||
| e83297f150 | |||
| eaca45dcdb | |||
| add6aad40b | |||
| 116c439b1d | |||
| 49b7953c44 | |||
| 3e6ab39429 | |||
| 75a37ec93d | |||
| 88934304c6 | |||
| 098a599579 | |||
| 5a2201d76b | |||
| ad736d9a06 | |||
| 7f6d46050e | |||
| f9c52baf21 | |||
| 0da3359a1a | |||
| 6b9707fee9 | |||
| 99beb007ab | |||
| 16a7702d09 | |||
| 53a3998873 | |||
| 08aa143db3 | |||
| ac141fdafe | |||
| b1188d6044 | |||
| 0d61af60c2 | |||
| 58696e4bd4 | |||
| 8b11e0787a | |||
| 09dc74d61a | |||
| 606ede7c0a | |||
| edd1b3e2f2 | |||
| 74d043731b | |||
| 843ab82905 | |||
| 79793b0d2e | |||
| 5f7ace2aef | |||
| 219b1a6f3d | |||
| 57c8468ce6 | |||
| ddee5f8671 | |||
| fbce5fab14 | |||
| 6a1597c6fb | |||
| c9e8482a35 | |||
| e602923751 |
@@ -8,7 +8,7 @@
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./mem0-plugin"
|
||||
"path": "./integrations/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.1.2"
|
||||
"version": "0.2.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./integrations/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.1.1"
|
||||
"version": "0.2.10"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged v* is
|
||||
# published. Can also be dispatched manually to re-publish a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. v1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
|
||||
# like vercel-ai-v* that also start with 'v'
|
||||
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
@@ -39,7 +54,6 @@ jobs:
|
||||
# packages_dir: dist/
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages_dir: dist/
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
name: CI Gate
|
||||
|
||||
# Single required status check for all PRs.
|
||||
#
|
||||
# Path-filtered CI workflows can't be marked as required in branch
|
||||
# protection: on a PR that doesn't touch their paths they never report, and
|
||||
# the required check hangs at "Expected" forever. This gate solves that. It
|
||||
# runs on every PR, detects which packages changed, calls only the relevant
|
||||
# package CI workflows (as reusable workflows), and the final "CI Gate" job
|
||||
# reports the aggregate result — success when every invoked pipeline passed
|
||||
# (skipped pipelines are fine), failure when any failed.
|
||||
#
|
||||
# Branch protection should require exactly one status check: "CI Gate".
|
||||
#
|
||||
# Package CI workflows keep their own push-to-main and workflow_dispatch
|
||||
# triggers; only their pull_request triggers moved here. To wire in a new
|
||||
# package: add a filter under the `changes` job, a call job that `uses:` the
|
||||
# package workflow, and list the call job in the gate's `needs`.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-gate-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changed packages
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
python_sdk: ${{ steps.filter.outputs.python_sdk }}
|
||||
ts_sdk: ${{ steps.filter.outputs.ts_sdk }}
|
||||
cli_python: ${{ steps.filter.outputs.cli_python }}
|
||||
cli_node: ${{ steps.filter.outputs.cli_node }}
|
||||
openclaw: ${{ steps.filter.outputs.openclaw }}
|
||||
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
|
||||
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
|
||||
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: filter
|
||||
with:
|
||||
# Each filter mirrors the package workflow's old pull_request
|
||||
# paths, plus the package workflow file itself and this gate file
|
||||
# (changing either must re-exercise the pipeline).
|
||||
filters: |
|
||||
python_sdk:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
ts_sdk:
|
||||
- 'mem0-ts/**'
|
||||
- '.github/workflows/ts-sdk-ci.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
cli_python:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
cli_node:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
openclaw:
|
||||
- 'integrations/openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
opencode_plugin:
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
pi_agent_plugin:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
docs_llms_txt:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
- 'scripts/check-llms-txt-coverage.py'
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
- '.github/workflows/docs-llms-txt-check.yml'
|
||||
- '.github/workflows/ci-gate.yml'
|
||||
|
||||
python-sdk:
|
||||
name: Python SDK
|
||||
needs: changes
|
||||
if: needs.changes.outputs.python_sdk == 'true'
|
||||
uses: ./.github/workflows/ci.yml
|
||||
secrets: inherit
|
||||
|
||||
ts-sdk:
|
||||
name: TypeScript SDK
|
||||
needs: changes
|
||||
if: needs.changes.outputs.ts_sdk == 'true'
|
||||
uses: ./.github/workflows/ts-sdk-ci.yml
|
||||
secrets: inherit
|
||||
|
||||
cli-python:
|
||||
name: Python CLI
|
||||
needs: changes
|
||||
if: needs.changes.outputs.cli_python == 'true'
|
||||
uses: ./.github/workflows/cli-python-ci.yml
|
||||
secrets: inherit
|
||||
|
||||
cli-node:
|
||||
name: Node CLI
|
||||
needs: changes
|
||||
if: needs.changes.outputs.cli_node == 'true'
|
||||
uses: ./.github/workflows/cli-node-ci.yml
|
||||
secrets: inherit
|
||||
|
||||
openclaw:
|
||||
name: OpenClaw
|
||||
needs: changes
|
||||
if: needs.changes.outputs.openclaw == 'true'
|
||||
uses: ./.github/workflows/openclaw-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
opencode-plugin:
|
||||
name: OpenCode Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.opencode_plugin == 'true'
|
||||
uses: ./.github/workflows/opencode-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
pi-agent-plugin:
|
||||
name: Pi Agent Plugin
|
||||
needs: changes
|
||||
if: needs.changes.outputs.pi_agent_plugin == 'true'
|
||||
uses: ./.github/workflows/pi-agent-plugin-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
docs-llms-txt:
|
||||
name: docs llms.txt
|
||||
needs: changes
|
||||
if: needs.changes.outputs.docs_llms_txt == 'true'
|
||||
uses: ./.github/workflows/docs-llms-txt-check.yml
|
||||
secrets: inherit
|
||||
|
||||
gate:
|
||||
name: CI Gate
|
||||
needs:
|
||||
- changes
|
||||
- python-sdk
|
||||
- ts-sdk
|
||||
- cli-python
|
||||
- cli-node
|
||||
- openclaw
|
||||
- opencode-plugin
|
||||
- pi-agent-plugin
|
||||
- docs-llms-txt
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Evaluate pipeline results
|
||||
env:
|
||||
NEEDS: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
|
||||
failed=$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key] | join(", ")')
|
||||
if [ -n "$failed" ]; then
|
||||
echo "::error::Failing pipelines: $failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All pipelines relevant to this change passed."
|
||||
+18
-59
@@ -1,20 +1,11 @@
|
||||
name: ci
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'embedchain/**'
|
||||
- '.github/workflows/**'
|
||||
- 'pyproject.toml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'embedchain/**'
|
||||
- 'pyproject.toml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
changelog_check:
|
||||
@@ -60,9 +51,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
mem0_changed: ${{ steps.filter.outputs.mem0 }}
|
||||
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
@@ -70,25 +60,28 @@ jobs:
|
||||
mem0:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- '.github/workflows/**'
|
||||
- '.github/workflows/ci.yml'
|
||||
- 'pyproject.toml'
|
||||
embedchain:
|
||||
- 'embedchain/**'
|
||||
|
||||
build_mem0:
|
||||
needs: check_changes
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Skip — no relevant changes
|
||||
if: needs.check_changes.outputs.mem0_changed != 'true'
|
||||
run: echo "No changes in mem0/, tests/, pyproject.toml, or ci.yml — skipping"
|
||||
- uses: actions/checkout@v4
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Clean up disk space
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: |
|
||||
df -h
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
@@ -96,61 +89,27 @@ jobs:
|
||||
sudo docker builder prune -a
|
||||
df -h
|
||||
- name: Install Hatch
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: pip install hatch
|
||||
- name: Load cached venv
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
id: cached-hatch-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
- name: Install GEOS Libraries
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
|
||||
- name: Install dependencies
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true' && steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||
pip install ruff
|
||||
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Run Linting
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: make lint
|
||||
- name: Run tests and generate coverage report
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: make test
|
||||
|
||||
build_embedchain:
|
||||
needs: check_changes
|
||||
if: needs.check_changes.outputs.embedchain_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Hatch
|
||||
run: pip install hatch
|
||||
- name: Load cached venv
|
||||
id: cached-hatch-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-embedchain-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||
- name: Install dependencies
|
||||
run: cd embedchain && make install_all
|
||||
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Run Formatting
|
||||
run: |
|
||||
mkdir -p embedchain/.ruff_cache && chmod -R 777 embedchain/.ruff_cache
|
||||
cd embedchain && hatch run format
|
||||
- name: Lint with ruff
|
||||
run: cd embedchain && make lint
|
||||
- name: Run tests and generate coverage report
|
||||
run: cd embedchain && make coverage
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: coverage.xml
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
name: Publish @mem0/cli 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# cli-node-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/cli 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||
if: startsWith(inputs.tag, 'cli-node-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -16,6 +28,8 @@ jobs:
|
||||
working-directory: cli/node
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -38,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: CLI Node CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,10 +9,7 @@ on:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged cli-v* is
|
||||
# published. Can also be dispatched manually to re-publish a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0-cli 📦 to PyPI
|
||||
if: startsWith(github.event.release.tag_name, 'cli-v')
|
||||
if: startsWith(inputs.tag, 'cli-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -16,6 +27,8 @@ jobs:
|
||||
working-directory: cli/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: CLI Python CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,10 +9,7 @@ on:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -6,13 +6,10 @@ name: docs - llms.txt check
|
||||
# python scripts/check-llms-txt-coverage.py # read-only
|
||||
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# manual runs remain standalone.
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
- 'scripts/check-llms-txt-coverage.py'
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
workflow_call:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
name: Publish @mem0/openclaw-mem0 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# openclaw-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||
if: startsWith(inputs.tag, 'openclaw-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openclaw
|
||||
working-directory: integrations/openclaw
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -28,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -38,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
name: openclaw checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- 'integrations/openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -28,13 +27,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd openclaw && pnpm exec tsc --noEmit
|
||||
run: cd integrations/openclaw && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -54,20 +53,20 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd openclaw && pnpm exec vitest run --coverage
|
||||
run: cd integrations/openclaw && pnpm exec vitest run --coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.node-version == 20
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: openclaw
|
||||
directory: openclaw/coverage
|
||||
directory: integrations/openclaw/coverage
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -86,15 +85,15 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd openclaw && pnpm build
|
||||
run: cd integrations/openclaw && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Publish @mem0/opencode-plugin 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# opencode-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. opencode-v0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/opencode-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'opencode-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.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,39 @@
|
||||
name: opencode-plugin checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: bun run type-check
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Publish @mem0/pi-agent-plugin 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# pi-agent-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. pi-agent-v0.1.1)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/pi-agent-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'pi-agent-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/pi-agent-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- 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: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.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,92 @@
|
||||
name: pi-agent-plugin checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd integrations/pi-agent-plugin && pnpm exec tsc --noEmit
|
||||
|
||||
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: 9
|
||||
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: cd integrations/pi-agent-plugin && pnpm exec vitest run
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/pi-agent-plugin && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
|
||||
test -f integrations/pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Release Router 🚦
|
||||
|
||||
# Single entry point for all release publishing.
|
||||
#
|
||||
# Package CD workflows no longer listen to release events themselves — this
|
||||
# router inspects the release tag and dispatches only the matching pipeline,
|
||||
# so each release produces one routed run instead of one real run plus seven
|
||||
# skipped ones.
|
||||
#
|
||||
# Re-publishing a release (e.g. after fixing registry settings) does NOT
|
||||
# require deleting and recreating it anymore — manually dispatch the
|
||||
# package's CD workflow from the tag instead:
|
||||
#
|
||||
# gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>
|
||||
#
|
||||
# Note: dispatching runs the workflow file as it exists at the given ref, so
|
||||
# this router can only dispatch tags created after the workflow_dispatch
|
||||
# conversion landed on main. For older tags, dispatch manually from main.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
route:
|
||||
name: Route ${{ github.event.release.tag_name }} to its CD pipeline
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Match tag prefix to CD workflow
|
||||
id: match
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
# Specific package prefixes first; the bare v* (Python SDK) arm
|
||||
# must stay last so prefixed tags that also start with 'v'
|
||||
# (vercel-ai-v*) can never be routed to the Python pipeline.
|
||||
case "$TAG" in
|
||||
ts-v*) workflow="ts-sdk-cd.yml" ;;
|
||||
cli-node-v*) workflow="cli-node-cd.yml" ;;
|
||||
cli-v*) workflow="cli-python-cd.yml" ;;
|
||||
vercel-ai-v*) workflow="vercel-ai-cd.yml" ;;
|
||||
openclaw-v*) workflow="openclaw-cd.yml" ;;
|
||||
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
|
||||
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
|
||||
v*) workflow="cd.yml" ;;
|
||||
*)
|
||||
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "workflow=$workflow" >> "$GITHUB_OUTPUT"
|
||||
echo ":outbox_tray: Routed \`$TAG\` → \`$workflow\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Dispatch ${{ steps.match.outputs.workflow }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
# --ref points at the tag so the dispatched run builds (and signs
|
||||
# provenance for) the exact tagged commit.
|
||||
gh workflow run "${{ steps.match.outputs.workflow }}" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--ref "refs/tags/$TAG" \
|
||||
-f tag="$TAG" \
|
||||
-f prerelease="${{ github.event.release.prerelease }}"
|
||||
@@ -1,13 +1,24 @@
|
||||
name: Publish mem0ai 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged ts-v* is
|
||||
# published. Can also be dispatched manually to re-publish a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish mem0ai 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||
if: startsWith(inputs.tag, 'ts-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -16,6 +27,8 @@ jobs:
|
||||
working-directory: mem0-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -38,7 +51,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: TypeScript SDK CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
- '.github/workflows/ts-sdk-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
check_changes:
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
||||
|
||||
# Dispatched by release.yml (Release Router) when a release tagged
|
||||
# vercel-ai-v* is published. Can also be dispatched manually to re-publish
|
||||
# a tag.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||
if: startsWith(inputs.tag, 'vercel-ai-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: vercel-ai-sdk
|
||||
working-directory: integrations/vercel-ai-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -28,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -38,7 +52,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
+2
-1
@@ -170,7 +170,6 @@ cython_debug/
|
||||
# Database
|
||||
db
|
||||
test-db
|
||||
!embedchain/embedchain/core/db/
|
||||
|
||||
.vscode
|
||||
.idea/
|
||||
@@ -190,3 +189,5 @@ eval/
|
||||
qdrant_storage/
|
||||
.crossnote
|
||||
testing.ipynb
|
||||
.weave/
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "evaluation"]
|
||||
path = evaluation
|
||||
url = https://github.com/mem0ai/memory-benchmarks
|
||||
branch = main
|
||||
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
|
||||
|
||||
### Key Directories
|
||||
|
||||
@@ -22,18 +22,18 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
|
||||
| `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 |
|
||||
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
|
||||
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
|
||||
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
|
||||
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `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. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/`, `mem0-oss-to-platform/` |
|
||||
| `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) |
|
||||
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
|
||||
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
|
||||
| `pr-reviews/` | Pull request review materials |
|
||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||
|
||||
@@ -50,8 +50,8 @@ mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||
|
||||
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
openclaw/ ──▶ mem0ai (npm)
|
||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
@@ -74,8 +74,8 @@ 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
|
||||
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd integrations/openclaw && pnpm install # OpenClaw plugin
|
||||
```
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
@@ -163,10 +163,10 @@ pnpm run dev # tsx src/index.ts (development)
|
||||
- **Test:** vitest (not jest)
|
||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||
|
||||
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
|
||||
|
||||
```bash
|
||||
cd vercel-ai-sdk
|
||||
cd integrations/vercel-ai-sdk
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # eslint
|
||||
@@ -181,10 +181,10 @@ pnpm run test:node # vitest (node runtime)
|
||||
- **Lint:** ESLint + Prettier
|
||||
- **Test:** jest + vitest (edge/node configs)
|
||||
|
||||
### OpenClaw Plugin (`openclaw/`)
|
||||
### OpenClaw Plugin (`integrations/openclaw/`)
|
||||
|
||||
```bash
|
||||
cd openclaw
|
||||
cd integrations/openclaw
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # vitest run
|
||||
@@ -246,18 +246,19 @@ make docs # or: cd docs && mintlify dev
|
||||
- **API spec:** `docs/openapi.json`
|
||||
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||
|
||||
### Evaluation (`evaluation/`)
|
||||
### Evaluation / Benchmarking
|
||||
|
||||
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
|
||||
|
||||
```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
|
||||
git clone https://github.com/mem0ai/memory-benchmarks.git
|
||||
cd memory-benchmarks
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
|
||||
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
|
||||
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
|
||||
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
@@ -330,7 +331,7 @@ make run-openai # OpenAI comparison
|
||||
- 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.
|
||||
- Ruff excludes `openmemory/` from root config.
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
@@ -343,8 +344,8 @@ make run-openai # OpenAI comparison
|
||||
|---------|--------|-----------|---------------|
|
||||
| `mem0-ts/` | — | Prettier | jest |
|
||||
| `cli/node/` | Biome | Biome | vitest |
|
||||
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `openclaw/` | — | — | vitest |
|
||||
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `integrations/openclaw/` | — | — | vitest |
|
||||
|
||||
### Type Checking
|
||||
|
||||
@@ -382,14 +383,14 @@ 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:** MCP tools in `integrations/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.
|
||||
- `integrations/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, split into two categories:
|
||||
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch. The two are loosely coupled via `.mem0-integration/` artifacts.
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch (the two are loosely coupled via `.mem0-integration/` artifacts); `mem0-oss-to-platform` migrates an existing project from Mem0 OSS to the hosted Platform SDK (plan, then execute on approval).
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
@@ -403,32 +404,58 @@ To add a new LLM, embedding, vector store, or reranker provider:
|
||||
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
|
||||
|
||||
### Adding a New Integration
|
||||
|
||||
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
|
||||
|
||||
1. Create `integrations/<name>/` and build the integration there.
|
||||
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
|
||||
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
|
||||
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
|
||||
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
|
||||
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
|
||||
|
||||
## 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 |
|
||||
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
|
||||
|
||||
| Workflow | File | Standalone Triggers | Tests |
|
||||
|----------|------|---------------------|-------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
|
||||
|
||||
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
|
||||
|
||||
### CD Workflows (automated publishing)
|
||||
|
||||
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
|
||||
|
||||
| Workflow | File | Tag Prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
|
||||
| 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`) |
|
||||
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
|
||||
- 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.
|
||||
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
|
||||
|
||||
### Utility Workflows
|
||||
|
||||
@@ -576,7 +603,6 @@ N/A
|
||||
- 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.
|
||||
|
||||
@@ -1026,7 +1026,8 @@ def get_user_preferences(user_id: str):
|
||||
### AutoGen Integration
|
||||
|
||||
```python
|
||||
from cookbooks.helper.mem0_teachability import Mem0Teachability
|
||||
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
|
||||
from helper.mem0_teachability import Mem0Teachability
|
||||
from mem0 import Memory
|
||||
|
||||
# Add memory capability to AutoGen agents
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
# Migration Guide: Upgrading to mem0 1.0.0
|
||||
|
||||
## TL;DR
|
||||
|
||||
**What changed?** We simplified the API by removing confusing version parameters. Now everything returns a consistent format: `{"results": [...]}`.
|
||||
|
||||
**What you need to do:**
|
||||
1. Upgrade: `pip install mem0ai==1.0.0`
|
||||
2. Remove `version` and `output_format` parameters from your code
|
||||
3. Update response handling to use `result["results"]` instead of treating responses as lists
|
||||
|
||||
**Time needed:** ~5-10 minutes for most projects
|
||||
|
||||
---
|
||||
|
||||
## Quick Migration Guide
|
||||
|
||||
### 1. Install the Update
|
||||
|
||||
```bash
|
||||
pip install mem0ai==1.0.0
|
||||
```
|
||||
|
||||
### 2. Update Your Code
|
||||
|
||||
**If you're using the Memory API:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
memory = Memory(config=MemoryConfig(version="v1.1"))
|
||||
result = memory.add("I like pizza")
|
||||
|
||||
# After
|
||||
memory = Memory() # That's it - version is automatic now
|
||||
result = memory.add("I like pizza")
|
||||
```
|
||||
|
||||
**If you're using the Client API:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
client.add(messages, output_format="v1.1")
|
||||
client.search(query, version="v2", output_format="v1.1")
|
||||
|
||||
# After
|
||||
client.add(messages) # Just remove those extra parameters
|
||||
client.search(query)
|
||||
```
|
||||
|
||||
### 3. Update How You Handle Responses
|
||||
|
||||
All responses now use the same format: a dictionary with `"results"` key.
|
||||
|
||||
```python
|
||||
# Before - you might have done this
|
||||
result = memory.add("I like pizza")
|
||||
for item in result: # Treating it as a list
|
||||
print(item)
|
||||
|
||||
# After - do this instead
|
||||
result = memory.add("I like pizza")
|
||||
for item in result["results"]: # Access the results key
|
||||
print(item)
|
||||
|
||||
# Graph relations (if you use them)
|
||||
if "relations" in result:
|
||||
for relation in result["relations"]:
|
||||
print(relation)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Enhanced Message Handling
|
||||
|
||||
The platform client (MemoryClient) now supports the same flexible message formats as the OSS version:
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# All three formats now work:
|
||||
|
||||
# 1. Single string (automatically converted to user message)
|
||||
client.add("I like pizza", user_id="alice")
|
||||
|
||||
# 2. Single message dictionary
|
||||
client.add({"role": "user", "content": "I like pizza"}, user_id="alice")
|
||||
|
||||
# 3. List of messages (conversation)
|
||||
client.add([
|
||||
{"role": "user", "content": "I like pizza"},
|
||||
{"role": "assistant", "content": "I'll remember that!"}
|
||||
], user_id="alice")
|
||||
```
|
||||
|
||||
### Async Mode Configuration
|
||||
|
||||
The `async_mode` parameter now defaults to `True` but can be configured:
|
||||
|
||||
```python
|
||||
# Default behavior (async_mode=True)
|
||||
client.add(messages, user_id="alice")
|
||||
|
||||
# Explicitly set async mode
|
||||
client.add(messages, user_id="alice", async_mode=True)
|
||||
|
||||
# Disable async mode if needed
|
||||
client.add(messages, user_id="alice", async_mode=False)
|
||||
```
|
||||
|
||||
**Note:** `async_mode=True` provides better performance for most use cases. Only set it to `False` if you have specific synchronous processing requirements.
|
||||
|
||||
---
|
||||
|
||||
## That's It!
|
||||
|
||||
For most users, that's all you need to know. The changes are:
|
||||
- ✅ No more `version` or `output_format` parameters
|
||||
- ✅ Consistent `{"results": [...]}` response format
|
||||
- ✅ Cleaner, simpler API
|
||||
|
||||
---
|
||||
|
||||
## Common Issues
|
||||
|
||||
**Getting `KeyError: 'results'`?**
|
||||
|
||||
Your code is still treating the response as a list. Update it:
|
||||
```python
|
||||
# Change this:
|
||||
for memory in response:
|
||||
|
||||
# To this:
|
||||
for memory in response["results"]:
|
||||
```
|
||||
|
||||
**Getting `TypeError: unexpected keyword argument`?**
|
||||
|
||||
You're still passing old parameters. Remove them:
|
||||
```python
|
||||
# Change this:
|
||||
client.add(messages, output_format="v1.1")
|
||||
|
||||
# To this:
|
||||
client.add(messages)
|
||||
```
|
||||
|
||||
**Seeing deprecation warnings?**
|
||||
|
||||
Remove any explicit `version="v1.0"` from your config:
|
||||
```python
|
||||
# Change this:
|
||||
memory = Memory(config=MemoryConfig(version="v1.0"))
|
||||
|
||||
# To this:
|
||||
memory = Memory()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What's New in 1.0.0
|
||||
|
||||
- **Better vector stores:** Fixed OpenSearch and improved reliability across all stores
|
||||
- **Cleaner API:** One way to do things, no more confusing options
|
||||
- **Enhanced GCP support:** Better Vertex AI configuration options
|
||||
- **Flexible message input:** Platform client now accepts strings, dicts, and lists (aligned with OSS)
|
||||
- **Configurable async_mode:** Now defaults to `True` but users can override if needed
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Check [GitHub Issues](https://github.com/mem0ai/mem0/issues)
|
||||
- Read the [documentation](https://docs.mem0.ai/)
|
||||
- Open a new issue if you're stuck
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Configuration Changes
|
||||
|
||||
**If you configured vector stores with version:**
|
||||
|
||||
```python
|
||||
# Before
|
||||
config = MemoryConfig(
|
||||
version="v1.1",
|
||||
vector_store=VectorStoreConfig(...)
|
||||
)
|
||||
|
||||
# After
|
||||
config = MemoryConfig(
|
||||
vector_store=VectorStoreConfig(...)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Your Migration
|
||||
|
||||
Quick sanity check:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
memory = Memory()
|
||||
|
||||
# Add should return a dict with "results"
|
||||
result = memory.add("I like pizza", user_id="test")
|
||||
assert "results" in result
|
||||
|
||||
# Search should return a dict with "results"
|
||||
search = memory.search("food", user_id="test")
|
||||
assert "results" in search
|
||||
|
||||
# Get all should return a dict with "results"
|
||||
all_memories = memory.get_all(user_id="test")
|
||||
assert "results" in all_memories
|
||||
|
||||
print("✅ Migration successful!")
|
||||
```
|
||||
@@ -86,6 +86,26 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
|
||||
|
||||
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
||||
|
||||
### Sign up as an agent
|
||||
|
||||
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
|
||||
|
||||
```bash
|
||||
# 1. Install
|
||||
npm install -g @mem0/cli # or: pip install mem0-cli
|
||||
|
||||
# 2. Sign up as an agent (replace `claude-code` with your name)
|
||||
mem0 init --agent --agent-caller claude-code
|
||||
|
||||
# 3. Add a memory
|
||||
mem0 add "I am using mem0"
|
||||
|
||||
# 4. Search
|
||||
mem0 search "am I using mem0"
|
||||
```
|
||||
|
||||
The human owner can claim the account later with `mem0 init --email <their-email>` — same key, memories preserved. Full guide: [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup).
|
||||
|
||||
| | Library | Self-Hosted Server | Cloud Platform |
|
||||
|---|---------|-------------------|----------------|
|
||||
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
||||
@@ -133,6 +153,7 @@ See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for config
|
||||
|
||||
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
|
||||
2. Embed the memory layer via SDK or API keys
|
||||
3. Using hosted Qdrant vectors? See the [Platform migration guide](https://docs.mem0.ai/migration/oss-to-platform) to import them into Mem0 Platform.
|
||||
|
||||
### CLI
|
||||
|
||||
@@ -165,9 +186,10 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
|
||||
```
|
||||
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. Use `/mem0-oss-to-platform` to migrate an existing project from Mem0 OSS to the hosted Platform SDK. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
|
||||
+4
-2
@@ -503,7 +503,7 @@
|
||||
},
|
||||
{
|
||||
"name": "init",
|
||||
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
|
||||
"usage": "mem0 init [OPTIONS]",
|
||||
"needsBackend": false,
|
||||
"needsConfig": false,
|
||||
@@ -516,7 +516,9 @@
|
||||
{ "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": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
|
||||
{ "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
|
||||
{ "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `@mem0/cli` are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.9] — 2026-06-19
|
||||
|
||||
### Security
|
||||
|
||||
- Telemetry no longer passes the Mem0 API key to its child process via
|
||||
command-line arguments. The context is now sent over stdin, so the key is no
|
||||
longer visible in the process list (`ps`, `/proc/<pid>/cmdline`, Activity
|
||||
Monitor). Fixes #4862.
|
||||
|
||||
## [0.2.8] — 2026-06-01
|
||||
|
||||
### Security
|
||||
|
||||
- Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs:
|
||||
- `jws` → 4.0.1 (CVE-2025-65945)
|
||||
- `langsmith` → ^0.6.0 (CVE-2026-45134)
|
||||
- `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343)
|
||||
- `picomatch` → ^2.3.2 (CVE-2026-33671)
|
||||
- `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996)
|
||||
- `path-to-regexp` → ^8.4.0 (CVE-2026-4926)
|
||||
- `rollup` → ^4.59.0 (CVE-2026-27606)
|
||||
- `glob` → ^10.5.0 (CVE-2025-64756)
|
||||
- `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/node/`.
|
||||
+13
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.9",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -40,8 +40,19 @@
|
||||
"typescript": "^5.4.0",
|
||||
"tsup": "^8.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vitest": "^1.5.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.1.0",
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"jws@4.0.0": "4.0.1",
|
||||
"langsmith@<0.6.0": "^0.6.0",
|
||||
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
||||
"picomatch@<2.3.2": "^2.3.2",
|
||||
"postcss@<8.5.10": ">=8.5.10",
|
||||
"esbuild": ">=0.28.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+310
-723
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- "@biomejs/biome"
|
||||
- esbuild
|
||||
|
||||
overrides:
|
||||
jws@4.0.0: 4.0.1
|
||||
langsmith@<0.6.0: ^0.6.0
|
||||
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
|
||||
picomatch@<2.3.2: ^2.3.2
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
"esbuild": ">=0.28.1"
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
*
|
||||
* Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
* agent runtime env var is present. The return value is a context **trigger
|
||||
* only** — the canonical agent identity is self-declared by the agent via
|
||||
* `--agent-caller <name>` (Proof Editor-style) and never sniffed from env
|
||||
* vars to fill the `agent_caller` field on the APIKey row.
|
||||
*
|
||||
* Returns a short name or null. Honest reporting depends on `--agent-caller`;
|
||||
* this list is just enough to enable the zero-friction auto-bootstrap UX.
|
||||
*/
|
||||
|
||||
const AGENT_CALLER_ENV: ReadonlyArray<readonly [string, readonly string[]]> = [
|
||||
["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
|
||||
["cursor", ["CURSOR_AGENT", "CURSOR_SESSION_ID"]],
|
||||
["codex", ["CODEX_CLI", "OPENAI_CODEX"]],
|
||||
["cline", ["CLINE_AGENT", "CLINE"]],
|
||||
["continue", ["CONTINUE_AGENT", "CONTINUE_SESSION"]],
|
||||
["aider", ["AIDER_SESSION"]],
|
||||
["goose", ["GOOSE_AGENT"]],
|
||||
["windsurf", ["WINDSURF_AGENT"]],
|
||||
] as const;
|
||||
|
||||
export function detectAgentCaller(): string | null {
|
||||
for (const [name, envVars] of AGENT_CALLER_ENV) {
|
||||
if (envVars.some((v) => process.env[v])) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import type { PlatformConfig } from "../config.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
import { captureNotice, isAgentMode } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
import {
|
||||
APIError,
|
||||
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
|
||||
if (resp.status === 204) {
|
||||
return {};
|
||||
}
|
||||
return resp.json();
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
// Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
// fallback for endpoints returning non-dict / non-dict-leading payloads)
|
||||
// and stash for end-of-command surfacing.
|
||||
let notice: string | null = null;
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data) &&
|
||||
"mem0_notice" in data
|
||||
) {
|
||||
notice = (data as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
|
||||
delete (data as Record<string, unknown>).mem0_notice;
|
||||
} else if (
|
||||
Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
typeof data[0] === "object" &&
|
||||
data[0] !== null &&
|
||||
"mem0_notice" in data[0]
|
||||
) {
|
||||
notice = (data[0] as Record<string, unknown>).mem0_notice as string;
|
||||
// biome-ignore lint/performance/noDelete: see above.
|
||||
delete (data[0] as Record<string, unknown>).mem0_notice;
|
||||
}
|
||||
if (!notice) {
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
|
||||
}
|
||||
captureNotice(notice);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async add(
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Agent Mode commands — bootstrap (unattended signup) and OTP-based claim.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import { colors, printError, printInfo, printSuccess } from "../branding.js";
|
||||
import { type Mem0Config, saveConfig } from "../config.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export interface BootstrapEnvelope {
|
||||
api_key: string;
|
||||
default_user_id: string;
|
||||
org_id: string;
|
||||
project_id: string;
|
||||
mcp_url?: string;
|
||||
smoke_test_url?: string;
|
||||
claim_command?: string;
|
||||
mem0_notice?: string;
|
||||
}
|
||||
|
||||
function isValidEnvelope(v: unknown): v is BootstrapEnvelope {
|
||||
return (
|
||||
!!v &&
|
||||
typeof v === "object" &&
|
||||
typeof (v as BootstrapEnvelope).api_key === "string" &&
|
||||
(v as BootstrapEnvelope).api_key.length > 0 &&
|
||||
typeof (v as BootstrapEnvelope).default_user_id === "string" &&
|
||||
(v as BootstrapEnvelope).default_user_id.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
*
|
||||
* @param config - Mem0Config mutated in place with the new platform values.
|
||||
* @param source - `--source` flag passthrough (analytics tag, free-form).
|
||||
* @param agentCaller - Self-declared agent identity passed via `--agent-caller`
|
||||
* (e.g. `claude-code`, `cursor`). May be null when the caller omitted the
|
||||
* flag; the agent can backfill later via `mem0 identify <name>`. Sent to the
|
||||
* backend in the request body and saved into `platform.agentCaller` for
|
||||
* local introspection.
|
||||
*/
|
||||
export async function bootstrapViaBackend(
|
||||
config: Mem0Config,
|
||||
{
|
||||
source,
|
||||
agentCaller,
|
||||
}: { source?: string | null; agentCaller?: string | null } = {},
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
const body: Record<string, unknown> = {};
|
||||
if (source) body.source = source;
|
||||
if (agentCaller) body.agent_caller = agentCaller;
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (resp.status === 429) {
|
||||
printError("Rate-limited. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (resp.status === 503) {
|
||||
printError("Agent Mode is temporarily disabled. Try again later.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const errBody = (await resp.json()) as {
|
||||
error?: string;
|
||||
detail?: string;
|
||||
};
|
||||
detail = errBody.error ?? errBody.detail ?? resp.statusText;
|
||||
} catch {
|
||||
/* leave detail as statusText */
|
||||
}
|
||||
// Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
// translates to a generic 403 "You do not have permission to perform
|
||||
// this action." That's opaque — surface it as the rate-limit message
|
||||
// it actually is.
|
||||
if (resp.status === 403 && /permission/i.test(detail)) {
|
||||
printError(
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
printError(`Bootstrap failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const envelope = (await resp.json()) as BootstrapEnvelope;
|
||||
if (!isValidEnvelope(envelope)) {
|
||||
// Defend against partial/malformed backend responses (e.g. {api_key: null}).
|
||||
// Without this guard, the typed `string` field is silently set to
|
||||
// undefined/null and persisted, producing confusing downstream errors.
|
||||
printError(
|
||||
"Bootstrap response missing required fields — please update the CLI.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.apiKey = envelope.api_key;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.agentMode = true;
|
||||
config.platform.createdVia = "agent_mode";
|
||||
config.platform.agentCaller = agentCaller ?? "";
|
||||
config.platform.claimedAt = "";
|
||||
config.platform.defaultUserId = envelope.default_user_id;
|
||||
// Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.userId = envelope.default_user_id;
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(
|
||||
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
|
||||
);
|
||||
if (envelope.mem0_notice) {
|
||||
console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
|
||||
} else {
|
||||
// Fallback for older backends without the unified notice field.
|
||||
const claimCmd = envelope.claim_command ?? "mem0 init --email <your-email>";
|
||||
console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
|
||||
}
|
||||
|
||||
if (!agentCaller) {
|
||||
console.log(
|
||||
` ${dim("If you are an AI agent, identify yourself: `mem0 identify <your-name>` (e.g. claude-code, cursor).")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
*
|
||||
* Hits /api/v1/auth/email_code/ to send a verification code, prompts for it
|
||||
* interactively (or accepts via `code`), then sends it to /verify/ alongside
|
||||
* `agent_mode_api_key`. Backend's verify_email_code runs upgrade-in-place
|
||||
* inline and returns the claim result.
|
||||
*/
|
||||
export async function claimViaOtp(
|
||||
config: Mem0Config,
|
||||
{ email, code }: { email: string; code?: string },
|
||||
): Promise<void> {
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
if (!config.platform.apiKey || !config.platform.agentMode) {
|
||||
printError(
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rawKey = config.platform.apiKey;
|
||||
|
||||
// Step 1: request OTP (unless --code was supplied)
|
||||
if (!code) {
|
||||
const sendResp = await fetch(`${baseUrl}/api/v1/auth/email_code/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (sendResp.status === 429) {
|
||||
printError("Too many attempts. Try again in a few minutes.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!sendResp.ok) {
|
||||
let detail: string = sendResp.statusText;
|
||||
try {
|
||||
const errBody = (await sendResp.json()) as { error?: string };
|
||||
if (errBody.error) detail = errBody.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Failed to send code: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
printSuccess(`Verification code sent to ${email}. Check your inbox.`);
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
printError(
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
`Re-run: mem0 init --email ${email} --code <code>`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
code = await promptLine(` ${brand("Verification Code")}`);
|
||||
if (!code) {
|
||||
printError("Code is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: verify + claim atomically
|
||||
const verifyResp = await fetch(`${baseUrl}/api/v1/auth/email_code/verify/`, {
|
||||
method: "POST",
|
||||
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code: code.trim(),
|
||||
agent_mode_api_key: rawKey,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
if (!verifyResp.ok) {
|
||||
let detail: string = verifyResp.statusText;
|
||||
let errCode = "";
|
||||
try {
|
||||
const errBody = (await verifyResp.json()) as {
|
||||
error?: string;
|
||||
code?: string;
|
||||
};
|
||||
if (errBody.error) detail = errBody.error;
|
||||
if (errBody.code) errCode = errBody.code;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Claim failed: ${detail}`);
|
||||
if (errCode === "email_already_claimed") {
|
||||
console.log(
|
||||
` ${dim("Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.")}`,
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const claimBody = (await verifyResp.json()) as {
|
||||
claimed?: boolean;
|
||||
claimed_at?: string;
|
||||
};
|
||||
if (!claimBody.claimed) {
|
||||
printError(`Unexpected verify response: ${JSON.stringify(claimBody)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.platform.agentMode = false;
|
||||
config.platform.claimedAt = claimBody.claimed_at ?? new Date().toISOString();
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
saveConfig(config);
|
||||
|
||||
printSuccess(`Agent claimed to ${email}. Your API key is unchanged.`);
|
||||
}
|
||||
|
||||
function promptLine(label: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(`${label}: `, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* `mem0 agent-rush <add|search> "..."` — wraps the AGENTRUSH platform endpoints.
|
||||
* Project routing is implicit (server-side); zero flags needed.
|
||||
*/
|
||||
|
||||
import readline from "node:readline";
|
||||
import { colors, printError, printSuccess } from "../branding.js";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const PII_WARNING = [
|
||||
"",
|
||||
"⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.",
|
||||
" Do not include real names, emails, secrets, work content, or PII.",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const ERROR_HINTS: Record<string, string> = {
|
||||
agentrush_search_first:
|
||||
"Run 3 'mem0 agent-rush search' commands before adding.",
|
||||
agentrush_search_quota: "You've used your 3 lifetime searches.",
|
||||
agentrush_add_quota: "You've used your 3 lifetime adds.",
|
||||
agentrush_not_agent_mode:
|
||||
"Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
|
||||
agentrush_length: "Memory text must be 50-1000 characters.",
|
||||
agentrush_no_urls: "URLs are not allowed.",
|
||||
agentrush_blocklist: "Content contains a blocked term.",
|
||||
agentrush_global_quota: "Event-wide cap reached. Try again later.",
|
||||
agentrush_not_provisioned:
|
||||
"AGENTRUSH is not provisioned in this environment.",
|
||||
};
|
||||
|
||||
async function callEndpoint(
|
||||
path: string,
|
||||
body: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const config = loadConfig();
|
||||
const baseUrl = (config.platform?.baseUrl ?? "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
if (!config.platform?.apiKey) {
|
||||
printError("Not initialized. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const resp = await fetch(`${baseUrl}${path}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Token ${config.platform.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
"X-Mem0-Client-Version": CLI_VERSION,
|
||||
"X-Mem0-Mode": "agent-rush",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
const json = await resp.json().catch(() => ({}));
|
||||
|
||||
if (!resp.ok) {
|
||||
const code =
|
||||
(json as { error?: { code?: string } }).error?.code ?? "unknown";
|
||||
printError(`AGENTRUSH error: ${code}`);
|
||||
if (ERROR_HINTS[code]) {
|
||||
console.log(` ${colors.dim(ERROR_HINTS[code])}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
function promptLine(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC.
|
||||
*
|
||||
* Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt`
|
||||
* so we never ask the same machine twice. On anything else, abort.
|
||||
*
|
||||
* Non-interactive (agent invocation, no TTY): print the warning to stderr
|
||||
* for the human reading the agent's transcript and proceed — agents can't
|
||||
* answer y/N prompts.
|
||||
*/
|
||||
async function ensureWarningAcknowledged(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
if (config.agentRush?.acknowledgedAt) return;
|
||||
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
// Agent context: surface the warning to stderr, don't block.
|
||||
console.error(PII_WARNING);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(PII_WARNING);
|
||||
const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase();
|
||||
if (answer !== "y" && answer !== "yes") {
|
||||
printError("Aborted.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
config.agentRush.acknowledgedAt = new Date().toISOString();
|
||||
saveConfig(config);
|
||||
}
|
||||
|
||||
export async function cmdAgentRushAdd(content: string): Promise<void> {
|
||||
await ensureWarningAcknowledged();
|
||||
const result = await callEndpoint("/v1/agent-rush/memories/", { content });
|
||||
printSuccess(
|
||||
`Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function cmdAgentRushSearch(query: string): Promise<void> {
|
||||
const result = (await callEndpoint("/v1/agent-rush/memories/search/", {
|
||||
query,
|
||||
})) as {
|
||||
results?: Array<{ memory?: string }>;
|
||||
memories?: Array<{ memory?: string }>;
|
||||
};
|
||||
|
||||
const memories = result.results ?? result.memories ?? [];
|
||||
|
||||
if (memories.length === 0) {
|
||||
console.log(colors.dim("(no results)"));
|
||||
return;
|
||||
}
|
||||
|
||||
memories.slice(0, 5).forEach((m, i) => {
|
||||
console.log(` ${i + 1}. ${m.memory ?? JSON.stringify(m)}`);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* mem0 identify — declare which agent owns the current agent-mode key.
|
||||
*
|
||||
* Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
* saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
* its own row with its real identity. Idempotent.
|
||||
*/
|
||||
|
||||
import { printError, printSuccess } from "../branding.js";
|
||||
import { loadConfig, saveConfig } from "../config.js";
|
||||
|
||||
const SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "node",
|
||||
} as const;
|
||||
|
||||
export async function runIdentify(name: string): Promise<void> {
|
||||
const config = loadConfig();
|
||||
if (!config.platform.apiKey) {
|
||||
printError("No API key configured. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.platform.agentMode) {
|
||||
printError("This command only works on unclaimed agent-mode keys.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clean = (name ?? "").trim();
|
||||
if (!clean) {
|
||||
printError("Agent name is required.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/caller/`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...SOURCE_HEADERS,
|
||||
Authorization: `Token ${config.platform.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: clean }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
} catch (err) {
|
||||
printError(
|
||||
`Network error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
let detail: string = resp.statusText;
|
||||
try {
|
||||
const body = (await resp.json()) as { error?: string };
|
||||
if (body.error) detail = body.error;
|
||||
} catch {
|
||||
/* leave as statusText */
|
||||
}
|
||||
printError(`Identify failed: ${detail}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
const canonical = body.agent_caller ?? clean;
|
||||
config.platform.agentCaller = canonical;
|
||||
saveConfig(config);
|
||||
printSuccess(`Identified as ${canonical}.`);
|
||||
}
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
redactKey,
|
||||
saveConfig,
|
||||
} from "../config.js";
|
||||
import { formatJsonEnvelope } from "../output.js";
|
||||
import { isAgentMode } from "../state.js";
|
||||
|
||||
const { brand, dim } = colors;
|
||||
|
||||
@@ -33,6 +35,65 @@ function validateEmail(email: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests. */
|
||||
export async function pingKey(
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
timeoutMs = 5000,
|
||||
): Promise<boolean> {
|
||||
// Returns false ONLY on a definitive "invalid key" signal (HTTP 401/403).
|
||||
// Network errors, timeouts, and 5xx responses return true so we prefer
|
||||
// reusing an existing key over silently minting a new shadow on a transient
|
||||
// blip (which would also clobber config + plugin-sync targets).
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, {
|
||||
headers: { Authorization: `Token ${apiKey}` },
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
return resp.status !== 401 && resp.status !== 403;
|
||||
} catch {
|
||||
return true; // unknown — prefer reuse
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeIdentify(
|
||||
key: string,
|
||||
baseUrl: string,
|
||||
agentCaller: string | undefined,
|
||||
): Promise<void> {
|
||||
// Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
// reused key. Silent no-op on any failure — reuse must not break.
|
||||
if (!agentCaller) return;
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`${baseUrl.replace(/\/+$/, "")}/api/v1/auth/agent_mode/caller/`,
|
||||
{
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
Authorization: `Token ${key}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ agent_caller: agentCaller }),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
},
|
||||
);
|
||||
if (resp.ok) {
|
||||
try {
|
||||
const body = (await resp.json()) as { agent_caller?: string };
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const cfg = loadConfig();
|
||||
cfg.platform.agentCaller = body.agent_caller ?? agentCaller;
|
||||
saveConfig(cfg);
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* swallow — best effort */
|
||||
}
|
||||
}
|
||||
|
||||
async function emailLogin(
|
||||
email: string,
|
||||
code: string | undefined,
|
||||
@@ -196,6 +257,7 @@ async function setupPlatform(config: Mem0Config): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
config.platform.apiKey = apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
}
|
||||
|
||||
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||
@@ -249,14 +311,35 @@ export async function runInit(
|
||||
email?: string;
|
||||
code?: string;
|
||||
force?: boolean;
|
||||
agent?: boolean;
|
||||
source?: string;
|
||||
agentCaller?: string;
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
const { detectAgentCaller } = await import("../agent-detect.js");
|
||||
const { bootstrapViaBackend, claimViaOtp } = await import("./agent-mode.js");
|
||||
const { isAgentMode } = await import("../state.js");
|
||||
const { captureEvent } = await import("../telemetry.js");
|
||||
|
||||
const fireInit = (
|
||||
mode: "agent" | "email" | "api_key" | "existing_key",
|
||||
claimed = false,
|
||||
) => {
|
||||
const props: Record<string, unknown> = { command: "init", mode };
|
||||
// Self-declared via --agent-caller; not sniffed from env vars.
|
||||
if (opts.agentCaller) props.agent_caller = opts.agentCaller;
|
||||
if (opts.source) props.signup_source = opts.source;
|
||||
if (claimed) props.claimed_agent_mode = true;
|
||||
captureEvent("cli.init", props);
|
||||
};
|
||||
|
||||
const config = createDefaultConfig();
|
||||
const savedConfig = loadConfig();
|
||||
const baseUrl =
|
||||
process.env.MEM0_BASE_URL ||
|
||||
savedConfig.platform.baseUrl ||
|
||||
DEFAULT_BASE_URL;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
|
||||
// Guards
|
||||
if (opts.code && !opts.email) {
|
||||
@@ -268,6 +351,84 @@ export async function runInit(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Claim flow: --email against an existing agent-mode config ───────────
|
||||
if (
|
||||
opts.email &&
|
||||
fs.existsSync(CONFIG_FILE) &&
|
||||
savedConfig.platform.agentMode &&
|
||||
savedConfig.platform.apiKey
|
||||
) {
|
||||
const email = opts.email.trim().toLowerCase();
|
||||
validateEmail(email);
|
||||
printInfo(`Claiming Agent Mode account to ${email}...`);
|
||||
await claimViaOtp(savedConfig, { email, code: opts.code });
|
||||
fireInit("email", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Agent Mode path runs BEFORE the existing-config guard ──────────────
|
||||
// Rule 1/2 will REUSE a valid existing key (not overwrite), so we must
|
||||
// short-circuit before the guard prompts the user about overwriting.
|
||||
// Rule 3 only mints when there's no valid key to reuse — in that case
|
||||
// overwriting is what the user wants.
|
||||
const agentCtx =
|
||||
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
|
||||
if (!opts.apiKey && !opts.email && agentCtx) {
|
||||
const emitReuseEnvelope = (source: "env" | "config") => {
|
||||
if (isAgentMode()) {
|
||||
formatJsonEnvelope({
|
||||
command: "init",
|
||||
data: {
|
||||
api_key_saved: false,
|
||||
api_key_source: source,
|
||||
agent_mode: false,
|
||||
message:
|
||||
"Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
printSuccess(
|
||||
source === "env"
|
||||
? "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
: "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
|
||||
);
|
||||
}
|
||||
};
|
||||
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
const envKey = (process.env.MEM0_API_KEY || "").trim();
|
||||
if (envKey && (await pingKey(envKey, baseUrl))) {
|
||||
await maybeIdentify(envKey, baseUrl, opts.agentCaller);
|
||||
emitReuseEnvelope("env");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 2: existing config api_key valid → reuse.
|
||||
if (
|
||||
savedConfig.platform.apiKey &&
|
||||
(await pingKey(savedConfig.platform.apiKey, baseUrl))
|
||||
) {
|
||||
await maybeIdentify(
|
||||
savedConfig.platform.apiKey,
|
||||
baseUrl,
|
||||
opts.agentCaller,
|
||||
);
|
||||
emitReuseEnvelope("config");
|
||||
fireInit("existing_key");
|
||||
return;
|
||||
}
|
||||
// Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
// agent_caller is self-declared via --agent-caller (Proof Editor-style),
|
||||
// not derived from env-var sniffing. detectAgentCaller() above is still
|
||||
// used as a context trigger (does this look like an agent?) but never
|
||||
// to fill identity.
|
||||
await bootstrapViaBackend(config, {
|
||||
source: opts.source ?? null,
|
||||
agentCaller: opts.agentCaller ?? null,
|
||||
});
|
||||
fireInit("agent");
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn if an existing config with an API key would be overwritten
|
||||
if (
|
||||
!opts.force &&
|
||||
@@ -324,6 +485,7 @@ export async function runInit(
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
@@ -339,13 +501,15 @@ export async function runInit(
|
||||
}
|
||||
|
||||
// ── API key flow ──────────────────────────────────────────────────────────
|
||||
// (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
// guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
// 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>]",
|
||||
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -356,6 +520,7 @@ export async function runInit(
|
||||
// Non-interactive: both flags provided
|
||||
if (opts.apiKey && opts.userId) {
|
||||
config.platform.apiKey = opts.apiKey;
|
||||
config.platform.createdVia = "api_key";
|
||||
config.defaults.userId = opts.userId;
|
||||
await validatePlatform(config);
|
||||
saveConfig(config);
|
||||
@@ -403,6 +568,7 @@ export async function runInit(
|
||||
config.platform.apiKey = apiKeyVal;
|
||||
config.platform.baseUrl = baseUrl;
|
||||
config.platform.userEmail = email;
|
||||
config.platform.createdVia = "email";
|
||||
config.defaults.userId =
|
||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function cmdAdd(
|
||||
file?: string;
|
||||
metadata?: string;
|
||||
immutable: boolean;
|
||||
noInfer: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string;
|
||||
output: string;
|
||||
@@ -136,7 +136,7 @@ export async function cmdAdd(
|
||||
runId: opts.runId,
|
||||
metadata: meta,
|
||||
immutable: opts.immutable,
|
||||
infer: !opts.noInfer,
|
||||
infer: opts.infer !== false,
|
||||
expires: opts.expires,
|
||||
categories: cats,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* `mem0 whoami` — print the active agent's default_user_id (AGENTRUSH identifier).
|
||||
* Reads from local config; no network call.
|
||||
*/
|
||||
|
||||
import { colors, printError, printInfo } from "../branding.js";
|
||||
import { loadConfig } from "../config.js";
|
||||
|
||||
export async function cmdWhoami(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
const sessionId = config.platform?.defaultUserId;
|
||||
if (!sessionId) {
|
||||
printError("No default_user_id found. Run `mem0 init --agent` first.");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Your AGENTRUSH identifier: ${colors.brand(sessionId)}`);
|
||||
printInfo("Find your row at https://mem0.ai/agentrush");
|
||||
}
|
||||
@@ -21,6 +21,12 @@ export interface PlatformConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
userEmail: string;
|
||||
// Agent Mode (unclaimed-shadow signup)
|
||||
agentMode: boolean; // true while the key is an unclaimed agent-mode key
|
||||
createdVia: string; // "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agentCaller: string; // canonical agent name when createdVia === "agent_mode" (e.g. "claude-code")
|
||||
claimedAt: string; // ISO timestamp once the agent has been claimed
|
||||
defaultUserId: string; // `user_<slug>` returned by bootstrap; auto-default scope
|
||||
}
|
||||
|
||||
export interface DefaultsConfig {
|
||||
@@ -34,11 +40,18 @@ export interface TelemetryConfig {
|
||||
anonymousId: string;
|
||||
}
|
||||
|
||||
export interface AgentRushConfig {
|
||||
// ISO timestamp the human acknowledged the "memories are public" warning.
|
||||
// Empty until first interactive `mem0 agent-rush add`.
|
||||
acknowledgedAt: string;
|
||||
}
|
||||
|
||||
export interface Mem0Config {
|
||||
version: number;
|
||||
defaults: DefaultsConfig;
|
||||
platform: PlatformConfig;
|
||||
telemetry: TelemetryConfig;
|
||||
agentRush: AgentRushConfig;
|
||||
}
|
||||
|
||||
export function createDefaultConfig(): Mem0Config {
|
||||
@@ -54,10 +67,18 @@ export function createDefaultConfig(): Mem0Config {
|
||||
apiKey: "",
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
userEmail: "",
|
||||
agentMode: false,
|
||||
createdVia: "",
|
||||
agentCaller: "",
|
||||
claimedAt: "",
|
||||
defaultUserId: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "",
|
||||
},
|
||||
agentRush: {
|
||||
acknowledgedAt: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,6 +100,11 @@ export function loadConfig(): Mem0Config {
|
||||
config.platform.apiKey = plat.api_key ?? "";
|
||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||
config.platform.userEmail = plat.user_email ?? "";
|
||||
config.platform.agentMode = Boolean(plat.agent_mode ?? false);
|
||||
config.platform.createdVia = plat.created_via ?? "";
|
||||
config.platform.agentCaller = plat.agent_caller ?? "";
|
||||
config.platform.claimedAt = plat.claimed_at ?? "";
|
||||
config.platform.defaultUserId = plat.default_user_id ?? "";
|
||||
|
||||
const defaults = data.defaults ?? {};
|
||||
config.defaults.userId = defaults.user_id ?? "";
|
||||
@@ -87,6 +113,8 @@ export function loadConfig(): Mem0Config {
|
||||
config.defaults.runId = defaults.run_id ?? "";
|
||||
const telemetry = data.telemetry ?? {};
|
||||
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||
const agentRush = data.agent_rush ?? {};
|
||||
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
|
||||
}
|
||||
|
||||
// Environment variable overrides
|
||||
@@ -118,14 +146,36 @@ export function saveConfig(config: Mem0Config): void {
|
||||
api_key: config.platform.apiKey,
|
||||
base_url: config.platform.baseUrl,
|
||||
user_email: config.platform.userEmail,
|
||||
agent_mode: config.platform.agentMode,
|
||||
created_via: config.platform.createdVia,
|
||||
agent_caller: config.platform.agentCaller,
|
||||
claimed_at: config.platform.claimedAt,
|
||||
default_user_id: config.platform.defaultUserId,
|
||||
},
|
||||
telemetry: {
|
||||
anonymous_id: config.telemetry.anonymousId,
|
||||
},
|
||||
agent_rush: {
|
||||
acknowledged_at: config.agentRush.acknowledgedAt,
|
||||
},
|
||||
};
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||
|
||||
// Propagate api_key to ecosystem touchpoints (Claude plugin env injection,
|
||||
// shell rc exports). Idempotent — updates only EXISTING entries; never
|
||||
// creates new ones. Best-effort: errors swallowed so config.json is
|
||||
// always authoritative, never blocked by plugin-state issues.
|
||||
if (config.platform.apiKey) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { syncApiKey } = require("./plugin-sync.js");
|
||||
syncApiKey(config.platform.apiKey);
|
||||
} catch {
|
||||
/* swallow */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function redactKey(key: string): string {
|
||||
|
||||
+112
-4
@@ -13,7 +13,12 @@ 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 {
|
||||
isAgentMode,
|
||||
setAgentMode,
|
||||
setCurrentCommand,
|
||||
takeNotice,
|
||||
} from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
import { CLI_VERSION } from "./version.js";
|
||||
|
||||
@@ -141,6 +146,11 @@ program
|
||||
.description(
|
||||
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
||||
)
|
||||
// Positional options: flags AFTER a subcommand name belong to that
|
||||
// subcommand, not the global program. Without this, `mem0 init --agent`
|
||||
// routes `--agent` to the program-level alias (for --json) and init's own
|
||||
// `--agent` (Agent Mode bootstrap) silently never fires.
|
||||
.enablePositionalOptions()
|
||||
.option("--version", "Show version and exit.")
|
||||
.on("option:version", () => {
|
||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
@@ -149,7 +159,7 @@ program
|
||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||
.option(
|
||||
"--agent",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||
"Output as JSON for agent/programmatic use. (alias: --json) Place BEFORE the subcommand: `mem0 --agent <cmd>`. On `init`, `mem0 init --agent` is the Agent Mode bootstrap flag instead.",
|
||||
)
|
||||
.usage("<command> [options]")
|
||||
.helpOption("--help", "Show this message and exit.")
|
||||
@@ -166,6 +176,14 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
||||
parentName && parentName !== "mem0"
|
||||
? `${parentName}.${commandName}`
|
||||
: commandName;
|
||||
// Stash the active command name in shared state so the JSON
|
||||
// error envelope (printError) can report which command failed
|
||||
// instead of an empty `"command": ""` field.
|
||||
setCurrentCommand(fullCommand);
|
||||
// init fires its own telemetry from runInit with full M1-M6 props
|
||||
// (mode/agent_caller/signup_source/claimed_agent_mode); skip the
|
||||
// auto-fire here so we don't double-count.
|
||||
if (fullCommand === "init") return;
|
||||
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||
captureEvent(
|
||||
`cli.${fullCommand}`,
|
||||
@@ -193,11 +211,32 @@ program
|
||||
"Verification code (use with --email for non-interactive login).",
|
||||
)
|
||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
||||
.option(
|
||||
"--agent",
|
||||
"Bootstrap an unattended Agent Mode account (no email required).",
|
||||
false,
|
||||
)
|
||||
.option(
|
||||
"--source <channel>",
|
||||
"Channel attribution for signup (e.g. github, hn, ph).",
|
||||
)
|
||||
.option(
|
||||
"--agent-caller <name>",
|
||||
"Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
||||
)
|
||||
// Accept `--json` at the init level too so the PRD-documented form
|
||||
// `mem0 init --agent --json` works without requiring users to move it
|
||||
// before the subcommand. Effect is identical to the global `--json`:
|
||||
// flip agent-mode output state.
|
||||
.option("--json", "Output as JSON (alias for global `--json`).", 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",
|
||||
"\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\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
|
||||
)
|
||||
.action(async (opts) => {
|
||||
// `--json` at init level mirrors the global flag — flip agent_mode
|
||||
// state so downstream formatters use JSON envelopes.
|
||||
if (opts.json) setAgentMode(true);
|
||||
const { runInit } = await import("./commands/init.js");
|
||||
await runInit({
|
||||
apiKey: opts.apiKey,
|
||||
@@ -205,9 +244,66 @@ program
|
||||
email: opts.email,
|
||||
code: opts.code,
|
||||
force: opts.force,
|
||||
agent: opts.agent,
|
||||
source: opts.source,
|
||||
agentCaller: opts.agentCaller,
|
||||
});
|
||||
});
|
||||
|
||||
// ── Setup: identify (post-bootstrap agent self-tag) ──────────────────────
|
||||
|
||||
program
|
||||
.command("identify <name>")
|
||||
.description(
|
||||
"Tag your active Agent Mode key with the AI agent that's using it (e.g. claude-code, cursor).",
|
||||
)
|
||||
.action(async (name: string) => {
|
||||
const { runIdentify } = await import("./commands/identify.js");
|
||||
await runIdentify(name);
|
||||
});
|
||||
|
||||
// ── Setup: whoami (print active agent identifier) ────────────────────────
|
||||
|
||||
program
|
||||
.command("whoami")
|
||||
.description("Print the active agent's AGENTRUSH identifier.")
|
||||
.action(async () => {
|
||||
const { cmdWhoami } = await import("./commands/whoami.js");
|
||||
await cmdWhoami();
|
||||
});
|
||||
|
||||
// ── AGENTRUSH subcommand group ────────────────────────────────────────────
|
||||
|
||||
const agentRush = program
|
||||
.command("agent-rush")
|
||||
.description("AGENTRUSH game commands.")
|
||||
.addHelpCommand(false)
|
||||
.configureHelp({ formatHelp: richFormatHelp });
|
||||
|
||||
agentRush
|
||||
.command("add <content...>")
|
||||
.description("Submit a memory to AGENTRUSH.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 agent-rush add "I used mem0 to build a coding agent"\n $ mem0 agent-rush add "Agents that remember are better agents"',
|
||||
)
|
||||
.action(async (parts: string[]) => {
|
||||
const { cmdAgentRushAdd } = await import("./commands/agent-rush.js");
|
||||
await cmdAgentRushAdd(parts.join(" "));
|
||||
});
|
||||
|
||||
agentRush
|
||||
.command("search <query...>")
|
||||
.description("Search AGENTRUSH memories.")
|
||||
.addHelpText(
|
||||
"after",
|
||||
'\nExamples:\n $ mem0 agent-rush search "agents and memory and tools"\n $ mem0 agent-rush search "coding assistant"',
|
||||
)
|
||||
.action(async (parts: string[]) => {
|
||||
const { cmdAgentRushSearch } = await import("./commands/agent-rush.js");
|
||||
await cmdAgentRushSearch(parts.join(" "));
|
||||
});
|
||||
|
||||
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||
|
||||
program
|
||||
@@ -769,4 +865,16 @@ program
|
||||
|
||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||
|
||||
program.parse();
|
||||
// Surface any unclaimed Agent Mode notice once per command, after the primary
|
||||
// output. In JSON/agent mode the notice is folded into the envelope by
|
||||
// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
|
||||
function surfaceNotice(): void {
|
||||
const notice = takeNotice();
|
||||
if (notice && !isAgentMode()) {
|
||||
process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
program.parseAsync().finally(() => {
|
||||
surfaceNotice();
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import boxen from "boxen";
|
||||
import Table from "cli-table3";
|
||||
import { colors, sym } from "./branding.js";
|
||||
import { takeNotice } from "./state.js";
|
||||
|
||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||
|
||||
@@ -244,6 +245,15 @@ export function formatJsonEnvelope(opts: {
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
if (opts.error) envelope.error = opts.error;
|
||||
envelope.data = opts.data;
|
||||
|
||||
// If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
// the notice inside the JSON envelope so an agent consuming the output
|
||||
// sees it without needing to inspect HTTP headers.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const { takeNotice } = require("./state.js");
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
|
||||
}
|
||||
if (opts.count !== undefined) envelope.count = opts.count;
|
||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
||||
|
||||
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
// agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
const notice = takeNotice();
|
||||
if (notice) envelope.mem0_notice = notice;
|
||||
|
||||
console.log(JSON.stringify(envelope, null, 2));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
*
|
||||
* Why: the CLI canonical state is ~/.mem0/config.json. MCP servers
|
||||
* (Claude Code plugin, Codex plugin) read MEM0_API_KEY from env or
|
||||
* their own config files. Without a sync, agent-mode bootstrap mints a
|
||||
* new key into config.json but the plugin's MCP keeps using the old
|
||||
* key from env — silent surprise.
|
||||
*
|
||||
* Design:
|
||||
* - Update ONLY entries that already exist; never create new ones
|
||||
* - Preserve surrounding content, formatting, other keys
|
||||
* - Atomic writes (tmp + rename) so a crash mid-write doesn't corrupt
|
||||
* - Idempotent — re-running with the same key is a no-op
|
||||
*
|
||||
* Targets:
|
||||
* - ~/.claude/settings.json::env::MEM0_API_KEY (Claude Code env injection)
|
||||
* - ~/.zshrc / ~/.bashrc `export MEM0_API_KEY="..."` lines
|
||||
*
|
||||
* Out of scope: Codex / Cursor MCP configs and the plugin's own
|
||||
* <plugin-dir>/.api_key file (plugin-managed, different schema).
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
|
||||
const SHELL_RCS = [
|
||||
path.join(os.homedir(), ".zshrc"),
|
||||
path.join(os.homedir(), ".bashrc"),
|
||||
path.join(os.homedir(), ".bash_profile"),
|
||||
];
|
||||
|
||||
// Use [ \t]* (not \s*) so a trailing newline at end-of-file is preserved
|
||||
// when the MEM0_API_KEY export is the last line of the rc file.
|
||||
const RC_LINE_RE =
|
||||
/^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["']?)([^"'\n]*)(["']?)[ \t]*$/m;
|
||||
|
||||
export function syncApiKey(apiKey: string): string[] {
|
||||
if (!apiKey) return [];
|
||||
const updated: string[] = [];
|
||||
if (updateClaudeSettings(CLAUDE_SETTINGS, apiKey)) {
|
||||
updated.push(CLAUDE_SETTINGS);
|
||||
}
|
||||
for (const rc of SHELL_RCS) {
|
||||
if (updateShellRc(rc, apiKey)) updated.push(rc);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateClaudeSettings(
|
||||
filePath: string,
|
||||
apiKey: string,
|
||||
): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let raw: string;
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
raw = fs.readFileSync(filePath, "utf-8");
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const env = data.env;
|
||||
if (!env || typeof env !== "object" || !("MEM0_API_KEY" in env)) {
|
||||
return false; // no existing entry — don't create one
|
||||
}
|
||||
const envObj = env as Record<string, string>;
|
||||
if (envObj.MEM0_API_KEY === apiKey) return false; // already in sync
|
||||
envObj.MEM0_API_KEY = apiKey;
|
||||
atomicWriteText(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
|
||||
export function updateShellRc(filePath: string, apiKey: string): boolean {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
let text: string;
|
||||
try {
|
||||
text = fs.readFileSync(filePath, "utf-8");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const match = text.match(RC_LINE_RE);
|
||||
if (!match) return false; // no existing line
|
||||
if (match[3] === apiKey) return false;
|
||||
const newText = text.replace(
|
||||
RC_LINE_RE,
|
||||
(_full, prefix) => `${prefix}"${apiKey}"`,
|
||||
);
|
||||
atomicWriteText(filePath, newText);
|
||||
return true;
|
||||
}
|
||||
|
||||
function atomicWriteText(filePath: string, content: string): void {
|
||||
const dir = path.dirname(filePath);
|
||||
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
||||
try {
|
||||
fs.writeFileSync(tmp, content, "utf-8");
|
||||
// Preserve permissions if original existed.
|
||||
if (fs.existsSync(filePath)) {
|
||||
try {
|
||||
const mode = fs.statSync(filePath).mode & 0o777;
|
||||
fs.chmodSync(tmp, mode);
|
||||
} catch {
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
fs.renameSync(tmp, filePath);
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.unlinkSync(tmp);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
let _pendingNotice = "";
|
||||
|
||||
export function isAgentMode(): boolean {
|
||||
return _agentMode;
|
||||
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
|
||||
export function setCurrentCommand(name: string): void {
|
||||
_currentCommand = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
|
||||
* command surfacing. Called from the platform backend after each response so
|
||||
* the notice prints once per command regardless of how many sub-requests
|
||||
* fired. Last-write-wins is fine — the message text is identical.
|
||||
*/
|
||||
export function captureNotice(notice: string | null | undefined): void {
|
||||
if (notice) _pendingNotice = notice;
|
||||
}
|
||||
|
||||
export function takeNotice(): string {
|
||||
const msg = _pendingNotice;
|
||||
_pendingNotice = "";
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,9 @@ export function captureEvent(
|
||||
}
|
||||
}
|
||||
|
||||
// M4: every cli.* event carries agent_mode based on the config flag
|
||||
// (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
// join init → add → search funnels in PostHog.
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId,
|
||||
@@ -123,6 +126,7 @@ export function captureEvent(
|
||||
source: "CLI",
|
||||
language: "node",
|
||||
cli_version: CLI_VERSION,
|
||||
agent_mode: Boolean(config.platform.agentMode),
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
...properties,
|
||||
@@ -141,11 +145,11 @@ export function captureEvent(
|
||||
anonDistinctIdToAlias: anonIdToAlias,
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||
{ detached: true, stdio: "ignore" },
|
||||
);
|
||||
const child = spawn(process.execPath, [SENDER_SCRIPT], {
|
||||
detached: true,
|
||||
stdio: ["pipe", "ignore", "ignore"],
|
||||
});
|
||||
child.stdin?.end(JSON.stringify(context));
|
||||
child.unref();
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Standalone telemetry sender — runs as a detached child process.
|
||||
*
|
||||
* Usage: node telemetry-sender.cjs '<json context>'
|
||||
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single
|
||||
* argv argument is still accepted as a legacy fallback)
|
||||
*
|
||||
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||
* of the parent CLI process. It:
|
||||
@@ -19,6 +20,31 @@
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
|
||||
function loadContext() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.argv[2]) {
|
||||
try {
|
||||
resolve(JSON.parse(process.argv[2]));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => (data += chunk));
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function httpsRequest(url, method, headers, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
@@ -108,7 +134,7 @@ async function sendIdentifyEvent(ctx, payload, anonId) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = JSON.parse(process.argv[2]);
|
||||
const ctx = await loadContext();
|
||||
const payload = ctx.payload;
|
||||
|
||||
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
|
||||
* in sync so that the Python and Node CLIs expose an identical surface
|
||||
* for the Agent Mode entrypoint. If you add a flag here, add the same
|
||||
* assertion on the Python side (and vice versa).
|
||||
*
|
||||
* Network-bound bootstrap is covered by the platform-side E2E suite
|
||||
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
|
||||
* the CLI surface that ships in the binary.
|
||||
*/
|
||||
|
||||
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 };
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function cleanHome(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
}
|
||||
|
||||
describe("init flag surface", () => {
|
||||
it("init --help lists --agent", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--agent");
|
||||
});
|
||||
|
||||
it("init --help describes Agent Mode", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Description must mention what --agent actually does so an agent
|
||||
// reading the help can self-discover the bootstrap entrypoint.
|
||||
expect(
|
||||
result.stdout.includes("Agent Mode") ||
|
||||
result.stdout.toLowerCase().includes("unattended"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("init --help lists --source", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--source");
|
||||
});
|
||||
|
||||
it("init --help lists --email and --code", () => {
|
||||
const result = run(["init", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("--email");
|
||||
expect(result.stdout).toContain("--code");
|
||||
});
|
||||
});
|
||||
|
||||
describe("argv preprocessing — --agent reaches init subcommand", () => {
|
||||
// Regression for the bug where the global --agent JSON-alias swallowed
|
||||
// the init-level --agent flag, making `mem0 init --agent` behave like
|
||||
// the plain interactive wizard.
|
||||
|
||||
it("init --agent triggers bootstrap branch (not the wizard)", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = (result.stdout + result.stderr).toLowerCase();
|
||||
// Either bootstrap-attempt error, or a connection/network error —
|
||||
// both prove the --agent path executed (the wizard would prompt for
|
||||
// input and succeed/hang, not surface a network error).
|
||||
expect(
|
||||
combined.includes("agent") ||
|
||||
combined.includes("connect") ||
|
||||
combined.includes("network") ||
|
||||
combined.includes("fetch") ||
|
||||
combined.includes("bootstrap"),
|
||||
).toBe(true);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON envelope on network failure", () => {
|
||||
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
|
||||
const home = cleanHome();
|
||||
const result = run(["init", "--agent", "--json"], {
|
||||
home,
|
||||
env: {
|
||||
MEM0_BASE_URL: "http://127.0.0.1:1",
|
||||
FORCE_COLOR: "0",
|
||||
},
|
||||
});
|
||||
const combined = result.stdout + result.stderr;
|
||||
// No raw Node stack should escape the agent-mode handler.
|
||||
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
|
||||
expect(combined).not.toContain("UnhandledPromiseRejection");
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
fs.rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("top-level help lists init", () => {
|
||||
// `mem0 --help` must list `init` so agents walking the top-level help
|
||||
// can discover the Agent Mode entrypoint without prior knowledge.
|
||||
|
||||
it("--help lists init", () => {
|
||||
const result = run(["--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("init");
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { Command } from "commander";
|
||||
import { createMockBackend } from "./setup.js";
|
||||
import type { Backend } from "../src/backend/base.js";
|
||||
import { setAgentMode } from "../src/state.js";
|
||||
@@ -41,8 +42,6 @@ describe("cmdAdd", () => {
|
||||
await cmdAdd(mockBackend, "I prefer dark mode", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||
@@ -54,8 +53,6 @@ describe("cmdAdd", () => {
|
||||
userId: "alice",
|
||||
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||
@@ -66,8 +63,6 @@ describe("cmdAdd", () => {
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "json",
|
||||
});
|
||||
expect(output).toContain("results");
|
||||
@@ -78,14 +73,59 @@ describe("cmdAdd", () => {
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "quiet",
|
||||
});
|
||||
expect(output).not.toContain("dark mode");
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdAdd forwards --no-infer (regression for #5261)", () => {
|
||||
it("forwards infer: false when --no-infer is set", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
// `infer: false` is the shape Commander produces for `--no-infer`.
|
||||
await cmdAdd(mockBackend, "store me verbatim", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
infer: false,
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledWith(
|
||||
"store me verbatim",
|
||||
undefined,
|
||||
expect.objectContaining({ infer: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards infer: true by default (flag absent)", async () => {
|
||||
const { cmdAdd } = await import("../src/commands/memory.js");
|
||||
await cmdAdd(mockBackend, "infer me", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
output: "text",
|
||||
});
|
||||
expect(mockBackend.add).toHaveBeenCalledWith(
|
||||
"infer me",
|
||||
undefined,
|
||||
expect.objectContaining({ infer: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("Commander stores --no-infer as opts.infer, not opts.noInfer", () => {
|
||||
// Pins the assumption the fix relies on: Commander's `--no-X` option
|
||||
// populates the positive camelCase key (`infer`), never `noInfer`.
|
||||
const withFlag = new Command();
|
||||
withFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
|
||||
withFlag.parse(["--no-infer"], { from: "user" });
|
||||
expect(withFlag.opts().infer).toBe(false);
|
||||
expect(withFlag.opts().noInfer).toBeUndefined();
|
||||
|
||||
const withoutFlag = new Command();
|
||||
withoutFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
|
||||
withoutFlag.parse([], { from: "user" });
|
||||
expect(withoutFlag.opts().infer).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cmdAdd deduplicates PENDING", () => {
|
||||
const DUPLICATE_PENDING = {
|
||||
results: [
|
||||
@@ -100,8 +140,6 @@ describe("cmdAdd deduplicates PENDING", () => {
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "text",
|
||||
});
|
||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||
@@ -113,8 +151,6 @@ describe("cmdAdd deduplicates PENDING", () => {
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "json",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
@@ -129,8 +165,6 @@ describe("cmdAdd deduplicates PENDING", () => {
|
||||
await cmdAdd(mockBackend, "test", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const data = JSON.parse(output);
|
||||
@@ -315,8 +349,6 @@ describe("agent mode", () => {
|
||||
await cmdAdd(mockBackend, "test preference", {
|
||||
userId: "alice",
|
||||
immutable: false,
|
||||
noInfer: false,
|
||||
|
||||
output: "agent",
|
||||
});
|
||||
const parsed = JSON.parse(output.trim());
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Unit tests for init internals — decision tree primitives + plugin sync.
|
||||
*
|
||||
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
|
||||
* in sync — if you add a behavioral assertion here, mirror it on the Python
|
||||
* side and vice versa.
|
||||
*
|
||||
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
|
||||
* flap silently mints a new shadow over a working key).
|
||||
* - `plugin_sync` must only update entries that already exist, preserve
|
||||
* trailing newlines, and never mangle other lines.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { pingKey } from "../src/commands/init.js";
|
||||
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
|
||||
|
||||
// ── pingKey ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("pingKey — network vs auth distinction", () => {
|
||||
const origFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = origFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("returns true for 200", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for 401 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for 403 (definitively invalid)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on network error (prefer reuse over re-mint)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true on timeout (prefer reuse)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
|
||||
await expect(pingKey("k", "http://x")).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateShellRc ────────────────────────────────────────────────────────
|
||||
|
||||
describe("updateShellRc — exists-only contract", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("updates existing export and preserves trailing newline", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
|
||||
expect(updateShellRc(rc, "newkey")).toBe(true);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("does NOT create a new export when none exists", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, "alias ll='ls -la'\n");
|
||||
expect(updateShellRc(rc, "newkey")).toBe(false);
|
||||
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
|
||||
});
|
||||
|
||||
it("preserves surrounding content", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
const original =
|
||||
"# my zshrc\n" +
|
||||
"alias ll='ls -la'\n" +
|
||||
"export MEM0_API_KEY='old'\n" +
|
||||
"export OTHER=keepme\n";
|
||||
fs.writeFileSync(rc, original);
|
||||
updateShellRc(rc, "newkey");
|
||||
const after = fs.readFileSync(rc, "utf-8");
|
||||
expect(after).toContain("alias ll='ls -la'\n");
|
||||
expect(after).toContain("export OTHER=keepme\n");
|
||||
expect(after).toContain("# my zshrc\n");
|
||||
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc");
|
||||
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
|
||||
expect(updateShellRc(rc, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for missing files", () => {
|
||||
const rc = path.join(tmpDir, ".zshrc"); // does not exist
|
||||
expect(updateShellRc(rc, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── updateClaudeSettings ─────────────────────────────────────────────────
|
||||
|
||||
describe("updateClaudeSettings — never creates entries", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
|
||||
});
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("does not create env block when none exists", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
|
||||
otherKey: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create MEM0_API_KEY entry in existing env block", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
|
||||
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
|
||||
});
|
||||
|
||||
it("updates existing entry and preserves siblings", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
|
||||
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
|
||||
expect(data.env.MEM0_API_KEY).toBe("fresh");
|
||||
expect(data.env.OTHER).toBe("y");
|
||||
});
|
||||
|
||||
it("is idempotent when value already matches", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(
|
||||
settings,
|
||||
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
|
||||
);
|
||||
expect(updateClaudeSettings(settings, "same")).toBe(false);
|
||||
});
|
||||
|
||||
it("is a no-op for malformed JSON", () => {
|
||||
const settings = path.join(tmpDir, "settings.json");
|
||||
fs.writeFileSync(settings, "{ this is not json");
|
||||
expect(updateClaudeSettings(settings, "x")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockLoadConfig = vi.fn();
|
||||
const mockSaveConfig = vi.fn();
|
||||
const mockSpawn = vi.fn();
|
||||
|
||||
vi.mock("../src/config.js", () => ({
|
||||
CONFIG_FILE: "/tmp/mem0-config.json",
|
||||
loadConfig: mockLoadConfig,
|
||||
saveConfig: mockSaveConfig,
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawn: mockSpawn,
|
||||
}));
|
||||
|
||||
describe("captureEvent", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockLoadConfig.mockReset();
|
||||
mockSaveConfig.mockReset();
|
||||
mockSpawn.mockReset();
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
});
|
||||
|
||||
it("pipes the telemetry context through stdin instead of argv", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
platform: {
|
||||
apiKey: "m0-node-secret",
|
||||
baseUrl: "https://api.mem0.ai",
|
||||
userEmail: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "cli-anon-node",
|
||||
},
|
||||
});
|
||||
|
||||
const stdin = { end: vi.fn() };
|
||||
const child = { stdin, unref: vi.fn() };
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const { captureEvent } = await import("../src/telemetry.js");
|
||||
captureEvent("node_test_event", { case: "stdin-secret" });
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
||||
const [execPath, args, options] = mockSpawn.mock.calls[0];
|
||||
expect(execPath).toBe(process.execPath);
|
||||
expect(args).toHaveLength(1);
|
||||
expect(String(args[0])).toContain("telemetry-sender.cjs");
|
||||
expect(JSON.stringify(args)).not.toContain("m0-node-secret");
|
||||
expect(options).toMatchObject({ detached: true, stdio: ["pipe", "ignore", "ignore"] });
|
||||
|
||||
expect(stdin.end).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(stdin.end.mock.calls[0][0]);
|
||||
expect(payload.mem0ApiKey).toBe("m0-node-secret");
|
||||
expect(payload.payload.event).toBe("node_test_event");
|
||||
expect(child.unref).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,4 +8,10 @@ export default defineConfig({
|
||||
define: {
|
||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
test: {
|
||||
// Integration tests spawn the CLI via `npx tsx` (15s subprocess
|
||||
// timeout); the first spawn in a file pays a cold-start cost that can
|
||||
// exceed vitest's 5s default on CI runners.
|
||||
testTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `mem0-cli` (Python) are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.8] — 2026-06-19
|
||||
|
||||
### Security
|
||||
|
||||
- Telemetry no longer passes the Mem0 API key to its child process via
|
||||
command-line arguments. The context is now sent over stdin, so the key is no
|
||||
longer visible in the process list (`ps`, `/proc/<pid>/cmdline`, Activity
|
||||
Monitor). Fixes #4862.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `__version__` now matches the packaged version (was stale at 0.2.4).
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/python/`.
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.4"
|
||||
version = "0.2.8"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.4"
|
||||
__version__ = "0.2.8"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Detect whether the CLI is being invoked from inside an AI-agent context.
|
||||
|
||||
Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
|
||||
agent runtime env var is present. The return value is a context **trigger
|
||||
only** — the canonical agent identity is self-declared by the agent via
|
||||
``--agent-caller <name>`` (Proof Editor-style) and never sniffed from env
|
||||
vars to fill the ``agent_caller`` field on the APIKey row.
|
||||
|
||||
Returns a short name or None. The list is curated, not exhaustive — env
|
||||
vars we don't recognise fall through to None (caller treated as
|
||||
non-agent). Honest reporting depends on ``--agent-caller``; this list is
|
||||
just enough to enable the zero-friction auto-bootstrap UX.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
|
||||
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
|
||||
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
|
||||
("cline", ("CLINE_AGENT", "CLINE")),
|
||||
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
|
||||
("aider", ("AIDER_SESSION",)),
|
||||
("goose", ("GOOSE_AGENT",)),
|
||||
("windsurf", ("WINDSURF_AGENT",)),
|
||||
)
|
||||
|
||||
|
||||
def detect_agent_caller() -> str | None:
|
||||
"""Return a canonical agent name if any agent env var is set, else None."""
|
||||
for name, env_vars in _AGENT_CALLER_ENV:
|
||||
if any(os.environ.get(v) for v in env_vars):
|
||||
return name
|
||||
return None
|
||||
@@ -237,6 +237,14 @@ def main_callback(
|
||||
cmd_version()
|
||||
raise typer.Exit()
|
||||
if ctx.invoked_subcommand:
|
||||
# Stash the active subcommand name so the JSON error envelope
|
||||
# (print_error in agent mode) can report which command failed
|
||||
# instead of an empty `"command": ""` field.
|
||||
from mem0_cli.state import set_current_command
|
||||
|
||||
set_current_command(ctx.invoked_subcommand)
|
||||
if ctx.invoked_subcommand and ctx.invoked_subcommand != "init":
|
||||
# init fires its own telemetry from init_cmd.run_init with full M1-M6 props.
|
||||
_fire_telemetry(ctx.invoked_subcommand)
|
||||
|
||||
|
||||
@@ -851,6 +859,19 @@ def init(
|
||||
force: bool = typer.Option(
|
||||
False, "--force", help="Overwrite existing config without confirmation."
|
||||
),
|
||||
agent_signal: bool = typer.Option(
|
||||
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
|
||||
),
|
||||
source: str | None = typer.Option(
|
||||
None,
|
||||
"--source",
|
||||
help="Channel attribution for signup (e.g. github, hn, ph).",
|
||||
),
|
||||
agent_caller: str | None = typer.Option(
|
||||
None,
|
||||
"--agent-caller",
|
||||
help="Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
|
||||
),
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
@@ -859,10 +880,97 @@ def init(
|
||||
mem0 init --api-key m0-xxx --user-id alice
|
||||
mem0 init --email alice@company.com
|
||||
mem0 init --email alice@company.com --code 482901
|
||||
mem0 init --agent --agent-caller claude-code # AI agent self-identifies on Agent Mode bootstrap
|
||||
mem0 init --email alice@company.com # Claims an existing Agent Mode key when one is present
|
||||
"""
|
||||
from mem0_cli.commands.init_cmd import run_init
|
||||
|
||||
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
||||
run_init(
|
||||
api_key=api_key,
|
||||
user_id=user_id,
|
||||
email=email,
|
||||
code=code,
|
||||
force=force,
|
||||
source=source,
|
||||
agent=agent_signal,
|
||||
agent_caller=agent_caller,
|
||||
)
|
||||
|
||||
|
||||
@app.command(rich_help_panel="Setup")
|
||||
def identify(
|
||||
name: str = typer.Argument(..., help="Agent identity (e.g. claude-code, cursor, my-bot)."),
|
||||
) -> None:
|
||||
"""Tag your active Agent Mode key with the AI agent that's using it.
|
||||
|
||||
Run this once after `mem0 init --agent` if you didn't pass --agent-caller.
|
||||
Idempotent — re-running just overwrites the value.
|
||||
|
||||
Example:
|
||||
mem0 identify claude-code
|
||||
"""
|
||||
from mem0_cli.commands.identify_cmd import run_identify
|
||||
|
||||
run_identify(name)
|
||||
|
||||
|
||||
@app.command(name="whoami", rich_help_panel="Setup")
|
||||
def whoami_cmd() -> None:
|
||||
"""Print your AGENTRUSH identifier (default_user_id).
|
||||
|
||||
Example:
|
||||
mem0 whoami
|
||||
"""
|
||||
from mem0_cli.commands.whoami_cmd import run_whoami
|
||||
|
||||
run_whoami()
|
||||
|
||||
|
||||
# ── AGENTRUSH sub-app ─────────────────────────────────────────────────────
|
||||
|
||||
agent_rush_app = typer.Typer(
|
||||
name="agent-rush",
|
||||
help="AGENTRUSH game commands",
|
||||
no_args_is_help=True,
|
||||
rich_markup_mode="rich",
|
||||
)
|
||||
|
||||
|
||||
@agent_rush_app.callback(invoke_without_command=True)
|
||||
def _agent_rush_callback(ctx: typer.Context) -> None:
|
||||
if ctx.invoked_subcommand:
|
||||
_fire_telemetry(f"agent-rush.{ctx.invoked_subcommand}")
|
||||
|
||||
|
||||
@agent_rush_app.command(name="add")
|
||||
def agent_rush_add(
|
||||
content: str = typer.Argument(..., help="Memory content (50-1000 characters, no URLs)."),
|
||||
) -> None:
|
||||
"""Submit a memory to AGENTRUSH.
|
||||
|
||||
Example:
|
||||
mem0 agent-rush add "I enjoy solving constraint-satisfaction problems."
|
||||
"""
|
||||
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_add
|
||||
|
||||
run_agent_rush_add(content)
|
||||
|
||||
|
||||
@agent_rush_app.command(name="search")
|
||||
def agent_rush_search(
|
||||
query: str = typer.Argument(..., help="Search query."),
|
||||
) -> None:
|
||||
"""Search AGENTRUSH memories.
|
||||
|
||||
Example:
|
||||
mem0 agent-rush search "constraint satisfaction"
|
||||
"""
|
||||
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_search
|
||||
|
||||
run_agent_rush_search(query)
|
||||
|
||||
|
||||
app.add_typer(agent_rush_app, name="agent-rush", rich_help_panel="Setup")
|
||||
|
||||
|
||||
# (entity_app registered at module level, below sub-group definitions)
|
||||
@@ -1198,11 +1306,28 @@ def main() -> None:
|
||||
import sys
|
||||
|
||||
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||
_json_flags = {"--json", "--agent"}
|
||||
if any(a in _json_flags for a in sys.argv[1:]):
|
||||
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
||||
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
||||
argv_rest = sys.argv[1:]
|
||||
is_init = "init" in argv_rest
|
||||
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
|
||||
if any(a in _global_flags for a in argv_rest):
|
||||
from mem0_cli.state import set_agent_mode
|
||||
|
||||
set_agent_mode(True)
|
||||
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
||||
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
||||
|
||||
app()
|
||||
try:
|
||||
app()
|
||||
finally:
|
||||
# Surface any unclaimed Agent Mode notice once per command, after the
|
||||
# primary output. In JSON/agent mode the notice is folded into the
|
||||
# envelope by format_json_envelope, so skip the stderr banner there
|
||||
# to avoid duplicate output.
|
||||
from mem0_cli.state import is_agent_mode, take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice and not is_agent_mode():
|
||||
from rich.console import Console
|
||||
|
||||
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||
|
||||
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||
from mem0_cli.state import is_agent_mode
|
||||
from mem0_cli.state import capture_notice, is_agent_mode
|
||||
|
||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||
resp = self._client.request(method, path, **kwargs)
|
||||
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
|
||||
resp.raise_for_status()
|
||||
if resp.status_code == 204:
|
||||
return {}
|
||||
return resp.json()
|
||||
data = resp.json()
|
||||
|
||||
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
|
||||
# fallback for endpoints that return non-dict / non-dict-leading
|
||||
# payloads) and stash it for end-of-command surfacing.
|
||||
notice = None
|
||||
if isinstance(data, dict) and "mem0_notice" in data:
|
||||
notice = data.pop("mem0_notice")
|
||||
elif (
|
||||
isinstance(data, list)
|
||||
and data
|
||||
and isinstance(data[0], dict)
|
||||
and "mem0_notice" in data[0]
|
||||
):
|
||||
notice = data[0].pop("mem0_notice")
|
||||
if notice is None:
|
||||
notice = resp.headers.get("X-Mem0-Notice-Message") or None
|
||||
capture_notice(notice)
|
||||
|
||||
return data
|
||||
|
||||
def add(
|
||||
self,
|
||||
|
||||
@@ -87,10 +87,12 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
||||
}
|
||||
print(_json.dumps(envelope))
|
||||
return
|
||||
from rich.markup import escape
|
||||
|
||||
sym = _sym("✗", "[error]")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
|
||||
if hint:
|
||||
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
||||
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
|
||||
|
||||
|
||||
def print_warning(console: Console, message: str) -> None:
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Agent Mode commands — bootstrap (unattended signup) and claim (OTP-based human upgrade)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from mem0_cli.branding import (
|
||||
BRAND_COLOR,
|
||||
DIM_COLOR,
|
||||
print_error,
|
||||
print_success,
|
||||
)
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def _validate_envelope(envelope: Any) -> None:
|
||||
"""Defend against partial/malformed backend responses.
|
||||
|
||||
A backend regression that returns ``{"api_key": null}`` would otherwise be
|
||||
silently persisted, producing confusing downstream errors far from the
|
||||
source. Fail fast with a clear message if the required fields are missing.
|
||||
"""
|
||||
if not isinstance(envelope, dict):
|
||||
print_error(err_console, "Bootstrap response was not a JSON object.")
|
||||
raise typer.Exit(1)
|
||||
for field in ("api_key", "default_user_id"):
|
||||
value = envelope.get(field)
|
||||
if not isinstance(value, str) or not value:
|
||||
print_error(
|
||||
err_console,
|
||||
f"Bootstrap response missing required field {field!r} — please update the CLI.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def bootstrap_via_backend(
|
||||
config: Mem0Config,
|
||||
*,
|
||||
source: str | None = None,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
|
||||
|
||||
Args:
|
||||
config: Mem0Config mutated in place with the new platform values.
|
||||
source: ``--source`` flag passthrough (analytics tag, free-form).
|
||||
agent_caller: Self-declared agent identity passed via ``--agent-caller``
|
||||
(e.g. ``claude-code``, ``cursor``). May be None when the caller
|
||||
omitted the flag; the agent can backfill later via
|
||||
``mem0 identify <name>``. Sent to the backend in the request body
|
||||
and saved into ``platform.agent_caller`` for local introspection.
|
||||
|
||||
Raises typer.Exit(1) on failure.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
body: dict[str, Any] = {}
|
||||
if source:
|
||||
body["source"] = source
|
||||
if agent_caller:
|
||||
body["agent_caller"] = agent_caller
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(
|
||||
f"{base_url}/api/v1/auth/agent_mode/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json=body,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error contacting Mem0: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code == 429:
|
||||
print_error(err_console, "Rate-limited. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code == 503:
|
||||
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
|
||||
raise typer.Exit(1)
|
||||
if resp.status_code != 200:
|
||||
detail = resp.text
|
||||
try:
|
||||
err_body = resp.json()
|
||||
detail = err_body.get("error") or err_body.get("detail") or resp.text
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
pass
|
||||
# Backend's @ratelimit decorator raises PermissionDenied, which DRF
|
||||
# translates to a generic 403 "You do not have permission to perform
|
||||
# this action." That's opaque — surface as the rate-limit it actually is.
|
||||
if resp.status_code == 403 and "permission" in str(detail).lower():
|
||||
print_error(
|
||||
err_console,
|
||||
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
print_error(err_console, f"Bootstrap failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
envelope = resp.json()
|
||||
_validate_envelope(envelope)
|
||||
config.platform.api_key = envelope["api_key"]
|
||||
config.platform.base_url = base_url
|
||||
config.platform.agent_mode = True
|
||||
config.platform.created_via = "agent_mode"
|
||||
config.platform.agent_caller = agent_caller or ""
|
||||
config.platform.claimed_at = ""
|
||||
config.platform.default_user_id = envelope["default_user_id"]
|
||||
# Adopt the slug-derived user_id as the default scope for memory ops.
|
||||
config.defaults.user_id = envelope["default_user_id"]
|
||||
save_config(config)
|
||||
|
||||
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
|
||||
notice = envelope.get("mem0_notice")
|
||||
if notice:
|
||||
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
|
||||
else:
|
||||
# Fallback if the backend hasn't deployed the unified notice yet.
|
||||
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
|
||||
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
|
||||
|
||||
if not agent_caller:
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]If you are an AI agent, identify yourself: "
|
||||
f"`mem0 identify <your-name>` (e.g. claude-code, cursor).[/]"
|
||||
)
|
||||
|
||||
|
||||
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
|
||||
"""Claim an existing Agent Mode account via OTP — no browser, no polling.
|
||||
|
||||
Reuses the standard email-code flow (`/api/v1/auth/email_code/` then
|
||||
`/.../verify/`) and adds the local agent-mode API key in the verify body
|
||||
as `agent_mode_api_key`. Backend's `verify_email_code` runs the
|
||||
upgrade-in-place transaction inline and returns claim result.
|
||||
|
||||
On success: flips `platform.agent_mode=false`, sets `claimed_at`, stamps
|
||||
`user_email`. The api_key value itself never changes.
|
||||
"""
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
if not config.platform.api_key or not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command requires an active Agent Mode config. Run `mem0 init` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
raw_key = config.platform.api_key
|
||||
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
# Step 1: request OTP (unless --code provided)
|
||||
if not code:
|
||||
send = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={"email": email},
|
||||
)
|
||||
if send.status_code == 429:
|
||||
print_error(err_console, "Too many attempts. Try again in a few minutes.")
|
||||
raise typer.Exit(1)
|
||||
if send.status_code != 200:
|
||||
try:
|
||||
detail = send.json().get("error", send.text)
|
||||
except Exception:
|
||||
detail = send.text
|
||||
print_error(err_console, f"Failed to send code: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
print_success(console, f"Verification code sent to {email}. Check your inbox.")
|
||||
|
||||
if not sys.stdin.isatty():
|
||||
print_error(
|
||||
err_console,
|
||||
"No --code provided and terminal is non-interactive.",
|
||||
hint=f"Re-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 2: verify + claim in one shot
|
||||
verify = client.post(
|
||||
f"{base_url}/api/v1/auth/email_code/verify/",
|
||||
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
|
||||
json={
|
||||
"email": email,
|
||||
"code": code.strip(),
|
||||
"agent_mode_api_key": raw_key,
|
||||
},
|
||||
)
|
||||
|
||||
if verify.status_code != 200:
|
||||
try:
|
||||
err_body = verify.json()
|
||||
detail = err_body.get("error", verify.text)
|
||||
code_str = err_body.get("code", "")
|
||||
except (json.JSONDecodeError, ValueError, AttributeError):
|
||||
detail = verify.text
|
||||
code_str = ""
|
||||
print_error(err_console, f"Claim failed: {detail}")
|
||||
if code_str == "email_already_claimed":
|
||||
console.print(
|
||||
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.[/]"
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
claim_body = verify.json()
|
||||
if not claim_body.get("claimed"):
|
||||
print_error(err_console, f"Unexpected verify response: {claim_body}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.agent_mode = False
|
||||
config.platform.claimed_at = claim_body.get("claimed_at") or _utcnow_iso()
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
save_config(config)
|
||||
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
|
||||
|
||||
|
||||
def _utcnow_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
@@ -0,0 +1,132 @@
|
||||
"""mem0 agent-rush — AGENTRUSH game commands.
|
||||
|
||||
Wraps the platform's /v1/agent-rush/{memories/, memories/search/} endpoints.
|
||||
Hardcoded routing; no flags needed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import print_error, print_success
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_PII_WARNING_LINES = (
|
||||
"",
|
||||
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
|
||||
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
|
||||
"",
|
||||
)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
"X-Mem0-Mode": "agent-rush",
|
||||
}
|
||||
|
||||
_ERROR_HINTS = {
|
||||
"agentrush_search_first": "Run 3 'mem0 agent-rush search' commands before adding.",
|
||||
"agentrush_search_quota": "You've used your 3 lifetime searches.",
|
||||
"agentrush_add_quota": "You've used your 3 lifetime adds.",
|
||||
"agentrush_not_agent_mode": "Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
|
||||
"agentrush_length": "Memory text must be 50-1000 characters.",
|
||||
"agentrush_no_urls": "URLs are not allowed.",
|
||||
"agentrush_blocklist": "Content contains a blocked term.",
|
||||
"agentrush_global_quota": "Event-wide cap reached. Try again later.",
|
||||
"agentrush_not_provisioned": "AGENTRUSH is not provisioned in this environment.",
|
||||
}
|
||||
|
||||
|
||||
def _call(path: str, body: dict) -> dict:
|
||||
config = load_config()
|
||||
if not config.platform.api_key:
|
||||
print_error(err_console, "Not initialized. Run `mem0 init --agent` first.")
|
||||
raise typer.Exit(1)
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.post(
|
||||
f"{base_url}{path}",
|
||||
headers={
|
||||
**_SOURCE_HEADERS,
|
||||
"Authorization": f"Token {config.platform.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json=body,
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
data = {}
|
||||
if resp.status_code >= 400:
|
||||
code = (
|
||||
(data.get("error") or {}).get("code", "unknown")
|
||||
if isinstance(data, dict)
|
||||
else "unknown"
|
||||
)
|
||||
print_error(err_console, f"AGENTRUSH error: {code}")
|
||||
hint = _ERROR_HINTS.get(code)
|
||||
if hint:
|
||||
console.print(f" [dim]{hint}[/dim]")
|
||||
raise typer.Exit(1)
|
||||
return data
|
||||
|
||||
|
||||
def _ensure_warning_acknowledged() -> None:
|
||||
"""Block the first interactive add on the PII warning; pass-through for agents.
|
||||
|
||||
Interactive (TTY): show prompt, require explicit 'y', persist
|
||||
`agent_rush.acknowledged_at` so we never ask the same machine twice.
|
||||
|
||||
Non-interactive (no TTY — typical when an agent runs the CLI): surface
|
||||
the warning to stderr for the human reading the agent transcript and
|
||||
proceed without prompting (agents can't answer y/N).
|
||||
"""
|
||||
config = load_config()
|
||||
if config.agent_rush.acknowledged_at:
|
||||
return
|
||||
|
||||
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
|
||||
if not is_tty:
|
||||
for line in _PII_WARNING_LINES:
|
||||
err_console.print(line)
|
||||
return
|
||||
|
||||
for line in _PII_WARNING_LINES:
|
||||
console.print(line)
|
||||
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
|
||||
if answer not in ("y", "yes"):
|
||||
print_error(err_console, "Aborted.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
|
||||
save_config(config)
|
||||
|
||||
|
||||
def run_agent_rush_add(content: str) -> None:
|
||||
_ensure_warning_acknowledged()
|
||||
result = _call("/v1/agent-rush/memories/", {"content": content})
|
||||
event_id = result.get("event_id", "?")
|
||||
print_success(console, f"Memory submitted (event_id: {event_id})")
|
||||
|
||||
|
||||
def run_agent_rush_search(query: str) -> None:
|
||||
result = _call("/v1/agent-rush/memories/search/", {"query": query})
|
||||
memories = result.get("results") or result.get("memories") or []
|
||||
if not memories:
|
||||
console.print("[dim](no results)[/dim]")
|
||||
return
|
||||
for i, m in enumerate(memories[:5], start=1):
|
||||
text = m.get("memory") if isinstance(m, dict) else str(m)
|
||||
console.print(f" {i}. {text}")
|
||||
@@ -0,0 +1,75 @@
|
||||
"""mem0 identify — declare which agent owns the current agent-mode key.
|
||||
|
||||
Used when `mem0 init --agent` ran without --agent-caller, so the backend
|
||||
saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
|
||||
its own row with its real identity. Idempotent — running it again just
|
||||
overwrites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import print_error, print_success
|
||||
from mem0_cli.config import load_config, save_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
_SOURCE_HEADERS = {
|
||||
"X-Mem0-Source": "cli",
|
||||
"X-Mem0-Client-Language": "python",
|
||||
}
|
||||
|
||||
|
||||
def run_identify(name: str) -> None:
|
||||
"""PATCH the active agent-mode key's agent_caller field."""
|
||||
config = load_config()
|
||||
if not config.platform.api_key:
|
||||
print_error(
|
||||
err_console,
|
||||
"No API key configured. Run `mem0 init --agent` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if not config.platform.agent_mode:
|
||||
print_error(
|
||||
err_console,
|
||||
"This command only works on unclaimed agent-mode keys.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
print_error(err_console, "Agent name is required.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
|
||||
try:
|
||||
with httpx.Client(timeout=30.0) as client:
|
||||
resp = client.patch(
|
||||
f"{base_url}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
**_SOURCE_HEADERS,
|
||||
"Authorization": f"Token {config.platform.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": name},
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
print_error(err_console, f"Network error: {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
if resp.status_code != 200:
|
||||
try:
|
||||
detail = resp.json().get("error", resp.text)
|
||||
except Exception:
|
||||
detail = resp.text
|
||||
print_error(err_console, f"Identify failed: {detail}")
|
||||
raise typer.Exit(1)
|
||||
|
||||
canonical = resp.json().get("agent_caller", name)
|
||||
config.platform.agent_caller = canonical
|
||||
save_config(config)
|
||||
print_success(console, f"Identified as {canonical}.")
|
||||
@@ -103,6 +103,25 @@ def _validate_email(email: str) -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool:
|
||||
"""Validate api_key against /v1/ping/.
|
||||
|
||||
Returns False ONLY on a definitive "invalid key" signal (HTTP 401 / 403).
|
||||
Network errors, timeouts, and 5xx responses return True so we prefer
|
||||
reusing an existing key over silently minting a new shadow on a transient
|
||||
blip (which would also clobber config + plugin-sync targets).
|
||||
"""
|
||||
try:
|
||||
resp = httpx.get(
|
||||
f"{base_url.rstrip('/')}/v1/ping/",
|
||||
headers={"Authorization": f"Token {api_key}"},
|
||||
timeout=timeout,
|
||||
)
|
||||
except httpx.HTTPError:
|
||||
return True # unknown — prefer reuse
|
||||
return resp.status_code not in (401, 403)
|
||||
|
||||
|
||||
def _email_login(
|
||||
email: str,
|
||||
code: str | None,
|
||||
@@ -182,21 +201,143 @@ def run_init(
|
||||
email: str | None = None,
|
||||
code: str | None = None,
|
||||
force: bool = False,
|
||||
source: str | None = None,
|
||||
agent: bool = False,
|
||||
agent_caller: str | None = None,
|
||||
) -> None:
|
||||
"""Interactive setup wizard for mem0 CLI.
|
||||
|
||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||
(non-interactive mode). When running in a non-TTY without the required
|
||||
flags, an error message is printed.
|
||||
|
||||
Agent Mode dispatch (no email/api-key flags):
|
||||
- If existing config has an active API key → reuse (existing_key path).
|
||||
- Else if any positive agent signal (--agent, --json global, agent env
|
||||
var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
|
||||
- Else fall through to the interactive wizard.
|
||||
|
||||
Claim dispatch:
|
||||
- If `--email` is set AND existing config has `agent_mode=true`, run the
|
||||
claim device-flow against the existing key instead of minting a new
|
||||
email-based key.
|
||||
"""
|
||||
from mem0_cli.agent_detect import detect_agent_caller
|
||||
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_otp
|
||||
from mem0_cli.state import is_agent_mode as _global_agent_mode
|
||||
from mem0_cli.telemetry import capture_event
|
||||
|
||||
def _fire_init(mode: str, *, claimed: bool = False) -> None:
|
||||
"""Fire cli.init telemetry with M1-M6 properties."""
|
||||
props: dict = {"command": "init", "mode": mode}
|
||||
if agent_caller:
|
||||
# Self-declared via --agent-caller; not sniffed from env vars.
|
||||
props["agent_caller"] = agent_caller
|
||||
if source:
|
||||
props["signup_source"] = source
|
||||
if claimed:
|
||||
props["claimed_agent_mode"] = True
|
||||
capture_event("cli.init", props)
|
||||
|
||||
config = Mem0Config()
|
||||
|
||||
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
||||
config.platform.base_url = base_url
|
||||
|
||||
if code and not email:
|
||||
print_error(err_console, "--code requires --email.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# ── Email + existing agent-mode config → claim flow ─────────────────
|
||||
if email and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
if existing.platform.agent_mode and existing.platform.api_key:
|
||||
email = email.strip().lower()
|
||||
_validate_email(email)
|
||||
print_info(console, f"Claiming Agent Mode account to {email}...")
|
||||
claim_via_otp(existing, email=email, code=code)
|
||||
_fire_init("email", claimed=True)
|
||||
return
|
||||
|
||||
# ── Agent Mode path runs BEFORE the existing-config guard ──────────
|
||||
# Rules 1/2 REUSE a valid existing key (not overwrite), so we must
|
||||
# short-circuit before the guard prompts. Rule 3 mints only when there
|
||||
# is no valid key to reuse — in that case overwriting is correct.
|
||||
_agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
|
||||
if not api_key and not email and _agent_ctx:
|
||||
from mem0_cli.output import format_json_envelope
|
||||
from mem0_cli.state import is_agent_mode as _is_json_mode
|
||||
|
||||
def _emit_reuse(source: str) -> None:
|
||||
if _is_json_mode():
|
||||
format_json_envelope(
|
||||
console,
|
||||
command="init",
|
||||
data={
|
||||
"api_key_saved": False,
|
||||
"api_key_source": source,
|
||||
"agent_mode": False,
|
||||
"message": "Existing Mem0 API key found and reused. No Agent Mode key was created.",
|
||||
},
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
|
||||
if source == "env"
|
||||
else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted."
|
||||
)
|
||||
print_success(console, msg)
|
||||
|
||||
def _maybe_identify(key: str) -> None:
|
||||
"""Best-effort PATCH agent_caller when --agent-caller is supplied on a
|
||||
reused key. Silent no-op on any failure — reuse must not break.
|
||||
"""
|
||||
if not agent_caller:
|
||||
return
|
||||
try:
|
||||
resp = httpx.patch(
|
||||
f"{base_url.rstrip('/')}/api/v1/auth/agent_mode/caller/",
|
||||
headers={
|
||||
"Authorization": f"Token {key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
json={"agent_caller": agent_caller},
|
||||
timeout=10.0,
|
||||
)
|
||||
# Also reflect in local config so introspection matches backend.
|
||||
if resp.status_code == 200 and CONFIG_FILE.exists():
|
||||
try:
|
||||
cfg = load_config()
|
||||
cfg.platform.agent_caller = resp.json().get("agent_caller", agent_caller)
|
||||
save_config(cfg)
|
||||
except Exception:
|
||||
pass
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
|
||||
# Rule 1: env MEM0_API_KEY valid → reuse, no new key.
|
||||
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
|
||||
if _env_key and _ping_key(_env_key, base_url):
|
||||
_maybe_identify(_env_key)
|
||||
_emit_reuse("env")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 2: existing config api_key valid → reuse.
|
||||
if CONFIG_FILE.exists():
|
||||
_existing = load_config()
|
||||
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
|
||||
_maybe_identify(_existing.platform.api_key)
|
||||
_emit_reuse("config")
|
||||
_fire_init("existing_key")
|
||||
return
|
||||
# Rule 3: mint a fresh shadow (no valid key to reuse).
|
||||
# agent_caller is the agent's self-declared identity from --agent-caller
|
||||
# (Proof Editor-style). Env-var auto-detect is still used above to
|
||||
# decide we're in an agent context, but never to fill identity.
|
||||
bootstrap_via_backend(config, source=source, agent_caller=agent_caller)
|
||||
_fire_init("agent")
|
||||
return
|
||||
|
||||
# Warn if an existing config with an API key would be overwritten
|
||||
if not force and CONFIG_FILE.exists():
|
||||
existing = load_config()
|
||||
@@ -242,6 +383,7 @@ def run_init(
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
@@ -258,6 +400,8 @@ def run_init(
|
||||
return
|
||||
|
||||
# ── API key flow (existing) ───────────────────────────────────────
|
||||
# (Agent Mode branch runs earlier — see above, before the existing-config
|
||||
# guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
|
||||
|
||||
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||
if not sys.stdin.isatty():
|
||||
@@ -265,7 +409,7 @@ def run_init(
|
||||
print_error(
|
||||
err_console,
|
||||
"Non-interactive terminal detected and --api-key is required.",
|
||||
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
||||
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
@@ -273,6 +417,7 @@ def run_init(
|
||||
# Fully non-interactive when both flags provided
|
||||
if api_key and user_id:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
config.defaults.user_id = user_id
|
||||
_validate_platform(config)
|
||||
save_config(config)
|
||||
@@ -313,6 +458,7 @@ def run_init(
|
||||
config.platform.api_key = api_key_val
|
||||
config.platform.base_url = base_url
|
||||
config.platform.user_email = email_addr
|
||||
config.platform.created_via = "email"
|
||||
config.defaults.user_id = (
|
||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||
)
|
||||
@@ -331,6 +477,7 @@ def run_init(
|
||||
# API key flow
|
||||
if api_key:
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
else:
|
||||
_setup_platform(config)
|
||||
|
||||
@@ -370,6 +517,7 @@ def _setup_platform(config: Mem0Config) -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
config.platform.api_key = api_key
|
||||
config.platform.created_via = "api_key"
|
||||
|
||||
|
||||
def _setup_defaults(config: Mem0Config) -> None:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""mem0 whoami — print the active agent's default_user_id (AGENTRUSH identifier)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from mem0_cli.branding import BRAND_COLOR, print_error, print_info
|
||||
from mem0_cli.config import load_config
|
||||
|
||||
console = Console()
|
||||
err_console = Console(stderr=True)
|
||||
|
||||
|
||||
def run_whoami() -> None:
|
||||
config = load_config()
|
||||
session_id = config.platform.default_user_id if config.platform else None
|
||||
if not session_id:
|
||||
print_error(
|
||||
err_console,
|
||||
"No default_user_id found. Run `mem0 init --agent` first.",
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
console.print(f"Your AGENTRUSH identifier: [{BRAND_COLOR}]{session_id}[/{BRAND_COLOR}]")
|
||||
print_info(console, "Find your row at https://mem0.ai/agentrush")
|
||||
@@ -28,6 +28,14 @@ class PlatformConfig:
|
||||
api_key: str = ""
|
||||
base_url: str = DEFAULT_BASE_URL
|
||||
user_email: str = ""
|
||||
# Agent Mode (unclaimed-shadow signup)
|
||||
agent_mode: bool = False # True while the key is an unclaimed agent-mode key
|
||||
created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
|
||||
agent_caller: str = (
|
||||
"" # canonical agent name when created_via == "agent_mode" (e.g. "claude-code")
|
||||
)
|
||||
claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
|
||||
default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -43,12 +51,20 @@ class TelemetryConfig:
|
||||
anonymous_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentRushConfig:
|
||||
# ISO timestamp the human acknowledged the "memories are public" warning.
|
||||
# Empty until first interactive `mem0 agent-rush add`.
|
||||
acknowledged_at: 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)
|
||||
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
|
||||
|
||||
|
||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||
@@ -83,6 +99,11 @@ def load_config() -> Mem0Config:
|
||||
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", "")
|
||||
config.platform.agent_mode = bool(plat.get("agent_mode", False))
|
||||
config.platform.created_via = plat.get("created_via", "")
|
||||
config.platform.agent_caller = plat.get("agent_caller", "")
|
||||
config.platform.claimed_at = plat.get("claimed_at", "")
|
||||
config.platform.default_user_id = plat.get("default_user_id", "")
|
||||
|
||||
defaults = data.get("defaults", {})
|
||||
config.defaults.user_id = defaults.get("user_id", "")
|
||||
@@ -92,6 +113,9 @@ def load_config() -> Mem0Config:
|
||||
telemetry = data.get("telemetry", {})
|
||||
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
||||
|
||||
agent_rush = data.get("agent_rush", {})
|
||||
config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "")
|
||||
|
||||
# Environment variable overrides
|
||||
env_key = os.environ.get("MEM0_API_KEY")
|
||||
if env_key:
|
||||
@@ -136,10 +160,18 @@ def save_config(config: Mem0Config) -> None:
|
||||
"api_key": config.platform.api_key,
|
||||
"base_url": config.platform.base_url,
|
||||
"user_email": config.platform.user_email,
|
||||
"agent_mode": config.platform.agent_mode,
|
||||
"created_via": config.platform.created_via,
|
||||
"agent_caller": config.platform.agent_caller,
|
||||
"claimed_at": config.platform.claimed_at,
|
||||
"default_user_id": config.platform.default_user_id,
|
||||
},
|
||||
"telemetry": {
|
||||
"anonymous_id": config.telemetry.anonymous_id,
|
||||
},
|
||||
"agent_rush": {
|
||||
"acknowledged_at": config.agent_rush.acknowledged_at,
|
||||
},
|
||||
}
|
||||
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
@@ -147,6 +179,19 @@ def save_config(config: Mem0Config) -> None:
|
||||
|
||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
||||
|
||||
# Propagate the active api_key to ecosystem touchpoints (Claude Code
|
||||
# plugin env injection, shell rc exports). Idempotent — only updates
|
||||
# EXISTING entries; never creates new ones. Best-effort: any IOError
|
||||
# in the sync is swallowed so config.json is always the authoritative
|
||||
# write, never blocked by plugin-state issues.
|
||||
if config.platform.api_key:
|
||||
try:
|
||||
from mem0_cli.plugin_sync import sync_api_key
|
||||
|
||||
sync_api_key(config.platform.api_key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def redact_key(key: str) -> str:
|
||||
"""Redact an API key for display: m0-xxx...xxx"""
|
||||
|
||||
@@ -229,6 +229,16 @@ def format_json_envelope(
|
||||
if error:
|
||||
envelope["error"] = error
|
||||
envelope["data"] = data
|
||||
|
||||
# If the platform flagged this as an unclaimed Agent Mode account, surface
|
||||
# the notice inside the JSON envelope so an agent consuming the output
|
||||
# sees it without needing to inspect HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
@@ -323,6 +333,15 @@ def format_agent_envelope(
|
||||
if count is not None:
|
||||
envelope["count"] = count
|
||||
envelope["data"] = sanitize_agent_data(command, data)
|
||||
|
||||
# Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
|
||||
# agent reading the JSON output sees it without inspecting HTTP headers.
|
||||
from mem0_cli.state import take_notice
|
||||
|
||||
notice = take_notice()
|
||||
if notice:
|
||||
envelope["mem0_notice"] = notice
|
||||
|
||||
console.print_json(json.dumps(envelope, default=str))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"""Sync the active Mem0 API key into other ecosystem touchpoints.
|
||||
|
||||
Why this exists:
|
||||
The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
|
||||
(Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
|
||||
vars or their own config files. Without a sync, an agent-mode bootstrap
|
||||
mints a new key into config.json but the plugin's MCP keeps using the
|
||||
old key from env — silent surprise.
|
||||
|
||||
Design:
|
||||
- Update ONLY entries that already exist (never create new ones)
|
||||
- Preserve all surrounding content / formatting / other keys
|
||||
- Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
|
||||
- Idempotent — re-running with the same key is a no-op
|
||||
- Skip on dry_run
|
||||
|
||||
Targets currently handled:
|
||||
- ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
|
||||
- ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
|
||||
|
||||
Out of scope (deliberately not touched):
|
||||
- Codex / Cursor MCP configs — would require schema-aware edits and
|
||||
those tools don't have mem0 entries by default
|
||||
- Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Files we know how to update safely.
|
||||
_CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
|
||||
_SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
|
||||
|
||||
|
||||
def sync_api_key(api_key: str) -> list[str]:
|
||||
"""Propagate ``api_key`` into known ecosystem touchpoints.
|
||||
|
||||
Returns the list of paths actually updated. Empty list means nothing
|
||||
needed updating (either targets didn't exist or already had this value).
|
||||
"""
|
||||
if not api_key:
|
||||
return []
|
||||
updated: list[str] = []
|
||||
if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
|
||||
updated.append(str(_CLAUDE_SETTINGS))
|
||||
for rc in _SHELL_RCS:
|
||||
if _update_shell_rc(rc, api_key):
|
||||
updated.append(str(rc))
|
||||
return updated
|
||||
|
||||
|
||||
def _update_claude_settings(path: Path, api_key: str) -> bool:
|
||||
"""Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return False
|
||||
env = data.get("env")
|
||||
if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
|
||||
# No existing entry — don't create one.
|
||||
return False
|
||||
if env["MEM0_API_KEY"] == api_key:
|
||||
return False # already in sync
|
||||
env["MEM0_API_KEY"] = api_key
|
||||
_atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
|
||||
return True
|
||||
|
||||
|
||||
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
|
||||
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
|
||||
# end-of-file is preserved when MEM0_API_KEY is the last line.
|
||||
_RC_LINE = re.compile(
|
||||
r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def _update_shell_rc(path: Path, api_key: str) -> bool:
|
||||
"""Update an existing ``export MEM0_API_KEY=...`` line in path."""
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
match = _RC_LINE.search(text)
|
||||
if not match:
|
||||
return False # no existing line
|
||||
if match.group(3) == api_key:
|
||||
return False
|
||||
new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
|
||||
_atomic_write_text(path, new_text)
|
||||
return True
|
||||
|
||||
|
||||
def _atomic_write_text(path: Path, content: str) -> None:
|
||||
"""Write content to path atomically (temp + rename)."""
|
||||
dirname = path.parent
|
||||
fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
# Preserve mode if the original existed.
|
||||
if path.exists():
|
||||
os.chmod(tmp_path, path.stat().st_mode & 0o777)
|
||||
os.replace(tmp_path, path)
|
||||
except Exception:
|
||||
with contextlib.suppress(OSError):
|
||||
os.unlink(tmp_path)
|
||||
raise
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
_agent_mode: bool = False
|
||||
_current_command: str = ""
|
||||
_pending_notice: str = ""
|
||||
|
||||
|
||||
def is_agent_mode() -> bool:
|
||||
@@ -22,3 +23,23 @@ def get_current_command() -> str:
|
||||
def set_current_command(name: str) -> None:
|
||||
global _current_command
|
||||
_current_command = name
|
||||
|
||||
|
||||
def capture_notice(notice: str | None) -> None:
|
||||
"""Stash a Mem0 backend notice for end-of-command surfacing.
|
||||
|
||||
Called from the platform backend after each response so the notice can
|
||||
be printed once per command (regardless of how many sub-requests fired).
|
||||
Last-write-wins is fine — the message text is identical across requests.
|
||||
"""
|
||||
global _pending_notice
|
||||
if notice:
|
||||
_pending_notice = notice
|
||||
|
||||
|
||||
def take_notice() -> str:
|
||||
"""Return and clear the pending notice."""
|
||||
global _pending_notice
|
||||
msg = _pending_notice
|
||||
_pending_notice = ""
|
||||
return msg
|
||||
|
||||
@@ -87,7 +87,6 @@ def capture_event(
|
||||
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()
|
||||
@@ -107,6 +106,9 @@ def capture_event(
|
||||
with contextlib.suppress(Exception):
|
||||
save_config(config)
|
||||
|
||||
# M4: every cli.* event carries agent_mode based on the config flag
|
||||
# (unclaimed Agent Mode key). This is the growth-doc property used to
|
||||
# join init → add → search funnels in PostHog.
|
||||
payload = {
|
||||
"api_key": POSTHOG_API_KEY,
|
||||
"distinct_id": distinct_id,
|
||||
@@ -115,7 +117,7 @@ def capture_event(
|
||||
"source": "CLI",
|
||||
"language": "python",
|
||||
"cli_version": __version__,
|
||||
"agent_mode": is_agent_mode(),
|
||||
"agent_mode": bool(config.platform.agent_mode),
|
||||
"python_version": sys.version,
|
||||
"os": sys.platform,
|
||||
"os_version": platform.version(),
|
||||
@@ -135,12 +137,19 @@ def capture_event(
|
||||
"anon_distinct_id_to_alias": anon_id_to_alias,
|
||||
}
|
||||
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||
child = subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
text=True,
|
||||
)
|
||||
if child.stdin:
|
||||
with contextlib.suppress(Exception):
|
||||
child.stdin.write(json.dumps(context))
|
||||
with contextlib.suppress(Exception):
|
||||
child.stdin.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Standalone telemetry sender — runs as a detached subprocess.
|
||||
|
||||
Usage: python -m mem0_cli.telemetry_sender '<json context>'
|
||||
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin;
|
||||
a single argv argument is still accepted as a legacy fallback)
|
||||
|
||||
This module is spawned by telemetry.capture_event() and runs independently
|
||||
of the parent CLI process. It:
|
||||
@@ -20,8 +21,18 @@ import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _load_context() -> dict:
|
||||
"""Load telemetry context from stdin, falling back to argv for compatibility."""
|
||||
raw = ""
|
||||
if not sys.stdin.isatty():
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw and len(sys.argv) > 1:
|
||||
raw = sys.argv[1]
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ctx = json.loads(sys.argv[1])
|
||||
ctx = _load_context()
|
||||
payload = ctx["payload"]
|
||||
|
||||
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
|
||||
|
||||
Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
|
||||
sync so that the Python and Node CLIs expose an identical surface for the
|
||||
Agent Mode entrypoint. If you add a flag here, add the same assertion on
|
||||
the Node side (and vice versa).
|
||||
|
||||
Network-bound bootstrap is covered by the platform-side E2E suite
|
||||
(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
|
||||
the CLI surface that ships in the binary.
|
||||
"""
|
||||
|
||||
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:
|
||||
return _ANSI_RE.sub("", text)
|
||||
|
||||
|
||||
def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
|
||||
env = os.environ.copy()
|
||||
for key in list(env.keys()):
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=15,
|
||||
)
|
||||
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 str(tmp_path)
|
||||
|
||||
|
||||
class TestInitFlagSurface:
|
||||
"""`mem0 init --help` must expose the Agent Mode flags."""
|
||||
|
||||
def test_init_help_lists_agent_flag(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--agent" in result.stdout
|
||||
|
||||
def test_init_help_describes_agent_mode(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
# Description must mention what --agent actually does so an agent
|
||||
# reading the help can self-discover the bootstrap entrypoint.
|
||||
assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
|
||||
|
||||
def test_init_help_lists_source_flag(self):
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--source" in result.stdout
|
||||
|
||||
def test_init_help_lists_email_and_code(self):
|
||||
# Claim flow flags must remain present alongside Agent Mode flags.
|
||||
result = _run(["init", "--help"])
|
||||
assert result.returncode == 0
|
||||
assert "--email" in result.stdout
|
||||
assert "--code" in result.stdout
|
||||
|
||||
|
||||
class TestArgvPreprocessing:
|
||||
"""`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
|
||||
|
||||
Regression for the bug where the top-level `--agent` JSON-alias was
|
||||
stripped from ``sys.argv`` before Typer could bind it to the init
|
||||
subcommand, making ``mem0 init --agent`` indistinguishable from a
|
||||
plain ``mem0 init`` (interactive wizard).
|
||||
"""
|
||||
|
||||
def test_init_with_agent_reaches_subcommand(self, clean_home):
|
||||
# We can't hit a real backend in unit tests, so we point the CLI at
|
||||
# a guaranteed-dead URL and assert the failure is the bootstrap
|
||||
# request failing — proving the --agent flag was honored and the
|
||||
# bootstrap branch ran, not the interactive wizard.
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
combined = _strip_ansi(result.stdout + result.stderr).lower()
|
||||
# Either we got a connection/network error from the bootstrap POST,
|
||||
# or the CLI surfaced an Agent Mode-specific failure message.
|
||||
assert (
|
||||
"agent" in combined
|
||||
or "connect" in combined
|
||||
or "network" in combined
|
||||
or "fetch" in combined
|
||||
or "bootstrap" in combined
|
||||
), f"Expected bootstrap attempt, got: {combined!r}"
|
||||
|
||||
|
||||
class TestJsonEnvelopeParity:
|
||||
"""`mem0 init --agent --json` should produce a JSON envelope on success.
|
||||
|
||||
Without a live backend we can only assert the failure shape: when the
|
||||
backend is unreachable, the CLI must still exit non-zero AND not crash
|
||||
on a Python traceback (which would mean we leaked an exception past
|
||||
the agent-mode handler).
|
||||
"""
|
||||
|
||||
def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
combined = _strip_ansi(result.stdout + result.stderr)
|
||||
assert "Traceback (most recent call last)" not in combined
|
||||
assert result.returncode != 0
|
||||
|
||||
|
||||
class TestInitInCommandList:
|
||||
"""`mem0 --help` must list `init` so agents walking the top-level help
|
||||
can discover the Agent Mode entrypoint without prior knowledge."""
|
||||
|
||||
def test_top_level_help_lists_init(self):
|
||||
result = _run(["--help"])
|
||||
assert result.returncode == 0
|
||||
assert "init" in result.stdout
|
||||
@@ -49,6 +49,7 @@ def _run(
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
@@ -56,7 +57,7 @@ def _run(
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
|
||||
@@ -8,8 +8,8 @@ from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.exceptions import Exit as ClickExit
|
||||
from rich.console import Console
|
||||
from typer import Exit as TyperExit
|
||||
|
||||
from mem0_cli.commands.config_cmd import (
|
||||
cmd_config_get,
|
||||
@@ -181,7 +181,7 @@ class TestAddCommand:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -206,7 +206,7 @@ class TestAddCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -764,7 +764,7 @@ class TestImportCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.utils.console", console),
|
||||
patch("mem0_cli.commands.utils.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
|
||||
|
||||
@@ -801,7 +801,7 @@ class TestEntitiesListCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_entities_list(mock_backend, "invalid", output="table")
|
||||
|
||||
@@ -944,7 +944,7 @@ class TestEntitiesDeleteCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_entities_delete(
|
||||
mock_backend,
|
||||
@@ -1308,7 +1308,7 @@ class TestAgentMode:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("sys.stdout", captured_stdout),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_get(mock_backend, "bad-id", output="text")
|
||||
|
||||
|
||||
@@ -67,7 +67,8 @@ class TestConfig:
|
||||
from mem0_cli.config import CONFIG_FILE
|
||||
|
||||
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||
assert mode == 0o600
|
||||
if os.name != "nt":
|
||||
assert mode == 0o600
|
||||
|
||||
def test_defaults_save_and_load(self, isolate_config):
|
||||
config = Mem0Config()
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
"""Unit tests for init internals — decision tree primitives + plugin sync.
|
||||
|
||||
These tests exercise the units that the high-level subprocess parity tests in
|
||||
``test_agent_mode.py`` deliberately can't reach:
|
||||
|
||||
- ``_ping_key`` must NOT treat network errors as "invalid key" (else a VPN
|
||||
flap silently mints a new shadow over a working key).
|
||||
- ``plugin_sync`` must only update entries that already exist, preserve
|
||||
trailing newlines, and never mangle other lines.
|
||||
- The 403→ratelimit translation in ``bootstrap_via_backend`` surfaces the
|
||||
real cause instead of DRF's opaque "You do not have permission" string.
|
||||
|
||||
Mirror surface lives in ``cli/node/tests/agent-mode.test.ts``; if you add a
|
||||
behavioral assertion here, mirror it on the Node side and vice versa.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from mem0_cli.commands.init_cmd import _ping_key
|
||||
from mem0_cli.plugin_sync import _update_claude_settings, _update_shell_rc
|
||||
|
||||
# ── _ping_key ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _Resp:
|
||||
def __init__(self, status_code: int) -> None:
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def test_ping_key_200_is_valid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(200))
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_401_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(401))
|
||||
assert _ping_key("k", "http://x") is False
|
||||
|
||||
|
||||
def test_ping_key_403_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(403))
|
||||
assert _ping_key("k", "http://x") is False
|
||||
|
||||
|
||||
def test_ping_key_5xx_is_not_definitively_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Transient upstream failure must NOT cause a shadow to be minted.
|
||||
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(503))
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_connect_error_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Network blip (DNS, captive portal, etc.) — must NOT trigger a re-mint.
|
||||
def boom(*a, **kw):
|
||||
raise httpx.ConnectError("nope")
|
||||
|
||||
monkeypatch.setattr(httpx, "get", boom)
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
def test_ping_key_timeout_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def boom(*a, **kw):
|
||||
raise httpx.ReadTimeout("slow")
|
||||
|
||||
monkeypatch.setattr(httpx, "get", boom)
|
||||
assert _ping_key("k", "http://x") is True
|
||||
|
||||
|
||||
# ── plugin_sync._update_shell_rc ──────────────────────────────────────────
|
||||
|
||||
|
||||
def test_shell_rc_updates_existing_export_preserves_trailing_newline(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text('export MEM0_API_KEY="old"\n', encoding="utf-8")
|
||||
changed = _update_shell_rc(rc, "newkey")
|
||||
assert changed is True
|
||||
assert rc.read_text(encoding="utf-8") == 'export MEM0_API_KEY="newkey"\n'
|
||||
|
||||
|
||||
def test_shell_rc_does_not_create_new_export(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text("alias ll='ls -la'\n", encoding="utf-8")
|
||||
changed = _update_shell_rc(rc, "newkey")
|
||||
assert changed is False
|
||||
assert rc.read_text(encoding="utf-8") == "alias ll='ls -la'\n"
|
||||
|
||||
|
||||
def test_shell_rc_preserves_surrounding_content(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
original = "# my zshrc\nalias ll='ls -la'\nexport MEM0_API_KEY='old'\nexport OTHER=keepme\n"
|
||||
rc.write_text(original, encoding="utf-8")
|
||||
_update_shell_rc(rc, "newkey")
|
||||
after = rc.read_text(encoding="utf-8")
|
||||
assert "alias ll='ls -la'\n" in after
|
||||
assert "export OTHER=keepme\n" in after
|
||||
assert "# my zshrc\n" in after
|
||||
assert 'export MEM0_API_KEY="newkey"\n' in after
|
||||
|
||||
|
||||
def test_shell_rc_idempotent_when_already_matching(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc"
|
||||
rc.write_text('export MEM0_API_KEY="same"\n', encoding="utf-8")
|
||||
assert _update_shell_rc(rc, "same") is False
|
||||
|
||||
|
||||
def test_shell_rc_missing_file_is_noop(tmp_path) -> None:
|
||||
rc = tmp_path / ".zshrc" # does not exist
|
||||
assert _update_shell_rc(rc, "x") is False
|
||||
|
||||
|
||||
# ── plugin_sync._update_claude_settings ────────────────────────────────────
|
||||
|
||||
|
||||
def test_claude_settings_does_not_create_env_block(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"otherKey": 1}), encoding="utf-8")
|
||||
changed = _update_claude_settings(settings, "newkey")
|
||||
assert changed is False
|
||||
# Original content unchanged.
|
||||
assert json.loads(settings.read_text(encoding="utf-8")) == {"otherKey": 1}
|
||||
|
||||
|
||||
def test_claude_settings_does_not_create_mem0_entry_in_existing_env(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"env": {"OTHER_KEY": "x"}}), encoding="utf-8")
|
||||
changed = _update_claude_settings(settings, "newkey")
|
||||
assert changed is False
|
||||
|
||||
|
||||
def test_claude_settings_updates_existing_entry(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(
|
||||
json.dumps({"env": {"MEM0_API_KEY": "old", "OTHER": "y"}}, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
changed = _update_claude_settings(settings, "fresh")
|
||||
assert changed is True
|
||||
data = json.loads(settings.read_text(encoding="utf-8"))
|
||||
assert data["env"]["MEM0_API_KEY"] == "fresh"
|
||||
assert data["env"]["OTHER"] == "y" # other keys preserved
|
||||
|
||||
|
||||
def test_claude_settings_idempotent(tmp_path) -> None:
|
||||
import json
|
||||
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text(json.dumps({"env": {"MEM0_API_KEY": "same"}}), encoding="utf-8")
|
||||
assert _update_claude_settings(settings, "same") is False
|
||||
|
||||
|
||||
def test_claude_settings_malformed_json_is_noop(tmp_path) -> None:
|
||||
settings = tmp_path / "settings.json"
|
||||
settings.write_text("{ this is not json", encoding="utf-8")
|
||||
assert _update_claude_settings(settings, "x") is False
|
||||
|
||||
|
||||
# ── bootstrap rate-limit translation ──────────────────────────────────────
|
||||
|
||||
|
||||
def test_bootstrap_403_permission_surfaces_ratelimit(monkeypatch, capsys) -> None:
|
||||
"""DRF 403 'You do not have permission' must be translated to the daily limit message."""
|
||||
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend
|
||||
from mem0_cli.config import Mem0Config
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.status_code = 403
|
||||
fake_resp.text = '{"detail": "You do not have permission to perform this action."}'
|
||||
fake_resp.json = MagicMock(
|
||||
return_value={"detail": "You do not have permission to perform this action."}
|
||||
)
|
||||
|
||||
class _Client:
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
def post(self, *a, **kw):
|
||||
return fake_resp
|
||||
|
||||
monkeypatch.setattr(httpx, "Client", _Client)
|
||||
cfg = Mem0Config()
|
||||
cfg.platform.base_url = "https://api.mem0.ai"
|
||||
import typer
|
||||
|
||||
with pytest.raises(typer.Exit):
|
||||
bootstrap_via_backend(cfg)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
combined = captured.out + captured.err
|
||||
assert "Daily Agent Mode signup limit reached" in combined
|
||||
assert "permission to perform this action" not in combined
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for telemetry subprocess secret handling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
from mem0_cli.telemetry import capture_event
|
||||
from mem0_cli.telemetry_sender import _load_context
|
||||
|
||||
|
||||
class _CaptureStdin:
|
||||
def __init__(self):
|
||||
self.buffer = ""
|
||||
self.closed = False
|
||||
|
||||
def write(self, value: str) -> None:
|
||||
self.buffer += value
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class _DummyProcess:
|
||||
def __init__(self):
|
||||
self.stdin = _CaptureStdin()
|
||||
|
||||
|
||||
def test_capture_event_writes_context_to_stdin_not_argv(isolate_config, monkeypatch):
|
||||
config = Mem0Config()
|
||||
config.platform.api_key = "m0-test-secret"
|
||||
config.telemetry.anonymous_id = "cli-anon-test"
|
||||
save_config(config)
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
proc = _DummyProcess()
|
||||
|
||||
def fake_popen(args, **kwargs):
|
||||
captured["args"] = args
|
||||
captured["kwargs"] = kwargs
|
||||
return proc
|
||||
|
||||
monkeypatch.setattr("mem0_cli.telemetry.subprocess.Popen", fake_popen)
|
||||
|
||||
capture_event("unit_test_event", {"case": "stdin-secret"})
|
||||
|
||||
argv = captured["args"]
|
||||
assert argv == [sys.executable, "-m", "mem0_cli.telemetry_sender"]
|
||||
assert all("m0-test-secret" not in arg for arg in argv)
|
||||
|
||||
kwargs = captured["kwargs"]
|
||||
assert kwargs["stdin"] == subprocess.PIPE
|
||||
assert kwargs["text"] is True
|
||||
|
||||
ctx = json.loads(proc.stdin.buffer)
|
||||
assert ctx["mem0_api_key"] == "m0-test-secret"
|
||||
assert ctx["payload"]["event"] == "unit_test_event"
|
||||
|
||||
assert proc.stdin.closed
|
||||
|
||||
|
||||
def test_load_context_reads_from_stdin(monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["telemetry_sender"])
|
||||
monkeypatch.setattr("sys.stdin", io.StringIO('{"payload": {"event": "stdin"}}'))
|
||||
|
||||
ctx = _load_context()
|
||||
|
||||
assert ctx["payload"]["event"] == "stdin"
|
||||
|
||||
|
||||
def test_load_context_falls_back_to_argv(monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["telemetry_sender", '{"payload": {"event": "argv"}}'])
|
||||
monkeypatch.setattr("sys.stdin", io.StringIO(""))
|
||||
|
||||
ctx = _load_context()
|
||||
|
||||
assert ctx["payload"]["event"] == "argv"
|
||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
|
||||
Modify project configuration including custom instructions, categories, and language preferences:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
|
||||
@@ -46,9 +46,9 @@ Ground-up rewrite of the memory pipeline with 20+ point benchmark improvements:
|
||||
- **~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
|
||||
- **Graph memory (built-in)**: entities extracted, embedded, and linked across memories, with no external graph store required
|
||||
|
||||
Breaking changes: Graph memory removed from OSS, `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
|
||||
Breaking changes: external graph stores removed from OSS (replaced by built-in graph memory), `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
|
||||
|
||||
</Update>
|
||||
|
||||
|
||||
@@ -4,6 +4,39 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-06-12" description="v1.0.13">
|
||||
|
||||
**Fixes:**
|
||||
- **Custom categories payload:** `customCategories` (a `Record<string, string>` map) is now converted via the new `customCategoryMapToList()` helper into the `Array<Record<string, string>>` shape the Mem0 SDK expects on `add` calls — previously the raw object was passed as `custom_categories` and silently ignored ([#5345](https://github.com/mem0ai/mem0/pull/5345))
|
||||
- **Skip runtime setup during metadata registration:** `register()` now detects `registrationMode === "cli-metadata"`, registers only the CLI commands, and returns early — avoiding backend initialization, service/tool registration, and hook installation during OpenClaw's metadata-only registration pass ([#5383](https://github.com/mem0ai/mem0/pull/5383))
|
||||
|
||||
**Security:**
|
||||
- Bumped `mem0ai` from `3.0.3` to `3.0.7` (latest Node SDK) — includes the transitive axios CVE remediation shipped in `3.0.6` ([#5460](https://github.com/mem0ai/mem0/pull/5460))
|
||||
- Added pnpm override `uuid@<11.1.1` → `>=11.1.1` to resolve an open MEDIUM Dependabot alert ([#5489](https://github.com/mem0ai/mem0/pull/5489))
|
||||
|
||||
**Improvements:**
|
||||
- **Repo consolidation:** Plugin moved from repo-root `openclaw/` to `integrations/openclaw/`; `package.json` `repository.directory` updated to match so npm provenance links to the correct subdirectory ([#5491](https://github.com/mem0ai/mem0/pull/5491))
|
||||
|
||||
**Tests:**
|
||||
- Added `customCategoryMapToList` unit tests and a `PlatformProvider` test asserting `custom_categories` is passed to the Mem0 SDK as a list ([#5345](https://github.com/mem0ai/mem0/pull/5345))
|
||||
- Added a regression test asserting `cli-metadata` registration registers only CLI commands and triggers no runtime side effects ([#5383](https://github.com/mem0ai/mem0/pull/5383))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-02" description="v1.0.12">
|
||||
|
||||
**Docs:**
|
||||
- **Agent Mode onboarding:** README now documents an autonomous setup path for AI agents — `mem0 init --agent --json` mints an evaluation Mem0 API key with no email, OTP, or browser and exports it as `MEM0_API_KEY` for `openclaw mem0 init`; a human owner can later run `mem0 init --email <email>` to claim ownership without disrupting the agent ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
**Security:**
|
||||
- Added pnpm overrides to remediate advisories in transitive dependencies: `langsmith@<0.6.0` → `^0.6.0`, `picomatch@<2.3.2` → `^2.3.2`, `vite` → `^8.0.5`, and `@qdrant/js-client-rest` → `^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
|
||||
|
||||
**Dependencies:**
|
||||
- Bumped `mem0ai` from `3.0.2` to `3.0.3` ([#5212](https://github.com/mem0ai/mem0/pull/5212))
|
||||
- Bumped dev dependencies `@vitest/coverage-v8` and `vitest` from `^4.0.18` to `^4.1.7`; added `vite@^8.0.5` and `@qdrant/js-client-rest@^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-29" description="v1.0.11">
|
||||
|
||||
**New Features:**
|
||||
|
||||
@@ -25,7 +25,7 @@ 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
|
||||
- **UI:** Removed the legacy external-graph-store visualization tab, page, and its references from dashboard, sidebar, project settings, playground, and billing
|
||||
|
||||
</Update>
|
||||
|
||||
|
||||
+215
-3
@@ -7,6 +7,92 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-06-17" description="v2.0.7">
|
||||
|
||||
**New Features:**
|
||||
- **LLMs:** Add Gemini via Vertex AI as LLM provider ([#4030](https://github.com/mem0ai/mem0/pull/4030))
|
||||
- **Embeddings:** Add native `embed_batch` to `OllamaEmbedding` for batched embedding requests ([#5415](https://github.com/mem0ai/mem0/pull/5415))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Fix `api_error_handler` silently dropping return values from async methods ([#5540](https://github.com/mem0ai/mem0/pull/5540))
|
||||
- **Core:** Fix `AsyncMemory.reset()` not resetting the entity store ([#5535](https://github.com/mem0ai/mem0/pull/5535))
|
||||
- **Core:** Fix `async delete_all` aborting on first error, leaving partial deletion ([#5529](https://github.com/mem0ai/mem0/pull/5529))
|
||||
- **Core:** Skip messages without a `content` key in message parsers to prevent `KeyError` crashes ([#5575](https://github.com/mem0ai/mem0/pull/5575))
|
||||
- **Core:** Preserve custom metadata fields during memory update ([#5480](https://github.com/mem0ai/mem0/pull/5480))
|
||||
- **LLMs:** Fix Anthropic `tool_choice` format and tool response parsing ([#5537](https://github.com/mem0ai/mem0/pull/5537))
|
||||
- **LLMs:** Fix Ollama `json` format mutating the caller's messages list in-place ([#5539](https://github.com/mem0ai/mem0/pull/5539))
|
||||
- **LLMs:** Omit `None` config values from Gemini `GenerateContentConfig` to prevent validation errors ([#5528](https://github.com/mem0ai/mem0/pull/5528))
|
||||
- **LLMs:** Honor reasoning-model params in `AzureOpenAIStructuredLLM` ([#5548](https://github.com/mem0ai/mem0/pull/5548))
|
||||
- **LLMs:** Honor reasoning-model params in `OpenAIStructuredLLM` ([#5458](https://github.com/mem0ai/mem0/pull/5458))
|
||||
- **LLMs:** Send `max_completion_tokens` for the GPT-5 family across all providers ([#5547](https://github.com/mem0ai/mem0/pull/5547))
|
||||
- **LLMs:** Accept and forward `**kwargs` in Together, LangChain, and Sarvam providers ([#5556](https://github.com/mem0ai/mem0/pull/5556))
|
||||
- **LLMs:** Fix Bedrock AI21 response parse default using `dict` literal instead of `set` ([#5527](https://github.com/mem0ai/mem0/pull/5527))
|
||||
- **LLMs:** Fix LiteLLM function-calling check blocking all calls on non-tool models ([#5536](https://github.com/mem0ai/mem0/pull/5536))
|
||||
- **LLMs:** Fix HuggingFace provider using `self.config` instead of raw `config` parameter ([#5538](https://github.com/mem0ai/mem0/pull/5538))
|
||||
- **Embeddings:** Honor `aws_session_token` in AWS Bedrock embeddings ([#5566](https://github.com/mem0ai/mem0/pull/5566))
|
||||
- **Rerankers:** Respect `config.top_k` in Cohere and ZeroEntropy fallback paths ([#5560](https://github.com/mem0ai/mem0/pull/5560))
|
||||
- **Vector Stores:** Fix FAISS filtered search dropping over-fetched candidates before filtering ([#5453](https://github.com/mem0ai/mem0/pull/5453))
|
||||
- **Vector Stores:** Fix Weaviate `reset()` crashing with missing `vector_size` argument ([#5531](https://github.com/mem0ai/mem0/pull/5531))
|
||||
- **Vector Stores:** Pass embedding dims in Weaviate `reset()` to avoid re-init crash ([#5570](https://github.com/mem0ai/mem0/pull/5570))
|
||||
- **Vector Stores:** Fix MongoDB `reset()` passing wrong argument to `create_col()` ([#5532](https://github.com/mem0ai/mem0/pull/5532))
|
||||
- **Vector Stores:** Fix Pinecone hybrid search crashing when `filters` is `None` ([#5533](https://github.com/mem0ai/mem0/pull/5533))
|
||||
- **Vector Stores:** Fix Redis crashing on empty or `None` filters in `search()` and `list()` ([#5446](https://github.com/mem0ai/mem0/pull/5446))
|
||||
- **Vector Stores:** Return `None` from `get()` for missing IDs in Milvus, Weaviate, and Supabase ([#5562](https://github.com/mem0ai/mem0/pull/5562))
|
||||
- **Vector Stores:** Return `None` from ChromaDB `get()` for missing IDs ([#5561](https://github.com/mem0ai/mem0/pull/5561))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-13" description="v2.0.6">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Add a contextual OSS-to-Platform notices system that surfaces occasional, situation-aware messages (first run, scale/performance thresholds, slow queries, and when temporal/decay features are relevant) pointing to the corresponding Mem0 Platform capabilities; disable via `MEM0_TELEMETRY=false` ([#5494](https://github.com/mem0ai/mem0/pull/5494))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory:** Prevent a crash in `parse_vision_messages` when vision support is disabled ([#5487](https://github.com/mem0ai/mem0/pull/5487))
|
||||
- **Vector Stores:** Expose the `https` option on the Qdrant vector store configuration so TLS endpoints can be targeted explicitly ([#5380](https://github.com/mem0ai/mem0/pull/5380))
|
||||
- **Vector Stores:** Use valid S3 Vectors entity index names, fixing index operations that failed on invalid names ([#5416](https://github.com/mem0ai/mem0/pull/5416))
|
||||
- **Vector Stores:** Fix `search()` crashing with a `TypeError` in the LangChain vector store when a result score is `None` ([#5072](https://github.com/mem0ai/mem0/pull/5072))
|
||||
- **Vector Stores:** Use `is not None` instead of a truthiness check for vector/payload in the PGVector `update()` path, so valid empty/zero values are no longer skipped ([#5488](https://github.com/mem0ai/mem0/pull/5488))
|
||||
- **Vector Stores:** Index the Valkey `memory` field as `TEXT` rather than `TAG` so full-text search behaves correctly ([#5443](https://github.com/mem0ai/mem0/pull/5443))
|
||||
- **Vector Stores:** Implement `$not` filter support in the ChromaDB vector store ([#5485](https://github.com/mem0ai/mem0/pull/5485))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-10" description="v2.0.5">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Warn at init time when hybrid/BM25 search silently degrades to semantic-only because the configured vector store does not implement `keyword_search`. Affected stores: Chroma, FAISS, Cassandra, LangChain, Neptune Analytics, S3 Vectors, Supabase, TurboPuffer, Valkey ([#5444](https://github.com/mem0ai/mem0/pull/5444))
|
||||
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_breakdown` dict with `semantic`, `keyword` (normalized BM25), `entity_boost`, and `temporal_boost` signals so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) consistently across all backends. 11 adapters previously returned raw distance metrics (lower = better) — FAISS, Chroma, Milvus, Redis, Cassandra, PGVector, S3 Vectors, Supabase, Valkey, Azure MySQL, and Vertex AI Vector Search — causing incorrect ranking in multi-store setups ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
||||
- **Memory:** Parallelize entity boost searches in `Memory.search()` and `AsyncMemory.search()`. Previously up to 8 entities were embedded and queried sequentially (16 serial round-trips with remote embedders); all entity lookups now run concurrently, eliminating multi-second latency on entity-rich queries ([#5377](https://github.com/mem0ai/mem0/pull/5377))
|
||||
- **Memory:** Reject empty or whitespace-only queries in `Memory.search()`, `AsyncMemory.search()`, `MemoryClient.search()`, and `AsyncMemoryClient.search()` before any embedding or API call is made. Also strips leading/trailing whitespace from valid queries ([#5258](https://github.com/mem0ai/mem0/pull/5258))
|
||||
- **LLMs:** Add `is_reasoning_model: Optional[bool]` override to `BaseLlmConfig` (surfaced on `OpenAILlmConfig` and `AzureOpenAILlmConfig`). Fixes silent zero-extraction when using Azure deployments with versioned `gpt-5.x` names that the automatic name-based heuristic cannot recognize ([#5327](https://github.com/mem0ai/mem0/pull/5327))
|
||||
- **LLMs:** Fix xAI LLM provider: add `XAIConfig` with `xai_base_url`, forward `tools`/`tool_choice` in `generate_response()`, and parse `tool_calls` in the response. Previously the provider raised `AttributeError` at init and silently dropped tool results ([#5190](https://github.com/mem0ai/mem0/pull/5190))
|
||||
- **Vector Stores:** Fix PGVector `ConnectionPool` hang in Docker Compose environments where the app container starts before Postgres is DNS-resolvable — switched to `open=False` to avoid blocking constructor or silent zombie pool ([#5155](https://github.com/mem0ai/mem0/pull/5155))
|
||||
- **Vector Stores:** Fix PGVector `sslmode` handling for PostgreSQL URIs — the `sslmode` query parameter is now correctly extracted and forwarded when building the async connection pool ([#5308](https://github.com/mem0ai/mem0/pull/5308))
|
||||
- **Vector Stores:** Fix S3 Vectors `list()` not applying metadata filters — filtering is now done client-side after fetching, with pagination preserved and `top_k` applied after filtering to prevent pre-truncation of matching rows ([#5018](https://github.com/mem0ai/mem0/pull/5018))
|
||||
- **Vector Stores:** Fix Upstash Vector `search()` routing all queries to the default namespace — `namespace` is now passed as a top-level keyword argument to `query_many()` instead of inside the per-query dict where it was silently ignored ([#5202](https://github.com/mem0ai/mem0/pull/5202))
|
||||
- **Core:** Replace mutable default arguments with `None` sentinels in embedder configs and the proxy module, preventing cross-request state contamination ([#5302](https://github.com/mem0ai/mem0/pull/5302))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-27" description="v2.0.4">
|
||||
|
||||
**New Features:**
|
||||
- **Client:** `delete()` and async `delete()` accept `delete_linked` (default `False`). When `True`, deleting a memory also removes the older memories it superseded (the v3 `linked_memory_ids` chain), transitively — the delete-side counterpart of `latest_only`, so a superseded memory does not resurface after the current one is deleted ([#5270](https://github.com/mem0ai/mem0/pull/5270))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-26" description="v2.0.3">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** PGVector adapter now supports rich filter operators (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `icontains`, wildcard `*`, `$or`, `$not`) in `search()`, `keyword_search()`, and `list()`. Previously only exact-equality filters worked — operator dicts were silently stringified and returned zero results ([#5263](https://github.com/mem0ai/mem0/pull/5263))
|
||||
- **Server:** Fixed `/search` endpoint returning 502 when `user_id`, `agent_id`, or `run_id` are sent as top-level request fields. The server now maps these into the `filters` dict before calling `Memory.search()`, matching the v3 API contract. Top-level entity ID fields are marked as deprecated in the OpenAPI schema and emit a warning log — clients should migrate to `filters={"user_id": "..."}` ([#5263](https://github.com/mem0ai/mem0/pull/5263))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-08" description="v2.0.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -63,8 +149,8 @@ mode: "wide"
|
||||
- **`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))
|
||||
- **External Graph Store 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, about 4,000 lines. The external graph store integration is no longer part of the OSS SDK; graph drivers (neo4j, memgraph, kuzu, etc.) can be uninstalled. Graph memory now runs natively as built-in entity linking. 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 now runs automatically and no longer needs a flag. 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))
|
||||
|
||||
@@ -924,6 +1010,64 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
</Tab>
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-06-17" description="v3.0.9">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **LLMs:** Fix Anthropic `tool_choice` format — was incorrectly sent as a bare string `"auto"` (rejected by the API); now correctly sent as `{ type: "auto" }`. Also fixes tool response parsing: `tool_use` blocks are now parsed into `toolCalls` objects instead of throwing. Updated default model to `claude-sonnet-4-6` and default `max_tokens` to `2000` to match the Python provider. Added `temperature`, `topP`, and `maxTokens` to `LLMConfig` so Anthropic params can be configured ([#5537](https://github.com/mem0ai/mem0/pull/5537))
|
||||
- **Memory (OSS):** Preserve custom metadata fields during `update()` — fields such as `category`, `priority`, and other user-defined keys were previously dropped on update; the existing payload is now spread before applying the new data ([#5480](https://github.com/mem0ai/mem0/pull/5480))
|
||||
- **Client:** Preserve user-defined schema keys in `createMemoryExport` ([#5594](https://github.com/mem0ai/mem0/pull/5594))
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Bump `esbuild` to `>=0.28.1` across all npm packages via pnpm overrides to remediate upstream vulnerability ([#5563](https://github.com/mem0ai/mem0/pull/5563))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-13" description="v3.0.8">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Add a contextual OSS-to-Platform notices system that surfaces occasional, situation-aware messages (first run, scale/performance thresholds, slow queries, and when temporal/decay features are relevant) pointing to the corresponding Mem0 Platform capabilities; disable via `MEM0_TELEMETRY=false` ([#5494](https://github.com/mem0ai/mem0/pull/5494))
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Upgrade `@langchain/community` to `^1.1.18` to remediate CVE-2026-27795 and CVE-2026-26019 ([#5510](https://github.com/mem0ai/mem0/pull/5510))
|
||||
- **Dependencies:** Resolve all open MEDIUM Dependabot alerts via pnpm overrides ([#5489](https://github.com/mem0ai/mem0/pull/5489))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-10" description="v3.0.7">
|
||||
|
||||
**New Features:**
|
||||
- **Embeddings:** Add `LMStudioEmbedding` provider for local embeddings via the LM Studio server ([#5377](https://github.com/mem0ai/mem0/pull/5377))
|
||||
- **Memory:** Add opt-in `explain: true` option to `Memory.search()`. When enabled, each result includes a `scoreBreakdown` object with `semantic`, `keyword`, `entityBoost`, and `temporalBoost` fields so callers can inspect and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory:** Parallelize entity boost searches in `Memory.search()`. All entity embed + store lookups now run concurrently instead of sequentially, eliminating multi-second latency on entity-rich queries with remote embedding providers ([#5377](https://github.com/mem0ai/mem0/pull/5377))
|
||||
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) — fixed score inversion in the Redis vector store adapter ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
||||
- **Embeddings:** Request `encoding_format: "float"` from the OpenAI embedder in both `embed()` and `embedBatch()`. Fixes incorrect vector dimensions when using OpenAI-compatible proxies that default to base64 encoding ([#5170](https://github.com/mem0ai/mem0/pull/5170))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-01" description="v3.0.6">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Bumped `axios` to `^1.16.0` to remediate high-severity prototype-pollution CVEs (credential theft, MITM, DoS). Pinned transitive dependencies via pnpm overrides: `jws` → 4.0.1 (CVE-2025-65945), `langsmith` → ^0.6.0 (CVE-2026-45134), `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343), `picomatch` → ^2.3.2 (CVE-2026-33671), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `path-to-regexp` → ^8.4.0 (CVE-2026-4926), `rollup` → ^4.59.0 (CVE-2026-27606), `glob` → ^10.5.0 (CVE-2025-64756), `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-27" description="v3.0.5">
|
||||
|
||||
**New Features:**
|
||||
- **Client:** `delete()` accepts an options object with `deleteLinked` (serialized as `delete_linked`, default `false`). When `true`, deleting a memory also removes the older memories it superseded (the v3 linked chain), transitively — the delete-side counterpart of `latestOnly`, so a superseded memory does not resurface after the current one is deleted ([#5270](https://github.com/mem0ai/mem0/pull/5270))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-26" description="v3.0.4">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** PGVector adapter now supports rich filter operators (`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `icontains`, wildcard `*`, `$or`, `$not`) in `search()`, `keywordSearch()`, and `list()`. Previously only exact-equality filters worked — operator objects were passed as raw values and returned incorrect results ([#5263](https://github.com/mem0ai/mem0/pull/5263))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-08" description="v3.0.3">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -969,7 +1113,7 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
- **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))
|
||||
- **External Graph Store Removed (OSS):** `graph_memory.ts` (675 lines), `graphs/tools.ts` (267 lines), `graphs/utils.ts` (116 lines), `graphs/configs.ts` (30 lines) deleted. The external graph store integration is no longer part of the OSS SDK; graph memory now runs natively as built-in entity linking ([#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
|
||||
@@ -1323,6 +1467,37 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
|
||||
|
||||
<Tab title="CLI">
|
||||
|
||||
<Update label="2026-06-01" description="Node v0.2.8">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `jws` → 4.0.1 (CVE-2025-65945), `langsmith` → ^0.6.0 (CVE-2026-45134), `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343), `picomatch` → ^2.3.2 (CVE-2026-33671), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `path-to-regexp` → ^8.4.0 (CVE-2026-4926), `rollup` → ^4.59.0 (CVE-2026-27606), `glob` → ^10.5.0 (CVE-2025-64756), `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-16" description="Python v0.2.6 / Node v0.2.6">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Claim flow error message:** The `email_already_claimed` tip in `mem0 init --email` previously suggested running `mem0 link <key>` — a command that doesn't exist. Replaced with honest copy pointing the user to sign in at app.mem0.ai with their existing credentials ([#5152](https://github.com/mem0ai/mem0/pull/5152))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-14" description="Python v0.2.5 / Node v0.2.5">
|
||||
|
||||
**New Features:**
|
||||
- **Agent Mode (`mem0 init --agent`):** Zero-friction signup for AI agents — mints a working Mem0 API key in under 5 seconds with no email, no dashboard, no OTP. Returns an unclaimed shadow account the human can later claim with `mem0 init --email <their-email>` (memories preserved, same key keeps working) ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Self-declared agent identity:** Agents pass `--agent-caller <name>` (e.g. `claude-code`, `cursor`, `codex`) on `mem0 init --agent` so signups attribute to the right tool in analytics. Proof Editor-style — the agent declares itself rather than the CLI sniffing it from env vars ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **`mem0 identify <name>`:** New subcommand to self-tag an Agent Mode key after the fact when the agent forgot to pass `--agent-caller` on init. Idempotent — re-running just overwrites ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Plugin sync:** `~/.claude/settings.json::env::MEM0_API_KEY` and `~/.zshrc`/`.bashrc` `export MEM0_API_KEY=` lines stay in sync with `~/.mem0/config.json` automatically. Idempotent — only updates EXISTING entries, never creates new ones ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Claim flow:** `mem0 init --email <email>` claims an existing Agent Mode shadow via OTP. Upgrade-in-place — the API key never changes, memories transfer to the human's account ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Decision tree network resilience:** `pingKey` now distinguishes network errors from invalid keys — returns false ONLY on HTTP 401/403, returns true on connection failures / timeouts / 5xx. Prevents a VPN flap from silently rotating the user's API key and rewriting plugin-sync targets ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Rate-limit error clarity:** DRF's opaque `"You do not have permission"` 403 from Agent Mode rate limits is now translated to `"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC."` ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **JSON envelope `command` field:** `mem0 init --agent --json` error envelopes now populate the `command` field correctly instead of returning an empty string ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
- **Bootstrap envelope validation:** Defends against partial/malformed backend responses (e.g. `{api_key: null}`) silently persisting null/undefined into typed string fields ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-22" description="Python v0.2.4 / Node v0.2.4">
|
||||
|
||||
**New Features:**
|
||||
@@ -1415,6 +1590,43 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
||||
|
||||
<Tab title="Plugins">
|
||||
|
||||
<Update label="2026-06-01" description="openclaw-mem0 v1.0.12">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `protobufjs` → ^7.5.5, `vite` → ^8.0.5, `langsmith` → ^0.6.0 (CVE-2026-45134), `picomatch` → ^2.3.2 (CVE-2026-33671), `@qdrant/js-client-rest` → ^1.18.0
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-10" description="Vercel AI SDK v3.0.0">
|
||||
|
||||
**Major Release** — Migrated to Vercel AI SDK v6 (`LanguageModelV3` / `ProviderV3`) and Mem0 v3 API.
|
||||
|
||||
**Breaking Changes:**
|
||||
- **AI SDK v6:** Upgraded from AI SDK v5 (`LanguageModelV2`) to v6 (`LanguageModelV3`). Users must upgrade `ai` to `^6.0.199` and all `@ai-sdk/*` provider packages to `^3.x` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Mem0 v3 API:** Memory endpoints migrated from `/v1/memories/` and `/v2/memories/search/` to `/v3/memories/add/` and `/v3/memories/search/`. Entity IDs (`user_id`, `agent_id`, `run_id`) now go inside the `filters` object for search requests ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Graph memory removed:** All `enable_graph`, graph prompts, and relation-extraction code removed. Graph memory is now a project-level setting on the Platform ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Deprecated params removed:** `org_id`, `project_id`, `org_name`, `project_name`, `output_format`, `filter_memories`, `async_mode`, `enable_graph`, `version`, `api_version` removed from `Mem0ConfigSettings` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
**New Features:**
|
||||
- **V3 provider contract:** `specificationVersion: 'v3'`, `supportedUrls` property, V3 content array in `doGenerate`, V3 stream lifecycle events in `doStream` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Mem0 source in responses:** Memories are attached as a `source` in `generateText`/`streamText` responses with `providerMetadata.mem0.memories` for programmatic access ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Async memory storage:** `addMemories` is now properly `await`ed — memories no longer silently fail to store ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Prompt mutation:** Prompt array is now cloned before injecting memory context, preventing side effects on the caller's array ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Null guard on content:** `doGenerate` guards against null `content` from upstream providers ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Stream response:** `doStream` now returns the full `LanguageModelV3StreamResult` object preserving all V3 fields ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Response normalization:** `getMemories` and `retrieveMemories` now handle both array and `{results: [...]}` envelope responses from the v3 API ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-01" description="Vercel AI SDK v2.0.6">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `glob` → ^10.5.0 (CVE-2025-64756), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `picomatch` → ^2.3.2 (CVE-2026-33671), `rollup` → ^4.59.0 (CVE-2026-27606)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
|
||||
|
||||
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: "Neon"
|
||||
description: "Use Neon as a vector store in Mem0, powered by PostgreSQL and pgvector."
|
||||
---
|
||||
|
||||
Use [Neon](https://neon.com/) as a vector store in Mem0, powered by PostgreSQL and the
|
||||
[pgvector extension](https://neon.com/docs/extensions/pgvector).
|
||||
|
||||
Neon is a serverless Postgres platform. Since Mem0 supports Postgres through the
|
||||
`pgvector` provider, Neon can be used with a standard Postgres connection string.
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from mem0 import Memory
|
||||
|
||||
load_dotenv()
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"connection_string": os.environ["DATABASE_URL"],
|
||||
"collection_name": "memories",
|
||||
"embedding_model_dims": 1536,
|
||||
"hnsw": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
results = m.search(
|
||||
"What movies should I recommend?",
|
||||
filters={"user_id": "alice"},
|
||||
)
|
||||
|
||||
print(results)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import "dotenv/config";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const databaseUrl = new URL(process.env.DATABASE_URL!);
|
||||
|
||||
const m = new Memory({
|
||||
vectorStore: {
|
||||
provider: "pgvector",
|
||||
config: {
|
||||
user: decodeURIComponent(databaseUrl.username),
|
||||
password: decodeURIComponent(databaseUrl.password),
|
||||
host: databaseUrl.hostname,
|
||||
port: Number(databaseUrl.port || 5432),
|
||||
dbname: databaseUrl.pathname.slice(1) || "neondb",
|
||||
collectionName: "memories",
|
||||
dimension: 1536,
|
||||
embeddingModelDims: 1536,
|
||||
hnsw: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: "user" as const, content: "I'm planning to watch a movie tonight. Any recommendations?" },
|
||||
{ role: "assistant" as const, content: "How about thriller movies? They can be quite engaging." },
|
||||
{ role: "user" as const, content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
|
||||
{ role: "assistant" as const, content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
|
||||
];
|
||||
|
||||
await m.add(messages, {
|
||||
userId: "alice",
|
||||
metadata: { category: "movies" },
|
||||
});
|
||||
|
||||
const results = await m.search("What movies should I recommend?", {
|
||||
filters: { user_id: "alice" },
|
||||
});
|
||||
|
||||
console.log(results);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## SQL Migration
|
||||
|
||||
You don't need to run any SQL migrations. Mem0 creates the collection table when it initializes the `pgvector` store.
|
||||
|
||||
## Environment
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-xx...
|
||||
DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neondb?sslmode=require
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `connection_string` | Neon Postgres connection string. | Required |
|
||||
| `collection_name` | Name for the vector collection. | `mem0` |
|
||||
| `embedding_model_dims` | Embedding model dimensions. | `1536` |
|
||||
| `hnsw` | Enables HNSW indexing. | `False` |
|
||||
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields,
|
||||
so parse `DATABASE_URL` before creating `Memory`.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `user` | Database user. | Required |
|
||||
| `password` | Database password. | Required |
|
||||
| `host` | Database host. | Required |
|
||||
| `port` | Database port. | `5432` |
|
||||
| `dbname` | Database name. | `vector_store` |
|
||||
| `collectionName` | Name for the vector collection. | `memories` |
|
||||
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
||||
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
||||
| `hnsw` | Enables HNSW indexing. | `false` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Indexing
|
||||
|
||||
The `pgvector` provider can create an HNSW index for faster vector search.
|
||||
|
||||
- Set `hnsw` to `true` to enable a Hierarchical Navigable Small World index.
|
||||
- Leave `hnsw` as `false` if you want to create or manage indexes yourself.
|
||||
|
||||
### Similarity Search
|
||||
|
||||
The `pgvector` provider uses cosine similarity for vector search. Make sure your
|
||||
embedding dimensions match the configured `embedding_model_dims` value.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Index Selection**:
|
||||
- Use `hnsw` for faster search performance when memory usage is not a constraint
|
||||
- Manage indexes manually if you need a different pgvector index strategy
|
||||
|
||||
2. **Connection String**:
|
||||
- Always use environment variables or even better, a secret manager for sensitive information in the connection string
|
||||
- Format: `postgresql://user:password@host:port/database`
|
||||
@@ -56,6 +56,30 @@ config = {
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `collection_name` | string | required | Name of the OpenSearch index |
|
||||
| `host` | string | required | OpenSearch endpoint URL |
|
||||
| `port` | int | 9200 | Port number |
|
||||
| `http_auth` | object | None | Authentication credentials (e.g., AWSV4SignerAuth) |
|
||||
| `embedding_model_dims` | int | 1536 | Dimension of embedding vectors |
|
||||
| `use_ssl` | bool | False | Enable SSL/TLS connection |
|
||||
| `verify_certs` | bool | False | Verify SSL certificates |
|
||||
| `auto_refresh` | bool | False | Automatically refresh index after insert. OpenSearch refreshes every ~1 second by default, so this is rarely needed. |
|
||||
|
||||
<Note>
|
||||
The defaults above match a local OpenSearch instance. The AWS OpenSearch Serverless
|
||||
example earlier on this page intentionally overrides them with `port=443`, `use_ssl=True`,
|
||||
and `verify_certs=True`, which are required when connecting to a Serverless collection.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
For **AWS OpenSearch Serverless**, keep `auto_refresh=False` (the default).
|
||||
The `indices.refresh()` API is not supported on Serverless collections.
|
||||
</Note>
|
||||
|
||||
### Add Memories
|
||||
|
||||
```python
|
||||
|
||||
@@ -76,6 +76,7 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `path` | Path for the qdrant database | `/tmp/qdrant` |
|
||||
| `url` | Full URL for the qdrant server | `None` |
|
||||
| `api_key` | API key for the qdrant server | `None` |
|
||||
| `https` | Whether to force HTTPS on or off. `None` lets the client decide; set `False` for plain HTTP Qdrant with API key authentication. | `None` |
|
||||
| `on_disk` | For enabling persistent storage | `False` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
@@ -90,4 +91,4 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `apiKey` | API key for the Qdrant server | `None` |
|
||||
| `onDisk` | For enabling persistent storage | `False` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
|
||||
@@ -323,10 +323,6 @@ Metadata: {'verified': True, 'updated_date': '2025-04-02'}
|
||||
That “no duplicates” promise comes from the inference pipeline. Keep `infer=True` when you rely on automatic updates. Raw imports (`infer=False`) skip conflict checks, so mixing the two modes for the same fact will create duplicates.
|
||||
</Warning>
|
||||
|
||||
**Maintains relationships:**
|
||||
|
||||
- If using graph memory, connections to other entities persist
|
||||
|
||||
### Pick the right inference mode
|
||||
|
||||
| Mode | What it does | Best for | Watch out for |
|
||||
|
||||
@@ -216,7 +216,6 @@ This information was retrieved from your memory history where you previously men
|
||||
|
||||
- **Smart Memory Management** - Organizes memories into searchable information *without setting up vector databases*
|
||||
- **Fast Retrieval** - Instant lookups with *sub-millisecond ping*, handles large datasets
|
||||
- **Graph Capabilities** - Builds knowledge *automatically* as you push information
|
||||
- **Simple Integration** - Uses Mem0 API in the backend, works with *any MCP client* with just a few lines of code
|
||||
|
||||
### Gemini 3 + Mem0 Benefits
|
||||
|
||||
@@ -156,14 +156,7 @@ Here are some examples of how Mem0 can be integrated into various applications:
|
||||
icon="aws"
|
||||
href="/cookbooks/integrations/aws-bedrock"
|
||||
>
|
||||
Mem0 with AWS Bedrock and Neptune.
|
||||
</Card>
|
||||
<Card
|
||||
title="Graph Memory on Neptune"
|
||||
icon="network-wired"
|
||||
href="/cookbooks/integrations/neptune-analytics"
|
||||
>
|
||||
Graph memory with Neptune Analytics.
|
||||
Mem0 with AWS Bedrock.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ Some benchmarks today — particularly smaller ones like LoCoMo and LongMemEval
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Mem0's memory system operates across two phases — **extraction** (writing) and **retrieval** (reading) — with an entity linking layer connecting them.
|
||||
Mem0's memory system operates across two phases, **extraction** (writing) and **retrieval** (reading), with a graph memory layer (entity linking) connecting them.
|
||||
|
||||
### Memory Extraction (Distillation)
|
||||
|
||||
@@ -27,14 +27,14 @@ When new conversations arrive, the extraction pipeline processes them through fi
|
||||
2. **Context Lookup** — Find related existing memories to avoid duplicates
|
||||
3. **Distill Memories** — Single-pass LLM extraction produces ADD-only facts from input + context
|
||||
4. **Deduplicate + Embed** — Hash-based deduplication, then vectorize new memories
|
||||
5. **Entity Linking** — Identify entities (proper nouns, quoted text, compound noun phrases) and link them across memories
|
||||
5. **Graph Memory (Entity Linking)**: Identify entities (proper nouns, quoted text, compound noun phrases) and link them across memories into a graph
|
||||
|
||||
Memories are distributed across three storage layers, each tuned for a specific retrieval pattern:
|
||||
|
||||
| Store | Contents | Purpose |
|
||||
|---|---|---|
|
||||
| **Vector Database** | Memory text, embeddings, metadata (timestamps, hash, categories, attributed_to) | Primary fact storage + semantic retrieval |
|
||||
| **Entity Store** | Entities + embeddings + linked memory IDs | Entity-based retrieval boost |
|
||||
| **Graph / Entity Store** | Entities + embeddings + linked memory IDs | Graph connections across memories + entity-based retrieval boost |
|
||||
| **SQL Database** | History log (ADD events) + rolling message window | Audit trail + extraction dedup context |
|
||||
|
||||
<Info>
|
||||
@@ -47,7 +47,7 @@ When a query arrives, the retrieval pipeline scores candidates across three sign
|
||||
|
||||
1. **Semantic Search** — Vector similarity scoring against memory embeddings
|
||||
2. **Keyword Search** — Normalized term matching via BM25 with verb-form lemmatization
|
||||
3. **Entity Search** — Entity graph matching boosts memories linked to query entities
|
||||
3. **Entity Search** — Entity matching boosts memories linked to query entities
|
||||
|
||||
Results are fused via rank scoring into a final top-K set. Different query types lean on different signals:
|
||||
|
||||
@@ -76,7 +76,7 @@ The combined score outperformed every individual signal across every category te
|
||||
|
||||
*Mean tokens: 6,956*
|
||||
|
||||
The two largest gains are **temporal queries (+29.6)** and **multi-hop reasoning (+23.1)**. Both categories directly test the ADD-only architecture (preserving temporal context) and entity linking (connecting facts across memories).
|
||||
The two largest gains are **temporal queries (+29.6)** and **multi-hop reasoning (+23.1)**. Both categories directly test the ADD-only architecture (preserving temporal context) and graph memory / entity linking (connecting facts across memories).
|
||||
|
||||
### LongMemEval
|
||||
|
||||
|
||||
@@ -21,35 +21,31 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
|
||||
- **Messages** – The ordered list of user/assistant turns you send to `add`.
|
||||
- **Infer** – Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
|
||||
- **Metadata** – Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
|
||||
- **User / Session identifiers** – `user_id`, `agent_id`, or `run_id` that scope the memory for future searches.
|
||||
- **User / Session identifiers** – `user_id`, `agent_id`, `app_id`, or `run_id` that scope the memory for future searches.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Mem0 offers two flows:
|
||||
|
||||
- **Mem0 Platform** – Fully managed API with dashboard, scaling, and graph features.
|
||||
- **Mem0 Platform** – Fully managed API with dashboard and scaling.
|
||||
- **Mem0 Open Source** – Local SDK that you run in your own environment.
|
||||
|
||||
Both flows take the same payload and pass it through the same pipeline.
|
||||
|
||||
<Frame caption="Architecture diagram illustrating the process of adding memories.">
|
||||
<img src="../../images/add_architecture.png" />
|
||||
</Frame>
|
||||
Both flows take the same payload and add memories through an additive pipeline.
|
||||
|
||||
<Steps>
|
||||
<Step title="Information extraction">
|
||||
Mem0 sends the messages through an LLM that pulls out key facts, decisions, or preferences to remember.
|
||||
</Step>
|
||||
<Step title="Conflict resolution">
|
||||
Existing memories are checked for duplicates or contradictions so the latest truth wins.
|
||||
<Step title="Additive storage">
|
||||
New memories are added without overwriting or deleting existing memories.
|
||||
</Step>
|
||||
<Step title="Storage">
|
||||
The resulting memories land in managed vector storage (and optional graph storage) so future searches return them quickly.
|
||||
<Step title="Retrieval">
|
||||
Future searches rank the most relevant memories for the query.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Warning>
|
||||
Duplicate protection only runs during that conflict-resolution step when you let Mem0 infer memories (`infer=True`, the default). If you switch to `infer=False`, Mem0 stores your payload exactly as provided, so duplicates will land. Mixing both modes for the same fact will save it twice.
|
||||
When you switch to `infer=False`, Mem0 stores your payload exactly as provided, so duplicates can land. Mixing both modes for the same fact can save it twice.
|
||||
</Warning>
|
||||
|
||||
You trigger this pipeline with a single `add` call—no manual orchestration needed.
|
||||
@@ -84,13 +80,13 @@ const messages = [
|
||||
];
|
||||
|
||||
await client.add(messages, {
|
||||
user_id: "alice",
|
||||
userId: "alice",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info icon="check">
|
||||
Expect a `memory_id` (or list of IDs) in the response. Check the Mem0 dashboard to confirm the new entry under the correct user.
|
||||
Expect a `status: "PENDING"` response with an `event_id`. Poll `GET /v1/event/{event_id}/` to confirm completion.
|
||||
</Info>
|
||||
|
||||
## Add with Mem0 Open Source
|
||||
@@ -142,7 +138,7 @@ const result = memory.add(messages, {
|
||||
</Tip>
|
||||
|
||||
<Warning>
|
||||
If you do choose `infer=False`, keep it consistent. Raw inserts skip conflict resolution, so a later `infer=True` call with the same content will create a second memory instead of updating the first.
|
||||
If you do choose `infer=False`, keep it consistent. Raw inserts skip inference, so a later `infer=True` call with the same content can create a second memory.
|
||||
</Warning>
|
||||
|
||||
## When Should You Add Memory?
|
||||
@@ -171,13 +167,13 @@ For full list of supported fields, required formats, and advanced options, see t
|
||||
|
||||
| Capability | Mem0 Platform | Mem0 OSS |
|
||||
| --- | --- | --- |
|
||||
| Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage |
|
||||
| Add behavior | ADD-only; memories accumulate | ADD-only; you control storage |
|
||||
| Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs |
|
||||
| Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI |
|
||||
|
||||
## Put it into practice
|
||||
|
||||
- Review the <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> guide to layer metadata, rerankers, and graph toggles.
|
||||
- Review the <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> guide to layer metadata and rerankers.
|
||||
- Explore the <Link href="/api-reference/memory/add-memories">Add Memories API reference</Link> for every request/response field.
|
||||
|
||||
## See it live
|
||||
|
||||
@@ -25,10 +25,6 @@ Mem0's search operation lets agents ask natural-language questions and get back
|
||||
|
||||
## Architecture
|
||||
|
||||
<Frame caption="Architecture diagram illustrating the memory search process.">
|
||||
<img src="../../images/search_architecture.png" />
|
||||
</Frame>
|
||||
|
||||
<Steps>
|
||||
<Step title="Query processing">
|
||||
Mem0 cleans and enriches your natural-language query so the downstream embedding search is accurate.
|
||||
@@ -160,6 +156,33 @@ const memories = memory.search("food preferences", {
|
||||
On Mem0 Platform v3, time-aware queries use Temporal Reasoning internally while preserving the normal search response shape. See <Link href="/platform/features/temporal-reasoning">Temporal Reasoning</Link>.
|
||||
</Note>
|
||||
|
||||
### Explain OSS search scores
|
||||
|
||||
OSS search combines semantic similarity with optional keyword and entity signals. Pass `explain=True` when tuning retrieval quality or debugging why a memory ranked where it did:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
filters={"user_id": "alice"},
|
||||
explain=True,
|
||||
)
|
||||
|
||||
print(results["results"][0]["score_details"])
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const results = await memory.search("food preferences", {
|
||||
filters: { user_id: "alice" },
|
||||
explain: true,
|
||||
});
|
||||
|
||||
console.log(results.results[0].score_details);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Each result includes `score_details` with the semantic score, normalized BM25 score, entity boost, raw combined score, maximum possible score, final score, and threshold used for filtering. The field is omitted unless `explain` is enabled, so existing response shapes stay unchanged.
|
||||
|
||||
## Filter patterns
|
||||
|
||||
Filters help narrow down search results. Common use cases:
|
||||
|
||||
@@ -104,7 +104,7 @@ results = memory.search(
|
||||
## Put it into practice
|
||||
|
||||
- Use the <Link href="/core-concepts/memory-operations/add">Add Memory</Link> guide to persist user preferences.
|
||||
- Follow <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> to tune metadata and graph writes.
|
||||
- Follow <Link href="/platform/advanced-memory-operations">Advanced Memory Operations</Link> to tune metadata and retrieval.
|
||||
|
||||
## See it live
|
||||
|
||||
|
||||
+14
-10
@@ -40,6 +40,7 @@
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"platform/overview",
|
||||
"platform/agent-signup",
|
||||
"vibecoding",
|
||||
"platform/mem0-mcp",
|
||||
"platform/cli",
|
||||
@@ -70,6 +71,7 @@
|
||||
"pages": [
|
||||
"platform/features/v2-memory-filters",
|
||||
"platform/features/entity-scoped-memory",
|
||||
"platform/features/graph-memory",
|
||||
"platform/features/async-client",
|
||||
"platform/features/multimodal-support",
|
||||
"platform/features/custom-categories",
|
||||
@@ -142,7 +144,8 @@
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes"
|
||||
"integrations/hermes",
|
||||
"integrations/pi-agent"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -244,6 +247,7 @@
|
||||
"components/vectordbs/dbs/cassandra",
|
||||
"components/vectordbs/dbs/s3_vectors",
|
||||
"components/vectordbs/dbs/databricks",
|
||||
"components/vectordbs/dbs/neon",
|
||||
"components/vectordbs/dbs/neptune_analytics",
|
||||
"components/vectordbs/dbs/turbopuffer"
|
||||
]
|
||||
@@ -301,7 +305,8 @@
|
||||
"group": "Migration",
|
||||
"icon": "arrow-right",
|
||||
"pages": [
|
||||
"migration/oss-v2-to-v3"
|
||||
"migration/oss-v2-to-v3",
|
||||
"migration/server-pgvector-upgrade"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -436,7 +441,7 @@
|
||||
"integrations/flowise",
|
||||
"integrations/langchain-tools",
|
||||
"integrations/agentops",
|
||||
"integrations/keywords",
|
||||
"integrations/respan",
|
||||
"integrations/raycast"
|
||||
]
|
||||
}
|
||||
@@ -451,7 +456,9 @@
|
||||
"pages": [
|
||||
"integrations/claude-code",
|
||||
"integrations/cursor",
|
||||
"integrations/codex"
|
||||
"integrations/codex",
|
||||
"integrations/opencode",
|
||||
"integrations/antigravity"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -459,7 +466,8 @@
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes"
|
||||
"integrations/hermes",
|
||||
"integrations/pi-agent"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -640,10 +648,6 @@
|
||||
"source": "/open-source/features/custom-fact-extraction-prompt",
|
||||
"destination": "/open-source/features/custom-instructions"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/graph-memory",
|
||||
"destination": "/migration/oss-v2-to-v3"
|
||||
},
|
||||
{
|
||||
"source": "/cookbooks/essentials/choosing-memory-architecture-vector-vs-graph",
|
||||
"destination": "/migration/oss-v2-to-v3"
|
||||
@@ -1018,7 +1022,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/features/graph-memory",
|
||||
"destination": "/migration/oss-v2-to-v3"
|
||||
"destination": "/platform/features/graph-memory"
|
||||
},
|
||||
{
|
||||
"source": "/features/:slug",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 276 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
@@ -309,19 +309,21 @@ Here are the available integrations for Mem0:
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Keywords AI"
|
||||
title="Respan"
|
||||
icon={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
viewBox="0 0 200 200"
|
||||
fill="none"
|
||||
>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.07513 1.1863C9.21663 1.07722 9.39144 1.01009 9.56624 1.01009C9.83261 1.01009 10.0823 1.12756 10.2405 1.33734L15.0101 7.4964V12.4136L16.4335 13.8401C16.7582 14.1673 16.7582 14.7043 16.4335 15.0316C16.1089 15.3588 15.5762 15.3588 15.2515 15.0316L13.3453 13.1016V8.07538L8.92529 2.36944V2.36105C8.64228 2.00024 8.70887 1.4716 9.07513 1.1863ZM18.976 14.4133C18.8344 14.3778 18.7003 14.3042 18.5894 14.1925L16.9163 12.5059C16.7249 12.3129 16.6416 12.0528 16.6749 11.8094V6.88385H16.6499L11.8553 0.691225C11.7282 0.529117 11.6716 0.333133 11.6803 0.140562C11.134 0.0481292 10.5726 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C13.9387 20 17.3456 17.7229 18.976 14.4133Z" fill="currentColor"></path>
|
||||
<path d="M2.00635 190.234V9.76584H53.3558V29.5101H26.7223V170.562H53.3558V190.234H2.00635Z" fill="currentColor"></path>
|
||||
<path d="M120.692 160.902C116.383 160.902 112.691 159.387 109.612 156.357C106.535 153.327 105.02 149.633 105.067 145.277C105.02 141.016 106.535 137.37 109.612 134.34C112.691 131.309 116.383 129.794 120.692 129.794C124.859 129.794 128.481 131.309 131.559 134.34C134.684 137.37 136.27 141.016 136.317 145.277C136.27 148.166 135.512 150.793 134.045 153.161C132.624 155.528 130.73 157.422 128.362 158.842C126.042 160.216 123.486 160.902 120.692 160.902Z" fill="currentColor"></path>
|
||||
<path d="M197.993 9.76584V190.234H146.643V170.562H173.278V29.5101H146.643V9.76584H197.993Z" fill="currentColor"></path>
|
||||
</svg>
|
||||
}
|
||||
href="/integrations/keywords"
|
||||
href="/integrations/respan"
|
||||
>
|
||||
Build AI applications with persistent memory and comprehensive LLM observability.
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Antigravity
|
||||
description: "Add persistent memory to Google Antigravity with the Mem0 plugin — MCP server, lifecycle hooks, and slash commands."
|
||||
---
|
||||
|
||||
Add persistent memory to [**Google Antigravity**](https://antigravity.google) (`agy` CLI and Desktop IDE) with the Mem0 plugin. Your agent forgets everything between sessions — Mem0 fixes that by storing decisions, preferences, and learnings so they carry over automatically.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 API key (starts with `m0-`):
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-antigravity" rel="nofollow">Get your API key</a> (free sign-up at <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-antigravity" rel="nofollow">app.mem0.ai</a>)
|
||||
|
||||
2. Add it to your shell profile so it persists across sessions:
|
||||
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc && source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Installation
|
||||
|
||||
**Option A — degit** (recommended):
|
||||
|
||||
```bash
|
||||
# Install the plugin (MCP server, hooks, scripts)
|
||||
npx degit mem0ai/mem0/integrations/mem0-plugin ~/.gemini/config/plugins/mem0
|
||||
```
|
||||
|
||||
This installs the MCP server, lifecycle hooks, and shared scripts.
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Included |
|
||||
|-----------|:--------:|
|
||||
| MCP Server (9 memory tools) | Yes |
|
||||
| Lifecycle Hooks | Yes |
|
||||
| 16 Slash Commands | Yes |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## Lifecycle Hooks
|
||||
|
||||
The plugin uses the same shell scripts as Claude Code, Cursor, and Codex — hooks bridge environment variables using `${extensionPath}` (Antigravity's plugin-root token).
|
||||
|
||||
| Hook | Event | What it does |
|
||||
|------|-------|-------------|
|
||||
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No tools appearing** — Restart your Antigravity session after installation
|
||||
- **"Connection failed"** — Verify your key is set: `echo $MEM0_API_KEY`
|
||||
- **MCP 401 Unauthorized** — If `${MEM0_API_KEY}` interpolation doesn't work in your `agy` version, replace with your literal key in `mcp_config.json`
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
Detailed MCP configuration for all clients
|
||||
</Card>
|
||||
<Card title="OpenCode Integration" icon="code" href="/integrations/opencode">
|
||||
Add Mem0 memory to OpenCode workflows
|
||||
</Card>
|
||||
</CardGroup>
|
||||
@@ -5,13 +5,6 @@ description: "Add persistent memory to Claude Code and Claude Cowork with the Me
|
||||
|
||||
Add persistent memory to [**Claude Code**](https://docs.anthropic.com/en/docs/claude-code) (CLI) and **Claude Cowork** (desktop app) with the Mem0 plugin. Your agent forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Lifecycle Hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
|
||||
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Claude Code, ensure you have:
|
||||
@@ -22,10 +15,25 @@ Before setting up Mem0 with Claude Code, ensure you have:
|
||||
|
||||
2. Claude Code CLI or Claude Cowork desktop app installed
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
3. Your API key added to your shell profile (persists across sessions):
|
||||
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Confirm it's set:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
echo $MEM0_API_KEY
|
||||
# Should print: m0-your-api-key
|
||||
```
|
||||
|
||||
## Installation
|
||||
@@ -84,6 +92,22 @@ Add to your Claude Code MCP config (`.mcp.json`):
|
||||
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## Post-Installation: Run `/mem0:onboard`
|
||||
|
||||
After installing the plugin, start a new Claude Code session and run:
|
||||
|
||||
```
|
||||
/mem0:onboard
|
||||
```
|
||||
|
||||
This runs the setup wizard which:
|
||||
1. Verifies your API key and MCP connection
|
||||
2. Detects and imports project files (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`)
|
||||
3. Installs coding-optimized memory categories
|
||||
4. Shows your identity (user ID, project scope, branch)
|
||||
|
||||
The onboarding is idempotent — safe to re-run anytime. It auto-triggers on first session in a new project, but you can always invoke it manually.
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Plugin Install | MCP Only |
|
||||
@@ -112,20 +136,13 @@ Once installed, the following tools are available in every Claude Code session:
|
||||
|
||||
When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecycle to automatically manage memory:
|
||||
|
||||
### Session Start
|
||||
On every new session, the plugin prompts Claude to call `search_memories` to load relevant context from prior sessions. On resumed or post-compaction sessions, it adjusts the prompt accordingly.
|
||||
|
||||
### User Prompt
|
||||
Before processing each user message, the plugin searches Mem0 for memories relevant to the current prompt and injects them into context. Short prompts (< 20 characters) are skipped to minimize latency.
|
||||
|
||||
### Pre-Compaction
|
||||
Before context compaction, the plugin prompts Claude to store a comprehensive session summary — including goals, accomplishments, decisions, modified files, and current state — so nothing is lost.
|
||||
|
||||
### Task Completed
|
||||
After each task completion, the plugin prompts Claude to extract and store key learnings: successful strategies, failed approaches, architectural decisions, and new conventions.
|
||||
|
||||
### Session End
|
||||
When Claude finishes responding, the plugin prompts for any unstored learnings and captures transcript state via the Mem0 REST API as a background safety net.
|
||||
| Hook | Event | What it does |
|
||||
|------|-------|-------------|
|
||||
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
|
||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Pre-compact** | `PreCompact` | Stores a session summary before context compaction |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
@@ -149,9 +166,10 @@ You: Add refresh token rotation to the auth system.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites)
|
||||
- **No tools appearing** — Restart your Claude Code session after installation
|
||||
- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations.
|
||||
- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations
|
||||
- **"Mem0 Inactive" banner every session** — Your API key isn't persisting. Add `export MEM0_API_KEY="m0-..."` to your `~/.zshrc` (or `~/.bashrc`) and run `source ~/.zshrc`
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
|
||||
+49
-124
@@ -1,16 +1,9 @@
|
||||
---
|
||||
title: Codex
|
||||
description: "Add persistent memory to OpenAI Codex with the Mem0 plugin — MCP server, memory protocol skill, and plugin marketplace support."
|
||||
description: "Add persistent memory to OpenAI Codex with the Mem0 plugin — MCP server, lifecycle hooks, and SDK skill."
|
||||
---
|
||||
|
||||
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP and using a skill-based memory protocol to automatically retrieve context and store learnings.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Memory Protocol Skill** — Instructs the agent to retrieve memories at task start, store learnings on completion, and capture session state before context loss
|
||||
3. **Plugin Marketplace** — Install via Codex's repo-level or personal plugin marketplace
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -22,17 +15,41 @@ Before setting up Mem0 with Codex, ensure you have:
|
||||
|
||||
2. OpenAI Codex access
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
3. Your API key added to your shell profile (persists across sessions):
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — Direct MCP (Recommended)
|
||||
### Option A — Plugin Marketplace (Recommended)
|
||||
|
||||
The fastest way to connect Codex to Mem0 — no downloads, no marketplace. Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
|
||||
Install the full plugin including MCP server, lifecycle hooks, and SDK skill.
|
||||
|
||||
1. Add the Mem0 marketplace:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add mem0ai/mem0
|
||||
```
|
||||
|
||||
2. Restart Codex, open the Plugin Directory, browse the **Mem0 Plugins** marketplace, and install **Mem0**.
|
||||
|
||||
<Info>
|
||||
Do not combine with Option B. The plugin manifest auto-registers the `mem0` MCP server, so adding both will create a duplicate registration.
|
||||
</Info>
|
||||
|
||||
### Option B — Direct MCP
|
||||
|
||||
The fastest way to connect Codex to Mem0 — no plugin, no marketplace. Add to `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
@@ -46,71 +63,16 @@ Make sure `MEM0_API_KEY` is exported in the shell you launch Codex from, then re
|
||||
Codex's `codex mcp add` CLI only supports stdio MCP servers. Because Mem0's MCP is HTTP/streamable, you configure it by editing `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
|
||||
</Info>
|
||||
|
||||
### Option B — Sideload the Plugin (Advanced)
|
||||
|
||||
For the full plugin experience — MCP server **plus** the Mem0 SDK skill, memory protocol skill, and opt-in lifecycle hooks — sideload the plugin from a local clone. The Mem0 repo already ships a marketplace manifest at [`.agents/plugins/marketplace.json`](https://github.com/mem0ai/mem0/blob/main/.agents/plugins/marketplace.json), so there's no JSON to author by hand. This follows the Codex [build-plugins](https://developers.openai.com/codex/plugins/build) local-testing workflow.
|
||||
|
||||
<Info>
|
||||
Don't combine Option B with Option A. The plugin manifest declares its MCP server via [`.codex-mcp.json`](https://github.com/mem0ai/mem0/blob/main/mem0-plugin/.codex-mcp.json), so Codex auto-registers the `mem0` MCP server when the plugin loads. Adding the same `[mcp_servers.mem0]` block to `~/.codex/config.toml` will create a duplicate registration.
|
||||
</Info>
|
||||
|
||||
**Step 1.** Clone the Mem0 repository anywhere on disk:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
**Step 2.** Register the bundled marketplace with Codex's CLI:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
This points Codex at the repo's `.agents/plugins/marketplace.json`. The bundled file uses `path: "./mem0-plugin"`, which Codex resolves relative to the clone root.
|
||||
|
||||
<Info>
|
||||
**Why we recommend this over hand-authoring `~/.agents/plugins/marketplace.json`:** Codex requires `source.path` in any marketplace manifest to be **relative** (starting with `./`) and **inside the marketplace root**. The repo's bundled manifest already satisfies this — the marketplace root is the clone directory, and `mem0-plugin/` lives inside it. With a personal `~/.agents/plugins/marketplace.json`, the root is `~/` and the clone has to live under `~/` too. The CLI form sidesteps that constraint.
|
||||
</Info>
|
||||
|
||||
**Step 3.** Restart Codex, run `/plugins`, browse the `Mem0 Plugins` marketplace, and install **Mem0**.
|
||||
|
||||
**Step 4 (optional) — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads them from `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`). Run the bundled installer once to merge the Mem0 entries into your global hooks file:
|
||||
|
||||
```bash
|
||||
python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py
|
||||
```
|
||||
|
||||
Then enable the hooks feature flag in `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
codex_hooks = true
|
||||
```
|
||||
|
||||
Restart Codex. The installer registers three hooks pointing at scripts inside your clone:
|
||||
|
||||
| Event | Behavior |
|
||||
|-------|----------|
|
||||
| `SessionStart` | Loads prior memories as bootstrap context |
|
||||
| `UserPromptSubmit` | Injects relevant memories before each prompt |
|
||||
| `Stop` | Reminds the agent to persist learnings at turn end |
|
||||
|
||||
Re-running the installer is idempotent. To remove the hooks: `python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py --uninstall`.
|
||||
|
||||
<Warning>
|
||||
The hooks file stores absolute paths into your clone (e.g. `~/codex-plugins/mem0-source/mem0-plugin/scripts/...`). If you move or delete the clone, the hooks will break silently — re-run the installer from the new location, or run `--uninstall` first.
|
||||
</Warning>
|
||||
This gives you the MCP tools but not the lifecycle hooks or SDK skill.
|
||||
|
||||
### Managing the Plugin
|
||||
|
||||
Codex provides CLI commands for managing marketplaces after install:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
```
|
||||
|
||||
To pull updates to the plugin source itself, `git pull` inside your clone (`~/codex-plugins/mem0-source`) and then run `codex plugin marketplace upgrade` to refresh Codex's plugin cache. Plugins are cached at `~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/`.
|
||||
To update, run `codex plugin marketplace upgrade` to pull the latest from the Mem0 repo.
|
||||
|
||||
<Info icon="check">
|
||||
After either option, start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
@@ -118,12 +80,11 @@ To pull updates to the plugin source itself, `git pull` inside your clone (`~/co
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Sideloaded Plugin | Direct MCP |
|
||||
|-----------|:-----------------:|:----------:|
|
||||
| Component | Plugin Install | MCP Only |
|
||||
|-----------|:--------------:|:--------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Memory Protocol Skill | Yes | No |
|
||||
| Lifecycle Hooks | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
| Lifecycle Hooks (opt-in) | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
@@ -141,49 +102,17 @@ Once installed, the following tools are available in every Codex session:
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## Memory Protocol Skill
|
||||
## Lifecycle Hooks
|
||||
|
||||
When the plugin is sideloaded, the memory protocol skill instructs the agent to:
|
||||
When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to automatically manage memory:
|
||||
|
||||
### On Every New Task
|
||||
1. Call `search_memories` with a query related to the current task to load relevant context
|
||||
2. Review returned memories to understand what was learned in prior sessions
|
||||
3. Optionally call `get_memories` to browse all stored memories
|
||||
|
||||
### After Completing Significant Work
|
||||
Store key learnings using `add_memory` with structured metadata:
|
||||
|
||||
| What to store | Metadata type |
|
||||
|--------------|---------------|
|
||||
| Architectural decisions | `{"type": "decision"}` |
|
||||
| Strategies that worked | `{"type": "task_learning"}` |
|
||||
| Failed approaches | `{"type": "anti_pattern"}` |
|
||||
| User preferences observed | `{"type": "user_preference"}` |
|
||||
| Environment discoveries | `{"type": "environmental"}` |
|
||||
| Conventions established | `{"type": "convention"}` |
|
||||
|
||||
### Before Losing Context
|
||||
Store a comprehensive session summary including goals, accomplishments, decisions, files modified, and current state with metadata `{"type": "session_state"}`.
|
||||
|
||||
## Plugin Manifest
|
||||
|
||||
The Codex plugin manifest (`.codex-plugin/plugin.json`) follows the Codex plugin specification:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications.",
|
||||
"skills": "./skills/",
|
||||
"mcpServers": "./.codex-mcp.json",
|
||||
"interface": {
|
||||
"displayName": "Mem0",
|
||||
"shortDescription": "Persistent memory layer for AI coding workflows",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Read", "Write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
| Hook | Event | What it does |
|
||||
|------|-------|-------------|
|
||||
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Pre-compact** | `PreCompact` | Stores a session summary before context compaction |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
@@ -206,14 +135,10 @@ You: Add WebSocket support for real-time notification delivery.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Codex session after plugin installation
|
||||
- **Duplicate `mem0` MCP server / "tool collision" errors** — You combined Option A (Direct MCP) with Option B (sideload). The sideloaded plugin auto-registers `mem0` from `.codex-mcp.json`, so remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`.
|
||||
- **`plugin/read failed in TUI`** — Codex can't find the plugin directory the marketplace points at. If you used `codex plugin marketplace add <path>`, confirm the path is your clone root and that `<clone>/.agents/plugins/marketplace.json` exists. If you hand-authored `~/.agents/plugins/marketplace.json`, `source.path` must be relative (start with `./`), inside the marketplace root (`~/` for personal installs), and end in `mem0-plugin` — e.g. `"./codex-plugins/mem0-source/mem0-plugin"`.
|
||||
- **Plugin not found in `/plugins`** — Run `codex plugin marketplace add ~/path/to/clone` again, or confirm the marketplace was registered with `codex plugin marketplace remove mem0-plugins` then re-add.
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files.
|
||||
- **Hooks not firing** — Confirm `codex_hooks = true` is in `~/.codex/config.toml` under `[features]`, and that `~/.codex/hooks.json` contains the Mem0 entries (re-run the installer if not). Restart Codex after enabling the flag.
|
||||
- **Hooks broke after moving the clone** — The installer bakes absolute paths into `~/.codex/hooks.json` pointing at scripts inside your clone. If you moved or renamed the clone directory, run `python3 <new-clone>/mem0-plugin/scripts/install_codex_hooks.py` from the new location — the installer is idempotent and replaces the old entries.
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Codex session after installation
|
||||
- **Duplicate `mem0` MCP / "tool collision" errors** — You combined Option A with Option B. Remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`; the plugin registers it automatically
|
||||
- **Hooks not firing** — Ensure the plugin is installed via the marketplace (Option A). MCP-only installs do not include hooks
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
|
||||
@@ -5,13 +5,6 @@ description: "Add persistent memory to Cursor with the Mem0 plugin — MCP serve
|
||||
|
||||
Add persistent memory to [**Cursor**](https://cursor.com) with the Mem0 plugin. Your AI assistant forgets everything between sessions — this plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context before every response.
|
||||
|
||||
## Overview
|
||||
|
||||
1. **MCP Server** — Connect to Mem0's remote MCP server for memory tools (add, search, update, delete)
|
||||
2. **Lifecycle Hooks** — Automatic memory capture at session start, compaction, and user prompts (Marketplace install)
|
||||
3. **SDK Skill** — Teaches the agent how to integrate the Mem0 SDK into your applications
|
||||
4. **Zero local dependencies** — Cloud-hosted MCP server, no local setup required
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Cursor, ensure you have:
|
||||
@@ -22,12 +15,20 @@ Before setting up Mem0 with Cursor, ensure you have:
|
||||
|
||||
2. Cursor installed ([cursor.com](https://cursor.com))
|
||||
|
||||
3. Your API key exported in your shell:
|
||||
3. Your API key added to your shell profile (persists across sessions):
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Warning>
|
||||
Already have `mem0` configured as an MCP server in Cursor? Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
</Warning>
|
||||
@@ -103,14 +104,13 @@ Once installed, the following tools are available in every Cursor session:
|
||||
|
||||
When installed via the Cursor Marketplace, Mem0 hooks into Cursor's lifecycle:
|
||||
|
||||
### Session Start
|
||||
On every new session, the plugin prompts the agent to call `search_memories` to load relevant context from prior sessions.
|
||||
|
||||
### User Prompt
|
||||
Before processing each user message, the plugin searches Mem0 for relevant memories and injects them into context. Short prompts are skipped to minimize latency.
|
||||
|
||||
### Pre-Compaction
|
||||
Before context compaction, the plugin captures a comprehensive session summary so nothing is lost when the context window resets.
|
||||
| Hook | Event | What it does |
|
||||
|------|-------|-------------|
|
||||
| **Session start** | `sessionStart` | Loads prior memories and displays status banner |
|
||||
| **User prompt** | `beforeSubmitPrompt` | Searches relevant memories before each message; skips short prompts |
|
||||
| **Pre-tool (2 handlers)** | `preToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
||||
| **Post-tool (2 handlers)** | `postToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Pre-compact** | `preCompact` | Stores a session summary before context compaction |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user