Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88e9f3973e | ||
|
|
4176293cc9 | ||
|
|
64487cbf31 | ||
|
|
d97b7e9131 | ||
|
|
c5bf46c44c | ||
|
|
ec5baaba20 | ||
|
|
12bb2588f0 | ||
|
|
8a9915d3fe | ||
|
|
789122bbea |
@@ -8,7 +8,7 @@
|
|||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"source": {
|
"source": {
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"path": "./integrations/mem0-plugin"
|
"path": "./mem0-plugin"
|
||||||
},
|
},
|
||||||
"policy": {
|
"policy": {
|
||||||
"installation": "AVAILABLE",
|
"installation": "AVAILABLE",
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"source": "./integrations/mem0-plugin",
|
"source": "./mem0-plugin",
|
||||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||||
"version": "0.2.11"
|
"version": "0.1.1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"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": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "mem0",
|
"name": "mem0",
|
||||||
"source": "./integrations/mem0-plugin",
|
"source": "./mem0-plugin",
|
||||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||||
"version": "0.2.11"
|
"version": "0.1.1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,18 @@
|
|||||||
name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||||
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
|
if: startsWith(github.event.release.tag_name, 'v')
|
||||||
# like vercel-ai-v* that also start with 'v'
|
|
||||||
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
@@ -54,6 +39,7 @@ jobs:
|
|||||||
# packages_dir: dist/
|
# packages_dir: dist/
|
||||||
|
|
||||||
- name: Publish distribution 📦 to PyPI
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
with:
|
||||||
packages_dir: dist/
|
packages_dir: dist/
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
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."
|
|
||||||
+59
-18
@@ -1,11 +1,20 @@
|
|||||||
name: ci
|
name: ci
|
||||||
|
|
||||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
|
||||||
# push-to-main runs remain standalone.
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_call:
|
paths:
|
||||||
|
- 'mem0/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'embedchain/**'
|
||||||
|
- '.github/workflows/**'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'mem0/**'
|
||||||
|
- 'tests/**'
|
||||||
|
- 'embedchain/**'
|
||||||
|
- 'pyproject.toml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
changelog_check:
|
changelog_check:
|
||||||
@@ -51,8 +60,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
mem0_changed: ${{ steps.filter.outputs.mem0 }}
|
mem0_changed: ${{ steps.filter.outputs.mem0 }}
|
||||||
|
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v2
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
@@ -60,28 +70,25 @@ jobs:
|
|||||||
mem0:
|
mem0:
|
||||||
- 'mem0/**'
|
- 'mem0/**'
|
||||||
- 'tests/**'
|
- 'tests/**'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/**'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
|
embedchain:
|
||||||
|
- 'embedchain/**'
|
||||||
|
|
||||||
build_mem0:
|
build_mem0:
|
||||||
needs: check_changes
|
needs: check_changes
|
||||||
|
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10", "3.11", "3.12"]
|
python-version: ["3.10", "3.11", "3.12"]
|
||||||
steps:
|
steps:
|
||||||
- name: Skip — no relevant changes
|
- uses: actions/checkout@v3
|
||||||
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 }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
- name: Clean up disk space
|
- name: Clean up disk space
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
df -h
|
df -h
|
||||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||||
@@ -89,27 +96,61 @@ jobs:
|
|||||||
sudo docker builder prune -a
|
sudo docker builder prune -a
|
||||||
df -h
|
df -h
|
||||||
- name: Install Hatch
|
- name: Install Hatch
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
run: pip install hatch
|
run: pip install hatch
|
||||||
- name: Load cached venv
|
- name: Load cached venv
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
id: cached-hatch-dependencies
|
id: cached-hatch-dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: .venv
|
path: .venv
|
||||||
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
|
||||||
- name: Install GEOS Libraries
|
- name: Install GEOS Libraries
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
|
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true' && steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
|
||||||
run: |
|
run: |
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||||
pip install ruff
|
pip install ruff
|
||||||
|
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
|
||||||
- name: Run Linting
|
- name: Run Linting
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
run: make lint
|
run: make lint
|
||||||
- name: Run tests and generate coverage report
|
- name: Run tests and generate coverage report
|
||||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
|
||||||
run: make test
|
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,25 +1,13 @@
|
|||||||
name: Publish @mem0/cli 📦 to npm
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish @mem0/cli 📦 to npm
|
name: Build and publish @mem0/cli 📦 to npm
|
||||||
if: startsWith(inputs.tag, 'cli-node-v')
|
if: startsWith(github.event.release.tag_name, 'cli-node-v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -28,8 +16,6 @@ jobs:
|
|||||||
working-directory: cli/node
|
working-directory: cli/node
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -52,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
name: CLI Node CI
|
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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -9,7 +7,10 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'cli/node/**'
|
- 'cli/node/**'
|
||||||
- '.github/workflows/cli-node-ci.yml'
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
workflow_call:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/node/**'
|
||||||
|
- '.github/workflows/cli-node-ci.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish mem0-cli 📦 to PyPI
|
name: Build and publish mem0-cli 📦 to PyPI
|
||||||
if: startsWith(inputs.tag, 'cli-v')
|
if: startsWith(github.event.release.tag_name, 'cli-v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -27,8 +16,6 @@ jobs:
|
|||||||
working-directory: cli/python
|
working-directory: cli/python
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
name: CLI Python CI
|
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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -9,7 +7,10 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'cli/python/**'
|
- 'cli/python/**'
|
||||||
- '.github/workflows/cli-python-ci.yml'
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
workflow_call:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'cli/python/**'
|
||||||
|
- '.github/workflows/cli-python-ci.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ name: docs - llms.txt check
|
|||||||
# python scripts/check-llms-txt-coverage.py # read-only
|
# python scripts/check-llms-txt-coverage.py # read-only
|
||||||
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
|
# 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:
|
on:
|
||||||
workflow_call:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'docs/**/*.mdx'
|
||||||
|
- 'docs/llms.txt'
|
||||||
|
- 'scripts/check-llms-txt-coverage.py'
|
||||||
|
- 'scripts/llms-txt-ignore.txt'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -1,35 +1,21 @@
|
|||||||
name: Publish @mem0/openclaw-mem0 📦 to npm
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
name: Build and publish @mem0/openclaw-mem0 📦 to npm
|
||||||
if: startsWith(inputs.tag, 'openclaw-v')
|
if: startsWith(github.event.release.tag_name, 'openclaw-v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: integrations/openclaw
|
working-directory: openclaw
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -42,7 +28,7 @@ jobs:
|
|||||||
node-version: '22'
|
node-version: '22'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -52,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
name: openclaw checks
|
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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'integrations/openclaw/**'
|
- 'openclaw/**'
|
||||||
|
- '.github/workflows/openclaw-checks.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'openclaw/**'
|
||||||
- '.github/workflows/openclaw-checks.yml'
|
- '.github/workflows/openclaw-checks.yml'
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@@ -27,13 +28,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
run: cd openclaw && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: cd integrations/openclaw && pnpm exec tsc --noEmit
|
run: cd openclaw && pnpm exec tsc --noEmit
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -53,20 +54,20 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
run: cd openclaw && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: cd integrations/openclaw && pnpm exec vitest run --coverage
|
run: cd openclaw && pnpm exec vitest run --coverage
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: matrix.node-version == 20
|
if: matrix.node-version == 20
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
flags: openclaw
|
flags: openclaw
|
||||||
directory: integrations/openclaw/coverage
|
directory: openclaw/coverage
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
@@ -85,15 +86,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
run: cd openclaw && pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cd integrations/openclaw && pnpm build
|
run: cd openclaw && pnpm build
|
||||||
|
|
||||||
- name: Verify dist output exists
|
- name: Verify dist output exists
|
||||||
run: |
|
run: |
|
||||||
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
test -f 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)
|
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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)
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
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,24 +1,13 @@
|
|||||||
name: Publish mem0ai 📦 to npm
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish mem0ai 📦 to npm
|
name: Build and publish mem0ai 📦 to npm
|
||||||
if: startsWith(inputs.tag, 'ts-v')
|
if: startsWith(github.event.release.tag_name, 'ts-v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
@@ -27,8 +16,6 @@ jobs:
|
|||||||
working-directory: mem0-ts
|
working-directory: mem0-ts
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -51,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
name: TypeScript SDK CI
|
name: TypeScript SDK CI
|
||||||
|
|
||||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
|
||||||
# push-to-main runs remain standalone.
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'mem0-ts/**'
|
- 'mem0-ts/**'
|
||||||
- '.github/workflows/ts-sdk-ci.yml'
|
- '.github/workflows/ts-sdk-ci.yml'
|
||||||
workflow_call:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'mem0-ts/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_changes:
|
check_changes:
|
||||||
|
|||||||
@@ -1,35 +1,21 @@
|
|||||||
name: Publish @mem0/vercel-ai-provider 📦 to npm
|
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:
|
on:
|
||||||
workflow_dispatch:
|
release:
|
||||||
inputs:
|
types: [published]
|
||||||
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:
|
jobs:
|
||||||
build-n-publish:
|
build-n-publish:
|
||||||
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
|
||||||
if: startsWith(inputs.tag, 'vercel-ai-v')
|
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: integrations/vercel-ai-sdk
|
working-directory: vercel-ai-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
ref: ${{ inputs.tag }}
|
|
||||||
|
|
||||||
- name: Install pnpm
|
- name: Install pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
@@ -42,7 +28,7 @@ jobs:
|
|||||||
node-version: '22'
|
node-version: '22'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml
|
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
@@ -52,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||||
else
|
else
|
||||||
|
|||||||
+1
-2
@@ -170,6 +170,7 @@ cython_debug/
|
|||||||
# Database
|
# Database
|
||||||
db
|
db
|
||||||
test-db
|
test-db
|
||||||
|
!embedchain/embedchain/core/db/
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
.idea/
|
.idea/
|
||||||
@@ -189,5 +190,3 @@ eval/
|
|||||||
qdrant_storage/
|
qdrant_storage/
|
||||||
.crossnote
|
.crossnote
|
||||||
testing.ipynb
|
testing.ipynb
|
||||||
.weave/
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[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
|
## Repository Structure
|
||||||
|
|
||||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
|
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
|
||||||
|
|
||||||
### Key Directories
|
### 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 |
|
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
| `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` |
|
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||||
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
|
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||||
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
|
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||||
| `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) |
|
| `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) |
|
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||||
| `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/` |
|
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
|
||||||
|
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
|
||||||
| `docs/` | Documentation site (Mintlify) |
|
| `docs/` | Documentation site (Mintlify) |
|
||||||
| `tests/` | Python SDK tests (pytest) |
|
| `tests/` | Python SDK tests (pytest) |
|
||||||
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
|
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
|
||||||
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
|
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
|
||||||
|
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
|
||||||
|
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
|
||||||
| `pr-reviews/` | Pull request review materials |
|
| `pr-reviews/` | Pull request review materials |
|
||||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
| `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/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
openclaw/ ──▶ mem0ai (npm)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
@@ -74,8 +74,8 @@ pre-commit install # install git hooks
|
|||||||
# TypeScript packages
|
# TypeScript packages
|
||||||
cd mem0-ts && pnpm install # TS SDK
|
cd mem0-ts && pnpm install # TS SDK
|
||||||
cd cli/node && pnpm install # Node CLI
|
cd cli/node && pnpm install # Node CLI
|
||||||
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
|
cd vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||||
cd integrations/openclaw && pnpm install # OpenClaw plugin
|
cd openclaw && pnpm install # OpenClaw plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build, Lint, and Test Commands
|
## Build, Lint, and Test Commands
|
||||||
@@ -163,10 +163,10 @@ pnpm run dev # tsx src/index.ts (development)
|
|||||||
- **Test:** vitest (not jest)
|
- **Test:** vitest (not jest)
|
||||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||||
|
|
||||||
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
|
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd integrations/vercel-ai-sdk
|
cd vercel-ai-sdk
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build # tsup
|
pnpm run build # tsup
|
||||||
pnpm run lint # eslint
|
pnpm run lint # eslint
|
||||||
@@ -181,10 +181,10 @@ pnpm run test:node # vitest (node runtime)
|
|||||||
- **Lint:** ESLint + Prettier
|
- **Lint:** ESLint + Prettier
|
||||||
- **Test:** jest + vitest (edge/node configs)
|
- **Test:** jest + vitest (edge/node configs)
|
||||||
|
|
||||||
### OpenClaw Plugin (`integrations/openclaw/`)
|
### OpenClaw Plugin (`openclaw/`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd integrations/openclaw
|
cd openclaw
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build # tsup
|
pnpm run build # tsup
|
||||||
pnpm run test # vitest run
|
pnpm run test # vitest run
|
||||||
@@ -246,19 +246,18 @@ make docs # or: cd docs && mintlify dev
|
|||||||
- **API spec:** `docs/openapi.json`
|
- **API spec:** `docs/openapi.json`
|
||||||
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||||
|
|
||||||
### Evaluation / Benchmarking
|
### Evaluation (`evaluation/`)
|
||||||
|
|
||||||
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
|
```bash
|
||||||
git clone https://github.com/mem0ai/memory-benchmarks.git
|
cd evaluation
|
||||||
cd memory-benchmarks
|
make run-mem0-add # Run mem0 add experiments
|
||||||
pip install -r requirements.txt
|
make run-mem0-search # Run mem0 search experiments
|
||||||
|
make run-mem0-plus-add # With graph memory
|
||||||
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
|
make run-mem0-plus-search # With graph memory
|
||||||
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
|
make run-rag # RAG baseline
|
||||||
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
|
make run-full-context # Full context baseline
|
||||||
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
|
make run-langmem # LangMem comparison
|
||||||
|
make run-openai # OpenAI comparison
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core APIs
|
## Core APIs
|
||||||
@@ -331,7 +330,7 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
|
|||||||
- Root SDK: line length **120**
|
- Root SDK: line length **120**
|
||||||
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||||
- **isort** with `profile = "black"` for import sorting.
|
- **isort** with `profile = "black"` for import sorting.
|
||||||
- Ruff excludes `openmemory/` from root config.
|
- Ruff excludes `embedchain/` and `openmemory/` from root config.
|
||||||
|
|
||||||
### TypeScript Conventions
|
### TypeScript Conventions
|
||||||
|
|
||||||
@@ -344,8 +343,8 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
|
|||||||
|---------|--------|-----------|---------------|
|
|---------|--------|-----------|---------------|
|
||||||
| `mem0-ts/` | — | Prettier | jest |
|
| `mem0-ts/` | — | Prettier | jest |
|
||||||
| `cli/node/` | Biome | Biome | vitest |
|
| `cli/node/` | Biome | Biome | vitest |
|
||||||
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||||
| `integrations/openclaw/` | — | — | vitest |
|
| `openclaw/` | — | — | vitest |
|
||||||
|
|
||||||
### Type Checking
|
### Type Checking
|
||||||
|
|
||||||
@@ -383,14 +382,12 @@ Model Context Protocol support in multiple places:
|
|||||||
|
|
||||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||||
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||||
- **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:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||||
|
|
||||||
### Plugin & Skills System
|
### Plugin & Skills System
|
||||||
|
|
||||||
- `integrations/mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
- `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:
|
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
|
||||||
- **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); `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
|
### Adding a New Provider
|
||||||
|
|
||||||
@@ -404,58 +401,32 @@ 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`)
|
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
|
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/CD
|
||||||
|
|
||||||
### CI Workflows (automated testing)
|
### CI Workflows (automated testing)
|
||||||
|
|
||||||
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 | Triggers | Tests |
|
||||||
|
|----------|------|----------|-------|
|
||||||
| Workflow | File | Standalone 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 |
|
||||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
|
| 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 |
|
||||||
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest 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 |
|
||||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build 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 |
|
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–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)
|
### 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 |
|
| Workflow | File | Tag Prefix | Target |
|
||||||
|----------|------|------------|--------|
|
|----------|------|------------|--------|
|
||||||
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
|
|
||||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||||
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@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`) |
|
| 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`) |
|
| 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.
|
- 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.
|
- 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
|
### Utility Workflows
|
||||||
|
|
||||||
@@ -603,6 +574,7 @@ N/A
|
|||||||
- Modify CI/CD workflows without explicit approval.
|
- 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.
|
- 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.
|
- 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.
|
- Skip pre-commit hooks.
|
||||||
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||||
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||||
|
|||||||
+43
-130
@@ -1,157 +1,72 @@
|
|||||||
# Contributing to Mem0
|
# Contributing to mem0
|
||||||
|
|
||||||
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
|
Let us make contribution easy, collaborative and fun.
|
||||||
community-driven project and we welcome contributions of all kinds — bug fixes,
|
|
||||||
new features, documentation, examples, and integrations.
|
|
||||||
|
|
||||||
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
|
## Submit your Contribution through PR
|
||||||
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
|
|
||||||
|
|
||||||
## Before You Start
|
To make a contribution, follow these steps:
|
||||||
|
|
||||||
### 1. Open an Issue First
|
1. Fork and clone this repository
|
||||||
|
2. Do the changes on your fork with dedicated feature branch `feature/f1`
|
||||||
|
3. If you modified the code (new feature or bug-fix), please add tests for it
|
||||||
|
4. Include proper documentation / docstring and examples to run the feature
|
||||||
|
5. Ensure that all tests pass
|
||||||
|
6. Submit a pull request
|
||||||
|
|
||||||
**Always open an issue before opening a pull request.** This lets us discuss the
|
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||||
change, avoid duplicate effort, and agree on the approach before you invest time
|
|
||||||
in code.
|
|
||||||
|
|
||||||
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
|
|
||||||
your bug or idea already exists.
|
|
||||||
- If it doesn't, open a
|
|
||||||
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
|
|
||||||
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
|
|
||||||
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
|
|
||||||
before starting significant work.
|
|
||||||
|
|
||||||
Every pull request must link to an issue using `Closes #<issue-number>`.
|
### 📦 Development Environment
|
||||||
|
|
||||||
### 2. Sign the Contributor License Agreement (CLA)
|
We use `hatch` for managing development environments. To set up:
|
||||||
|
|
||||||
**We cannot accept or merge any pull request until you have signed our Contributor
|
|
||||||
License Agreement (CLA).**
|
|
||||||
|
|
||||||
When you open your first PR, the CLA bot will automatically comment with a link to
|
|
||||||
sign. Signing takes less than a minute and only needs to be done once. Pull
|
|
||||||
requests from contributors who have not signed the CLA will be blocked from
|
|
||||||
merging.
|
|
||||||
|
|
||||||
## Repository Layout
|
|
||||||
|
|
||||||
The two most common contribution targets are the SDKs:
|
|
||||||
|
|
||||||
| Package | Path | Language | Package manager |
|
|
||||||
| --------------------- | ---------- | ------------ | --------------- |
|
|
||||||
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
|
|
||||||
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
|
|
||||||
|
|
||||||
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
|
|
||||||
(`integrations/`), the self-hosted `server/`, `openmemory/`, and the docs site
|
|
||||||
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. **Fork** the repository and **clone** your fork.
|
|
||||||
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
|
|
||||||
`fix/issue-1234`).
|
|
||||||
3. Make your changes — add **tests**, **documentation**, and **examples** as
|
|
||||||
appropriate.
|
|
||||||
4. Run **linting and tests** for every package you touched (see below).
|
|
||||||
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
|
|
||||||
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
|
|
||||||
6. Push and open a **pull request** against `main`, linking the issue with
|
|
||||||
`Closes #<number>` and filling out the
|
|
||||||
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
|
|
||||||
|
|
||||||
### Contributing to the Python SDK (`mem0/`)
|
|
||||||
|
|
||||||
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
|
|
||||||
**Do not use `pip` or `conda` for dependency management.**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
|
# Activate environment for specific Python version:
|
||||||
hatch shell dev_py_3_11
|
hatch shell dev_py_3_9 # Python 3.9
|
||||||
|
hatch shell dev_py_3_10 # Python 3.10
|
||||||
|
hatch shell dev_py_3_11 # Python 3.11
|
||||||
|
hatch shell dev_py_3_12 # Python 3.12
|
||||||
|
|
||||||
# Install pre-commit hooks (runs ruff + isort on commit)
|
# The environment will automatically install all dev dependencies
|
||||||
pre-commit install
|
# Run tests within the activated shell:
|
||||||
|
|
||||||
# Lint, format, and sort imports
|
|
||||||
make lint
|
|
||||||
make format
|
|
||||||
make sort
|
|
||||||
|
|
||||||
# Run the test suite (run `make install_all` first if deps are missing)
|
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Linter / formatter:** Ruff (line length **120**)
|
### 📌 Pre-commit
|
||||||
- **Import sorting:** isort (`profile = "black"`)
|
|
||||||
- **Tests:** pytest (in `tests/`)
|
|
||||||
|
|
||||||
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
|
To ensure our standards, make sure to install pre-commit before starting to contribute.
|
||||||
environment details.
|
|
||||||
|
|
||||||
### Contributing to the TypeScript SDK (`mem0-ts/`)
|
|
||||||
|
|
||||||
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
|
|
||||||
`npm` or `yarn`.**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd mem0-ts
|
pre-commit install
|
||||||
pnpm install
|
|
||||||
|
|
||||||
pnpm run build # tsup (CJS + ESM)
|
|
||||||
pnpm run test # jest (all tests)
|
|
||||||
pnpm run test:unit # unit tests with coverage
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Build:** tsup
|
### 🧪 Testing
|
||||||
- **Formatter:** Prettier
|
|
||||||
- **Tests:** jest
|
|
||||||
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
|
|
||||||
- Use ES module `import` syntax — never `require()`.
|
|
||||||
|
|
||||||
## Good Contribution Practices
|
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||||
|
|
||||||
- **Keep PRs small and focused.** One logical change per PR is easier to review and
|
```bash
|
||||||
merge.
|
# Run tests with default Python version
|
||||||
- **Follow existing patterns.** Match the style, structure, and conventions of the
|
make test
|
||||||
code around you. Don't introduce new frameworks or abstractions without
|
|
||||||
discussion.
|
|
||||||
- **Write tests** that would fail without your change — regression tests for bugs,
|
|
||||||
coverage for new features.
|
|
||||||
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
|
|
||||||
must be added to `docs/llms.txt` (run
|
|
||||||
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
|
|
||||||
- **Add examples** when introducing new user-facing behavior.
|
|
||||||
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
|
|
||||||
via the CI Gate.
|
|
||||||
- **Never commit secrets** — no `.env` files, API keys, or credentials.
|
|
||||||
- **Don't add core dependencies lightly.** New Python dependencies belong in an
|
|
||||||
optional group in `pyproject.toml`, not the core `dependencies` list.
|
|
||||||
- **Be responsive** to review feedback and keep your branch up to date with `main`.
|
|
||||||
|
|
||||||
## Pull Request Checklist
|
# Test specific Python versions:
|
||||||
|
make test-py-3.9 # Python 3.9 environment
|
||||||
|
make test-py-3.10 # Python 3.10 environment
|
||||||
|
make test-py-3.11 # Python 3.11 environment
|
||||||
|
make test-py-3.12 # Python 3.12 environment
|
||||||
|
|
||||||
Before requesting review, make sure:
|
# When using hatch shells, run tests with:
|
||||||
|
make test # After activating a shell with hatch shell test_XX
|
||||||
|
```
|
||||||
|
|
||||||
- [ ] An issue exists and is linked with `Closes #<number>`
|
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||||
- [ ] You have signed the CLA
|
|
||||||
- [ ] Your code follows the project's style guidelines (lint passes)
|
|
||||||
- [ ] You performed a self-review of your changes
|
|
||||||
- [ ] Tests are added/updated and pass locally
|
|
||||||
- [ ] Documentation is updated if needed
|
|
||||||
|
|
||||||
## Reporting Security Issues
|
We look forward to your pull requests and can't wait to see your contributions!
|
||||||
|
|
||||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
### 🚀 Releasing
|
||||||
Please follow our [Security Policy](./SECURITY.md) to report them privately.
|
|
||||||
|
|
||||||
## Releasing
|
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||||
|
|
||||||
All packages are published automatically via GitHub Actions when a GitHub Release
|
#### Tag Prefixes
|
||||||
is created with the correct tag prefix.
|
|
||||||
|
|
||||||
### Tag Prefixes
|
|
||||||
|
|
||||||
| Package | Registry | Tag Prefix | Example |
|
| Package | Registry | Tag Prefix | Example |
|
||||||
|---------|----------|------------|---------|
|
|---------|----------|------------|---------|
|
||||||
@@ -162,17 +77,15 @@ is created with the correct tag prefix.
|
|||||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||||
|
|
||||||
### How to Release
|
#### How to Release
|
||||||
|
|
||||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||||
|
|
||||||
### Publishing Details
|
#### Publishing Details
|
||||||
|
|
||||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||||
|
|
||||||
We look forward to your pull requests and can't wait to see your contributions!
|
|
||||||
|
|||||||
@@ -1026,8 +1026,7 @@ def get_user_preferences(user_id: str):
|
|||||||
### AutoGen Integration
|
### AutoGen Integration
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
|
from cookbooks.helper.mem0_teachability import Mem0Teachability
|
||||||
from helper.mem0_teachability import Mem0Teachability
|
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
|
||||||
# Add memory capability to AutoGen agents
|
# Add memory capability to AutoGen agents
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# 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!")
|
||||||
|
```
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
| Benchmark | Old | New | Tokens | Latency p50 |
|
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||||
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
|
| **LongMemEval** | 67.8 | **93.4** | 6.8K | 1.09s |
|
||||||
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||||
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||||
|
|
||||||
@@ -58,13 +58,12 @@ All benchmarks run on the same production-representative model stack. Single-pas
|
|||||||
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
|
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
|
||||||
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
|
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
|
||||||
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
|
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
|
||||||
- **Temporal Reasoning** -- time-aware retrieval that ranks the right dated instance for queries about current state, past events, and upcoming plans.
|
|
||||||
|
|
||||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||||
|
|
||||||
## Research Highlights
|
## Research Highlights
|
||||||
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||||
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
|
- **93.4 on LongMemEval** -- +26 points, with +53.6 on assistant memory recall
|
||||||
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||||
- [Read the full paper](https://mem0.ai/research)
|
- [Read the full paper](https://mem0.ai/research)
|
||||||
|
|
||||||
@@ -86,26 +85,6 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
|
|||||||
|
|
||||||
## 🚀 Quickstart Guide <a name="quickstart"></a>
|
## 🚀 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 |
|
| | Library | Self-Hosted Server | Cloud Platform |
|
||||||
|---|---------|-------------------|----------------|
|
|---|---------|-------------------|----------------|
|
||||||
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
|
||||||
@@ -153,7 +132,6 @@ 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)
|
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
|
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
|
### CLI
|
||||||
|
|
||||||
@@ -169,28 +147,6 @@ mem0 search "What does Alice prefer?" --user-id alice
|
|||||||
|
|
||||||
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
|
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
|
||||||
|
|
||||||
### Agent Skills
|
|
||||||
|
|
||||||
Teach your AI coding assistant (Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any tool that supports the skills standard) how to build with Mem0. Two categories:
|
|
||||||
|
|
||||||
**Reference skills — always on** (SDK knowledge loaded into the assistant's context):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0
|
|
||||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
|
|
||||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pipeline skills — run on demand** (execute an end-to-end workflow in an existing repo):
|
|
||||||
|
|
||||||
```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. 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
|
### Basic Usage
|
||||||
|
|
||||||
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
|
||||||
|
|||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
We take the security of Mem0 and our community seriously. Thank you for helping
|
|
||||||
keep Mem0 and its users safe by disclosing vulnerabilities responsibly.
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Please **do not** report security vulnerabilities through public GitHub issues,
|
|
||||||
pull requests, or discussions.
|
|
||||||
|
|
||||||
If you believe you have found a security vulnerability in Mem0, please report it
|
|
||||||
privately through one of the following channels:
|
|
||||||
|
|
||||||
1. **GitHub Private Vulnerability Reporting** — open a
|
|
||||||
[private security advisory](https://github.com/mem0ai/mem0/security/advisories/new)
|
|
||||||
directly on this repository.
|
|
||||||
2. **Email** the maintainers at **support@mem0.ai** with the subject line:
|
|
||||||
|
|
||||||
`SECURITY: Mem0 vulnerability report`
|
|
||||||
|
|
||||||
To help us triage and resolve the issue quickly, please include as much of the
|
|
||||||
following as you can:
|
|
||||||
|
|
||||||
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, OpenMemory)
|
|
||||||
- Affected version, tag, or commit
|
|
||||||
- Clear, step-by-step reproduction instructions
|
|
||||||
- The security impact and a proof of concept, if available
|
|
||||||
- Any suggested fix or mitigation
|
|
||||||
|
|
||||||
## Response Process
|
|
||||||
|
|
||||||
- We will acknowledge receipt of your report within **72 hours**.
|
|
||||||
- We will work with you privately to confirm the issue and assess its impact.
|
|
||||||
- Once a fix or mitigation is ready, we will coordinate a disclosure timeline
|
|
||||||
with you and credit you for the discovery, unless you prefer to remain anonymous.
|
|
||||||
|
|
||||||
## Public Disclosure
|
|
||||||
|
|
||||||
Please avoid sharing technical details of the vulnerability publicly until the
|
|
||||||
maintainers have reviewed the issue and a fix or mitigation has been released. We
|
|
||||||
are committed to resolving valid reports promptly and keeping you informed
|
|
||||||
throughout the process.
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
We release security fixes against the latest published version of each package.
|
|
||||||
Whenever possible, please reproduce the issue on the most recent release before
|
|
||||||
reporting.
|
|
||||||
+2
-4
@@ -503,7 +503,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "init",
|
"name": "init",
|
||||||
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
|
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
|
||||||
"usage": "mem0 init [OPTIONS]",
|
"usage": "mem0 init [OPTIONS]",
|
||||||
"needsBackend": false,
|
"needsBackend": false,
|
||||||
"needsConfig": false,
|
"needsConfig": false,
|
||||||
@@ -516,9 +516,7 @@
|
|||||||
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
|
||||||
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
|
{ "name": "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": "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)." }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
# 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/`.
|
|
||||||
+2
-13
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/cli",
|
"name": "@mem0/cli",
|
||||||
"version": "0.2.9",
|
"version": "0.2.4",
|
||||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -40,19 +40,8 @@
|
|||||||
"typescript": "^5.4.0",
|
"typescript": "^5.4.0",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"vite": "^6.0.0",
|
"vitest": "^1.5.0",
|
||||||
"vitest": "^4.1.0",
|
|
||||||
"@biomejs/biome": "^1.7.0",
|
"@biomejs/biome": "^1.7.0",
|
||||||
"@types/node": "^20.0.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
+724
-311
File diff suppressed because it is too large
Load Diff
@@ -1,14 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 type { PlatformConfig } from "../config.js";
|
||||||
import { captureNotice, isAgentMode } from "../state.js";
|
import { isAgentMode } from "../state.js";
|
||||||
import { CLI_VERSION } from "../version.js";
|
import { CLI_VERSION } from "../version.js";
|
||||||
import {
|
import {
|
||||||
APIError,
|
APIError,
|
||||||
@@ -90,39 +90,7 @@ export class PlatformBackend implements Backend {
|
|||||||
if (resp.status === 204) {
|
if (resp.status === 204) {
|
||||||
return {};
|
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(
|
async add(
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/**
|
|
||||||
* `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)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,8 +21,6 @@ import {
|
|||||||
redactKey,
|
redactKey,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
} from "../config.js";
|
} from "../config.js";
|
||||||
import { formatJsonEnvelope } from "../output.js";
|
|
||||||
import { isAgentMode } from "../state.js";
|
|
||||||
|
|
||||||
const { brand, dim } = colors;
|
const { brand, dim } = colors;
|
||||||
|
|
||||||
@@ -35,65 +33,6 @@ 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(
|
async function emailLogin(
|
||||||
email: string,
|
email: string,
|
||||||
code: string | undefined,
|
code: string | undefined,
|
||||||
@@ -257,7 +196,6 @@ async function setupPlatform(config: Mem0Config): Promise<void> {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
config.platform.apiKey = apiKey;
|
config.platform.apiKey = apiKey;
|
||||||
config.platform.createdVia = "api_key";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupDefaults(config: Mem0Config): Promise<void> {
|
async function setupDefaults(config: Mem0Config): Promise<void> {
|
||||||
@@ -311,35 +249,14 @@ export async function runInit(
|
|||||||
email?: string;
|
email?: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
agent?: boolean;
|
|
||||||
source?: string;
|
|
||||||
agentCaller?: string;
|
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): 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 config = createDefaultConfig();
|
||||||
const savedConfig = loadConfig();
|
const savedConfig = loadConfig();
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
process.env.MEM0_BASE_URL ||
|
process.env.MEM0_BASE_URL ||
|
||||||
savedConfig.platform.baseUrl ||
|
savedConfig.platform.baseUrl ||
|
||||||
DEFAULT_BASE_URL;
|
DEFAULT_BASE_URL;
|
||||||
config.platform.baseUrl = baseUrl;
|
|
||||||
|
|
||||||
// Guards
|
// Guards
|
||||||
if (opts.code && !opts.email) {
|
if (opts.code && !opts.email) {
|
||||||
@@ -351,84 +268,6 @@ export async function runInit(
|
|||||||
process.exit(1);
|
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
|
// Warn if an existing config with an API key would be overwritten
|
||||||
if (
|
if (
|
||||||
!opts.force &&
|
!opts.force &&
|
||||||
@@ -485,7 +324,6 @@ export async function runInit(
|
|||||||
config.platform.apiKey = apiKeyVal;
|
config.platform.apiKey = apiKeyVal;
|
||||||
config.platform.baseUrl = baseUrl;
|
config.platform.baseUrl = baseUrl;
|
||||||
config.platform.userEmail = email;
|
config.platform.userEmail = email;
|
||||||
config.platform.createdVia = "email";
|
|
||||||
config.defaults.userId =
|
config.defaults.userId =
|
||||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
|
||||||
@@ -501,15 +339,13 @@ export async function runInit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── API key flow ──────────────────────────────────────────────────────────
|
// ── 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
|
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||||
if (!process.stdin.isTTY) {
|
if (!process.stdin.isTTY) {
|
||||||
if (!opts.apiKey) {
|
if (!opts.apiKey) {
|
||||||
printError(
|
printError(
|
||||||
"Non-interactive terminal detected and --api-key is required.",
|
"Non-interactive terminal detected and --api-key is required.",
|
||||||
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
"Usage: mem0 init --api-key <key> [--user-id <id>]",
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -520,7 +356,6 @@ export async function runInit(
|
|||||||
// Non-interactive: both flags provided
|
// Non-interactive: both flags provided
|
||||||
if (opts.apiKey && opts.userId) {
|
if (opts.apiKey && opts.userId) {
|
||||||
config.platform.apiKey = opts.apiKey;
|
config.platform.apiKey = opts.apiKey;
|
||||||
config.platform.createdVia = "api_key";
|
|
||||||
config.defaults.userId = opts.userId;
|
config.defaults.userId = opts.userId;
|
||||||
await validatePlatform(config);
|
await validatePlatform(config);
|
||||||
saveConfig(config);
|
saveConfig(config);
|
||||||
@@ -568,7 +403,6 @@ export async function runInit(
|
|||||||
config.platform.apiKey = apiKeyVal;
|
config.platform.apiKey = apiKeyVal;
|
||||||
config.platform.baseUrl = baseUrl;
|
config.platform.baseUrl = baseUrl;
|
||||||
config.platform.userEmail = email;
|
config.platform.userEmail = email;
|
||||||
config.platform.createdVia = "email";
|
|
||||||
config.defaults.userId =
|
config.defaults.userId =
|
||||||
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function cmdAdd(
|
|||||||
file?: string;
|
file?: string;
|
||||||
metadata?: string;
|
metadata?: string;
|
||||||
immutable: boolean;
|
immutable: boolean;
|
||||||
infer?: boolean;
|
noInfer: boolean;
|
||||||
expires?: string;
|
expires?: string;
|
||||||
categories?: string;
|
categories?: string;
|
||||||
output: string;
|
output: string;
|
||||||
@@ -136,7 +136,7 @@ export async function cmdAdd(
|
|||||||
runId: opts.runId,
|
runId: opts.runId,
|
||||||
metadata: meta,
|
metadata: meta,
|
||||||
immutable: opts.immutable,
|
immutable: opts.immutable,
|
||||||
infer: opts.infer !== false,
|
infer: !opts.noInfer,
|
||||||
expires: opts.expires,
|
expires: opts.expires,
|
||||||
categories: cats,
|
categories: cats,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* `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,12 +21,6 @@ export interface PlatformConfig {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
userEmail: 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 {
|
export interface DefaultsConfig {
|
||||||
@@ -40,18 +34,11 @@ export interface TelemetryConfig {
|
|||||||
anonymousId: string;
|
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 {
|
export interface Mem0Config {
|
||||||
version: number;
|
version: number;
|
||||||
defaults: DefaultsConfig;
|
defaults: DefaultsConfig;
|
||||||
platform: PlatformConfig;
|
platform: PlatformConfig;
|
||||||
telemetry: TelemetryConfig;
|
telemetry: TelemetryConfig;
|
||||||
agentRush: AgentRushConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createDefaultConfig(): Mem0Config {
|
export function createDefaultConfig(): Mem0Config {
|
||||||
@@ -67,18 +54,10 @@ export function createDefaultConfig(): Mem0Config {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
baseUrl: DEFAULT_BASE_URL,
|
baseUrl: DEFAULT_BASE_URL,
|
||||||
userEmail: "",
|
userEmail: "",
|
||||||
agentMode: false,
|
|
||||||
createdVia: "",
|
|
||||||
agentCaller: "",
|
|
||||||
claimedAt: "",
|
|
||||||
defaultUserId: "",
|
|
||||||
},
|
},
|
||||||
telemetry: {
|
telemetry: {
|
||||||
anonymousId: "",
|
anonymousId: "",
|
||||||
},
|
},
|
||||||
agentRush: {
|
|
||||||
acknowledgedAt: "",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +79,6 @@ export function loadConfig(): Mem0Config {
|
|||||||
config.platform.apiKey = plat.api_key ?? "";
|
config.platform.apiKey = plat.api_key ?? "";
|
||||||
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
|
||||||
config.platform.userEmail = plat.user_email ?? "";
|
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 ?? {};
|
const defaults = data.defaults ?? {};
|
||||||
config.defaults.userId = defaults.user_id ?? "";
|
config.defaults.userId = defaults.user_id ?? "";
|
||||||
@@ -113,8 +87,6 @@ export function loadConfig(): Mem0Config {
|
|||||||
config.defaults.runId = defaults.run_id ?? "";
|
config.defaults.runId = defaults.run_id ?? "";
|
||||||
const telemetry = data.telemetry ?? {};
|
const telemetry = data.telemetry ?? {};
|
||||||
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
|
||||||
const agentRush = data.agent_rush ?? {};
|
|
||||||
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Environment variable overrides
|
// Environment variable overrides
|
||||||
@@ -146,36 +118,14 @@ export function saveConfig(config: Mem0Config): void {
|
|||||||
api_key: config.platform.apiKey,
|
api_key: config.platform.apiKey,
|
||||||
base_url: config.platform.baseUrl,
|
base_url: config.platform.baseUrl,
|
||||||
user_email: config.platform.userEmail,
|
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: {
|
telemetry: {
|
||||||
anonymous_id: config.telemetry.anonymousId,
|
anonymous_id: config.telemetry.anonymousId,
|
||||||
},
|
},
|
||||||
agent_rush: {
|
|
||||||
acknowledged_at: config.agentRush.acknowledgedAt,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
|
||||||
fs.chmodSync(CONFIG_FILE, 0o600);
|
fs.chmodSync(CONFIG_FILE, 0o600);
|
||||||
|
|
||||||
// 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 {
|
export function redactKey(key: string): string {
|
||||||
|
|||||||
+4
-112
@@ -13,12 +13,7 @@ import { colors, printError, printWarning } from "./branding.js";
|
|||||||
import type { Mem0Config } from "./config.js";
|
import type { Mem0Config } from "./config.js";
|
||||||
import { loadConfig, saveConfig } from "./config.js";
|
import { loadConfig, saveConfig } from "./config.js";
|
||||||
import { richFormatHelp } from "./help.js";
|
import { richFormatHelp } from "./help.js";
|
||||||
import {
|
import { setAgentMode } from "./state.js";
|
||||||
isAgentMode,
|
|
||||||
setAgentMode,
|
|
||||||
setCurrentCommand,
|
|
||||||
takeNotice,
|
|
||||||
} from "./state.js";
|
|
||||||
import { captureEvent } from "./telemetry.js";
|
import { captureEvent } from "./telemetry.js";
|
||||||
import { CLI_VERSION } from "./version.js";
|
import { CLI_VERSION } from "./version.js";
|
||||||
|
|
||||||
@@ -146,11 +141,6 @@ program
|
|||||||
.description(
|
.description(
|
||||||
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
|
`◆ 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.")
|
.option("--version", "Show version and exit.")
|
||||||
.on("option:version", () => {
|
.on("option:version", () => {
|
||||||
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||||
@@ -159,7 +149,7 @@ program
|
|||||||
.option("--json", "Output as JSON for agent/programmatic use.")
|
.option("--json", "Output as JSON for agent/programmatic use.")
|
||||||
.option(
|
.option(
|
||||||
"--agent",
|
"--agent",
|
||||||
"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.",
|
"Output as JSON for agent/programmatic use. (alias: --json)",
|
||||||
)
|
)
|
||||||
.usage("<command> [options]")
|
.usage("<command> [options]")
|
||||||
.helpOption("--help", "Show this message and exit.")
|
.helpOption("--help", "Show this message and exit.")
|
||||||
@@ -176,14 +166,6 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|||||||
parentName && parentName !== "mem0"
|
parentName && parentName !== "mem0"
|
||||||
? `${parentName}.${commandName}`
|
? `${parentName}.${commandName}`
|
||||||
: 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);
|
const isAgent = !!(program.opts().json || program.opts().agent);
|
||||||
captureEvent(
|
captureEvent(
|
||||||
`cli.${fullCommand}`,
|
`cli.${fullCommand}`,
|
||||||
@@ -211,32 +193,11 @@ program
|
|||||||
"Verification code (use with --email for non-interactive login).",
|
"Verification code (use with --email for non-interactive login).",
|
||||||
)
|
)
|
||||||
.option("--force", "Overwrite existing config without confirmation.", false)
|
.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(
|
.addHelpText(
|
||||||
"after",
|
"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\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",
|
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
|
||||||
)
|
)
|
||||||
.action(async (opts) => {
|
.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");
|
const { runInit } = await import("./commands/init.js");
|
||||||
await runInit({
|
await runInit({
|
||||||
apiKey: opts.apiKey,
|
apiKey: opts.apiKey,
|
||||||
@@ -244,66 +205,9 @@ program
|
|||||||
email: opts.email,
|
email: opts.email,
|
||||||
code: opts.code,
|
code: opts.code,
|
||||||
force: opts.force,
|
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 ───────────────────────────────────────────────────────────
|
// ── Memory: add ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
program
|
program
|
||||||
@@ -865,16 +769,4 @@ program
|
|||||||
|
|
||||||
// ── Entrypoint ────────────────────────────────────────────────────────────
|
// ── Entrypoint ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Surface any unclaimed Agent Mode notice once per command, after the primary
|
program.parse();
|
||||||
// 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,7 +5,6 @@
|
|||||||
import boxen from "boxen";
|
import boxen from "boxen";
|
||||||
import Table from "cli-table3";
|
import Table from "cli-table3";
|
||||||
import { colors, sym } from "./branding.js";
|
import { colors, sym } from "./branding.js";
|
||||||
import { takeNotice } from "./state.js";
|
|
||||||
|
|
||||||
const { brand, accent, success, error: errorColor, dim } = colors;
|
const { brand, accent, success, error: errorColor, dim } = colors;
|
||||||
|
|
||||||
@@ -245,15 +244,6 @@ export function formatJsonEnvelope(opts: {
|
|||||||
if (opts.count !== undefined) envelope.count = opts.count;
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
if (opts.error) envelope.error = opts.error;
|
if (opts.error) envelope.error = opts.error;
|
||||||
envelope.data = opts.data;
|
envelope.data = opts.data;
|
||||||
|
|
||||||
// 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));
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,12 +356,6 @@ export function formatAgentEnvelope(opts: {
|
|||||||
}
|
}
|
||||||
if (opts.count !== undefined) envelope.count = opts.count;
|
if (opts.count !== undefined) envelope.count = opts.count;
|
||||||
envelope.data = sanitizeAgentData(opts.command, opts.data);
|
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));
|
console.log(JSON.stringify(envelope, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,7 +5,6 @@
|
|||||||
|
|
||||||
let _agentMode = false;
|
let _agentMode = false;
|
||||||
let _currentCommand = "";
|
let _currentCommand = "";
|
||||||
let _pendingNotice = "";
|
|
||||||
|
|
||||||
export function isAgentMode(): boolean {
|
export function isAgentMode(): boolean {
|
||||||
return _agentMode;
|
return _agentMode;
|
||||||
@@ -22,19 +21,3 @@ export function getCurrentCommand(): string {
|
|||||||
export function setCurrentCommand(name: string): void {
|
export function setCurrentCommand(name: string): void {
|
||||||
_currentCommand = name;
|
_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,9 +115,6 @@ 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 = {
|
const payload = {
|
||||||
api_key: POSTHOG_API_KEY,
|
api_key: POSTHOG_API_KEY,
|
||||||
distinct_id: distinctId,
|
distinct_id: distinctId,
|
||||||
@@ -126,7 +123,6 @@ export function captureEvent(
|
|||||||
source: "CLI",
|
source: "CLI",
|
||||||
language: "node",
|
language: "node",
|
||||||
cli_version: CLI_VERSION,
|
cli_version: CLI_VERSION,
|
||||||
agent_mode: Boolean(config.platform.agentMode),
|
|
||||||
node_version: process.version,
|
node_version: process.version,
|
||||||
os: process.platform,
|
os: process.platform,
|
||||||
...properties,
|
...properties,
|
||||||
@@ -145,11 +141,11 @@ export function captureEvent(
|
|||||||
anonDistinctIdToAlias: anonIdToAlias,
|
anonDistinctIdToAlias: anonIdToAlias,
|
||||||
};
|
};
|
||||||
|
|
||||||
const child = spawn(process.execPath, [SENDER_SCRIPT], {
|
const child = spawn(
|
||||||
detached: true,
|
process.execPath,
|
||||||
stdio: ["pipe", "ignore", "ignore"],
|
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||||
});
|
{ detached: true, stdio: "ignore" },
|
||||||
child.stdin?.end(JSON.stringify(context));
|
);
|
||||||
child.unref();
|
child.unref();
|
||||||
} catch {
|
} catch {
|
||||||
/* silently swallow */
|
/* silently swallow */
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Standalone telemetry sender — runs as a detached child process.
|
* Standalone telemetry sender — runs as a detached child process.
|
||||||
*
|
*
|
||||||
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single
|
* Usage: node telemetry-sender.cjs '<json context>'
|
||||||
* argv argument is still accepted as a legacy fallback)
|
|
||||||
*
|
*
|
||||||
* This script is spawned by telemetry.captureEvent() and runs independently
|
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||||
* of the parent CLI process. It:
|
* of the parent CLI process. It:
|
||||||
@@ -20,31 +19,6 @@
|
|||||||
const https = require("https");
|
const https = require("https");
|
||||||
const fs = require("fs");
|
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) {
|
function httpsRequest(url, method, headers, body) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
@@ -134,7 +108,7 @@ async function sendIdentifyEvent(ctx, payload, anonId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const ctx = await loadContext();
|
const ctx = JSON.parse(process.argv[2]);
|
||||||
const payload = ctx.payload;
|
const payload = ctx.payload;
|
||||||
|
|
||||||
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,7 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { Command } from "commander";
|
|
||||||
import { createMockBackend } from "./setup.js";
|
import { createMockBackend } from "./setup.js";
|
||||||
import type { Backend } from "../src/backend/base.js";
|
import type { Backend } from "../src/backend/base.js";
|
||||||
import { setAgentMode } from "../src/state.js";
|
import { setAgentMode } from "../src/state.js";
|
||||||
@@ -42,6 +41,8 @@ describe("cmdAdd", () => {
|
|||||||
await cmdAdd(mockBackend, "I prefer dark mode", {
|
await cmdAdd(mockBackend, "I prefer dark mode", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||||
@@ -53,6 +54,8 @@ describe("cmdAdd", () => {
|
|||||||
userId: "alice",
|
userId: "alice",
|
||||||
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(mockBackend.add).toHaveBeenCalledOnce();
|
expect(mockBackend.add).toHaveBeenCalledOnce();
|
||||||
@@ -63,6 +66,8 @@ describe("cmdAdd", () => {
|
|||||||
await cmdAdd(mockBackend, "test", {
|
await cmdAdd(mockBackend, "test", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "json",
|
output: "json",
|
||||||
});
|
});
|
||||||
expect(output).toContain("results");
|
expect(output).toContain("results");
|
||||||
@@ -73,59 +78,14 @@ describe("cmdAdd", () => {
|
|||||||
await cmdAdd(mockBackend, "test", {
|
await cmdAdd(mockBackend, "test", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "quiet",
|
output: "quiet",
|
||||||
});
|
});
|
||||||
expect(output).not.toContain("dark mode");
|
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", () => {
|
describe("cmdAdd deduplicates PENDING", () => {
|
||||||
const DUPLICATE_PENDING = {
|
const DUPLICATE_PENDING = {
|
||||||
results: [
|
results: [
|
||||||
@@ -140,6 +100,8 @@ describe("cmdAdd deduplicates PENDING", () => {
|
|||||||
await cmdAdd(mockBackend, "test", {
|
await cmdAdd(mockBackend, "test", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "text",
|
output: "text",
|
||||||
});
|
});
|
||||||
expect(output.match(/Queued/g)?.length).toBe(1);
|
expect(output.match(/Queued/g)?.length).toBe(1);
|
||||||
@@ -151,6 +113,8 @@ describe("cmdAdd deduplicates PENDING", () => {
|
|||||||
await cmdAdd(mockBackend, "test", {
|
await cmdAdd(mockBackend, "test", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "json",
|
output: "json",
|
||||||
});
|
});
|
||||||
const data = JSON.parse(output);
|
const data = JSON.parse(output);
|
||||||
@@ -165,6 +129,8 @@ describe("cmdAdd deduplicates PENDING", () => {
|
|||||||
await cmdAdd(mockBackend, "test", {
|
await cmdAdd(mockBackend, "test", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "agent",
|
output: "agent",
|
||||||
});
|
});
|
||||||
const data = JSON.parse(output);
|
const data = JSON.parse(output);
|
||||||
@@ -349,6 +315,8 @@ describe("agent mode", () => {
|
|||||||
await cmdAdd(mockBackend, "test preference", {
|
await cmdAdd(mockBackend, "test preference", {
|
||||||
userId: "alice",
|
userId: "alice",
|
||||||
immutable: false,
|
immutable: false,
|
||||||
|
noInfer: false,
|
||||||
|
|
||||||
output: "agent",
|
output: "agent",
|
||||||
});
|
});
|
||||||
const parsed = JSON.parse(output.trim());
|
const parsed = JSON.parse(output.trim());
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
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,10 +8,4 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
__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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# 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]
|
[project]
|
||||||
name = "mem0-cli"
|
name = "mem0-cli"
|
||||||
version = "0.2.8"
|
version = "0.2.4"
|
||||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||||
|
|
||||||
__version__ = "0.2.8"
|
__version__ = "0.2.4"
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
"""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,14 +237,6 @@ def main_callback(
|
|||||||
cmd_version()
|
cmd_version()
|
||||||
raise typer.Exit()
|
raise typer.Exit()
|
||||||
if ctx.invoked_subcommand:
|
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)
|
_fire_telemetry(ctx.invoked_subcommand)
|
||||||
|
|
||||||
|
|
||||||
@@ -859,19 +851,6 @@ def init(
|
|||||||
force: bool = typer.Option(
|
force: bool = typer.Option(
|
||||||
False, "--force", help="Overwrite existing config without confirmation."
|
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:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
@@ -880,97 +859,10 @@ def init(
|
|||||||
mem0 init --api-key m0-xxx --user-id alice
|
mem0 init --api-key m0-xxx --user-id alice
|
||||||
mem0 init --email alice@company.com
|
mem0 init --email alice@company.com
|
||||||
mem0 init --email alice@company.com --code 482901
|
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
|
from mem0_cli.commands.init_cmd import run_init
|
||||||
|
|
||||||
run_init(
|
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
|
||||||
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)
|
# (entity_app registered at module level, below sub-group definitions)
|
||||||
@@ -1306,28 +1198,11 @@ def main() -> None:
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
# Allow --json/--agent anywhere in the command line (not just before subcommand).
|
||||||
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
|
_json_flags = {"--json", "--agent"}
|
||||||
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
|
if any(a in _json_flags for a in sys.argv[1:]):
|
||||||
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
|
from mem0_cli.state import set_agent_mode
|
||||||
|
|
||||||
set_agent_mode(True)
|
set_agent_mode(True)
|
||||||
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
|
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
|
||||||
|
|
||||||
try:
|
app()
|
||||||
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:
|
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
|
||||||
from mem0_cli.state import capture_notice, is_agent_mode
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
|
||||||
resp = self._client.request(method, path, **kwargs)
|
resp = self._client.request(method, path, **kwargs)
|
||||||
@@ -48,26 +48,7 @@ class PlatformBackend(Backend):
|
|||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
if resp.status_code == 204:
|
if resp.status_code == 204:
|
||||||
return {}
|
return {}
|
||||||
data = resp.json()
|
return 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(
|
def add(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -87,12 +87,10 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
|
|||||||
}
|
}
|
||||||
print(_json.dumps(envelope))
|
print(_json.dumps(envelope))
|
||||||
return
|
return
|
||||||
from rich.markup import escape
|
|
||||||
|
|
||||||
sym = _sym("✗", "[error]")
|
sym = _sym("✗", "[error]")
|
||||||
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
|
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
|
||||||
if hint:
|
if hint:
|
||||||
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
|
console.print(f" [{DIM_COLOR}]{hint}[/]")
|
||||||
|
|
||||||
|
|
||||||
def print_warning(console: Console, message: str) -> None:
|
def print_warning(console: Console, message: str) -> None:
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
"""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()
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
"""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}")
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
"""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,25 +103,6 @@ def _validate_email(email: str) -> None:
|
|||||||
raise typer.Exit(1)
|
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(
|
def _email_login(
|
||||||
email: str,
|
email: str,
|
||||||
code: str | None,
|
code: str | None,
|
||||||
@@ -201,143 +182,21 @@ def run_init(
|
|||||||
email: str | None = None,
|
email: str | None = None,
|
||||||
code: str | None = None,
|
code: str | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
source: str | None = None,
|
|
||||||
agent: bool = False,
|
|
||||||
agent_caller: str | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Interactive setup wizard for mem0 CLI.
|
"""Interactive setup wizard for mem0 CLI.
|
||||||
|
|
||||||
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
When both *api_key* and *user_id* are supplied, all prompts are skipped
|
||||||
(non-interactive mode). When running in a non-TTY without the required
|
(non-interactive mode). When running in a non-TTY without the required
|
||||||
flags, an error message is printed.
|
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()
|
config = Mem0Config()
|
||||||
|
|
||||||
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
|
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:
|
if code and not email:
|
||||||
print_error(err_console, "--code requires --email.")
|
print_error(err_console, "--code requires --email.")
|
||||||
raise typer.Exit(1)
|
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
|
# Warn if an existing config with an API key would be overwritten
|
||||||
if not force and CONFIG_FILE.exists():
|
if not force and CONFIG_FILE.exists():
|
||||||
existing = load_config()
|
existing = load_config()
|
||||||
@@ -383,7 +242,6 @@ def run_init(
|
|||||||
config.platform.api_key = api_key_val
|
config.platform.api_key = api_key_val
|
||||||
config.platform.base_url = base_url
|
config.platform.base_url = base_url
|
||||||
config.platform.user_email = email
|
config.platform.user_email = email
|
||||||
config.platform.created_via = "email"
|
|
||||||
config.defaults.user_id = (
|
config.defaults.user_id = (
|
||||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
)
|
)
|
||||||
@@ -400,8 +258,6 @@ def run_init(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# ── API key flow (existing) ───────────────────────────────────────
|
# ── 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
|
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
@@ -409,7 +265,7 @@ def run_init(
|
|||||||
print_error(
|
print_error(
|
||||||
err_console,
|
err_console,
|
||||||
"Non-interactive terminal detected and --api-key is required.",
|
"Non-interactive terminal detected and --api-key is required.",
|
||||||
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
|
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
|
||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
@@ -417,7 +273,6 @@ def run_init(
|
|||||||
# Fully non-interactive when both flags provided
|
# Fully non-interactive when both flags provided
|
||||||
if api_key and user_id:
|
if api_key and user_id:
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
config.platform.created_via = "api_key"
|
|
||||||
config.defaults.user_id = user_id
|
config.defaults.user_id = user_id
|
||||||
_validate_platform(config)
|
_validate_platform(config)
|
||||||
save_config(config)
|
save_config(config)
|
||||||
@@ -458,7 +313,6 @@ def run_init(
|
|||||||
config.platform.api_key = api_key_val
|
config.platform.api_key = api_key_val
|
||||||
config.platform.base_url = base_url
|
config.platform.base_url = base_url
|
||||||
config.platform.user_email = email_addr
|
config.platform.user_email = email_addr
|
||||||
config.platform.created_via = "email"
|
|
||||||
config.defaults.user_id = (
|
config.defaults.user_id = (
|
||||||
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
|
||||||
)
|
)
|
||||||
@@ -477,7 +331,6 @@ def run_init(
|
|||||||
# API key flow
|
# API key flow
|
||||||
if api_key:
|
if api_key:
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
config.platform.created_via = "api_key"
|
|
||||||
else:
|
else:
|
||||||
_setup_platform(config)
|
_setup_platform(config)
|
||||||
|
|
||||||
@@ -517,7 +370,6 @@ def _setup_platform(config: Mem0Config) -> None:
|
|||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
config.platform.api_key = api_key
|
config.platform.api_key = api_key
|
||||||
config.platform.created_via = "api_key"
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_defaults(config: Mem0Config) -> None:
|
def _setup_defaults(config: Mem0Config) -> None:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
"""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,14 +28,6 @@ class PlatformConfig:
|
|||||||
api_key: str = ""
|
api_key: str = ""
|
||||||
base_url: str = DEFAULT_BASE_URL
|
base_url: str = DEFAULT_BASE_URL
|
||||||
user_email: str = ""
|
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
|
@dataclass
|
||||||
@@ -51,20 +43,12 @@ class TelemetryConfig:
|
|||||||
anonymous_id: str = ""
|
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
|
@dataclass
|
||||||
class Mem0Config:
|
class Mem0Config:
|
||||||
version: int = CONFIG_VERSION
|
version: int = CONFIG_VERSION
|
||||||
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
|
||||||
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
platform: PlatformConfig = field(default_factory=PlatformConfig)
|
||||||
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
||||||
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
|
|
||||||
|
|
||||||
|
|
||||||
SHORT_KEY_ALIASES: dict[str, str] = {
|
SHORT_KEY_ALIASES: dict[str, str] = {
|
||||||
@@ -99,11 +83,6 @@ def load_config() -> Mem0Config:
|
|||||||
config.platform.api_key = plat.get("api_key", "")
|
config.platform.api_key = plat.get("api_key", "")
|
||||||
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
|
||||||
config.platform.user_email = plat.get("user_email", "")
|
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", {})
|
defaults = data.get("defaults", {})
|
||||||
config.defaults.user_id = defaults.get("user_id", "")
|
config.defaults.user_id = defaults.get("user_id", "")
|
||||||
@@ -113,9 +92,6 @@ def load_config() -> Mem0Config:
|
|||||||
telemetry = data.get("telemetry", {})
|
telemetry = data.get("telemetry", {})
|
||||||
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
|
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
|
# Environment variable overrides
|
||||||
env_key = os.environ.get("MEM0_API_KEY")
|
env_key = os.environ.get("MEM0_API_KEY")
|
||||||
if env_key:
|
if env_key:
|
||||||
@@ -160,18 +136,10 @@ def save_config(config: Mem0Config) -> None:
|
|||||||
"api_key": config.platform.api_key,
|
"api_key": config.platform.api_key,
|
||||||
"base_url": config.platform.base_url,
|
"base_url": config.platform.base_url,
|
||||||
"user_email": config.platform.user_email,
|
"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": {
|
"telemetry": {
|
||||||
"anonymous_id": config.telemetry.anonymous_id,
|
"anonymous_id": config.telemetry.anonymous_id,
|
||||||
},
|
},
|
||||||
"agent_rush": {
|
|
||||||
"acknowledged_at": config.agent_rush.acknowledged_at,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(CONFIG_FILE, "w") as f:
|
with open(CONFIG_FILE, "w") as f:
|
||||||
@@ -179,19 +147,6 @@ def save_config(config: Mem0Config) -> None:
|
|||||||
|
|
||||||
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
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:
|
def redact_key(key: str) -> str:
|
||||||
"""Redact an API key for display: m0-xxx...xxx"""
|
"""Redact an API key for display: m0-xxx...xxx"""
|
||||||
|
|||||||
@@ -229,16 +229,6 @@ def format_json_envelope(
|
|||||||
if error:
|
if error:
|
||||||
envelope["error"] = error
|
envelope["error"] = error
|
||||||
envelope["data"] = data
|
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))
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
@@ -333,15 +323,6 @@ def format_agent_envelope(
|
|||||||
if count is not None:
|
if count is not None:
|
||||||
envelope["count"] = count
|
envelope["count"] = count
|
||||||
envelope["data"] = sanitize_agent_data(command, data)
|
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))
|
console.print_json(json.dumps(envelope, default=str))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
"""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,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
_agent_mode: bool = False
|
_agent_mode: bool = False
|
||||||
_current_command: str = ""
|
_current_command: str = ""
|
||||||
_pending_notice: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
def is_agent_mode() -> bool:
|
def is_agent_mode() -> bool:
|
||||||
@@ -23,23 +22,3 @@ def get_current_command() -> str:
|
|||||||
def set_current_command(name: str) -> None:
|
def set_current_command(name: str) -> None:
|
||||||
global _current_command
|
global _current_command
|
||||||
_current_command = name
|
_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,6 +87,7 @@ def capture_event(
|
|||||||
try:
|
try:
|
||||||
from mem0_cli import __version__
|
from mem0_cli import __version__
|
||||||
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
from mem0_cli.config import CONFIG_FILE, load_config, save_config
|
||||||
|
from mem0_cli.state import is_agent_mode
|
||||||
|
|
||||||
config = load_config()
|
config = load_config()
|
||||||
distinct_id = pre_resolved_email or _get_distinct_id()
|
distinct_id = pre_resolved_email or _get_distinct_id()
|
||||||
@@ -106,9 +107,6 @@ def capture_event(
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
save_config(config)
|
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 = {
|
payload = {
|
||||||
"api_key": POSTHOG_API_KEY,
|
"api_key": POSTHOG_API_KEY,
|
||||||
"distinct_id": distinct_id,
|
"distinct_id": distinct_id,
|
||||||
@@ -117,7 +115,7 @@ def capture_event(
|
|||||||
"source": "CLI",
|
"source": "CLI",
|
||||||
"language": "python",
|
"language": "python",
|
||||||
"cli_version": __version__,
|
"cli_version": __version__,
|
||||||
"agent_mode": bool(config.platform.agent_mode),
|
"agent_mode": is_agent_mode(),
|
||||||
"python_version": sys.version,
|
"python_version": sys.version,
|
||||||
"os": sys.platform,
|
"os": sys.platform,
|
||||||
"os_version": platform.version(),
|
"os_version": platform.version(),
|
||||||
@@ -137,19 +135,12 @@ def capture_event(
|
|||||||
"anon_distinct_id_to_alias": anon_id_to_alias,
|
"anon_distinct_id_to_alias": anon_id_to_alias,
|
||||||
}
|
}
|
||||||
|
|
||||||
child = subprocess.Popen(
|
subprocess.Popen(
|
||||||
[sys.executable, "-m", "mem0_cli.telemetry_sender"],
|
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
start_new_session=True,
|
start_new_session=True,
|
||||||
close_fds=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:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Standalone telemetry sender — runs as a detached subprocess.
|
"""Standalone telemetry sender — runs as a detached subprocess.
|
||||||
|
|
||||||
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin;
|
Usage: python -m mem0_cli.telemetry_sender '<json context>'
|
||||||
a single argv argument is still accepted as a legacy fallback)
|
|
||||||
|
|
||||||
This module is spawned by telemetry.capture_event() and runs independently
|
This module is spawned by telemetry.capture_event() and runs independently
|
||||||
of the parent CLI process. It:
|
of the parent CLI process. It:
|
||||||
@@ -21,18 +20,8 @@ import sys
|
|||||||
import urllib.request
|
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:
|
def main() -> None:
|
||||||
ctx = _load_context()
|
ctx = json.loads(sys.argv[1])
|
||||||
payload = ctx["payload"]
|
payload = ctx["payload"]
|
||||||
|
|
||||||
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
"""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,7 +49,6 @@ def _run(
|
|||||||
if key.startswith("MEM0_"):
|
if key.startswith("MEM0_"):
|
||||||
del env[key]
|
del env[key]
|
||||||
env.pop("FORCE_COLOR", None)
|
env.pop("FORCE_COLOR", None)
|
||||||
env["PYTHONIOENCODING"] = "utf-8"
|
|
||||||
if home_dir:
|
if home_dir:
|
||||||
env["HOME"] = home_dir
|
env["HOME"] = home_dir
|
||||||
if env_override:
|
if env_override:
|
||||||
@@ -57,7 +56,7 @@ def _run(
|
|||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "mem0_cli", *args],
|
[sys.executable, "-m", "mem0_cli", *args],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
encoding="utf-8",
|
text=True,
|
||||||
env=env,
|
env=env,
|
||||||
)
|
)
|
||||||
return subprocess.CompletedProcess(
|
return subprocess.CompletedProcess(
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from io import StringIO
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from click.exceptions import Exit as ClickExit
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from typer import Exit as TyperExit
|
|
||||||
|
|
||||||
from mem0_cli.commands.config_cmd import (
|
from mem0_cli.commands.config_cmd import (
|
||||||
cmd_config_get,
|
cmd_config_get,
|
||||||
@@ -181,7 +181,7 @@ class TestAddCommand:
|
|||||||
patch("mem0_cli.commands.memory.console", console),
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_add(
|
cmd_add(
|
||||||
mock_backend,
|
mock_backend,
|
||||||
@@ -206,7 +206,7 @@ class TestAddCommand:
|
|||||||
with (
|
with (
|
||||||
patch("mem0_cli.commands.memory.console", console),
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_add(
|
cmd_add(
|
||||||
mock_backend,
|
mock_backend,
|
||||||
@@ -764,7 +764,7 @@ class TestImportCommand:
|
|||||||
with (
|
with (
|
||||||
patch("mem0_cli.commands.utils.console", console),
|
patch("mem0_cli.commands.utils.console", console),
|
||||||
patch("mem0_cli.commands.utils.err_console", err_console),
|
patch("mem0_cli.commands.utils.err_console", err_console),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
|
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
|
||||||
|
|
||||||
@@ -801,7 +801,7 @@ class TestEntitiesListCommand:
|
|||||||
with (
|
with (
|
||||||
patch("mem0_cli.commands.entities.console", console),
|
patch("mem0_cli.commands.entities.console", console),
|
||||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_entities_list(mock_backend, "invalid", output="table")
|
cmd_entities_list(mock_backend, "invalid", output="table")
|
||||||
|
|
||||||
@@ -944,7 +944,7 @@ class TestEntitiesDeleteCommand:
|
|||||||
with (
|
with (
|
||||||
patch("mem0_cli.commands.entities.console", console),
|
patch("mem0_cli.commands.entities.console", console),
|
||||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_entities_delete(
|
cmd_entities_delete(
|
||||||
mock_backend,
|
mock_backend,
|
||||||
@@ -1308,7 +1308,7 @@ class TestAgentMode:
|
|||||||
patch("mem0_cli.commands.memory.console", console),
|
patch("mem0_cli.commands.memory.console", console),
|
||||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||||
patch("sys.stdout", captured_stdout),
|
patch("sys.stdout", captured_stdout),
|
||||||
pytest.raises((SystemExit, TyperExit)),
|
pytest.raises((SystemExit, ClickExit)),
|
||||||
):
|
):
|
||||||
cmd_get(mock_backend, "bad-id", output="text")
|
cmd_get(mock_backend, "bad-id", output="text")
|
||||||
|
|
||||||
|
|||||||
@@ -67,8 +67,7 @@ class TestConfig:
|
|||||||
from mem0_cli.config import CONFIG_FILE
|
from mem0_cli.config import CONFIG_FILE
|
||||||
|
|
||||||
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||||
if os.name != "nt":
|
assert mode == 0o600
|
||||||
assert mode == 0o600
|
|
||||||
|
|
||||||
def test_defaults_save_and_load(self, isolate_config):
|
def test_defaults_save_and_load(self, isolate_config):
|
||||||
config = Mem0Config()
|
config = Mem0Config()
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""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"
|
|
||||||
@@ -555,7 +555,7 @@
|
|||||||
"# - Enables creation of AI agents with long-term memory and learning abilities.\n",
|
"# - Enables creation of AI agents with long-term memory and learning abilities.\n",
|
||||||
"# - Improves consistency and reduces repetition in user-agent interactions.\n",
|
"# - Improves consistency and reduces repetition in user-agent interactions.\n",
|
||||||
"\n",
|
"\n",
|
||||||
"from helper.mem0_teachability import Mem0Teachability\n",
|
"from cookbooks.helper.mem0_teachability import Mem0Teachability\n",
|
||||||
"\n",
|
"\n",
|
||||||
"teachability = Mem0Teachability(\n",
|
"teachability = Mem0Teachability(\n",
|
||||||
" verbosity=2, # for visibility of what's happening\n",
|
" verbosity=2, # for visibility of what's happening\n",
|
||||||
@@ -5,5 +5,3 @@ openapi: get /v1/event/{event_id}/
|
|||||||
---
|
---
|
||||||
|
|
||||||
Retrieve details about a specific event by passing its `event_id`. This endpoint is particularly helpful for tracking the status, payload, and completion details of asynchronous memory operations.
|
Retrieve details about a specific event by passing its `event_id`. This endpoint is particularly helpful for tracking the status, payload, and completion details of asynchronous memory operations.
|
||||||
|
|
||||||
For `POST /v3/memories/add/`, the event confirms that the write pipeline completed. Temporal reasoning enrichment runs asynchronously by default, so the event may be `SUCCEEDED` slightly before temporal ranking signals are available to subsequent `search` calls.
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
|
|||||||
| `app_id` | string | No* | Associates the memory with an app. |
|
| `app_id` | string | No* | Associates the memory with an app. |
|
||||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
||||||
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
||||||
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
|
|
||||||
|
|
||||||
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
||||||
|
|
||||||
@@ -85,10 +84,3 @@ The request is queued for background processing. The response contains an `event
|
|||||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
<Info>
|
|
||||||
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
|
|
||||||
</Info>
|
|
||||||
|
|
||||||
<Info>
|
|
||||||
Python uses `expiration_date`; TypeScript uses `expirationDate`.
|
|
||||||
</Info>
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ description: "Submit an export job to create a structured memory export using a
|
|||||||
openapi: post /v1/exports/
|
openapi: post /v1/exports/
|
||||||
---
|
---
|
||||||
|
|
||||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `app_id`, or `run_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `run_id`, or `session_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ openapi: post /v3/memories/
|
|||||||
|
|
||||||
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
|
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
|
||||||
|
|
||||||
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
|
||||||
|
|
||||||
Python uses `show_expired`; TypeScript uses `showExpired`.
|
|
||||||
|
|
||||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
|
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
@@ -36,7 +32,6 @@ memories = client.get_all(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
show_expired=False,
|
|
||||||
page=1,
|
page=1,
|
||||||
page_size=50
|
page_size=50
|
||||||
)
|
)
|
||||||
@@ -51,14 +46,12 @@ memories = client.get_all(
|
|||||||
{
|
{
|
||||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||||
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
||||||
"expiration_date": null,
|
|
||||||
"created_at": "2024-07-01T12:00:00Z",
|
"created_at": "2024-07-01T12:00:00Z",
|
||||||
"updated_at": "2024-07-01T12:00:00Z"
|
"updated_at": "2024-07-01T12:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
||||||
"memory": "Alex prefers vegetarian restaurants",
|
"memory": "Alex prefers vegetarian restaurants",
|
||||||
"expiration_date": null,
|
|
||||||
"created_at": "2024-07-05T15:30:00Z",
|
"created_at": "2024-07-05T15:30:00Z",
|
||||||
"updated_at": "2024-07-05T15:30:00Z"
|
"updated_at": "2024-07-05T15:30:00Z"
|
||||||
}
|
}
|
||||||
@@ -71,3 +64,4 @@ memories = client.get_all(
|
|||||||
<Info>
|
<Info>
|
||||||
The response is a paginated envelope with `count`, `next`, `previous`, and `results`. Use `page` and `page_size` query params to step through results.
|
The response is a paginated envelope with `count`, `next`, `previous`, and `results`. Use `page` and `page_size` query params to step through results.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ description: "Retrieve the latest structured memory export after submitting an e
|
|||||||
openapi: post /v1/exports/get
|
openapi: post /v1/exports/get
|
||||||
---
|
---
|
||||||
|
|
||||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, or `updated_at` to get the most recent export matching your filters.
|
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `run_id`, `session_id`, or `app_id` to get the most recent export matching your filters.
|
||||||
@@ -8,10 +8,6 @@ Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retr
|
|||||||
|
|
||||||
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
|
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
|
||||||
|
|
||||||
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
|
||||||
|
|
||||||
Python uses `show_expired`; TypeScript uses `showExpired`.
|
|
||||||
|
|
||||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
- `gte`: Greater than or equal to
|
- `gte`: Greater than or equal to
|
||||||
@@ -24,17 +20,16 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
|
|||||||
|
|
||||||
### Search parameter defaults
|
### Search parameter defaults
|
||||||
|
|
||||||
| Parameter | Default |
|
| Parameter | V1/V2 | V3 |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
| `top_k` | `10` (range 1–1000) |
|
| `top_k` | Supported (default 10) | Supported (1-1000, default 10) |
|
||||||
| `threshold` | `0.1` (pass `0.0` to disable) |
|
| `threshold` | No default | Default `0.1` (pass `0.0` to disable) |
|
||||||
| `rerank` | `false` (pass `true` to enable) |
|
| `rerank` | Default `true` | Default `false` (pass `true` to enable) |
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```python Platform API Example
|
```python Platform API Example
|
||||||
related_memories = client.search(
|
related_memories = client.search(
|
||||||
query="What are Alice's hobbies?",
|
query="What are Alice's hobbies?",
|
||||||
show_expired=False,
|
|
||||||
filters={
|
filters={
|
||||||
"OR": [
|
"OR": [
|
||||||
{
|
{
|
||||||
@@ -54,12 +49,10 @@ related_memories = client.search(
|
|||||||
{
|
{
|
||||||
"id": "ea925981-272f-40dd-b576-be64e4871429",
|
"id": "ea925981-272f-40dd-b576-be64e4871429",
|
||||||
"memory": "Likes to play cricket and plays cricket on weekends.",
|
"memory": "Likes to play cricket and plays cricket on weekends.",
|
||||||
"user_id": "alice",
|
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"category": "hobbies"
|
"category": "hobbies"
|
||||||
},
|
},
|
||||||
"score": 0.82,
|
"score": 0.82,
|
||||||
"expiration_date": null,
|
|
||||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||||
"updated_at": null,
|
"updated_at": null,
|
||||||
"categories": ["hobbies"]
|
"categories": ["hobbies"]
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: 'Update Memory'
|
title: 'Update Memory'
|
||||||
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
|
description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
|
||||||
openapi: put /v1/memories/{memory_id}/
|
openapi: put /v1/memories/{memory_id}/
|
||||||
---
|
---
|
||||||
|
|
||||||
Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
|
|
||||||
|
|
||||||
```python
|
|
||||||
client.update("mem_123", expiration_date="2030-01-31")
|
|
||||||
client.update("mem_123", expiration_date=None)
|
|
||||||
```
|
|
||||||
|
|
||||||
TypeScript uses `expirationDate`.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Remove Organization Member"
|
|
||||||
description: "Remove a member from an organization to revoke their access to its projects and resources."
|
|
||||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
|
|
||||||
---
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Update Organization Member"
|
|
||||||
description: "Update an existing member's role within an organization to change their permissions and access level."
|
|
||||||
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
|
|
||||||
---
|
|
||||||
@@ -14,7 +14,7 @@ Organizations and projects are **optional** features. You can use Mem0 without t
|
|||||||
|
|
||||||
## Key Capabilities
|
## Key Capabilities
|
||||||
|
|
||||||
- **Multi-org/project Support**: Organization and project are resolved automatically from your API key via `/v1/ping/` — no org or project params are accepted by `MemoryClient.__init__`. Use a project-specific API key to target a particular project.
|
- **Multi-org/project Support**: Specify organization and project when initializing the Mem0 client to attribute API usage appropriately
|
||||||
- **Member Management**: Control access to data through organization and project membership
|
- **Member Management**: Control access to data through organization and project membership
|
||||||
- **Access Control**: Only members can access memories and data within their organization/project scope
|
- **Access Control**: Only members can access memories and data within their organization/project scope
|
||||||
- **Team Isolation**: Maintain data separation between different teams and projects for secure collaboration
|
- **Team Isolation**: Maintain data separation between different teams and projects for secure collaboration
|
||||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
|||||||
|
|
||||||
### Update Project Settings
|
### Update Project Settings
|
||||||
|
|
||||||
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
|
Modify project configuration including custom instructions, categories, graph settings, and language preferences:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Update project with custom categories
|
# Update project with custom categories
|
||||||
@@ -98,17 +98,6 @@ client.project.update(
|
|||||||
# Use the input language for memory storage and retrieval
|
# Use the input language for memory storage and retrieval
|
||||||
client.project.update(multilingual=True)
|
client.project.update(multilingual=True)
|
||||||
|
|
||||||
# Set retrieval criteria to control which memories are surfaced in search
|
|
||||||
client.project.update(
|
|
||||||
retrieval_criteria=[
|
|
||||||
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
|
|
||||||
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Enable Memory Decay (boosts recently-accessed memories at search time)
|
|
||||||
client.project.update(decay=True)
|
|
||||||
|
|
||||||
# Update multiple settings at once
|
# Update multiple settings at once
|
||||||
client.project.update(
|
client.project.update(
|
||||||
custom_instructions="...",
|
custom_instructions="...",
|
||||||
@@ -120,47 +109,6 @@ client.project.update(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Set Retrieval Criteria
|
|
||||||
|
|
||||||
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
|
|
||||||
|
|
||||||
```python
|
|
||||||
client.project.update(
|
|
||||||
retrieval_criteria=[
|
|
||||||
{
|
|
||||||
"name": "joy",
|
|
||||||
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
|
|
||||||
"weight": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "curiosity",
|
|
||||||
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
|
|
||||||
"weight": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "access_frequency",
|
|
||||||
"description": "How often this memory has been accessed or surfaced recently.",
|
|
||||||
"weight": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Pass an empty list to clear all criteria and restore default retrieval behaviour.
|
|
||||||
|
|
||||||
#### Toggle Memory Decay
|
|
||||||
|
|
||||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay) — a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
|
||||||
|
|
||||||
```bash cURL
|
|
||||||
curl -X PATCH https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/ \
|
|
||||||
-H "Authorization: Token $MEM0_API_KEY" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"decay": true}'
|
|
||||||
```
|
|
||||||
|
|
||||||
The current state is returned on every project read (and supports `?fields=decay` for a minimal response). Toggling has no effect on stored memories, only on how v3 search ranks them.
|
|
||||||
|
|
||||||
### Delete Project
|
### Delete Project
|
||||||
|
|
||||||
<Warning>
|
<Warning>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Remove Project Member"
|
|
||||||
description: "Remove a member from a project to revoke their access to its memories, configuration, and resources."
|
|
||||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
|
||||||
---
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Update Project Member"
|
|
||||||
description: "Update an existing member's role within a project to change their permissions and access level."
|
|
||||||
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
|
||||||
---
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Update Project"
|
|
||||||
description: "Update a project's settings, including name, custom instructions, and other configuration options."
|
|
||||||
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/"
|
|
||||||
---
|
|
||||||
@@ -4,34 +4,6 @@ description: "Major product launches, headline features, and milestones for Mem0
|
|||||||
mode: "wide"
|
mode: "wide"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Update label="2026-05-13" description="Temporal Reasoning for Mem0 Platform v3">
|
|
||||||
|
|
||||||
**Temporal Reasoning — Time-Aware Retrieval for Platform v3**
|
|
||||||
|
|
||||||
Mem0 Platform v3 can now interpret time-aware memories and queries so assistants retrieve the right information for questions about the past, upcoming plans, and current state.
|
|
||||||
|
|
||||||
- **Time-aware search intent** — Queries like `last week`, `upcoming`, `right now`, and `as of March 2025` return contextually appropriate results automatically
|
|
||||||
- **Enabled by default** — No per-request toggle required for v3 writes or searches
|
|
||||||
- **Anchored relative queries** — `reference_date` anchors relative search phrases for tests, backfills, and reproducible demos
|
|
||||||
- **Normal response shape** — Temporal reasoning affects ranking while preserving existing client response patterns
|
|
||||||
|
|
||||||
See [Temporal Reasoning](/platform/features/temporal-reasoning) for usage details.
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-05-08" description="Memory Decay">
|
|
||||||
|
|
||||||
**Memory Decay — Recently-Used Memories Surface Higher, Automatically**
|
|
||||||
|
|
||||||
Per-project search-time ranking bias that boosts recently-touched memories and gently dampens stale ones. Off by default; opt in per project via the `decay` field on the project endpoint, or via `client.project.update(decay=True)` in the SDKs (Python `v2.0.2` / TypeScript `v3.0.3`).
|
|
||||||
|
|
||||||
- **Soft bias, never a filter.** The scaling factor stays in `0.3×–1.5×`. Decay can reorder candidates but never zeros them out — anything that surfaced before decay can still surface after.
|
|
||||||
- **Reinforcement loop.** Every memory returned in a search has its access history updated, so frequently-used facts naturally float to the top over time.
|
|
||||||
- **Public score still clamped to `[0, 1]`.** Existing API contract preserved; no client-side changes needed.
|
|
||||||
- **v3 search only**, fully reversible. See [Memory Decay docs](/platform/features/memory-decay).
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-04-14" description="Mem0 SDK v2.0.0 / v3.0.0">
|
<Update label="2026-04-14" description="Mem0 SDK v2.0.0 / v3.0.0">
|
||||||
|
|
||||||
**New Memory Algorithm — State-of-the-Art Accuracy at ~3-4x Lower Cost**
|
**New Memory Algorithm — State-of-the-Art Accuracy at ~3-4x Lower Cost**
|
||||||
@@ -46,9 +18,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
|
- **~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
|
- **ADD-only extraction** — Memories accumulate; nothing is overwritten or deleted
|
||||||
- **Hybrid retrieval** — Semantic + BM25 keyword + entity boost, scored in parallel
|
- **Hybrid retrieval** — Semantic + BM25 keyword + entity boost, scored in parallel
|
||||||
- **Graph memory (built-in)**: entities extracted, embedded, and linked across memories, with no external graph store required
|
- **Entity linking** — Entities extracted, embedded, and linked across memories
|
||||||
|
|
||||||
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).
|
Breaking changes: Graph memory removed from OSS, `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
@@ -113,7 +85,7 @@ Launched a unified Mem0 plugin across three major AI development environments
|
|||||||
|
|
||||||
Major expansion of the provider ecosystem:
|
Major expansion of the provider ecosystem:
|
||||||
|
|
||||||
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE). **Note:** All external graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE) were subsequently removed in v2.0.0 (2026-04-14). Graph memory is now built-in entity linking with no external graph store required; see the [v2.0.0 entry above](#mem0-sdk-v2-0-0-v3-0-0).
|
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
|
||||||
- **Turbopuffer** — New vector database provider for Python SDK
|
- **Turbopuffer** — New vector database provider for Python SDK
|
||||||
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
||||||
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
||||||
@@ -127,4 +99,4 @@ Major expansion of the provider ecosystem:
|
|||||||
|
|
||||||
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
|
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
@@ -4,39 +4,6 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
|||||||
mode: "wide"
|
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">
|
<Update label="2026-04-29" description="v1.0.11">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
|
|||||||
@@ -4,28 +4,10 @@ description: "Release notes for the Mem0 hosted platform — backend, dashboard,
|
|||||||
mode: "wide"
|
mode: "wide"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Update label="2026-05-13" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Memory:** Added Temporal Reasoning for Platform v3 to improve ranking for time-aware queries such as `last week`, `upcoming`, `right now`, and `as of ...`
|
|
||||||
- **Search:** Added `reference_date` support to anchor relative temporal queries for tests, backfills, and reproducible demos
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **API:** Temporal reasoning preserves the normal client response shape for search and get-all results
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-05-04" description="">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Memory Decay:** Per-project search-time ranking bias that boosts recently-used memories and gently dampens stale ones. Opt-in via `decay` on the project endpoint; off by default. The scaling factor stays in `0.3×–1.5×`, the public `score` remains clamped to `[0, 1]`, and the bias never filters a candidate out. See [Memory Decay docs](/platform/features/memory-decay).
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-04-16" description="">
|
<Update label="2026-04-16" description="">
|
||||||
|
|
||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **UI:** Removed the legacy external-graph-store visualization tab, page, and its references from dashboard, sidebar, project settings, playground, and billing
|
- **UI:** Removed Graph Memory tab, page, and all references from dashboard, sidebar, project settings, playground, and billing
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
@@ -312,3 +294,4 @@ mode: "wide"
|
|||||||
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
|
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
|
|||||||
+5
-359
@@ -7,180 +7,6 @@ mode: "wide"
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Python">
|
<Tab title="Python">
|
||||||
|
|
||||||
<Update label="2026-06-27" description="v2.0.10">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Client:** Expose `expiration_date` on `MemoryClient.update()` and `AsyncMemoryClient.update()` — callers can now set or clear a memory's expiration date; `None` is preserved and forwarded to the API ([#5874](https://github.com/mem0ai/mem0/pull/5874))
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory (OSS):** Apply `remove_code_blocks()` to the LangChain path in async `_create_procedural_memory` so code fences are stripped consistently ([#5711](https://github.com/mem0ai/mem0/pull/5711))
|
|
||||||
- **Rerankers:** Score HuggingFace cross-encoder results with per-document sigmoid instead of set-relative min-max, preventing a single low-score document from collapsing all relevance scores to zero ([#5715](https://github.com/mem0ai/mem0/pull/5715))
|
|
||||||
- **Core:** Validate and trim entity IDs (`user_id`, `agent_id`, `run_id`) in `delete_all()` for both sync and async `Memory` ([#5735](https://github.com/mem0ai/mem0/pull/5735))
|
|
||||||
- **Vector Stores:** Use `.get()` for `hash` and `created_at` in the Redis `insert()` and `update()` paths so entity payloads that omit those fields no longer raise `KeyError` ([#5709](https://github.com/mem0ai/mem0/pull/5709))
|
|
||||||
- **Memory:** Fix scale-threshold notices not firing for Redis and search-engine backends by resolving `col_info()` signature differences and adding `num_docs` to the count-extraction lookup ([#5687](https://github.com/mem0ai/mem0/pull/5687))
|
|
||||||
- **Vector Stores:** Escape special characters in Valkey FT.SEARCH tag filter values to prevent wildcard and operator injection through tenant-isolation filters ([#5750](https://github.com/mem0ai/mem0/pull/5750))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-06-24" description="v2.0.9">
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory (OSS):** Improve entity extraction precision by avoiding sentence-start common noun noise, preserving useful topic phrases, and exact-deduplicating entity links before semantic matching ([#5829](https://github.com/mem0ai/mem0/pull/5829))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-06-24" description="v2.0.8">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Embeddings:** Add native `embed_batch` to five embedders — LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI — for batched embedding requests ([#5609](https://github.com/mem0ai/mem0/pull/5609))
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Core:** Guard against malformed `image_url` entries in `parse_vision_messages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
|
|
||||||
- **Core:** Return `attributed_to` from `get()`, `get_all()`, and `search()` ([#5629](https://github.com/mem0ai/mem0/pull/5629))
|
|
||||||
- **Core:** Fix `reset()` only dropping the history table and leaving stale messages behind ([#5541](https://github.com/mem0ai/mem0/pull/5541))
|
|
||||||
- **Core:** Guard against an entity `embed_batch` count mismatch in the v3 add pipeline ([#5604](https://github.com/mem0ai/mem0/pull/5604))
|
|
||||||
- **Core:** Fix an async `delete_all` race condition that corrupted the entity store's `linked_memory_ids` ([#5553](https://github.com/mem0ai/mem0/pull/5553))
|
|
||||||
- **LLMs:** Skip the JSON `response_format` for Groq compound models that reject it ([#5513](https://github.com/mem0ai/mem0/pull/5513))
|
|
||||||
- **LLMs:** Preserve reasoning fields during base-to-provider config conversion ([#5638](https://github.com/mem0ai/mem0/pull/5638))
|
|
||||||
- **LLMs:** Pass the configured `anthropic_base_url` to the Anthropic client ([#5626](https://github.com/mem0ai/mem0/pull/5626))
|
|
||||||
- **LLMs:** Stop the Azure provider from mutating and corrupting caller messages during content rewrite ([#5731](https://github.com/mem0ai/mem0/pull/5731))
|
|
||||||
- **LLMs & Embeddings:** Repair HTTP proxy support for `httpx>=0.28` and preserve `proxies` in `LlmFactory` ([#5447](https://github.com/mem0ai/mem0/pull/5447))
|
|
||||||
- **Embeddings:** Forward `embedding_dims` to Titan V2 in the AWS Bedrock embedder ([#5671](https://github.com/mem0ai/mem0/pull/5671))
|
|
||||||
- **Rerankers:** Log reranking failures instead of swallowing them silently ([#5717](https://github.com/mem0ai/mem0/pull/5717))
|
|
||||||
- **Rerankers:** Clamp out-of-range LLM scores instead of mis-parsing them ([#5635](https://github.com/mem0ai/mem0/pull/5635))
|
|
||||||
- **Rerankers:** Export all five rerankers from the package root ([#5636](https://github.com/mem0ai/mem0/pull/5636))
|
|
||||||
- **Vector Stores:** Point the FastEmbed-missing warning at `mem0ai[extras]` ([#5622](https://github.com/mem0ai/mem0/pull/5622))
|
|
||||||
- **Vector Stores:** Preserve empty Azure AI Search update values ([#5524](https://github.com/mem0ai/mem0/pull/5524))
|
|
||||||
- **Vector Stores:** Add an `auto_refresh` option for OpenSearch Serverless compatibility ([#3893](https://github.com/mem0ai/mem0/pull/3893))
|
|
||||||
- **Vector Stores:** Wrap a scalar `vector_id` in a list for Chroma `delete()` ([#5703](https://github.com/mem0ai/mem0/pull/5703))
|
|
||||||
- **Vector Stores:** Wrap Chroma `update()` ids, embeddings, and metadatas in lists ([#5757](https://github.com/mem0ai/mem0/pull/5757))
|
|
||||||
- **Vector Stores:** Wrap a scalar `vector_id` in a list for Milvus `delete()` ([#5704](https://github.com/mem0ai/mem0/pull/5704))
|
|
||||||
- **Vector Stores:** Map all comparison operators in the Pinecone `_create_filter()` ([#5707](https://github.com/mem0ai/mem0/pull/5707))
|
|
||||||
- **Vector Stores:** Return `None` instead of `{}` from Chroma `_generate_where_clause` for empty filters ([#5713](https://github.com/mem0ai/mem0/pull/5713))
|
|
||||||
- **Vector Stores:** Return `[[]]` from the OpenSearch `list()` error path to honor the `list()` contract ([#5727](https://github.com/mem0ai/mem0/pull/5727))
|
|
||||||
- **Vector Stores:** Return `[[]]` from the Pinecone `list()` error path instead of a dict ([#5706](https://github.com/mem0ai/mem0/pull/5706))
|
|
||||||
- **Vector Stores:** Return `[[]]` for an uninitialized FAISS index to honor the `list()` contract ([#5725](https://github.com/mem0ai/mem0/pull/5725))
|
|
||||||
- **Vector Stores:** Wrap the MongoDB `list()` return in an outer list to match the interface contract ([#5729](https://github.com/mem0ai/mem0/pull/5729))
|
|
||||||
- **Vector Stores:** Deep-copy Redis `DEFAULT_FIELDS` so instances keep distinct dims ([#5633](https://github.com/mem0ai/mem0/pull/5633))
|
|
||||||
- **Vector Stores:** Pass the required `vectors` arg in Vertex AI `list()` and similarity search ([#5627](https://github.com/mem0ai/mem0/pull/5627))
|
|
||||||
- **Vector Stores:** Return `None` from Redis `get()` for missing IDs ([#5625](https://github.com/mem0ai/mem0/pull/5625))
|
|
||||||
- **Vector Stores:** Drop a stray `print` in Weaviate `list_cols` ([#5637](https://github.com/mem0ai/mem0/pull/5637))
|
|
||||||
- **Graph:** Keep distinct entities that share a substring prefix ([#5630](https://github.com/mem0ai/mem0/pull/5630))
|
|
||||||
- **Client:** Check the HTTP status before parsing the ping response in `_validate_api_key` ([#5639](https://github.com/mem0ai/mem0/pull/5639))
|
|
||||||
- **Server:** Fetch filtered dashboard memories beyond the default page ([#5753](https://github.com/mem0ai/mem0/pull/5753))
|
|
||||||
- **Server:** Return 404/400 instead of 502 for not-found and invalid input ([#5634](https://github.com/mem0ai/mem0/pull/5634))
|
|
||||||
- **Server:** Return 404 instead of 500 for a malformed API key id on revoke ([#5640](https://github.com/mem0ai/mem0/pull/5640))
|
|
||||||
- **Server:** Use `127.0.0.1` in the dashboard healthcheck to avoid IPv6 localhost resolution ([#5612](https://github.com/mem0ai/mem0/pull/5612))
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Vector Stores:** Batch BM25 sparse encoding in Qdrant insert ([#5592](https://github.com/mem0ai/mem0/pull/5592))
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
- **Vector Stores:** Sanitize Milvus and Baidu filter values to prevent expression injection ([#5746](https://github.com/mem0ai/mem0/pull/5746))
|
|
||||||
- **Vector Stores:** Reject dict filter values in MongoDB to prevent NoSQL operator injection ([#5748](https://github.com/mem0ai/mem0/pull/5748))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<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_details` dict with `semantic_score`, `bm25_score`, `entity_boost`, `raw_score`, `max_possible_score`, `final_score`, and `threshold` 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:**
|
|
||||||
- **Telemetry:** Stitch OSS and platform PostHog identities on `MemoryClient` init so `$identify` events fire and a single user is no longer tracked as two or three disconnected personas ([#5040](https://github.com/mem0ai/mem0/pull/5040))
|
|
||||||
- **Security:** Harden against SQL injection and prompt injection ([#4997](https://github.com/mem0ai/mem0/pull/4997))
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **SDK:** Expose `decay` on `project.update` ([#5062](https://github.com/mem0ai/mem0/pull/5062))
|
|
||||||
|
|
||||||
**Improvements:**
|
|
||||||
- **Plugin:** Hand `mem0` search decisions to the agent ([#4992](https://github.com/mem0ai/mem0/pull/4992))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-04-25" description="v2.0.1">
|
<Update label="2026-04-25" description="v2.0.1">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
@@ -215,7 +41,7 @@ mode: "wide"
|
|||||||
**Breaking Changes:**
|
**Breaking Changes:**
|
||||||
- **`add()` returns ADD-only events** — No more `"UPDATE"` or `"DELETE"` events. Memories accumulate; nothing is overwritten ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
- **`add()` returns ADD-only events** — No more `"UPDATE"` or `"DELETE"` events. Memories accumulate; nothing is overwritten ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
- **`search()` default `threshold` is now `0.1`** — Pass `threshold=0.0` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
- **`search()` default `threshold` is now `0.1`** — Pass `threshold=0.0` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
- **`search()` `score` is now a combined multi-signal score** — The top-level `score` fuses semantic similarity, BM25 keyword match, entity signals, and temporal boosts into one value. Absolute numbers shift versus the old raw cosine score; retune any hard thresholds against representative queries ([#4805](https://github.com/mem0ai/mem0/pull/4805), [#4836](https://github.com/mem0ai/mem0/pull/4836))
|
- **`search()` `score` is now a combined multi-signal score** — The top-level `score` fuses semantic similarity, BM25 keyword match, and entity boost into one value. Absolute numbers shift versus the old raw cosine score; retune any hard thresholds against representative queries. Per-signal scores are not exposed on the response ([#4805](https://github.com/mem0ai/mem0/pull/4805), [#4836](https://github.com/mem0ai/mem0/pull/4836))
|
||||||
- **`search()` default `rerank` is now `False`** — Pass `rerank=True` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
- **`search()` default `rerank` is now `False`** — Pass `rerank=True` for previous behavior ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
- **`top_k` default changed 100 → 20** in `Memory.get_all()` and `Memory.search()` (sync + async). Pass `top_k=100` explicitly to restore the old behavior ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
- **`top_k` default changed 100 → 20** in `Memory.get_all()` and `Memory.search()` (sync + async). Pass `top_k=100` explicitly to restore the old behavior ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
- **Entity ID validation:** `user_id` / `agent_id` / `run_id` are trimmed; empty-string and whitespace-only values now raise `ValueError` ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
- **Entity ID validation:** `user_id` / `agent_id` / `run_id` are trimmed; empty-string and whitespace-only values now raise `ValueError` ([#4843](https://github.com/mem0ai/mem0/pull/4843))
|
||||||
@@ -223,8 +49,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))
|
- **`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))
|
- **`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))
|
- **`org_id` and `project_id` removed** — Removed from `MemoryClient` constructor and all method signatures ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
- **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))
|
- **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 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))
|
- **`enable_graph` removed from Client SDK** — Graph memory is now a project-level setting on the Platform. Remove `enable_graph` from `MemoryClient.add()` / `search()` / `get_all()` / `update_project()` calls ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
||||||
- **`custom_fact_extraction_prompt` renamed to `custom_instructions`** — Update config and memory module references ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
- **`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))
|
- **Typed option classes** — Added Pydantic v2 typed classes: `AddMemoryOptions`, `SearchMemoryOptions`, `GetAllMemoryOptions`, `DeleteAllMemoryOptions`, `UpdateMemoryOptions`, `ProjectUpdateOptions` ([#4740](https://github.com/mem0ai/mem0/pull/4740))
|
||||||
|
|
||||||
@@ -244,7 +70,7 @@ mode: "wide"
|
|||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
||||||
|
|
||||||
See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-v2) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
@@ -1084,118 +910,6 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
|
|||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
|
|
||||||
<Update label="2026-06-27" description="v3.0.12">
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **Client:** Add `expirationDate` to `AddMemoryOptions`, `update()`, and the `Memory` interface; add `showExpired` to `SearchMemoryOptions` and `GetAllMemoryOptions` ([#5874](https://github.com/mem0ai/mem0/pull/5874))
|
|
||||||
- **LLMs:** Add `MiniMaxLLM` provider backed by the OpenAI-compatible MiniMax API (`api.minimax.io/v1`, default model `MiniMax-M2.7`) ([#5858](https://github.com/mem0ai/mem0/pull/5858))
|
|
||||||
- **LLMs:** Add `LiteLLM` provider for routing requests through a local or hosted LiteLLM proxy ([#5830](https://github.com/mem0ai/mem0/pull/5830))
|
|
||||||
- **Vector Stores:** Add `connectionString` and `ssl` options to the PGVector config, allowing connection via URI instead of individual host/user/password/port fields ([#5789](https://github.com/mem0ai/mem0/pull/5789))
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory (OSS):** Validate and trim entity IDs (`userId`, `agentId`, `runId`) in `deleteAll()` via `validateAndTrimEntityId` ([#5735](https://github.com/mem0ai/mem0/pull/5735))
|
|
||||||
- **Vector Stores:** Use nullish coalescing for `hash` and timestamps in the Redis `insert()` and `update()` paths so entity payloads that omit those fields no longer crash ([#5860](https://github.com/mem0ai/mem0/pull/5860))
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
- **Dependencies:** Bump `undici` to `>=6.27.0` via pnpm override to remediate CVE-2026-12151 ([#5861](https://github.com/mem0ai/mem0/pull/5861))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-06-24" description="v3.0.11">
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory (OSS):** Align entity extraction with Python by reducing generic entity noise, preserving useful topic phrases, and exact-deduplicating entity links before semantic matching ([#5829](https://github.com/mem0ai/mem0/pull/5829))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-06-24" description="v3.0.10">
|
|
||||||
|
|
||||||
**Bug Fixes:**
|
|
||||||
- **Memory (OSS):** Guard against malformed `image_url` entries in `parseVisionMessages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
|
|
||||||
- **Memory (OSS):** Return `attributedTo` from `get()`, `search()`, and `getAll()` ([#5675](https://github.com/mem0ai/mem0/pull/5675))
|
|
||||||
- **Memory (OSS):** Preserve message roles in the extraction input so assistant facts aren't attributed to the user ([#5643](https://github.com/mem0ai/mem0/pull/5643))
|
|
||||||
- **Memory (OSS):** Reject empty or blank messages in `Memory.add()` to prevent hallucinated memories ([#5545](https://github.com/mem0ai/mem0/pull/5545))
|
|
||||||
- **Memory (OSS):** Check `message.role` instead of `content` when detecting system messages ([#3921](https://github.com/mem0ai/mem0/pull/3921))
|
|
||||||
- **LLMs:** Honor the configured `baseURL` in `AnthropicLLM` ([#5740](https://github.com/mem0ai/mem0/pull/5740))
|
|
||||||
- **Client:** Preserve `customCategories` names through key conversion ([#5741](https://github.com/mem0ai/mem0/pull/5741))
|
|
||||||
- **Client:** Prevent hallucinated memories on an empty messages payload ([#5613](https://github.com/mem0ai/mem0/pull/5613))
|
|
||||||
- **Client:** Preserve user metadata keys across the case-conversion round-trip ([#5515](https://github.com/mem0ai/mem0/pull/5515))
|
|
||||||
|
|
||||||
**Security:**
|
|
||||||
- **Dependencies:** Upgrade `form-data` to `>=4.0.6` across pnpm workspaces to remediate CVE-2026-12143 ([#5618](https://github.com/mem0ai/mem0/pull/5618))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<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:**
|
|
||||||
- **Telemetry:** Stitch OSS and platform PostHog identities on `MemoryClient` init so `$identify` events fire and a single user is no longer tracked as two or three disconnected personas ([#5040](https://github.com/mem0ai/mem0/pull/5040))
|
|
||||||
- **Vector Stores:** Fix inverted vector distance in PGVector implementation ([#4944](https://github.com/mem0ai/mem0/pull/4944))
|
|
||||||
- **Security:** Harden against SQL injection and prompt injection ([#4997](https://github.com/mem0ai/mem0/pull/4997))
|
|
||||||
|
|
||||||
**New Features:**
|
|
||||||
- **SDK:** Expose `decay` on `project.update` ([#5062](https://github.com/mem0ai/mem0/pull/5062))
|
|
||||||
|
|
||||||
</Update>
|
|
||||||
|
|
||||||
<Update label="2026-04-25" description="v3.0.2">
|
<Update label="2026-04-25" description="v3.0.2">
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
@@ -1229,7 +943,7 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-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))
|
- **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:**
|
**Breaking Changes:**
|
||||||
- **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))
|
- **Graph Memory Removed (OSS):** `graph_memory.ts` (675 lines), `graphs/tools.ts` (267 lines), `graphs/utils.ts` (116 lines), `graphs/configs.ts` (30 lines) deleted. Graph memory is no longer supported in the OSS SDK — use Platform API for graph features ([#4805](https://github.com/mem0ai/mem0/pull/4805))
|
||||||
- **camelCase Parameters (Client SDK):** All user-facing parameters converted from snake_case to camelCase. Mapping is transparent at API boundary via `camelToSnakeKeys()` / `snakeToCamelKeys()` ([#4776](https://github.com/mem0ai/mem0/pull/4776))
|
- **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
|
```typescript
|
||||||
// Before
|
// Before
|
||||||
@@ -1583,37 +1297,6 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
|
|||||||
|
|
||||||
<Tab title="CLI">
|
<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">
|
<Update label="2026-04-22" description="Python v0.2.4 / Node v0.2.4">
|
||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
@@ -1706,43 +1389,6 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
|||||||
|
|
||||||
<Tab title="Plugins">
|
<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">
|
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
|
||||||
|
|
||||||
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user