Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b91b2b8fc4 |
@@ -8,7 +8,7 @@
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./integrations/mem0-plugin"
|
||||
"path": "./mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"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.",
|
||||
"version": "0.2.10"
|
||||
"version": "0.2.8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./integrations/mem0-plugin"
|
||||
"path": "./mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.2.10"
|
||||
"version": "0.2.8"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. v1.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
|
||||
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
|
||||
# like vercel-ai-v* that also start with 'v'
|
||||
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
|
||||
if: startsWith(github.event.release.tag_name, 'v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
@@ -54,6 +39,7 @@ jobs:
|
||||
# packages_dir: dist/
|
||||
|
||||
- name: Publish distribution 📦 to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages_dir: dist/
|
||||
|
||||
@@ -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."
|
||||
@@ -1,11 +1,9 @@
|
||||
name: ci
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
changelog_check:
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
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
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -28,8 +16,6 @@ jobs:
|
||||
working-directory: cli/node
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -52,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
name: CLI Node CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -9,7 +7,10 @@ on:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
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
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -27,8 +16,6 @@ jobs:
|
||||
working-directory: cli/python
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
name: CLI Python CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -9,7 +7,10 @@ on:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
|
||||
jobs:
|
||||
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 --write # scaffold placeholders
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# manual runs remain standalone.
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
- 'scripts/check-llms-txt-coverage.py'
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
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
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/openclaw
|
||||
working-directory: openclaw
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -42,7 +28,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -52,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
name: openclaw checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'integrations/openclaw/**'
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -27,13 +28,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd integrations/openclaw && pnpm exec tsc --noEmit
|
||||
run: cd openclaw && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -53,20 +54,20 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- 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
|
||||
if: matrix.node-version == 20
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: openclaw
|
||||
directory: integrations/openclaw/coverage
|
||||
directory: openclaw/coverage
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -85,15 +86,15 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd integrations/openclaw && pnpm build
|
||||
run: cd openclaw && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
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
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
name: Build and publish @mem0/opencode-plugin 📦 to npm
|
||||
if: startsWith(inputs.tag, 'opencode-v')
|
||||
if: startsWith(github.event.release.tag_name, 'opencode-v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
working-directory: mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -50,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
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/**'
|
||||
- 'mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- '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
|
||||
working-directory: mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
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
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -27,8 +16,6 @@ jobs:
|
||||
working-directory: mem0-ts
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -51,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: TypeScript SDK CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
- '.github/workflows/ts-sdk-ci.yml'
|
||||
workflow_call:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
|
||||
jobs:
|
||||
check_changes:
|
||||
|
||||
@@ -1,35 +1,21 @@
|
||||
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:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
|
||||
required: true
|
||||
type: string
|
||||
prerelease:
|
||||
description: 'Publish under the version preid dist-tag instead of latest'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build-n-publish:
|
||||
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
|
||||
permissions:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: integrations/vercel-ai-sdk
|
||||
working-directory: vercel-ai-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -42,7 +28,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml
|
||||
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
@@ -52,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ inputs.prerelease }}" = "true" ]; then
|
||||
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
|
||||
npx npm@latest publish --provenance --access public --tag "$PREID"
|
||||
else
|
||||
|
||||
@@ -22,18 +22,17 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
|
||||
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
||||
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
|
||||
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
|
||||
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
|
||||
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `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. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
| `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 |
|
||||
| `pr-reviews/` | Pull request review materials |
|
||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||
|
||||
@@ -50,8 +49,8 @@ mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||
|
||||
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
openclaw/ ──▶ mem0ai (npm)
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
@@ -74,8 +73,8 @@ pre-commit install # install git hooks
|
||||
# TypeScript packages
|
||||
cd mem0-ts && pnpm install # TS SDK
|
||||
cd cli/node && pnpm install # Node CLI
|
||||
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd integrations/openclaw && pnpm install # OpenClaw plugin
|
||||
cd vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd openclaw && pnpm install # OpenClaw plugin
|
||||
```
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
@@ -163,10 +162,10 @@ pnpm run dev # tsx src/index.ts (development)
|
||||
- **Test:** vitest (not jest)
|
||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||
|
||||
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
|
||||
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||
|
||||
```bash
|
||||
cd integrations/vercel-ai-sdk
|
||||
cd vercel-ai-sdk
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # eslint
|
||||
@@ -181,10 +180,10 @@ pnpm run test:node # vitest (node runtime)
|
||||
- **Lint:** ESLint + Prettier
|
||||
- **Test:** jest + vitest (edge/node configs)
|
||||
|
||||
### OpenClaw Plugin (`integrations/openclaw/`)
|
||||
### OpenClaw Plugin (`openclaw/`)
|
||||
|
||||
```bash
|
||||
cd integrations/openclaw
|
||||
cd openclaw
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # vitest run
|
||||
@@ -343,8 +342,8 @@ make run-openai # OpenAI comparison
|
||||
|---------|--------|-----------|---------------|
|
||||
| `mem0-ts/` | — | Prettier | jest |
|
||||
| `cli/node/` | Biome | Biome | vitest |
|
||||
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `integrations/openclaw/` | — | — | vitest |
|
||||
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `openclaw/` | — | — | vitest |
|
||||
|
||||
### Type Checking
|
||||
|
||||
@@ -382,14 +381,14 @@ Model Context Protocol support in multiple places:
|
||||
|
||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||
- **Plugin:** MCP tools in `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
|
||||
|
||||
- `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:
|
||||
- **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).
|
||||
- **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.
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
@@ -403,44 +402,23 @@ To add a new LLM, embedding, vector store, or reranker provider:
|
||||
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
|
||||
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
|
||||
|
||||
### Adding a New Integration
|
||||
|
||||
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
|
||||
|
||||
1. Create `integrations/<name>/` and build the integration there.
|
||||
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
|
||||
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
|
||||
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
|
||||
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
|
||||
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
|
||||
|
||||
## CI/CD
|
||||
|
||||
### CI Workflows (automated testing)
|
||||
|
||||
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
|
||||
|
||||
| Workflow | File | Standalone Triggers | Tests |
|
||||
|----------|------|---------------------|-------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
|
||||
|
||||
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
|
||||
| Workflow | File | Triggers | Tests |
|
||||
|----------|------|----------|-------|
|
||||
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to `mem0-plugin/.opencode-plugin/`, PRs, manual | Bun: tsc type-check + build + dist artifact check |
|
||||
|
||||
### CD Workflows (automated publishing)
|
||||
|
||||
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
|
||||
|
||||
| Workflow | File | Tag Prefix | Target |
|
||||
|----------|------|------------|--------|
|
||||
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
|
||||
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
|
||||
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
|
||||
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
|
||||
@@ -448,13 +426,9 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
|
||||
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
|
||||
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
|
||||
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
|
||||
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
|
||||
|
||||
### Utility Workflows
|
||||
|
||||
|
||||
@@ -1026,8 +1026,7 @@ def get_user_preferences(user_id: str):
|
||||
### AutoGen Integration
|
||||
|
||||
```python
|
||||
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
|
||||
from helper.mem0_teachability import Mem0Teachability
|
||||
from cookbooks.helper.mem0_teachability import Mem0Teachability
|
||||
from mem0 import Memory
|
||||
|
||||
# Add memory capability to AutoGen agents
|
||||
|
||||
@@ -186,10 +186,9 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
|
||||
```
|
||||
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. 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.
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
|
||||
@@ -5,21 +5,6 @@ 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.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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.7",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -40,8 +40,7 @@
|
||||
"typescript": "^5.4.0",
|
||||
"tsup": "^8.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.1.0",
|
||||
"vitest": "^1.5.0",
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/node": "^20.0.0"
|
||||
}
|
||||
|
||||
Generated
+493
-364
File diff suppressed because it is too large
Load Diff
@@ -1,13 +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"
|
||||
@@ -8,10 +8,4 @@ export default defineConfig({
|
||||
define: {
|
||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
test: {
|
||||
// Integration tests spawn the CLI via `npx tsx` (15s subprocess
|
||||
// timeout); the first spawn in a file pays a cold-start cost that can
|
||||
// exceed vitest's 5s default on CI runners.
|
||||
testTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,13 +32,12 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
|
||||
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",
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=15,
|
||||
)
|
||||
@@ -100,13 +99,12 @@ class TestArgvPreprocessing:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
text=True,
|
||||
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,
|
||||
)
|
||||
@@ -135,13 +133,12 @@ class TestJsonEnvelopeParity:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
text=True,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -49,7 +49,6 @@ def _run(
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
@@ -57,7 +56,7 @@ def _run(
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
text=True,
|
||||
env=env,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
|
||||
@@ -8,8 +8,8 @@ from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.exceptions import Exit as ClickExit
|
||||
from rich.console import Console
|
||||
from typer import Exit as TyperExit
|
||||
|
||||
from mem0_cli.commands.config_cmd import (
|
||||
cmd_config_get,
|
||||
@@ -181,7 +181,7 @@ class TestAddCommand:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -206,7 +206,7 @@ class TestAddCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -764,7 +764,7 @@ class TestImportCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.utils.console", console),
|
||||
patch("mem0_cli.commands.utils.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
|
||||
|
||||
@@ -801,7 +801,7 @@ class TestEntitiesListCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_entities_list(mock_backend, "invalid", output="table")
|
||||
|
||||
@@ -944,7 +944,7 @@ class TestEntitiesDeleteCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_entities_delete(
|
||||
mock_backend,
|
||||
@@ -1308,7 +1308,7 @@ class TestAgentMode:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("sys.stdout", captured_stdout),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
):
|
||||
cmd_get(mock_backend, "bad-id", output="text")
|
||||
|
||||
|
||||
@@ -67,8 +67,7 @@ class TestConfig:
|
||||
from mem0_cli.config import CONFIG_FILE
|
||||
|
||||
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):
|
||||
config = Mem0Config()
|
||||
|
||||
@@ -555,7 +555,7 @@
|
||||
"# - Enables creation of AI agents with long-term memory and learning abilities.\n",
|
||||
"# - Improves consistency and reduces repetition in user-agent interactions.\n",
|
||||
"\n",
|
||||
"from helper.mem0_teachability import Mem0Teachability\n",
|
||||
"from cookbooks.helper.mem0_teachability import Mem0Teachability\n",
|
||||
"\n",
|
||||
"teachability = Mem0Teachability(\n",
|
||||
" verbosity=2, # for visibility of what's happening\n",
|
||||
@@ -4,39 +4,6 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-06-12" description="v1.0.13">
|
||||
|
||||
**Fixes:**
|
||||
- **Custom categories payload:** `customCategories` (a `Record<string, string>` map) is now converted via the new `customCategoryMapToList()` helper into the `Array<Record<string, string>>` shape the Mem0 SDK expects on `add` calls — previously the raw object was passed as `custom_categories` and silently ignored ([#5345](https://github.com/mem0ai/mem0/pull/5345))
|
||||
- **Skip runtime setup during metadata registration:** `register()` now detects `registrationMode === "cli-metadata"`, registers only the CLI commands, and returns early — avoiding backend initialization, service/tool registration, and hook installation during OpenClaw's metadata-only registration pass ([#5383](https://github.com/mem0ai/mem0/pull/5383))
|
||||
|
||||
**Security:**
|
||||
- Bumped `mem0ai` from `3.0.3` to `3.0.7` (latest Node SDK) — includes the transitive axios CVE remediation shipped in `3.0.6` ([#5460](https://github.com/mem0ai/mem0/pull/5460))
|
||||
- Added pnpm override `uuid@<11.1.1` → `>=11.1.1` to resolve an open MEDIUM Dependabot alert ([#5489](https://github.com/mem0ai/mem0/pull/5489))
|
||||
|
||||
**Improvements:**
|
||||
- **Repo consolidation:** Plugin moved from repo-root `openclaw/` to `integrations/openclaw/`; `package.json` `repository.directory` updated to match so npm provenance links to the correct subdirectory ([#5491](https://github.com/mem0ai/mem0/pull/5491))
|
||||
|
||||
**Tests:**
|
||||
- Added `customCategoryMapToList` unit tests and a `PlatformProvider` test asserting `custom_categories` is passed to the Mem0 SDK as a list ([#5345](https://github.com/mem0ai/mem0/pull/5345))
|
||||
- Added a regression test asserting `cli-metadata` registration registers only CLI commands and triggers no runtime side effects ([#5383](https://github.com/mem0ai/mem0/pull/5383))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-02" description="v1.0.12">
|
||||
|
||||
**Docs:**
|
||||
- **Agent Mode onboarding:** README now documents an autonomous setup path for AI agents — `mem0 init --agent --json` mints an evaluation Mem0 API key with no email, OTP, or browser and exports it as `MEM0_API_KEY` for `openclaw mem0 init`; a human owner can later run `mem0 init --email <email>` to claim ownership without disrupting the agent ([#5123](https://github.com/mem0ai/mem0/pull/5123))
|
||||
|
||||
**Security:**
|
||||
- Added pnpm overrides to remediate advisories in transitive dependencies: `langsmith@<0.6.0` → `^0.6.0`, `picomatch@<2.3.2` → `^2.3.2`, `vite` → `^8.0.5`, and `@qdrant/js-client-rest` → `^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
|
||||
|
||||
**Dependencies:**
|
||||
- Bumped `mem0ai` from `3.0.2` to `3.0.3` ([#5212](https://github.com/mem0ai/mem0/pull/5212))
|
||||
- Bumped dev dependencies `@vitest/coverage-v8` and `vitest` from `^4.0.18` to `^4.1.7`; added `vite@^8.0.5` and `@qdrant/js-client-rest@^1.18.0` ([#5294](https://github.com/mem0ai/mem0/pull/5294))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-29" description="v1.0.11">
|
||||
|
||||
**New Features:**
|
||||
|
||||
@@ -7,42 +7,6 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-06-13" description="v2.0.6">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Add a contextual OSS-to-Platform notices system that surfaces occasional, situation-aware messages (first run, scale/performance thresholds, slow queries, and when temporal/decay features are relevant) pointing to the corresponding Mem0 Platform capabilities; disable via `MEM0_TELEMETRY=false` ([#5494](https://github.com/mem0ai/mem0/pull/5494))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory:** Prevent a crash in `parse_vision_messages` when vision support is disabled ([#5487](https://github.com/mem0ai/mem0/pull/5487))
|
||||
- **Vector Stores:** Expose the `https` option on the Qdrant vector store configuration so TLS endpoints can be targeted explicitly ([#5380](https://github.com/mem0ai/mem0/pull/5380))
|
||||
- **Vector Stores:** Use valid S3 Vectors entity index names, fixing index operations that failed on invalid names ([#5416](https://github.com/mem0ai/mem0/pull/5416))
|
||||
- **Vector Stores:** Fix `search()` crashing with a `TypeError` in the LangChain vector store when a result score is `None` ([#5072](https://github.com/mem0ai/mem0/pull/5072))
|
||||
- **Vector Stores:** Use `is not None` instead of a truthiness check for vector/payload in the PGVector `update()` path, so valid empty/zero values are no longer skipped ([#5488](https://github.com/mem0ai/mem0/pull/5488))
|
||||
- **Vector Stores:** Index the Valkey `memory` field as `TEXT` rather than `TAG` so full-text search behaves correctly ([#5443](https://github.com/mem0ai/mem0/pull/5443))
|
||||
- **Vector Stores:** Implement `$not` filter support in the ChromaDB vector store ([#5485](https://github.com/mem0ai/mem0/pull/5485))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-10" description="v2.0.5">
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Warn at init time when hybrid/BM25 search silently degrades to semantic-only because the configured vector store does not implement `keyword_search`. Affected stores: Chroma, FAISS, Cassandra, LangChain, Neptune Analytics, S3 Vectors, Supabase, TurboPuffer, Valkey ([#5444](https://github.com/mem0ai/mem0/pull/5444))
|
||||
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_breakdown` dict with `semantic`, `keyword` (normalized BM25), `entity_boost`, and `temporal_boost` signals so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) consistently across all backends. 11 adapters previously returned raw distance metrics (lower = better) — FAISS, Chroma, Milvus, Redis, Cassandra, PGVector, S3 Vectors, Supabase, Valkey, Azure MySQL, and Vertex AI Vector Search — causing incorrect ranking in multi-store setups ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
||||
- **Memory:** Parallelize entity boost searches in `Memory.search()` and `AsyncMemory.search()`. Previously up to 8 entities were embedded and queried sequentially (16 serial round-trips with remote embedders); all entity lookups now run concurrently, eliminating multi-second latency on entity-rich queries ([#5377](https://github.com/mem0ai/mem0/pull/5377))
|
||||
- **Memory:** Reject empty or whitespace-only queries in `Memory.search()`, `AsyncMemory.search()`, `MemoryClient.search()`, and `AsyncMemoryClient.search()` before any embedding or API call is made. Also strips leading/trailing whitespace from valid queries ([#5258](https://github.com/mem0ai/mem0/pull/5258))
|
||||
- **LLMs:** Add `is_reasoning_model: Optional[bool]` override to `BaseLlmConfig` (surfaced on `OpenAILlmConfig` and `AzureOpenAILlmConfig`). Fixes silent zero-extraction when using Azure deployments with versioned `gpt-5.x` names that the automatic name-based heuristic cannot recognize ([#5327](https://github.com/mem0ai/mem0/pull/5327))
|
||||
- **LLMs:** Fix xAI LLM provider: add `XAIConfig` with `xai_base_url`, forward `tools`/`tool_choice` in `generate_response()`, and parse `tool_calls` in the response. Previously the provider raised `AttributeError` at init and silently dropped tool results ([#5190](https://github.com/mem0ai/mem0/pull/5190))
|
||||
- **Vector Stores:** Fix PGVector `ConnectionPool` hang in Docker Compose environments where the app container starts before Postgres is DNS-resolvable — switched to `open=False` to avoid blocking constructor or silent zombie pool ([#5155](https://github.com/mem0ai/mem0/pull/5155))
|
||||
- **Vector Stores:** Fix PGVector `sslmode` handling for PostgreSQL URIs — the `sslmode` query parameter is now correctly extracted and forwarded when building the async connection pool ([#5308](https://github.com/mem0ai/mem0/pull/5308))
|
||||
- **Vector Stores:** Fix S3 Vectors `list()` not applying metadata filters — filtering is now done client-side after fetching, with pagination preserved and `top_k` applied after filtering to prevent pre-truncation of matching rows ([#5018](https://github.com/mem0ai/mem0/pull/5018))
|
||||
- **Vector Stores:** Fix Upstash Vector `search()` routing all queries to the default namespace — `namespace` is now passed as a top-level keyword argument to `query_many()` instead of inside the per-query dict where it was silently ignored ([#5202](https://github.com/mem0ai/mem0/pull/5202))
|
||||
- **Core:** Replace mutable default arguments with `None` sentinels in embedder configs and the proxy module, preventing cross-request state contamination ([#5302](https://github.com/mem0ai/mem0/pull/5302))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-27" description="v2.0.4">
|
||||
|
||||
**New Features:**
|
||||
@@ -975,38 +939,6 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
</Tab>
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<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:**
|
||||
@@ -1420,13 +1352,6 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
|
||||
|
||||
<Tab title="CLI">
|
||||
|
||||
<Update label="2026-06-01" description="Node v0.2.8">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `jws` → 4.0.1 (CVE-2025-65945), `langsmith` → ^0.6.0 (CVE-2026-45134), `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343), `picomatch` → ^2.3.2 (CVE-2026-33671), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `path-to-regexp` → ^8.4.0 (CVE-2026-4926), `rollup` → ^4.59.0 (CVE-2026-27606), `glob` → ^10.5.0 (CVE-2025-64756), `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-05-16" description="Python v0.2.6 / Node v0.2.6">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -1543,43 +1468,6 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
||||
|
||||
<Tab title="Plugins">
|
||||
|
||||
<Update label="2026-06-01" description="openclaw-mem0 v1.0.12">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `protobufjs` → ^7.5.5, `vite` → ^8.0.5, `langsmith` → ^0.6.0 (CVE-2026-45134), `picomatch` → ^2.3.2 (CVE-2026-33671), `@qdrant/js-client-rest` → ^1.18.0
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-10" description="Vercel AI SDK v3.0.0">
|
||||
|
||||
**Major Release** — Migrated to Vercel AI SDK v6 (`LanguageModelV3` / `ProviderV3`) and Mem0 v3 API.
|
||||
|
||||
**Breaking Changes:**
|
||||
- **AI SDK v6:** Upgraded from AI SDK v5 (`LanguageModelV2`) to v6 (`LanguageModelV3`). Users must upgrade `ai` to `^6.0.199` and all `@ai-sdk/*` provider packages to `^3.x` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Mem0 v3 API:** Memory endpoints migrated from `/v1/memories/` and `/v2/memories/search/` to `/v3/memories/add/` and `/v3/memories/search/`. Entity IDs (`user_id`, `agent_id`, `run_id`) now go inside the `filters` object for search requests ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Graph memory removed:** All `enable_graph`, graph prompts, and relation-extraction code removed. Graph memory is now a project-level setting on the Platform ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Deprecated params removed:** `org_id`, `project_id`, `org_name`, `project_name`, `output_format`, `filter_memories`, `async_mode`, `enable_graph`, `version`, `api_version` removed from `Mem0ConfigSettings` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
**New Features:**
|
||||
- **V3 provider contract:** `specificationVersion: 'v3'`, `supportedUrls` property, V3 content array in `doGenerate`, V3 stream lifecycle events in `doStream` ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Mem0 source in responses:** Memories are attached as a `source` in `generateText`/`streamText` responses with `providerMetadata.mem0.memories` for programmatic access ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Async memory storage:** `addMemories` is now properly `await`ed — memories no longer silently fail to store ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Prompt mutation:** Prompt array is now cloned before injecting memory context, preventing side effects on the caller's array ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Null guard on content:** `doGenerate` guards against null `content` from upstream providers ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Stream response:** `doStream` now returns the full `LanguageModelV3StreamResult` object preserving all V3 fields ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
- **Response normalization:** `getMemories` and `retrieveMemories` now handle both array and `{results: [...]}` envelope responses from the v3 API ([#4741](https://github.com/mem0ai/mem0/pull/4741))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-01" description="Vercel AI SDK v2.0.6">
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs: `glob` → ^10.5.0 (CVE-2025-64756), `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996), `picomatch` → ^2.3.2 (CVE-2026-33671), `rollup` → ^4.59.0 (CVE-2026-27606)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
|
||||
|
||||
**Mem0 Plugin for Claude Code, Cursor, and Codex**
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
---
|
||||
title: "Neon"
|
||||
description: "Use Neon as a vector store in Mem0, powered by PostgreSQL and pgvector."
|
||||
---
|
||||
|
||||
Use [Neon](https://neon.com/) as a vector store in Mem0, powered by PostgreSQL and the
|
||||
[pgvector extension](https://neon.com/docs/extensions/pgvector).
|
||||
|
||||
Neon is a serverless Postgres platform. Since Mem0 supports Postgres through the
|
||||
`pgvector` provider, Neon can be used with a standard Postgres connection string.
|
||||
|
||||
## Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from mem0 import Memory
|
||||
|
||||
load_dotenv()
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"connection_string": os.environ["DATABASE_URL"],
|
||||
"collection_name": "memories",
|
||||
"embedding_model_dims": 1536,
|
||||
"hnsw": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
results = m.search(
|
||||
"What movies should I recommend?",
|
||||
filters={"user_id": "alice"},
|
||||
)
|
||||
|
||||
print(results)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import "dotenv/config";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const databaseUrl = new URL(process.env.DATABASE_URL!);
|
||||
|
||||
const m = new Memory({
|
||||
vectorStore: {
|
||||
provider: "pgvector",
|
||||
config: {
|
||||
user: decodeURIComponent(databaseUrl.username),
|
||||
password: decodeURIComponent(databaseUrl.password),
|
||||
host: databaseUrl.hostname,
|
||||
port: Number(databaseUrl.port || 5432),
|
||||
dbname: databaseUrl.pathname.slice(1) || "neondb",
|
||||
collectionName: "memories",
|
||||
dimension: 1536,
|
||||
embeddingModelDims: 1536,
|
||||
hnsw: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: "user" as const, content: "I'm planning to watch a movie tonight. Any recommendations?" },
|
||||
{ role: "assistant" as const, content: "How about thriller movies? They can be quite engaging." },
|
||||
{ role: "user" as const, content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
|
||||
{ role: "assistant" as const, content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
|
||||
];
|
||||
|
||||
await m.add(messages, {
|
||||
userId: "alice",
|
||||
metadata: { category: "movies" },
|
||||
});
|
||||
|
||||
const results = await m.search("What movies should I recommend?", {
|
||||
filters: { user_id: "alice" },
|
||||
});
|
||||
|
||||
console.log(results);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## SQL Migration
|
||||
|
||||
You don't need to run any SQL migrations. Mem0 creates the collection table when it initializes the `pgvector` store.
|
||||
|
||||
## Environment
|
||||
|
||||
```env
|
||||
OPENAI_API_KEY=sk-xx...
|
||||
DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neondb?sslmode=require
|
||||
```
|
||||
|
||||
## Config
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `connection_string` | Neon Postgres connection string. | Required |
|
||||
| `collection_name` | Name for the vector collection. | `mem0` |
|
||||
| `embedding_model_dims` | Embedding model dimensions. | `1536` |
|
||||
| `hnsw` | Enables HNSW indexing. | `False` |
|
||||
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields,
|
||||
so parse `DATABASE_URL` before creating `Memory`.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `user` | Database user. | Required |
|
||||
| `password` | Database password. | Required |
|
||||
| `host` | Database host. | Required |
|
||||
| `port` | Database port. | `5432` |
|
||||
| `dbname` | Database name. | `vector_store` |
|
||||
| `collectionName` | Name for the vector collection. | `memories` |
|
||||
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
||||
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
||||
| `hnsw` | Enables HNSW indexing. | `false` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Indexing
|
||||
|
||||
The `pgvector` provider can create an HNSW index for faster vector search.
|
||||
|
||||
- Set `hnsw` to `true` to enable a Hierarchical Navigable Small World index.
|
||||
- Leave `hnsw` as `false` if you want to create or manage indexes yourself.
|
||||
|
||||
### Similarity Search
|
||||
|
||||
The `pgvector` provider uses cosine similarity for vector search. Make sure your
|
||||
embedding dimensions match the configured `embedding_model_dims` value.
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Index Selection**:
|
||||
- Use `hnsw` for faster search performance when memory usage is not a constraint
|
||||
- Manage indexes manually if you need a different pgvector index strategy
|
||||
|
||||
2. **Connection String**:
|
||||
- Always use environment variables or even better, a secret manager for sensitive information in the connection string
|
||||
- Format: `postgresql://user:password@host:port/database`
|
||||
@@ -76,7 +76,6 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `path` | Path for the qdrant database | `/tmp/qdrant` |
|
||||
| `url` | Full URL for the qdrant server | `None` |
|
||||
| `api_key` | API key for the qdrant server | `None` |
|
||||
| `https` | Whether to force HTTPS on or off. `None` lets the client decide; set `False` for plain HTTP Qdrant with API key authentication. | `None` |
|
||||
| `on_disk` | For enabling persistent storage | `False` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
@@ -91,4 +90,4 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| `apiKey` | API key for the Qdrant server | `None` |
|
||||
| `onDisk` | For enabling persistent storage | `False` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
@@ -156,33 +156,6 @@ const memories = memory.search("food preferences", {
|
||||
On Mem0 Platform v3, time-aware queries use Temporal Reasoning internally while preserving the normal search response shape. See <Link href="/platform/features/temporal-reasoning">Temporal Reasoning</Link>.
|
||||
</Note>
|
||||
|
||||
### Explain OSS search scores
|
||||
|
||||
OSS search combines semantic similarity with optional keyword and entity signals. Pass `explain=True` when tuning retrieval quality or debugging why a memory ranked where it did:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
filters={"user_id": "alice"},
|
||||
explain=True,
|
||||
)
|
||||
|
||||
print(results["results"][0]["score_details"])
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const results = await memory.search("food preferences", {
|
||||
filters: { user_id: "alice" },
|
||||
explain: true,
|
||||
});
|
||||
|
||||
console.log(results.results[0].score_details);
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Each result includes `score_details` with the semantic score, normalized BM25 score, entity boost, raw combined score, maximum possible score, final score, and threshold used for filtering. The field is omitted unless `explain` is enabled, so existing response shapes stay unchanged.
|
||||
|
||||
## Filter patterns
|
||||
|
||||
Filters help narrow down search results. Common use cases:
|
||||
|
||||
+3
-7
@@ -143,8 +143,7 @@
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes",
|
||||
"integrations/pi-agent"
|
||||
"integrations/hermes"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -246,7 +245,6 @@
|
||||
"components/vectordbs/dbs/cassandra",
|
||||
"components/vectordbs/dbs/s3_vectors",
|
||||
"components/vectordbs/dbs/databricks",
|
||||
"components/vectordbs/dbs/neon",
|
||||
"components/vectordbs/dbs/neptune_analytics",
|
||||
"components/vectordbs/dbs/turbopuffer"
|
||||
]
|
||||
@@ -304,8 +302,7 @@
|
||||
"group": "Migration",
|
||||
"icon": "arrow-right",
|
||||
"pages": [
|
||||
"migration/oss-v2-to-v3",
|
||||
"migration/server-pgvector-upgrade"
|
||||
"migration/oss-v2-to-v3"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -465,8 +462,7 @@
|
||||
"icon": "robot",
|
||||
"pages": [
|
||||
"integrations/openclaw",
|
||||
"integrations/hermes",
|
||||
"integrations/pi-agent"
|
||||
"integrations/hermes"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -28,7 +28,7 @@ echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc && source ~/.bashrc
|
||||
|
||||
```bash
|
||||
# Install the plugin (MCP server, hooks, scripts)
|
||||
npx degit mem0ai/mem0/integrations/mem0-plugin ~/.gemini/config/plugins/mem0
|
||||
npx degit mem0ai/mem0/mem0-plugin ~/.gemini/config/plugins/mem0
|
||||
```
|
||||
|
||||
This installs the MCP server, lifecycle hooks, and shared scripts.
|
||||
|
||||
+191
-243
@@ -7,338 +7,285 @@ Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [Google ADK (Agent Dev
|
||||
|
||||
## Overview
|
||||
|
||||
In this guide, we'll create a Google ADK agent that:
|
||||
1. Uses ADK's native `MemoryService` interface to connect Mem0
|
||||
2. Automatically injects relevant memories using ADK's built-in `load_memory` tool
|
||||
3. Persists session history to Mem0 after each turn via an after-agent callback
|
||||
4. Shares memory seamlessly across multi-agent hierarchies
|
||||
1. Store and retrieve memories from Mem0 within Google ADK agents
|
||||
2. Multi-agent workflows with shared memory across hierarchies
|
||||
3. Retrieve relevant memories from past conversations
|
||||
4. Personalized responses based on user history
|
||||
|
||||
## Setup and Configuration
|
||||
## Prerequisites
|
||||
|
||||
Install the necessary libraries:
|
||||
Before setting up Mem0 with Google ADK, ensure you have:
|
||||
|
||||
1. Installed the required packages:
|
||||
```bash
|
||||
pip install google-adk mem0ai python-dotenv
|
||||
```
|
||||
|
||||
Set up your API keys:
|
||||
2. Valid API keys:
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-google-ai-adk" rel="nofollow">Mem0 API Key</a>
|
||||
- Google AI Studio API Key
|
||||
|
||||
<Note>Remember to get your API key from <a href="https://app.mem0.ai" rel="nofollow">Mem0 Platform</a> and set up a [Google AI Studio API Key](https://aistudio.google.com/apikey).</Note>
|
||||
## Basic Integration Example
|
||||
|
||||
The following example demonstrates how to create a Google ADK agent with Mem0 memory integration:
|
||||
|
||||
```python
|
||||
import os
|
||||
import asyncio
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.genai import types
|
||||
from mem0 import MemoryClient
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Set up environment variables
|
||||
# os.environ["GOOGLE_API_KEY"] = "your-google-api-key"
|
||||
# os.environ["MEM0_API_KEY"] = "your-mem0-api-key"
|
||||
```
|
||||
|
||||
## Implement Mem0MemoryService
|
||||
# Initialize Mem0 client
|
||||
mem0 = MemoryClient()
|
||||
|
||||
Create a custom `MemoryService` by implementing ADK's `BaseMemoryService`. Save the following as **`mem0_memory_service.py`**:
|
||||
# Define memory function tools
|
||||
def search_memory(query: str, user_id: str) -> dict:
|
||||
"""Search through past conversations and memories"""
|
||||
# For Platform API, user_id goes in filters
|
||||
filters = {"user_id": user_id}
|
||||
memories = mem0.search(query, filters=filters)
|
||||
if memories.get('results', []):
|
||||
memory_list = memories['results']
|
||||
memory_context = "\n".join([f"- {mem['memory']}" for mem in memory_list])
|
||||
return {"status": "success", "memories": memory_context}
|
||||
return {"status": "no_memories", "message": "No relevant memories found"}
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Optional
|
||||
from typing_extensions import override
|
||||
|
||||
from google.adk.memory.base_memory_service import BaseMemoryService, SearchMemoryResponse
|
||||
from google.adk.memory.memory_entry import MemoryEntry
|
||||
from google.adk.sessions import Session
|
||||
from google.genai.types import Content, Part
|
||||
from mem0 import MemoryClient
|
||||
|
||||
|
||||
class Mem0MemoryService(BaseMemoryService):
|
||||
"""MemoryService implementation backed by the Mem0 Platform."""
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
super().__init__()
|
||||
api_key = api_key or os.environ.get("MEM0_API_KEY")
|
||||
self._client: Optional[MemoryClient] = MemoryClient(api_key=api_key) if api_key else None
|
||||
|
||||
@override
|
||||
async def search_memory(
|
||||
self, *, app_name: str, user_id: str, query: str
|
||||
) -> SearchMemoryResponse:
|
||||
"""Search for memories relevant to the current user and query."""
|
||||
if not self._client:
|
||||
return SearchMemoryResponse(memories=[])
|
||||
|
||||
try:
|
||||
results = await asyncio.to_thread(
|
||||
self._client.search,
|
||||
query,
|
||||
filters={"AND": [{"user_id": user_id}, {"app_id": app_name}]},
|
||||
top_k=5,
|
||||
)
|
||||
|
||||
entries = []
|
||||
for mem in results.get("results", []):
|
||||
text = mem.get("memory", "")
|
||||
if not text:
|
||||
continue
|
||||
|
||||
raw_ts = mem.get("created_at") or mem.get("updated_at")
|
||||
entries.append(
|
||||
MemoryEntry(
|
||||
content=Content(parts=[Part(text=text)]),
|
||||
author=mem.get("metadata", {}).get("author", "user"),
|
||||
timestamp=str(raw_ts) if raw_ts else None,
|
||||
)
|
||||
)
|
||||
|
||||
return SearchMemoryResponse(memories=entries)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Mem0MemoryService] search_memory error: {e}")
|
||||
return SearchMemoryResponse(memories=[])
|
||||
|
||||
@override
|
||||
async def add_session_to_memory(self, session: Session) -> None:
|
||||
"""Persist a completed ADK session into Mem0."""
|
||||
if not self._client:
|
||||
return
|
||||
|
||||
user_id = session.user_id
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
app_name = getattr(session, "app_name", None)
|
||||
|
||||
try:
|
||||
messages = []
|
||||
for event in session.events:
|
||||
if not (event.content and event.content.parts):
|
||||
continue
|
||||
role = getattr(event.content, "role", None) or "user"
|
||||
if role == "model":
|
||||
role = "assistant"
|
||||
elif role not in ("user", "assistant"):
|
||||
continue
|
||||
text_parts = [
|
||||
p.text for p in event.content.parts if hasattr(p, "text") and p.text
|
||||
]
|
||||
if text_parts:
|
||||
messages.append({"role": role, "content": " ".join(text_parts)})
|
||||
|
||||
if messages:
|
||||
metadata = {"app_id": app_name} if app_name else {}
|
||||
await asyncio.to_thread(
|
||||
self._client.add, messages, user_id=user_id, metadata=metadata
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"[Mem0MemoryService] add_session_to_memory error: {e}")
|
||||
```
|
||||
|
||||
## Add Auto-Save Callback
|
||||
|
||||
This after-agent callback fires at the end of every turn and saves the session to Mem0. Save as **`memory_callbacks.py`**:
|
||||
|
||||
```python
|
||||
async def save_session_to_memory(callback_context) -> None:
|
||||
"""Persist the completed session to Mem0 after each agent turn."""
|
||||
def save_memory(content: str, user_id: str) -> dict:
|
||||
"""Save important information to memory"""
|
||||
try:
|
||||
await callback_context.add_session_to_memory()
|
||||
except ValueError:
|
||||
pass
|
||||
result = mem0.add([{"role": "user", "content": content}], user_id=user_id)
|
||||
return {"status": "success", "message": "Information saved to memory", "result": result}
|
||||
except Exception as e:
|
||||
print(f"[save_session_to_memory] error: {e}")
|
||||
```
|
||||
return {"status": "error", "message": f"Failed to save memory: {str(e)}"}
|
||||
|
||||
## Basic Integration Example
|
||||
|
||||
The following example demonstrates creating an ADK agent with automatic Mem0 memory:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.tools import load_memory
|
||||
from google.genai.types import Content, Part
|
||||
|
||||
from mem0_memory_service import Mem0MemoryService
|
||||
from memory_callbacks import save_session_to_memory
|
||||
|
||||
memory_service = Mem0MemoryService()
|
||||
session_service = InMemorySessionService()
|
||||
|
||||
agent = LlmAgent(
|
||||
# Create agent with memory capabilities
|
||||
personal_assistant = Agent(
|
||||
name="personal_assistant",
|
||||
model="gemini-2.0-flash",
|
||||
instruction="""You are a helpful personal assistant.
|
||||
Relevant memories from past conversations are provided to you automatically.
|
||||
Use them to personalize your responses.""",
|
||||
instruction="""You are a helpful personal assistant with memory capabilities.
|
||||
Use the search_memory function to recall past conversations and user preferences.
|
||||
Use the save_memory function to store important information about the user.
|
||||
Always personalize your responses based on available memory.""",
|
||||
description="A personal assistant that remembers user preferences and past interactions",
|
||||
tools=[load_memory],
|
||||
after_agent_callback=save_session_to_memory,
|
||||
tools=[search_memory, save_memory]
|
||||
)
|
||||
|
||||
runner = Runner(
|
||||
agent=agent,
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
app_name="memory_assistant",
|
||||
)
|
||||
async def chat_with_agent(user_input: str, user_id: str) -> str:
|
||||
"""
|
||||
Handle user input with automatic memory integration.
|
||||
|
||||
Args:
|
||||
user_input: The user's message
|
||||
user_id: Unique identifier for the user
|
||||
|
||||
async def chat(user_input: str, user_id: str) -> str:
|
||||
Returns:
|
||||
The agent's response
|
||||
"""
|
||||
# Set up session and runner
|
||||
session_service = InMemorySessionService()
|
||||
session = await session_service.create_session(
|
||||
app_name="memory_assistant",
|
||||
user_id=user_id,
|
||||
session_id=f"session_{user_id}"
|
||||
)
|
||||
content = Content(role="user", parts=[Part(text=user_input)])
|
||||
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
|
||||
if event.is_final_response() and event.content and event.content.parts:
|
||||
return event.content.parts[0].text
|
||||
runner = Runner(agent=personal_assistant, app_name="memory_assistant", session_service=session_service)
|
||||
|
||||
# Create content and run agent
|
||||
content = types.Content(role='user', parts=[types.Part(text=user_input)])
|
||||
events = runner.run(user_id=user_id, session_id=session.id, new_message=content)
|
||||
|
||||
# Extract final response
|
||||
for event in events:
|
||||
if event.is_final_response():
|
||||
response = event.content.parts[0].text
|
||||
|
||||
return response
|
||||
|
||||
return "No response generated"
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
print(asyncio.run(chat(
|
||||
response = asyncio.run(chat_with_agent(
|
||||
"I love Italian food and I'm planning a trip to Rome next month",
|
||||
user_id="alice",
|
||||
)))
|
||||
|
||||
print(asyncio.run(chat(
|
||||
"Any food recommendations for my trip?",
|
||||
user_id="alice",
|
||||
)))
|
||||
user_id="alice"
|
||||
))
|
||||
print(response)
|
||||
```
|
||||
|
||||
## Multi-Agent Hierarchy with Shared Memory
|
||||
|
||||
Because `memory_service` is passed to the `Runner`, every agent in the hierarchy shares the same memory automatically. Only the root coordinator needs the auto-save callback — ADK fires it once when the full turn completes:
|
||||
Create specialized agents in a hierarchy that share memory:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from google.adk.agents import LlmAgent
|
||||
from google.adk.runners import Runner
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.tools.agent_tool import AgentTool
|
||||
from google.adk.tools import load_memory
|
||||
from google.genai.types import Content, Part
|
||||
|
||||
from mem0_memory_service import Mem0MemoryService
|
||||
from memory_callbacks import save_session_to_memory
|
||||
|
||||
memory_service = Mem0MemoryService()
|
||||
session_service = InMemorySessionService()
|
||||
|
||||
travel_agent = LlmAgent(
|
||||
# Travel specialist agent
|
||||
travel_agent = Agent(
|
||||
name="travel_specialist",
|
||||
model="gemini-2.0-flash",
|
||||
instruction="""You are a travel planning specialist.
|
||||
Relevant memories about the user's travel preferences are provided automatically.
|
||||
Use them to make personalized recommendations.""",
|
||||
instruction="""You are a travel planning specialist. Use search_memory to
|
||||
understand the user's travel preferences and history before making recommendations.
|
||||
After providing advice, use save_memory to save travel-related information.""",
|
||||
description="Specialist in travel planning and recommendations",
|
||||
tools=[load_memory],
|
||||
tools=[search_memory, save_memory]
|
||||
)
|
||||
|
||||
health_agent = LlmAgent(
|
||||
# Health advisor agent
|
||||
health_agent = Agent(
|
||||
name="health_advisor",
|
||||
model="gemini-2.0-flash",
|
||||
instruction="""You are a health and wellness advisor.
|
||||
Relevant memories about the user's health goals are provided automatically.
|
||||
Use them to give personalized advice.""",
|
||||
instruction="""You are a health and wellness advisor. Use search_memory to
|
||||
understand the user's health goals and dietary preferences.
|
||||
After providing advice, use save_memory to save health-related information.""",
|
||||
description="Specialist in health and wellness advice",
|
||||
tools=[load_memory],
|
||||
tools=[search_memory, save_memory]
|
||||
)
|
||||
|
||||
coordinator = LlmAgent(
|
||||
# Coordinator agent that delegates to specialists
|
||||
coordinator_agent = Agent(
|
||||
name="coordinator",
|
||||
model="gemini-2.0-flash",
|
||||
instruction="""You are a coordinator that delegates requests to specialist agents.
|
||||
For travel-related questions, delegate to the travel specialist.
|
||||
For health-related questions, delegate to the health advisor.
|
||||
Relevant memories about the user are provided automatically.""",
|
||||
For travel-related questions (trips, hotels, flights, destinations), delegate to the travel specialist.
|
||||
For health-related questions (fitness, diet, wellness, exercise), delegate to the health advisor.
|
||||
Use search_memory to understand the user before delegation.""",
|
||||
description="Coordinates requests between specialist agents",
|
||||
tools=[
|
||||
load_memory,
|
||||
AgentTool(agent=travel_agent, skip_summarization=False),
|
||||
AgentTool(agent=health_agent, skip_summarization=False),
|
||||
],
|
||||
after_agent_callback=save_session_to_memory,
|
||||
AgentTool(agent=health_agent, skip_summarization=False)
|
||||
]
|
||||
)
|
||||
|
||||
runner = Runner(
|
||||
agent=coordinator,
|
||||
session_service=session_service,
|
||||
memory_service=memory_service,
|
||||
app_name="specialist_system",
|
||||
)
|
||||
def chat_with_specialists(user_input: str, user_id: str) -> str:
|
||||
"""
|
||||
Handle user input with specialist agent delegation and memory.
|
||||
|
||||
Args:
|
||||
user_input: The user's message
|
||||
user_id: Unique identifier for the user
|
||||
|
||||
async def chat_with_specialists(user_input: str, user_id: str) -> str:
|
||||
session = await session_service.create_session(
|
||||
Returns:
|
||||
The specialist agent's response
|
||||
"""
|
||||
session_service = InMemorySessionService()
|
||||
session = session_service.create_session(
|
||||
app_name="specialist_system",
|
||||
user_id=user_id,
|
||||
session_id=f"session_{user_id}"
|
||||
)
|
||||
content = Content(role="user", parts=[Part(text=user_input)])
|
||||
async for event in runner.run_async(user_id=user_id, session_id=session.id, new_message=content):
|
||||
if event.is_final_response() and event.content and event.content.parts:
|
||||
return event.content.parts[0].text
|
||||
runner = Runner(agent=coordinator_agent, app_name="specialist_system", session_service=session_service)
|
||||
|
||||
content = types.Content(role='user', parts=[types.Part(text=user_input)])
|
||||
events = runner.run(user_id=user_id, session_id=session.id, new_message=content)
|
||||
|
||||
for event in events:
|
||||
if event.is_final_response():
|
||||
response = event.content.parts[0].text
|
||||
|
||||
# Store the conversation in shared memory
|
||||
conversation = [
|
||||
{"role": "user", "content": user_input},
|
||||
{"role": "assistant", "content": response}
|
||||
]
|
||||
mem0.add(conversation, user_id=user_id)
|
||||
|
||||
return response
|
||||
|
||||
return "No response generated"
|
||||
|
||||
# Example usage
|
||||
response = chat_with_specialists("Plan a healthy meal for my Italy trip", user_id="alice")
|
||||
print(response)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Quick Start Chat Interface
|
||||
|
||||
Simple interactive chat with memory and Google ADK:
|
||||
|
||||
```python
|
||||
def interactive_chat():
|
||||
"""Interactive chat interface with memory and ADK"""
|
||||
user_id = input("Enter your user ID: ") or "demo_user"
|
||||
print(f"Chat started for user: {user_id}")
|
||||
print("Type 'quit' to exit")
|
||||
print("=" * 50)
|
||||
|
||||
while True:
|
||||
user_input = input("\nYou: ")
|
||||
|
||||
if user_input.lower() == 'quit':
|
||||
print("Goodbye! Your conversation has been saved to memory.")
|
||||
break
|
||||
else:
|
||||
response = chat_with_specialists(user_input, user_id)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
response = asyncio.run(chat_with_specialists("Plan a healthy meal for my Italy trip", user_id="alice"))
|
||||
print(response)
|
||||
interactive_chat()
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Automatic Memory Injection**: ADK's built-in `load_memory` tool searches Mem0 at the start of each turn and injects relevant memories directly into the agent context — no prompt instructions needed.
|
||||
2. **Automatic Session Saving**: The `save_session_to_memory` callback persists every completed turn to Mem0 without any manual calls.
|
||||
3. **Native ADK Integration**: `Mem0MemoryService` implements ADK's `BaseMemoryService` and integrates via the `Runner` — works natively across the entire agent hierarchy.
|
||||
4. **User Scoping**: `user_id` is passed automatically from the ADK session context, ensuring memories are always scoped to the correct user.
|
||||
5. **Multi-Agent Support**: A single `Mem0MemoryService` instance shared through the `Runner` gives all agents — coordinators and specialists — access to the same user memory.
|
||||
### 1. Memory-Enhanced Function Tools
|
||||
- **Function Tools**: Standard Python functions that can search and save memories
|
||||
- **Tool Context**: Access to session state and memory through function parameters
|
||||
- **Structured Returns**: Dictionary-based returns with status indicators for better LLM understanding
|
||||
|
||||
### 2. Multi-Agent Memory Sharing
|
||||
- **Agent-as-a-Tool**: Specialists can be called as tools while maintaining shared memory
|
||||
- **Hierarchical Delegation**: Coordinator agents route to specialists based on context
|
||||
- **Memory Categories**: Store interactions with metadata for better organization
|
||||
|
||||
### 3. Flexible Memory Operations
|
||||
- **Search Capabilities**: Retrieve relevant memories through conversation history
|
||||
- **User Segmentation**: Organize memories by user ID
|
||||
- **Memory Management**: Built-in tools for saving and retrieving information
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Using Vertex AI
|
||||
|
||||
To use Google Cloud Vertex AI instead of AI Studio, set the following environment variables before creating agents:
|
||||
Customize memory behavior and agent setup:
|
||||
|
||||
```python
|
||||
import os
|
||||
# Configure memory search with filters
|
||||
# For Platform API, all filters including user_id go in filters object
|
||||
memories = mem0.search(
|
||||
query="travel preferences",
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": "alice"},
|
||||
{"categories": {"contains": "travel"}}
|
||||
]
|
||||
},
|
||||
top_k=5
|
||||
)
|
||||
|
||||
# Configure agent with custom model settings
|
||||
agent = Agent(
|
||||
name="custom_agent",
|
||||
model="gemini-2.0-flash", # or use LiteLLM for other models
|
||||
instruction="Custom agent behavior",
|
||||
tools=[memory_tools],
|
||||
# Additional ADK configurations
|
||||
)
|
||||
|
||||
# Use Google Cloud Vertex AI instead of AI Studio
|
||||
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"
|
||||
os.environ["GOOGLE_CLOUD_PROJECT"] = "your-project-id"
|
||||
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
|
||||
```
|
||||
|
||||
### Advanced Memory Filtering
|
||||
|
||||
You can customize how memories are searched by modifying `Mem0MemoryService.search_memory`. For example, to filter by category:
|
||||
|
||||
```python
|
||||
results = await asyncio.to_thread(
|
||||
self._client.search,
|
||||
query,
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"app_id": app_name},
|
||||
{"categories": {"contains": "travel"}}
|
||||
]
|
||||
},
|
||||
top_k=10,
|
||||
)
|
||||
```
|
||||
|
||||
<Note>`InMemorySessionService` stores sessions in memory and is intended for prototyping. For production, use a persistent session service and clean up sessions when they are no longer needed.</Note>
|
||||
|
||||
## Conclusion
|
||||
|
||||
By implementing `Mem0MemoryService` as an ADK `BaseMemoryService`, you get persistent, user-scoped memory across single agents and complex multi-agent hierarchies with minimal code. Memory injection and session saving happen automatically, keeping your agent prompts clean and your token usage efficient.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Healthcare Agent Cookbook" icon="heart-pulse" href="/cookbooks/integrations/healthcare-google-adk">
|
||||
Build HIPAA-compliant healthcare agents with Google ADK
|
||||
@@ -347,3 +294,4 @@ By implementing `Mem0MemoryService` as an ADK `BaseMemoryService`, you get persi
|
||||
Compare with OpenAI's agent framework
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ bunx @mem0/opencode-plugin@latest install
|
||||
**Or let your agent do it** — paste this into OpenCode:
|
||||
|
||||
```
|
||||
Install @mem0/opencode-plugin by following https://raw.githubusercontent.com/mem0ai/mem0/main/integrations/mem0-plugin/.opencode-plugin/README.md
|
||||
Install @mem0/opencode-plugin by following https://raw.githubusercontent.com/mem0ai/mem0/main/mem0-plugin/.opencode-plugin/README.md
|
||||
```
|
||||
|
||||
All commands auto-add the plugin and MCP server to your `~/.config/opencode/opencode.json`. Restart OpenCode — you get the MCP server, lifecycle hooks, and all `/mem0:` slash commands.
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
---
|
||||
title: Pi Agent
|
||||
description: "Add persistent memory to Pi Agent with the Mem0 plugin semantic search, auto-capture, and dream consolidation."
|
||||
---
|
||||
|
||||
Add persistent memory to [**Pi Agent**](https://pi.dev) with `@mem0/pi-agent-plugin`. Your agent forgets everything between sessions — this plugin fixes that by automatically capturing knowledge from conversations, storing it in Mem0's cloud memory layer, and retrieving relevant context before every response.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin provides:
|
||||
1. **Auto-capture** — Extracts durable facts from both user and assistant messages automatically
|
||||
2. **Semantic recall** — Retrieves relevant memories via the `mem0_memory` tool before each response
|
||||
3. **Dream consolidation** — Periodic maintenance: merges duplicates, resolves contradictions, prunes stale entries
|
||||
4. **Monorepo-aware scoping** — Uses git root for project detection, consistent across subdirectories
|
||||
5. **Confirmation dialogs** — Destructive commands ask before acting via Pi's built-in UI
|
||||
6. **8 skills + 8 commands** — Essential memory management from slash commands and agent-guided workflows
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
- <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-pi-agent" rel="nofollow">Sign up at app.mem0.ai</a>
|
||||
- <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-pi-agent" rel="nofollow">Get your API key</a> (starts with `m0-`)
|
||||
|
||||
2. Pi Agent installed ([pi.dev](https://pi.dev))
|
||||
|
||||
3. Your API key added to your shell profile:
|
||||
|
||||
<CodeGroup>
|
||||
```bash zsh
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
```bash bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pi install npm:@mem0/pi-agent-plugin
|
||||
```
|
||||
|
||||
That's it. The extension loads automatically on every Pi session. No config files needed — `MEM0_API_KEY` from your environment is picked up automatically.
|
||||
|
||||
<Info>
|
||||
Start a new Pi session and run `/mem0-status` to verify the connection. You should see your user ID, detected project, and memory count.
|
||||
</Info>
|
||||
|
||||
### Optional Configuration
|
||||
|
||||
For advanced settings, create `~/.pi/agent/mem0-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "m0-your-key-here",
|
||||
"userId": "your-username",
|
||||
"autoCapture": true,
|
||||
"defaultScope": "project",
|
||||
"searchThreshold": 0.3,
|
||||
"dream": {
|
||||
"enabled": true,
|
||||
"auto": true,
|
||||
"minHours": 24,
|
||||
"minSessions": 5,
|
||||
"minMemories": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `apiKey` | `string` | `$MEM0_API_KEY` | Mem0 API key. Environment variable takes precedence. |
|
||||
| `userId` | `string` | `$MEM0_USER_ID` or `"default"` | User identity for memory scoping |
|
||||
| `autoCapture` | `boolean` | `true` | Store facts from conversations automatically |
|
||||
| `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` |
|
||||
| `searchThreshold` | `number` | `0.3` | Minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search`, `/mem0-forget`, and `/mem0-pin`, enforced on each result's relevance score. Raise it to be stricter; lower it if relevant results are missed. |
|
||||
| `dream.enabled` | `boolean` | `true` | Enable dream consolidation |
|
||||
| `dream.auto` | `boolean` | `true` | Auto-trigger dreams when thresholds are met |
|
||||
| `dream.minHours` | `number` | `24` | Minimum hours between auto-dreams |
|
||||
| `dream.minSessions` | `number` | `5` | Minimum sessions before first auto-dream |
|
||||
| `dream.minMemories` | `number` | `20` | Minimum memories before auto-dream triggers |
|
||||
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| `mem0_memory` tool | Agent-callable tool for search, add, get_all, delete, delete_all |
|
||||
| 8 slash commands | Essential memory management from the command line |
|
||||
| 8 skills | Guide the agent on how to use each capability |
|
||||
| Auto-capture | Extracts and stores facts on every `agent_end` event |
|
||||
| System prompt | Appends memory policy to every agent turn |
|
||||
| Dream consolidation | Automated memory maintenance with session/time/count gates |
|
||||
|
||||
## Agent Tool
|
||||
|
||||
The `mem0_memory` tool is registered with Pi and callable by the agent during conversations:
|
||||
|
||||
| Action | Required Params | Description |
|
||||
|--------|----------------|-------------|
|
||||
| `search` | `query` | Semantic search across memories |
|
||||
| `add` | `content` | Store a new memory |
|
||||
| `get_all` | — | List all memories in scope |
|
||||
| `delete` | `memory_id` | Delete a specific memory |
|
||||
| `delete_all` | — | Delete all memories in scope |
|
||||
|
||||
All actions accept an optional `scope` parameter: `project` (default), `session`, or `global`.
|
||||
|
||||
Tool output is truncated to 200 lines / 50KB to prevent context overflow.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/mem0-remember <text>` | Store a memory verbatim (no inference) |
|
||||
| `/mem0-forget <query>` | Search and delete memories (with confirmation dialog) |
|
||||
| `/mem0-search <query>` | Semantic search across memories |
|
||||
| `/mem0-tour [scope]` | Browse all memories grouped by category |
|
||||
| `/mem0-dream` | Consolidate — merge duplicates, prune stale, resolve contradictions |
|
||||
| `/mem0-pin <query>` | Pin a memory to protect from dream pruning (preserves memory ID) |
|
||||
| `/mem0-scope <scope>` | Change default scope for this session (project, session, global) |
|
||||
| `/mem0-status` | Connection health, identity, and memory count |
|
||||
|
||||
## Memory Scopes
|
||||
|
||||
Memories are scoped using Mem0's `user_id`, `app_id`, and `run_id` parameters:
|
||||
|
||||
| Scope | Filters | Use Case |
|
||||
|-------|---------|----------|
|
||||
| `project` | user_id + app_id (git root) | **Default.** Project-specific knowledge — decisions, architecture, config |
|
||||
| `session` | user_id + app_id + run_id | Ephemeral context for the current session only |
|
||||
| `global` | user_id only | All memories across all your projects |
|
||||
|
||||
The `app_id` is auto-detected from the git repository root (`git rev-parse --show-toplevel`), so all subdirectories within a monorepo share the same memory pool. Falls back to the working directory name for non-git directories. The `run_id` is derived from Pi's session file path.
|
||||
|
||||
## Dream Consolidation
|
||||
|
||||
### Confirmation Dialogs
|
||||
|
||||
Destructive and mutating commands use Pi's built-in `ctx.ui.confirm()` dialog before acting:
|
||||
|
||||
- `/mem0-forget` asks "Delete this memory?" before deleting a single match
|
||||
- `/mem0-pin` asks "Pin this memory?" before modifying it
|
||||
- Cancelling either operation is always safe — no changes are made
|
||||
|
||||
### Pin
|
||||
|
||||
`/mem0-pin` uses Mem0's `update()` API to prepend `[PINNED]` to the memory text. This preserves the original memory ID — no add+delete cycle that would lose history or change the UUID.
|
||||
|
||||
### Dream Consolidation
|
||||
|
||||
The plugin includes automated memory maintenance ("dream") that merges duplicates, resolves contradictions, and prunes stale entries. When enabled, dreams auto-trigger after enough sessions, time, and memories accumulate (configurable via `dream.*` settings). Run `/mem0-dream` to trigger consolidation manually at any time. Pinned memories (via `/mem0-pin`) are protected from pruning.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
# Session 1
|
||||
You: I prefer dark mode and concise answers.
|
||||
# Mem0 auto-captures preferences
|
||||
|
||||
# Session 2 (days later)
|
||||
You: What do you know about my preferences?
|
||||
# Pi retrieves stored memories — no re-explaining needed
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"No API key found"** — Verify `MEM0_API_KEY` is set: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites)
|
||||
- **Extension not loading** — Check Pi startup output for errors. Run `pi -e ./src/entry.ts` from the plugin directory for verbose output
|
||||
- **Memories not capturing** — Verify `autoCapture` is `true` (default). Check `/mem0-status` for connection health
|
||||
- **Wrong project detected** — The plugin uses the git repository root as `app_id`. If not in a git repo, it falls back to the working directory name. Run `/mem0-status` to see the detected project
|
||||
- **Dream not triggering** — All three gates must pass (time, sessions, memories). Use `/mem0-dream` to force it manually
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Claude Code Integration" icon="terminal" href="/integrations/claude-code">
|
||||
Add Mem0 memory to Claude Code
|
||||
</Card>
|
||||
<Card title="OpenClaw Integration" icon="plug" href="/integrations/openclaw">
|
||||
Add Mem0 memory to OpenClaw agents
|
||||
</Card>
|
||||
</CardGroup>
|
||||
+116
-151
@@ -6,33 +6,25 @@ description: "Use the Mem0 AI SDK Provider with Vercel AI SDK for persistent mem
|
||||
The [**Mem0 AI SDK Provider**](https://www.npmjs.com/package/@mem0/vercel-ai-provider) is a library developed by **Mem0** to integrate with the Vercel AI SDK. This library brings enhanced AI interaction capabilities to your applications by introducing persistent memory functionality.
|
||||
|
||||
<Note type="info">
|
||||
Mem0 AI SDK Provider v3.0.0 supports <strong>Vercel AI SDK v6</strong> (<code>LanguageModelV3</code> / <code>ProviderV3</code>). If you are upgrading from v2.x, see the <a href="https://ai-sdk.dev/docs/migration-guides/migration-guide-6-0">AI SDK v6 migration guide</a>.
|
||||
Mem0 AI SDK now supports <strong>Vercel AI SDK V5</strong>.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
1. Offers persistent memory storage for conversational AI
|
||||
2. Enables smooth integration with the Vercel AI SDK v6
|
||||
3. Ensures compatibility with multiple LLM providers (OpenAI, Anthropic, Google, Groq, Cohere)
|
||||
2. Enables smooth integration with the Vercel AI SDK
|
||||
3. Ensures compatibility with multiple LLM providers
|
||||
4. Supports structured message formats for clarity
|
||||
5. Facilitates streaming response capabilities
|
||||
6. Attaches Mem0 memories as sources in responses for programmatic access
|
||||
|
||||
## Setup and Configuration
|
||||
|
||||
Install the SDK provider and AI SDK:
|
||||
Install the SDK provider using npm:
|
||||
|
||||
```bash
|
||||
npm install @mem0/vercel-ai-provider ai@^6
|
||||
npm install @mem0/vercel-ai-provider
|
||||
```
|
||||
|
||||
### Peer Dependencies
|
||||
|
||||
`@mem0/vercel-ai-provider` v3.0.0 requires:
|
||||
- `ai` v6+ (`^6.0.199`)
|
||||
- `@ai-sdk/provider` v3+ (`^3.0.10`)
|
||||
- Provider packages at v3+: `@ai-sdk/openai@^3`, `@ai-sdk/anthropic@^3`, `@ai-sdk/google@^3`, `@ai-sdk/groq@^3`, `@ai-sdk/cohere@^3`
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setting Up Mem0
|
||||
@@ -49,7 +41,7 @@ npm install @mem0/vercel-ai-provider ai@^6
|
||||
mem0ApiKey: "m0-xxx",
|
||||
apiKey: "provider-api-key",
|
||||
config: {
|
||||
// Options for the upstream LLM provider (e.g. baseURL)
|
||||
// Options for LLM Provider
|
||||
},
|
||||
// Optional Mem0 Global Config
|
||||
mem0Config: {
|
||||
@@ -65,153 +57,154 @@ npm install @mem0/vercel-ai-provider ai@^6
|
||||
3. Add Memories to Enhance Context:
|
||||
|
||||
```typescript
|
||||
import { LanguageModelV2Prompt } from "@ai-sdk/provider";
|
||||
import { addMemories } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const messages = [
|
||||
const messages: LanguageModelV2Prompt = [
|
||||
{ role: "user", content: [{ type: "text", text: "I love red cars." }] },
|
||||
];
|
||||
|
||||
await addMemories(messages, { user_id: "borat" });
|
||||
```
|
||||
|
||||
### Standalone Features
|
||||
### Standalone Features:
|
||||
|
||||
```typescript
|
||||
await addMemories(messages, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
await getMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
```
|
||||
```typescript
|
||||
await addMemories(messages, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
await getMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
```
|
||||
> For standalone features, such as `addMemories`, `retrieveMemories`, and `getMemories`, you must either set `MEM0_API_KEY` as an environment variable or pass it directly in the function call.
|
||||
|
||||
> For standalone features, such as `addMemories`, `retrieveMemories`, and `getMemories`, you must either set `MEM0_API_KEY` as an environment variable or pass it directly in the function call.
|
||||
> `getMemories` will return raw memories in the form of an array of objects, while `retrieveMemories` will return a response in string format with a system prompt ingested with the retrieved memories.
|
||||
|
||||
> `getMemories` will return raw memories in the form of an array of objects, while `retrieveMemories` will return a response in string format with a system prompt ingested with the retrieved memories.
|
||||
> `getMemories` returns an array of memory objects.
|
||||
|
||||
### 1. Basic Text Generation with Memory Context
|
||||
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0();
|
||||
const mem0 = createMem0();
|
||||
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-5-mini", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
```
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Combining OpenAI Provider with Memory Utils
|
||||
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { retrieveMemories } from "@mem0/vercel-ai-provider";
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { retrieveMemories } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const prompt = "Suggest me a good car to buy.";
|
||||
const memories = await retrieveMemories(prompt, { user_id: "borat" });
|
||||
const prompt = "Suggest me a good car to buy.";
|
||||
const memories = await retrieveMemories(prompt, { user_id: "borat" });
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-5-mini"),
|
||||
prompt: prompt,
|
||||
system: memories,
|
||||
});
|
||||
```
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4-turbo"),
|
||||
prompt: prompt,
|
||||
system: memories,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Structured Message Format with Memory
|
||||
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0();
|
||||
const mem0 = createMem0();
|
||||
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-5-mini", { user_id: "borat" }),
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Suggest me a good car to buy." },
|
||||
{ type: "text", text: "Why is it better than the other cars for me?" },
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Suggest me a good car to buy." },
|
||||
{ type: "text", text: "Why is it better than the other cars for me?" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Streaming Responses with Memory Context
|
||||
### 3. Streaming Responses with Memory Context
|
||||
|
||||
```typescript
|
||||
import { streamText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
```typescript
|
||||
import { streamText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0();
|
||||
const mem0 = createMem0();
|
||||
|
||||
const { textStream } = streamText({
|
||||
model: mem0("gpt-5-mini", {
|
||||
user_id: "borat",
|
||||
}),
|
||||
prompt: "Suggest me a good car to buy! Why is it better than the other cars for me? Give options for every price range.",
|
||||
});
|
||||
const { textStream } = streamText({
|
||||
model: mem0("gpt-4-turbo", {
|
||||
user_id: "borat",
|
||||
}),
|
||||
prompt: "Suggest me a good car to buy! Why is it better than the other cars for me? Give options for every price range.",
|
||||
});
|
||||
|
||||
for await (const textPart of textStream) {
|
||||
process.stdout.write(textPart);
|
||||
}
|
||||
```
|
||||
for await (const textPart of textStream) {
|
||||
process.stdout.write(textPart);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Generate Responses with Tools Call
|
||||
### 4. Generate Responses with Tools Call
|
||||
|
||||
```typescript
|
||||
import { generateText, tool } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
import { z } from "zod";
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
import { z } from "zod";
|
||||
|
||||
const mem0 = createMem0({
|
||||
provider: "anthropic",
|
||||
apiKey: "anthropic-api-key",
|
||||
mem0Config: {
|
||||
user_id: "borat"
|
||||
}
|
||||
});
|
||||
const mem0 = createMem0({
|
||||
provider: "anthropic",
|
||||
apiKey: "anthropic-api-key",
|
||||
mem0Config: {
|
||||
// Global User ID
|
||||
user_id: "borat"
|
||||
}
|
||||
});
|
||||
|
||||
const result = await generateText({
|
||||
model: mem0('claude-sonnet-4-20250514'),
|
||||
tools: {
|
||||
weather: tool({
|
||||
description: 'Get the weather in a location',
|
||||
parameters: z.object({
|
||||
location: z.string().describe('The location to get the weather for'),
|
||||
}),
|
||||
execute: async ({ location }) => ({
|
||||
location,
|
||||
temperature: 72 + Math.floor(Math.random() * 21) - 10,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
prompt: "What the temperature in the city that I live in?",
|
||||
});
|
||||
const prompt = "What the temperature in the city that I live in?"
|
||||
|
||||
console.log(result);
|
||||
```
|
||||
const result = await generateText({
|
||||
model: mem0('claude-3-5-sonnet-20240620'),
|
||||
tools: {
|
||||
weather: tool({
|
||||
description: 'Get the weather in a location',
|
||||
parameters: z.object({
|
||||
location: z.string().describe('The location to get the weather for'),
|
||||
}),
|
||||
execute: async ({ location }) => ({
|
||||
location,
|
||||
temperature: 72 + Math.floor(Math.random() * 21) - 10,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
### 6. Get Sources from Memory
|
||||
console.log(result);
|
||||
```
|
||||
|
||||
`generateText` and `streamText` responses include Mem0 memories as a source, giving you programmatic access to the memories that influenced the response:
|
||||
### 5. Get sources from memory
|
||||
|
||||
```typescript
|
||||
const { text, sources } = await generateText({
|
||||
model: mem0("gpt-5-mini", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
model: mem0("gpt-4-turbo"),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
|
||||
// sources[0].title === "Mem0 Memories"
|
||||
// sources[0].providerMetadata.mem0.memories — array of memory objects
|
||||
console.log(sources);
|
||||
```
|
||||
|
||||
The same can be done for `streamText` as well.
|
||||
|
||||
### 7. File Support with Memory Context
|
||||
### 6. File Support with Memory Context
|
||||
|
||||
Mem0 AI SDK supports file processing with memory context. Here's an example of analyzing a PDF file:
|
||||
|
||||
@@ -233,11 +226,15 @@ const mem0 = createMem0({
|
||||
});
|
||||
|
||||
async function main() {
|
||||
// Read the PDF file
|
||||
const filePath = join(process.cwd(), 'my_pdf.pdf');
|
||||
const fileBuffer = readFileSync(filePath);
|
||||
|
||||
// Convert the file's arrayBuffer to a Base64 data URL
|
||||
const arrayBuffer = fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
// Convert Uint8Array to an array of characters
|
||||
const charArray = Array.from(uint8Array, byte => String.fromCharCode(byte));
|
||||
const binaryString = charArray.join('');
|
||||
const base64Data = Buffer.from(binaryString, 'binary').toString('base64');
|
||||
@@ -277,56 +274,24 @@ main();
|
||||
|
||||
| Provider | Configuration Value |
|
||||
|----------|-------------------|
|
||||
| OpenAI | `openai` |
|
||||
| Anthropic | `anthropic` |
|
||||
| Google / Gemini | `google` or `gemini` |
|
||||
| Groq | `groq` |
|
||||
| Cohere | `cohere` |
|
||||
| OpenAI | openai |
|
||||
| Anthropic | anthropic |
|
||||
| Google | google |
|
||||
| Groq | groq |
|
||||
|
||||
> **Note**: You can use either `google` or `gemini` as the provider value for Google Gemini models. Both map to the `@ai-sdk/google` package internally.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Mem0ConfigSettings
|
||||
|
||||
These options can be passed per-request when creating a model instance:
|
||||
|
||||
| Option | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `user_id` | `string` | User identifier for memory scoping |
|
||||
| `agent_id` | `string` | Agent identifier |
|
||||
| `app_id` | `string` | Application identifier |
|
||||
| `run_id` | `string` | Run/session identifier |
|
||||
| `metadata` | `object` | Custom metadata for memories |
|
||||
| `filters` | `object` | Filters for memory search |
|
||||
| `infer` | `boolean` | Enable inference-based retrieval |
|
||||
| `top_k` | `number` | Number of memories to retrieve (default: 10) |
|
||||
| `threshold` | `number` | Relevance threshold for search |
|
||||
| `rerank` | `boolean` | Enable reranking of results |
|
||||
| `page` | `number` | Page number for pagination |
|
||||
| `page_size` | `number` | Results per page |
|
||||
> **Note**: You can use `google` as provider for Gemini (Google) models. They are same and internally they use `@ai-sdk/google` package.
|
||||
|
||||
## Key Features
|
||||
|
||||
- `createMem0()`: Initializes a new Mem0 provider instance implementing `ProviderV3`.
|
||||
- `retrieveMemories()`: Retrieves memory context for prompts as a formatted system prompt string.
|
||||
- `createMem0()`: Initializes a new Mem0 provider instance.
|
||||
- `retrieveMemories()`: Retrieves memory context for prompts.
|
||||
- `getMemories()`: Get memories from your profile in array format.
|
||||
- `addMemories()`: Adds user memories to enhance contextual responses.
|
||||
|
||||
## Migrating from v2.x
|
||||
|
||||
If you're upgrading from `@mem0/vercel-ai-provider` v2.x:
|
||||
|
||||
1. **Upgrade AI SDK**: `npm install ai@^6` and update all `@ai-sdk/*` provider packages to `^3.x`
|
||||
2. **Remove deprecated params**: Remove `org_id`, `project_id`, `output_format`, `filter_memories`, `async_mode`, `enable_graph` from your config
|
||||
3. **Remove graph memory**: All graph-related options (`enable_graph`, graph prompts) have been removed. Graph memory is now a project-level setting on the Mem0 Platform
|
||||
4. **Update imports**: `LanguageModelV2Prompt` is now `LanguageModelV3Prompt` if you import types directly
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **User Identification**: Use a unique `user_id` for consistent memory retrieval.
|
||||
2. **Memory Cleanup**: Regularly clean up unused memory data.
|
||||
3. **Sources**: Access `result.sources` to inspect which memories influenced the response.
|
||||
|
||||
> **Note**: We also have support for `agent_id`, `app_id`, and `run_id`. Refer [Docs](/api-reference/memory/add-memories).
|
||||
|
||||
|
||||
+2
-5
@@ -228,7 +228,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
|
||||
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
|
||||
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
|
||||
- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
|
||||
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
|
||||
|
||||
## Open Source
|
||||
@@ -259,7 +258,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [Camel AI](https://docs.mem0.ai/integrations/camel-ai) [Both]: Use when the user is on Camel AI.
|
||||
- [ChatDev](https://docs.mem0.ai/integrations/chatdev) [Both]: Use when the user is on ChatDev.
|
||||
- [Hermes](https://docs.mem0.ai/integrations/hermes) [Both]: Use when the user is on Hermes.
|
||||
- [Pi Agent](https://docs.mem0.ai/integrations/pi-agent) [Platform]: Use when adding persistent memory to Pi Agent with the Mem0 plugin.
|
||||
- [OpenAI Agents SDK](https://docs.mem0.ai/integrations/openai-agents-sdk) [Both]: Use when the user is on the OpenAI Agents SDK.
|
||||
- [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) [Both]: Use when the user is on Google's Agent Development Kit.
|
||||
- [Mastra](https://docs.mem0.ai/integrations/mastra) [Both]: Use when the user is on Mastra (TypeScript).
|
||||
@@ -398,9 +396,9 @@ Each subdirectory is a Claude Code Skill (`SKILL.md` + supporting assets). Load
|
||||
|
||||
### Editor Plugin (shared glue)
|
||||
|
||||
Source: https://github.com/mem0ai/mem0/tree/main/integrations/mem0-plugin
|
||||
Source: https://github.com/mem0ai/mem0/tree/main/mem0-plugin
|
||||
|
||||
The `integrations/mem0-plugin/` directory provides MCP server connection, lifecycle hooks, and skill bundling for Claude Code, Cursor, Codex, OpenCode, and Antigravity. It exposes 9 MCP tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`.
|
||||
The `mem0-plugin/` directory provides MCP server connection, lifecycle hooks, and skill bundling for Claude Code, Cursor, Codex, OpenCode, and Antigravity. It exposes 9 MCP tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`.
|
||||
|
||||
Editor-specific setup docs (already listed above under `## Integrations > AI Coding Tools`):
|
||||
|
||||
@@ -476,7 +474,6 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
|
||||
- [Elasticsearch](https://docs.mem0.ai/components/vectordbs/dbs/elasticsearch) [OSS]: Use when Elasticsearch is the backing store.
|
||||
- [OpenSearch](https://docs.mem0.ai/components/vectordbs/dbs/opensearch) [OSS]: Use when OpenSearch is the backing store.
|
||||
- [Supabase](https://docs.mem0.ai/components/vectordbs/dbs/supabase) [OSS]: Use when Supabase with pgvector is the backing store.
|
||||
- [Neon](https://docs.mem0.ai/components/vectordbs/dbs/neon) [OSS]: Use when Neon Postgres with pgvector is the backing store.
|
||||
- [Upstash Vector](https://docs.mem0.ai/components/vectordbs/dbs/upstash-vector) [OSS]: Use for serverless Upstash Vector.
|
||||
- [Vectorize](https://docs.mem0.ai/components/vectordbs/dbs/vectorize) [OSS]: Use when the store is Cloudflare Vectorize.
|
||||
- [Vertex AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/vertex_ai) [OSS]: Use when the store is Google Cloud Vertex Vector Search.
|
||||
|
||||
@@ -6,15 +6,17 @@ versionFrom: "Open Source"
|
||||
versionTo: "Platform"
|
||||
---
|
||||
|
||||
## Overview
|
||||
# Migrate from Open Source to Platform
|
||||
|
||||
Move your Mem0 implementation to managed infrastructure with enterprise features.
|
||||
|
||||
| Scope | Effort | Downtime |
|
||||
| --------------------- | -------------- | ---------------------------- |
|
||||
| Infrastructure & Code | Low (~30 mins) | None (Parallel run possible) |
|
||||
|
||||
<Note>
|
||||
<Info>
|
||||
Using Mem0 Open Source with **hosted Qdrant**? You can migrate your existing memories to Mem0 Platform with a one-line script below.
|
||||
</Note>
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
**Why migrate to Platform?**
|
||||
@@ -28,29 +30,12 @@ versionTo: "Platform"
|
||||
- **Production Grade**: Auto-scaling, high availability, dedicated support
|
||||
</Info>
|
||||
|
||||
### Plan
|
||||
## Plan
|
||||
|
||||
1. **Sign up**: Create an account on <a href="https://app.mem0.ai?utm_source=oss&utm_medium=migration-oss-to-platform" rel="nofollow">Mem0 Platform</a>.
|
||||
2. **Get API Key**: Navigate to **Settings > API Keys** and generate a new key.
|
||||
3. **Review Usage**: Identify where you instantiate `Memory` and where you call `search` or `get_all`.
|
||||
|
||||
## Migrate with Agent Skill
|
||||
|
||||
Paste this prompt into your coding agent. It uses a migration skill to produce a plan; once you review and approve it, the agent implements the changes.
|
||||
|
||||
```text
|
||||
Migrate my project from Mem0 OSS to the Mem0 Platform SDK using the
|
||||
mem0-oss-to-platform skill in the mem0ai/mem0 repo, at
|
||||
skills/mem0-oss-to-platform/
|
||||
|
||||
Get the skill whichever way is easiest:
|
||||
- install it: npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
|
||||
- if the mem0 repo is cloned locally, read it from skills/mem0-oss-to-platform/
|
||||
- otherwise fetch that folder from github.com/mem0ai/mem0 (SKILL.md + references/)
|
||||
|
||||
Then read SKILL.md and begin the migration.
|
||||
```
|
||||
|
||||
## Migrate
|
||||
|
||||
### 1. Import Memories Into Platform
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
---
|
||||
title: "Server: Upgrading the pgvector Docker Image"
|
||||
description: "Migrate your self-hosted Mem0 server from the archived ankane/pgvector image to the official pgvector/pgvector image."
|
||||
icon: "arrow-right"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The self-hosted Mem0 server has upgraded its PostgreSQL Docker image:
|
||||
|
||||
| | Before | After |
|
||||
| --- | --- | --- |
|
||||
| Docker image | `ankane/pgvector:v0.5.1` | `pgvector/pgvector:pg17` |
|
||||
| PostgreSQL | 15 | 17 |
|
||||
| pgvector | 0.5.1 | 0.8.0 |
|
||||
| Credentials | Hardcoded `postgres` / `postgres` | Set via `POSTGRES_USER` / `POSTGRES_PASSWORD` env vars |
|
||||
|
||||
<Warning>
|
||||
The `ankane/pgvector` image is **archived and no longer maintained**. The new `pgvector/pgvector` image is the official distribution maintained by the pgvector project.
|
||||
</Warning>
|
||||
|
||||
<Info>
|
||||
**Should you migrate?**
|
||||
- You are running the Mem0 server via `docker-compose.yaml` in the `server/` directory.
|
||||
- You want to stay on a maintained, actively-patched PostgreSQL + pgvector image.
|
||||
- You want pgvector 0.8.0 features (improved HNSW performance, parallel index builds).
|
||||
</Info>
|
||||
|
||||
## Fresh Installs
|
||||
|
||||
No migration is needed. Copy the example env file, set your password, and start the stack:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
cp .env.example .env
|
||||
# Edit .env — set POSTGRES_PASSWORD (required) and OPENAI_API_KEY at minimum
|
||||
make up
|
||||
```
|
||||
|
||||
## Migrating an Existing Install
|
||||
|
||||
PostgreSQL 17 cannot read data files created by PostgreSQL 15 directly. You need to export your data from the old container and import it into the new one.
|
||||
|
||||
### 1. Back Up Your Data
|
||||
|
||||
With the **old** stack still running:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker compose exec -T postgres pg_dumpall -U postgres > mem0_backup.sql
|
||||
```
|
||||
|
||||
Verify the dump is non-empty:
|
||||
|
||||
```bash
|
||||
ls -lh mem0_backup.sql
|
||||
```
|
||||
|
||||
<Warning>
|
||||
Do not skip this step. The next step permanently deletes your Postgres data volume.
|
||||
</Warning>
|
||||
|
||||
### 2. Stop the Old Stack and Remove the Volume
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### 3. Update Your `.env`
|
||||
|
||||
Postgres credentials are no longer hardcoded in `docker-compose.yaml`. Add them to your `.env`:
|
||||
|
||||
```bash
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=<your-password> # required — compose will refuse to start without it
|
||||
POSTGRES_COLLECTION_NAME=memories
|
||||
```
|
||||
|
||||
<Info>
|
||||
`POSTGRES_PASSWORD` is **required** — `docker compose up` will refuse to start without it. If you previously relied on the hardcoded default, set `POSTGRES_PASSWORD=postgres`.
|
||||
</Info>
|
||||
|
||||
### 4. Start Only Postgres
|
||||
|
||||
Start **only** the Postgres container first — do **not** start the mem0 API yet.
|
||||
The API runs `alembic upgrade head` on startup, which creates empty tables that
|
||||
would conflict with the restore.
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
Wait for Postgres to become healthy:
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres pg_isready -q && echo "ready" || echo "not ready"
|
||||
```
|
||||
|
||||
### 5. Restore Your Data
|
||||
|
||||
```bash
|
||||
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
|
||||
```
|
||||
|
||||
You may see notices like `role "postgres" already exists` — these are safe to ignore.
|
||||
|
||||
<Warning>
|
||||
You must restore **before** starting the mem0 API container. The API runs
|
||||
database migrations on startup which create empty tables — restoring after
|
||||
that would fail with duplicate-key errors and lose your API keys and settings.
|
||||
</Warning>
|
||||
|
||||
### 6. Start the API
|
||||
|
||||
Now start the mem0 API container. Alembic will detect the existing tables and
|
||||
only apply any new migrations:
|
||||
|
||||
```bash
|
||||
docker compose up -d mem0
|
||||
```
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```bash
|
||||
# Check service health
|
||||
cd server && make health
|
||||
|
||||
# Confirm memories are accessible
|
||||
curl -s http://localhost:8888/memories?user_id=<your-user-id> \
|
||||
-H "X-API-Key: <your-api-key>"
|
||||
```
|
||||
|
||||
## Rollback
|
||||
|
||||
If something goes wrong, revert the image tag in `docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
image: ankane/pgvector:v0.5.1
|
||||
```
|
||||
|
||||
Then destroy the new volume, start the old image, and restore from your backup:
|
||||
|
||||
```bash
|
||||
docker compose down -v
|
||||
docker compose up -d --build
|
||||
docker compose exec -T postgres psql -U postgres < mem0_backup.sql
|
||||
```
|
||||
|
||||
## Need Help?
|
||||
|
||||
- Join our [Discord community](https://mem0.ai/discord) for real-time support
|
||||
- Open an issue on [GitHub](https://github.com/mem0ai/mem0/issues)
|
||||
@@ -226,20 +226,6 @@ curl -X POST http://localhost:8888/search \
|
||||
}'
|
||||
```
|
||||
|
||||
Set `explain` to inspect the scoring signals used by OSS hybrid search:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8888/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "vegetable pizza",
|
||||
"user_id": "alice",
|
||||
"explain": true
|
||||
}'
|
||||
```
|
||||
|
||||
Each returned memory includes `score_details` only when explanation mode is enabled.
|
||||
|
||||
### Explore with OpenAPI docs
|
||||
|
||||
1. Navigate to `http://localhost:8888/docs` (Compose) or `http://localhost:8000/docs` (raw Docker / uvicorn).
|
||||
|
||||
@@ -45,12 +45,10 @@ Let your assistant execute an end-to-end workflow in an existing repo. Invoked a
|
||||
```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
|
||||
```
|
||||
|
||||
- `/mem0-integrate` — wire Mem0 into an existing repository using a goal-driven, test-first pipeline. Detects the stack, asks whether to use Platform or OSS, writes failing tests first, and keeps the integration additive and feature-flagged.
|
||||
- `/mem0-test-integration` — verify what `/mem0-integrate` produced. Runs the repo's native test suite and a real end-to-end smoke flow against your API key, then produces a scorecard.
|
||||
- `/mem0-oss-to-platform` — migrate an existing project from Mem0 OSS to the hosted Platform SDK. Audits where Mem0 is used, writes a reviewable migration plan, then executes it on approval.
|
||||
|
||||
See the [skills index](https://github.com/mem0ai/mem0/tree/main/skills) for the full catalog.
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ type RetrievedMemory = {
|
||||
|
||||
type NewMemory = {
|
||||
id: string;
|
||||
data?: {
|
||||
data: {
|
||||
memory: string;
|
||||
};
|
||||
event: "ADD" | "UPDATE" | "DELETE" | "GET";
|
||||
event: "ADD" | "DELETE";
|
||||
};
|
||||
|
||||
type NewMemoryAnnotation = {
|
||||
@@ -47,16 +47,14 @@ const useMemories = (): Memory[] => {
|
||||
() =>
|
||||
annotations?.filter(isMemoryAnnotation).flatMap((a) => {
|
||||
if (a.type === "mem0-update") {
|
||||
return a.memories
|
||||
.filter((m): m is NewMemory & { data: { memory: string } } => m.data != null)
|
||||
.map(
|
||||
(m): Memory => ({
|
||||
event: m.event,
|
||||
id: m.id,
|
||||
memory: m.data.memory,
|
||||
score: 1,
|
||||
})
|
||||
);
|
||||
return a.memories.map(
|
||||
(m): Memory => ({
|
||||
event: m.event,
|
||||
id: m.id,
|
||||
memory: m.data.memory,
|
||||
score: 1,
|
||||
})
|
||||
);
|
||||
} else if (a.type === "mem0-get") {
|
||||
return a.memories.map((m) => ({
|
||||
event: "GET",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"ai": "^4.1.46",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.6",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.477.0",
|
||||
"next": "15.5.18",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the `@mem0/opencode-plugin` will be documented in this file.
|
||||
|
||||
## 0.1.3 — File-context injection, session summaries & activity timeline, anonymous telemetry
|
||||
|
||||
### Added
|
||||
|
||||
- **File-context injection (`tool.execute.before` / Read):** Before the agent reads a file, the plugin searches mem0 for memories referencing that file path and injects prior work as system context. Gates on file size (>= 1,500 bytes). Gives the agent "I've worked on this file before" awareness automatically.
|
||||
- **Stop hook session summary (`experimental.session.compacting`):** Enhanced session compaction to store a structured `session_summary` memory with `infer=True`, letting the mem0 backend AI extract key facts (request, decisions, learnings, next steps). Previously only stored a raw stats string.
|
||||
- **SessionStart activity timeline:** The initial memory loading now formats recent memories with type icons (⚖️ decision, 🔴 bug_fix, 🔵 task_learning, etc.) and relative age indicators (2h ago, 1d ago) instead of bare text. Provides a visual "Recent Activity" timeline on first message.
|
||||
- **PostHog telemetry (`telemetry.ts`):** Anonymous, fire-and-forget usage events. Opt out with `MEM0_TELEMETRY=false`. Only fires when an API key is present; never sends memory content, prompts, or the API key — only an anonymized `sha256(apiKey)[:32]` identity plus event type, platform, and plugin version. Emits the same schema as the Mem0 editor plugin (`plugin.*` events, `source: "plugin"`, `platform: "opencode"`) so OpenCode appears as a `platform` in the shared plugin dashboard. Events: `plugin.session_start` (with memory count) and `plugin.tool_use` (`add` / `search` / `update` / `delete`).
|
||||
|
||||
### Changed
|
||||
|
||||
- **`experimental.session.compacting` handler:** Now stores `metadata.type=session_summary` with `metadata.source=opencode-stop` instead of `metadata.type=session_state` with `metadata.source=pre-compaction`. Includes a structured prompt that instructs mem0's AI to extract request, decisions, learnings, and next steps.
|
||||
- **Initial context formatting:** Memories shown on first message now include type icons and age labels for quick scanning.
|
||||
|
||||
## 0.1.2 — Automatic coding categories & global search
|
||||
|
||||
### Added
|
||||
|
||||
- **Auto-configured coding categories:** The plugin now automatically sets up 17 coding categories (e.g. `architecture_decisions`, `api_design`, `security`, `debugging_notes`) on the Mem0 project at startup. Runs in the background on every session start via `autoSetupCategories()`, is fully idempotent, and never blocks initialization. Uses SHA-256 fingerprints of the category list and API key — stored in `~/.mem0/categories_setup.json` — to skip redundant API calls on subsequent sessions.
|
||||
- **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, all `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Applies to all plugin search paths: initial load, per-message recall, resume detection, error-pattern lookup, and compaction context.
|
||||
- **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed.
|
||||
- **`MEM0_GLOBAL_SEARCH` environment variable:** Exported to child shells via the `shell.env` hook (`"true"` or `"false"`).
|
||||
|
||||
### Changed
|
||||
|
||||
- **Search filters are now dynamic:** All search paths throughout the plugin construct filters based on the `global_search` setting instead of always using `AND [user_id, app_id]`.
|
||||
- **Resume-context searches broadened:** Resume and error-pattern searches no longer include `metadata.type` sub-filters (`session_state`, `decision`, `anti_pattern`, `bug_fix`), broadening recall.
|
||||
- **System context message updated:** Informs the model when global search is active (`"Global search is ON — searches return all memories across all users and projects. Writes still use user_id=..., app_id=..."`).
|
||||
- **`/mem0:onboard` Step 5 is no longer interactive:** Removed the manual category installation prompt. Categories now configure automatically in the background; the onboarding step only verifies status and stores a fallback `project_profile` memory if the background run hasn't finished yet.
|
||||
- **`/mem0:switch-project` skill expanded:** Description and execution updated to document the `--global` and `--no-global` flags alongside the existing project-name argument.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
- CI/CD publish flow test (`#5288`).
|
||||
- Fixed tsconfig, added `publishConfig` and bun lockfile (`#5273`).
|
||||
- Renamed package to `@mem0/opencode-plugin` (`#5272`).
|
||||
- Added plugin array to bundled `opencode.json` (`#5271`).
|
||||
|
||||
## 0.1.0 — Initial release
|
||||
|
||||
- **OpenCode plugin** (`@mem0/opencode-plugin` on npm): Pure TypeScript plugin using the `mem0ai` TS SDK — no Python, no shell scripts. Hooks into all 6 OpenCode events (`chat.message`, `tool.execute.before`, `tool.execute.after`, `experimental.chat.system.transform`, `experimental.session.compacting`, `shell.env`). Features: session start memory loading, per-prompt semantic search, error pattern detection with memory lookup, resume/remember intent detection, auto-capture every 3rd message, periodic save nudges, full metadata defaults injection (confidence, source, type, session_id, files, branch), identity injection for search/get/delete filters, type-filtered error pre-fetch (anti_pattern + bug_fix), pre-compaction memory capture, MEMORY.md write blocking, and secret redaction.
|
||||
- **16 OpenCode-native skills** bundled in `opencode-skills/`: `context-loader`, `dream`, `export`, `forget`, `health`, `import`, `list-projects`, `mem0` (SDK reference), `memory-reviewer`, `onboard`, `peek`, `pin`, `remember`, `stats`, `switch-project`, `tour`. All skills are pure MCP-tool-based — no Python scripts, no shell scripts, no Claude Code dependencies.
|
||||
- **Auto-install skills and commands (`installSkills()`):** On plugin load, copies all 16 skills to `.opencode/skills/` and creates command wrapper files in `.opencode/commands/` so they appear in the OpenCode `/` palette.
|
||||
- **CLI installer (`cli.ts`):** `bunx @mem0/opencode-plugin install` auto-configures plugin and MCP server in `~/.config/opencode/opencode.json`.
|
||||
@@ -1,51 +0,0 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { buildEvent, captureEvent, isTelemetryEnabled } from "./telemetry";
|
||||
|
||||
const KEY = "m0-testkey123";
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
});
|
||||
|
||||
describe("opencode telemetry", () => {
|
||||
test("buildEvent uses the shared plugin.* schema with platform=opencode", () => {
|
||||
const payload = buildEvent("session_start", { memory_count: 5 }, KEY);
|
||||
expect(payload).not.toBeNull();
|
||||
const props = payload!.properties as Record<string, unknown>;
|
||||
expect(payload!.event).toBe("plugin.session_start");
|
||||
expect(props.source).toBe("plugin");
|
||||
expect(props.platform).toBe("opencode");
|
||||
expect(props.memory_count).toBe(5);
|
||||
expect(props.$process_person_profile).toBe(false);
|
||||
expect(typeof props.plugin_version).toBe("string");
|
||||
});
|
||||
|
||||
test("distinct_id is sha256(apiKey)[:32] — matches the editor plugin", async () => {
|
||||
const { createHash } = await import("node:crypto");
|
||||
const expected = createHash("sha256").update(KEY).digest("hex").slice(0, 32);
|
||||
expect(buildEvent("session_start", {}, KEY)!.distinct_id).toBe(expected);
|
||||
});
|
||||
|
||||
test("system properties win over caller-supplied ones", () => {
|
||||
const props = buildEvent("x", { platform: "HACK", source: "HACK" }, KEY)!
|
||||
.properties as Record<string, unknown>;
|
||||
expect(props.platform).toBe("opencode");
|
||||
expect(props.source).toBe("plugin");
|
||||
});
|
||||
|
||||
test("returns null without an API key (no anonymous events)", () => {
|
||||
expect(buildEvent("session_start", {}, undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test("opt-out via MEM0_TELEMETRY disables events", () => {
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
expect(isTelemetryEnabled()).toBe(false);
|
||||
expect(buildEvent("session_start", {}, KEY)).toBeNull();
|
||||
});
|
||||
|
||||
test("captureEvent never throws (and sends nothing when opted out)", () => {
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
expect(() => captureEvent("session_start", {}, KEY)).not.toThrow();
|
||||
expect(() => captureEvent("session_start", {}, undefined)).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Plugin telemetry for the Mem0 OpenCode plugin — anonymous usage tracking
|
||||
* via PostHog.
|
||||
*
|
||||
* Emits the SAME event schema as the Mem0 editor plugin's telemetry.py
|
||||
* (event names prefixed `plugin.`, `source: "plugin"`, `platform: "opencode"`,
|
||||
* `distinct_id = sha256(apiKey)[:32]`) so OpenCode shows up as just another
|
||||
* `platform` value in the shared plugin dashboard instead of a separate
|
||||
* event namespace.
|
||||
*
|
||||
* Fire-and-forget: never throws, never blocks, failures are swallowed. Only
|
||||
* fires when an API key is present (same as the editor plugin — anonymous
|
||||
* installs without a key emit nothing). Disable with MEM0_TELEMETRY=false.
|
||||
*
|
||||
* Never sends: memory content, API keys, raw user/project IDs. Only sends:
|
||||
* event type, platform, plugin version, anonymized hash of the API key.
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||
const REQUEST_TIMEOUT_MS = 2_000;
|
||||
|
||||
function _loadPluginVersion(): string {
|
||||
// Source context: telemetry.ts sits next to package.json (./).
|
||||
// Bundled context: dist/index.js sits one level below it (../).
|
||||
for (const rel of ["./package.json", "../package.json"]) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(new URL(rel, import.meta.url), "utf-8"));
|
||||
if (pkg?.name === "@mem0/opencode-plugin" && pkg.version) return pkg.version;
|
||||
} catch {
|
||||
/* try next candidate */
|
||||
}
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const PLUGIN_VERSION = _loadPluginVersion();
|
||||
|
||||
export function isTelemetryEnabled(): boolean {
|
||||
const val = process.env.MEM0_TELEMETRY;
|
||||
if (val === undefined) return true;
|
||||
const s = val.toLowerCase();
|
||||
return s !== "false" && s !== "0" && s !== "no" && s !== "off";
|
||||
}
|
||||
|
||||
function distinctId(apiKey: string): string {
|
||||
// Matches telemetry.py `_distinct_id()` so the same user is one person in
|
||||
// PostHog whether they use OpenCode or any other Mem0 editor plugin.
|
||||
return createHash("sha256").update(apiKey).digest("hex").slice(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the PostHog event payload, or null when telemetry is disabled or no
|
||||
* API key is available. Pure (aside from env/version reads) and exported for
|
||||
* testing. System-controlled properties are applied last so a caller cannot
|
||||
* override `source`/`platform`/etc.
|
||||
*/
|
||||
export function buildEvent(
|
||||
eventType: string,
|
||||
properties: Record<string, unknown>,
|
||||
apiKey: string | undefined,
|
||||
): Record<string, unknown> | null {
|
||||
if (!isTelemetryEnabled() || !apiKey) return null;
|
||||
return {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
distinct_id: distinctId(apiKey),
|
||||
event: `plugin.${eventType}`,
|
||||
properties: {
|
||||
...properties,
|
||||
source: "plugin",
|
||||
platform: "opencode",
|
||||
plugin_version: PLUGIN_VERSION,
|
||||
os: process.platform,
|
||||
sample_rate: 1.0,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Send a usage event, fire-and-forget. Never throws, never blocks. */
|
||||
export function captureEvent(
|
||||
eventType: string,
|
||||
properties: Record<string, unknown>,
|
||||
apiKey: string | undefined,
|
||||
): void {
|
||||
const payload = buildEvent(eventType, properties, apiKey);
|
||||
if (!payload) return;
|
||||
try {
|
||||
void fetch(POSTHOG_HOST, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
}).catch(() => {
|
||||
/* fire-and-forget */
|
||||
});
|
||||
} catch {
|
||||
/* never throw */
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Shared formatting helpers for mem0 plugin hooks.
|
||||
|
||||
Constants and utilities used by file_context.py, session_timeline.py,
|
||||
and any future hook that displays memories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
TYPE_ICONS = {
|
||||
"decision": "⚖️",
|
||||
"anti_pattern": "\U0001f534",
|
||||
"bug_fix": "\U0001f534",
|
||||
"convention": "\U0001f504",
|
||||
"task_learning": "\U0001f535",
|
||||
"user_preference": "\U0001f7e3",
|
||||
"session_summary": "\U0001f4cb",
|
||||
"session_state": "\U0001f4cb",
|
||||
"project_profile": "\U0001f4d6",
|
||||
"compact_summary": "\U0001f4cb",
|
||||
"auto_capture": "✅",
|
||||
"environmental": "🌐",
|
||||
"health_check": "🩺",
|
||||
}
|
||||
|
||||
|
||||
def format_age(memory: dict) -> str:
|
||||
"""Format how long ago a memory was created, e.g. '2h ago', '3d ago'."""
|
||||
created = memory.get("created_at", "")
|
||||
if not created:
|
||||
return ""
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
if created.endswith("Z"):
|
||||
created = created[:-1] + "+00:00"
|
||||
dt = datetime.fromisoformat(created)
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = now - dt
|
||||
seconds = int(delta.total_seconds())
|
||||
if seconds < 3600:
|
||||
return f"{seconds // 60}m ago"
|
||||
if seconds < 86400:
|
||||
return f"{seconds // 3600}h ago"
|
||||
days = seconds // 86400
|
||||
if days == 1:
|
||||
return "1d ago"
|
||||
if days < 30:
|
||||
return f"{days}d ago"
|
||||
return f"{days // 30}mo ago"
|
||||
except Exception:
|
||||
return ""
|
||||
@@ -1,264 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture a structured session summary on Stop hook.
|
||||
|
||||
Runs on every Stop (end of each assistant turn). Each invocation reads
|
||||
the transcript JSONL, extracts the latest assistant message and all
|
||||
files touched so far, then stores via mem0 API with infer=True. Uses
|
||||
run_id=session_id to scope infer dedup to the session, so the final
|
||||
stored summary reflects the most recent turn — not just the first.
|
||||
|
||||
Input: JSON on stdin with transcript_path, session_id, cwd, agent_id
|
||||
Output: stderr logs only (exit 0 always — must not block)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import date, timedelta
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _identity import resolve_api_key, resolve_user_id
|
||||
from _project import resolve_branch, resolve_project_id
|
||||
|
||||
log = logging.getLogger("mem0-session-summary")
|
||||
log.setLevel(logging.DEBUG)
|
||||
_handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-session-summary] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
if os.environ.get("MEM0_DEBUG"):
|
||||
_log_dir = os.path.expanduser("~/.mem0")
|
||||
try:
|
||||
os.makedirs(_log_dir, exist_ok=True)
|
||||
_fh = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
|
||||
_fh.setFormatter(logging.Formatter("[mem0-session-summary] %(asctime)s %(message)s"))
|
||||
log.addHandler(_fh)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_TAIL_LINES = 3000
|
||||
MAX_SUMMARY_CHARS = 50000
|
||||
SUMMARY_EXPIRY_DAYS = 90
|
||||
|
||||
SYSTEM_TAG_RE = re.compile(
|
||||
r"<(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>"
|
||||
r".*?"
|
||||
r"</(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def tail_lines(filepath: str, n: int) -> list[str]:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
file_size = f.tell()
|
||||
if file_size == 0:
|
||||
return []
|
||||
chunk_size = min(file_size, n * 4096)
|
||||
f.seek(max(0, file_size - chunk_size))
|
||||
data = f.read().decode("utf-8", errors="replace")
|
||||
return data.splitlines()[-n:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def extract_last_assistant_message(lines: list[str]) -> str:
|
||||
"""Walk transcript backwards, return text content of the last assistant message."""
|
||||
for line in reversed(lines):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if '"type":"assistant"' not in line and '"type": "assistant"' not in line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("type") != "assistant":
|
||||
continue
|
||||
message = entry.get("message", {})
|
||||
content = message.get("content", [])
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
if isinstance(content, list):
|
||||
parts = []
|
||||
for block in content:
|
||||
if isinstance(block, str):
|
||||
parts.append(block)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
return "\n".join(parts).strip()
|
||||
return ""
|
||||
|
||||
|
||||
def extract_files_touched(lines: list[str]) -> list[str]:
|
||||
"""Extract unique file paths from tool_use content blocks in transcript."""
|
||||
files = set()
|
||||
file_ext_re = re.compile(
|
||||
r"[a-zA-Z0-9_./-]+\.(?:py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html)"
|
||||
)
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
if '"tool_use"' not in line and '"file_path"' not in line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
content = entry.get("message", {}).get("content", [])
|
||||
if not isinstance(content, list):
|
||||
continue
|
||||
for block in content:
|
||||
if not isinstance(block, dict) or block.get("type") != "tool_use":
|
||||
continue
|
||||
inp = block.get("input", {})
|
||||
if not isinstance(inp, dict):
|
||||
continue
|
||||
fp = inp.get("file_path", "")
|
||||
if fp:
|
||||
files.add(fp)
|
||||
command = inp.get("command", "")
|
||||
if command:
|
||||
for match in file_ext_re.findall(command):
|
||||
files.add(match)
|
||||
return sorted(files)[:20]
|
||||
|
||||
|
||||
def strip_tags(text: str) -> str:
|
||||
return SYSTEM_TAG_RE.sub("", text).strip()
|
||||
|
||||
|
||||
def build_summary_prompt(assistant_msg: str, files: list[str]) -> str:
|
||||
"""Build a structured prompt that helps mem0's AI extract a good summary."""
|
||||
files_section = ""
|
||||
if files:
|
||||
file_list = ", ".join(files[:10])
|
||||
files_section = f"\n\nFiles touched during this session: {file_list}"
|
||||
|
||||
return (
|
||||
f"Session summary — store the following as a structured session summary.\n\n"
|
||||
f"What the assistant accomplished in this session:\n"
|
||||
f"{assistant_msg[:MAX_SUMMARY_CHARS]}"
|
||||
f"{files_section}\n\n"
|
||||
f"Extract and remember: what was requested, what was investigated, "
|
||||
f"key decisions made, what was completed, and what needs to happen next."
|
||||
)
|
||||
|
||||
|
||||
def store_summary(
|
||||
api_key: str,
|
||||
summary_prompt: str,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
project_id: str,
|
||||
branch: str,
|
||||
files: list[str],
|
||||
) -> bool:
|
||||
expires = (date.today() + timedelta(days=SUMMARY_EXPIRY_DAYS)).isoformat()
|
||||
metadata = {
|
||||
"type": "session_summary",
|
||||
"source": "stop-hook",
|
||||
"session_id": session_id,
|
||||
}
|
||||
if branch:
|
||||
metadata["branch"] = branch
|
||||
if files:
|
||||
metadata["files_touched"] = json.dumps(files[:20])
|
||||
|
||||
body = {
|
||||
"messages": [{"role": "user", "content": summary_prompt}],
|
||||
"user_id": user_id,
|
||||
"app_id": project_id,
|
||||
"run_id": session_id,
|
||||
"metadata": metadata,
|
||||
"infer": True,
|
||||
"expiration_date": expires,
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v3/memories/add/",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status in (200, 201):
|
||||
log.info("Session summary stored")
|
||||
return True
|
||||
log.warning("API returned status %d", resp.status)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
api_key = resolve_api_key()
|
||||
if not api_key:
|
||||
log.debug("MEM0_API_KEY not set, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
hook_input = json.loads(sys.stdin.read())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
log.debug("No valid JSON on stdin")
|
||||
return
|
||||
|
||||
# Guard: skip subagent sessions (only root sessions get summaries)
|
||||
agent_id = hook_input.get("agent_id", "")
|
||||
if agent_id:
|
||||
log.debug("Subagent session (agent_id=%s), skipping", agent_id)
|
||||
return
|
||||
|
||||
transcript_path = hook_input.get("transcript_path", "")
|
||||
if not transcript_path:
|
||||
log.debug("No transcript_path provided")
|
||||
return
|
||||
|
||||
session_id = hook_input.get("session_id", "")
|
||||
cwd = hook_input.get("cwd") or None
|
||||
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id(cwd)
|
||||
branch = resolve_branch(cwd)
|
||||
|
||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||
if not lines:
|
||||
log.debug("Transcript empty or unreadable: %s", transcript_path)
|
||||
return
|
||||
|
||||
assistant_msg = extract_last_assistant_message(lines)
|
||||
if not assistant_msg or len(assistant_msg.strip()) < 100:
|
||||
log.debug("Assistant message too short (%d chars) — skipping", len(assistant_msg.strip()))
|
||||
return
|
||||
|
||||
assistant_msg = strip_tags(assistant_msg)
|
||||
files = extract_files_touched(lines)
|
||||
|
||||
summary_prompt = build_summary_prompt(assistant_msg, files)
|
||||
|
||||
log.info("Capturing session summary (%d chars, %d files)", len(assistant_msg), len(files))
|
||||
store_summary(api_key, summary_prompt, user_id, session_id, project_id, branch, files)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
log.error("Unexpected error: %s", e)
|
||||
sys.exit(0)
|
||||
@@ -1,133 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""File-context injection for PreToolUse/Read hook.
|
||||
|
||||
When Claude is about to read a file, this script searches mem0 for
|
||||
memories that reference that file path and returns a compact timeline
|
||||
of prior work. This gives Claude context like "last time you fixed a
|
||||
null pointer here" before it reads the file.
|
||||
|
||||
Modeled after claude-mem's file-context handler but adapted for mem0's
|
||||
cloud API architecture.
|
||||
|
||||
Input: file_path (positional arg), env vars for identity
|
||||
Output: JSON to stdout with hookSpecificOutput.additionalContext
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _formatting import TYPE_ICONS, format_age
|
||||
from _identity import resolve_api_key, resolve_user_id
|
||||
from _project import resolve_project_id
|
||||
from _search import search_memories
|
||||
|
||||
FILE_READ_GATE_MIN_BYTES = 1500
|
||||
MAX_RESULTS = 5
|
||||
SEARCH_TIMEOUT = 5
|
||||
|
||||
|
||||
def gate_file(file_path: str, cwd: str) -> str | None:
|
||||
"""Return the resolved absolute path if the file passes gating, else None."""
|
||||
if not file_path:
|
||||
return None
|
||||
p = Path(file_path)
|
||||
if not p.is_absolute():
|
||||
p = Path(cwd) / p
|
||||
try:
|
||||
p = p.resolve()
|
||||
if not p.is_file():
|
||||
return None
|
||||
if p.stat().st_size < FILE_READ_GATE_MIN_BYTES:
|
||||
return None
|
||||
return str(p)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def relative_path(abs_path: str, cwd: str) -> str:
|
||||
try:
|
||||
return os.path.relpath(abs_path, cwd)
|
||||
except ValueError:
|
||||
return abs_path
|
||||
|
||||
|
||||
def format_timeline(memories: list[dict], file_path: str) -> str:
|
||||
"""Format memories into a compact timeline for context injection."""
|
||||
if not memories:
|
||||
return ""
|
||||
|
||||
rel = file_path
|
||||
lines = [
|
||||
f"Prior work on `{rel}` — {len(memories)} memories found.",
|
||||
"Need details? Use `search_memories` with the memory ID.",
|
||||
"",
|
||||
]
|
||||
|
||||
for m in memories:
|
||||
mid = m.get("id", "?")[:8]
|
||||
text = (m.get("memory", "") or "")[:150].replace("\n", " ").strip()
|
||||
meta = m.get("metadata") or {}
|
||||
cat = meta.get("type", "unknown")
|
||||
icon = TYPE_ICONS.get(cat, "❓")
|
||||
age = format_age(m)
|
||||
age_str = f" ({age})" if age else ""
|
||||
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def search_file_context(
|
||||
api_key: str, user_id: str, project_id: str, file_path: str, cwd: str
|
||||
) -> str:
|
||||
"""Search mem0 for memories related to a file path."""
|
||||
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
|
||||
rel = relative_path(file_path, cwd)
|
||||
basename = os.path.basename(file_path)
|
||||
|
||||
query = f"{rel} {basename}" if rel != basename else rel
|
||||
results = search_memories(
|
||||
api_key, user_id, project_id, query,
|
||||
top_k=MAX_RESULTS, threshold=0.3,
|
||||
global_search=global_search,
|
||||
)
|
||||
|
||||
results = results[:MAX_RESULTS]
|
||||
|
||||
return format_timeline(results, rel)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(0)
|
||||
|
||||
file_path = sys.argv[1]
|
||||
cwd = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
|
||||
|
||||
api_key = resolve_api_key()
|
||||
if not api_key:
|
||||
sys.exit(0)
|
||||
|
||||
resolved = gate_file(file_path, cwd)
|
||||
if not resolved:
|
||||
sys.exit(0)
|
||||
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id(cwd)
|
||||
|
||||
timeline = search_file_context(api_key, user_id, project_id, resolved, cwd)
|
||||
if not timeline:
|
||||
sys.exit(0)
|
||||
|
||||
print(timeline, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PreToolUse (matcher: Read)
|
||||
#
|
||||
# Injects prior work context before Claude reads a file. Searches mem0
|
||||
# for memories referencing the file path and returns a compact timeline.
|
||||
#
|
||||
# Modeled after claude-mem's file-context handler, adapted for mem0 cloud API.
|
||||
#
|
||||
# Input: JSON on stdin with tool_name, tool_input (file_path), cwd
|
||||
# Output: JSON with hookSpecificOutput.additionalContext + permissionDecision
|
||||
#
|
||||
# Must never block the Read — silent exit on any failure.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Extract file path from tool_input
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Resolve API key (covers Desktop app users who set it in shell profile)
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
||||
fi
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
|
||||
|
||||
# Call the Python worker — it handles gating (file size, existence)
|
||||
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TIMELINE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Return context injection with permissionDecision: allow
|
||||
jq -cn --arg ctx "$TIMELINE" '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: $ctx,
|
||||
permissionDecision: "allow"
|
||||
}
|
||||
}' 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: preToolUse (matcher: Read) — Cursor variant
|
||||
#
|
||||
# Same as on_file_read.sh but uses CURSOR_PLUGIN_ROOT for path resolution
|
||||
# and sources Cursor-specific identity.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
||||
fi
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
|
||||
|
||||
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$TIMELINE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
jq -cn --arg ctx "$TIMELINE" '{
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: $ctx,
|
||||
permissionDecision: "allow"
|
||||
}
|
||||
}' 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop
|
||||
#
|
||||
# Captures a structured session summary when a Claude Code session ends.
|
||||
# Parses the transcript, extracts the last assistant message and files
|
||||
# touched, then stores via mem0 API with infer=True for AI extraction.
|
||||
#
|
||||
# Guards:
|
||||
# - Skips subagent sessions (agent_id present)
|
||||
# - Skips if no API key
|
||||
# - Skips if no transcript_path
|
||||
# - Dedup via marker file
|
||||
#
|
||||
# Input: JSON on stdin with transcript_path, session_id, agent_id, cwd
|
||||
# Output: Nothing to stdout (background capture). Always exits 0.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Guard: skip subagent sessions
|
||||
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
|
||||
if [ -n "$AGENT_ID" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Resolve identity if needed
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
||||
|
||||
# Honor auto_save=false in ~/.mem0/settings.json
|
||||
if [ "${MEM0_AUTO_SAVE:-true}" = "false" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$TRANSCRIPT_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run capture in the background — fires every turn now, so avoid blocking
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null &
|
||||
|
||||
# Telemetry
|
||||
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: stop — Cursor variant
|
||||
#
|
||||
# Same as on_stop.sh but uses CURSOR_PLUGIN_ROOT and Cursor-specific
|
||||
# identity resolution.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||
fi
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
|
||||
if [ -n "$AGENT_ID" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# Pin platform so this hook's telemetry is attributed to cursor.
|
||||
export MEM0_PLATFORM=cursor
|
||||
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
||||
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$TRANSCRIPT_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null || true
|
||||
|
||||
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
@@ -1,106 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fetch recent memories and format a compact timeline for SessionStart.
|
||||
|
||||
Searches mem0 cloud API for the most recent memories in the project
|
||||
and formats them as a compact activity timeline injected below the
|
||||
existing SessionStart banner.
|
||||
|
||||
Input: env vars for identity (MEM0_API_KEY, MEM0_RESOLVED_USER_ID, etc.)
|
||||
Output: Compact timeline text to stdout (empty if nothing found)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _formatting import TYPE_ICONS, format_age
|
||||
from _identity import resolve_api_key, resolve_user_id
|
||||
from _project import resolve_project_id
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_RECENT = 10
|
||||
MAX_SUMMARIES = 3
|
||||
FETCH_TIMEOUT = 5
|
||||
|
||||
|
||||
def fetch_recent_memories(api_key: str, user_id: str, project_id: str) -> list[dict]:
|
||||
"""Fetch the most recent memories for this project via GET list endpoint."""
|
||||
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
|
||||
|
||||
if global_search:
|
||||
filters = {"OR": [{"user_id": "*"}]}
|
||||
else:
|
||||
filters = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
|
||||
|
||||
body = json.dumps({"filters": filters}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v3/memories/?page=1&page_size={MAX_RECENT}",
|
||||
data=body,
|
||||
headers={
|
||||
"Authorization": f"Token {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
|
||||
result = json.loads(r.read())
|
||||
if isinstance(result, dict) and "results" in result:
|
||||
return result["results"][:MAX_RECENT]
|
||||
if isinstance(result, list):
|
||||
return result[:MAX_RECENT]
|
||||
return []
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def format_timeline(memories: list[dict]) -> str:
|
||||
"""Format memories into a compact recent activity timeline."""
|
||||
if not memories:
|
||||
return ""
|
||||
|
||||
lines = ["### Recent Activity", ""]
|
||||
|
||||
for m in memories:
|
||||
mid = m.get("id", "?")[:8]
|
||||
text = (m.get("memory", "") or "")[:120].replace("\n", " ").strip()
|
||||
meta = m.get("metadata") or {}
|
||||
cat = meta.get("type", "unknown")
|
||||
icon = TYPE_ICONS.get(cat, "❓")
|
||||
age = format_age(m)
|
||||
age_str = f" ({age})" if age else ""
|
||||
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Search mem0 for details on any of these, or for past decisions and task learnings relevant to the current task.")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
api_key = resolve_api_key()
|
||||
if not api_key:
|
||||
return
|
||||
|
||||
user_id = resolve_user_id()
|
||||
project_id = resolve_project_id(os.environ.get("MEM0_CWD"))
|
||||
|
||||
memories = fetch_recent_memories(api_key, user_id, project_id)
|
||||
if not memories:
|
||||
return
|
||||
|
||||
timeline = format_timeline(memories)
|
||||
if timeline:
|
||||
print(timeline, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
@@ -1,21 +0,0 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
allowBuilds:
|
||||
'@google/genai': set this to true or false
|
||||
better-sqlite3: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
protobufjs: set this to true or false
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- better-sqlite3
|
||||
- esbuild
|
||||
- protobufjs
|
||||
|
||||
overrides:
|
||||
"protobufjs@<7.5.5": "^7.5.5"
|
||||
"vite": "^8.0.5"
|
||||
"langsmith@<0.6.0": "^0.6.0"
|
||||
"picomatch@<2.3.2": "^2.3.2"
|
||||
"@qdrant/js-client-rest": "^1.18.0"
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
@@ -1,71 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.2 (2026-06-12)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Visible command feedback** — `/mem0-remember`, `/mem0-forget`, `/mem0-pin`, and `/mem0-scope` now render their results as persistent message blocks (`pi.sendMessage({ display: true })`) instead of `ctx.ui.notify(..., "info")`. The Pi TUI draws `"info"` notifications as dim, collapsible status text that overwrites the previous line, so command results were easily missed (felt like "no feedback"). Warnings and errors still use `ctx.ui.notify`, which renders prominently.
|
||||
- **Relevance-filtered search** — `/mem0-search`, `/mem0-forget`, and `/mem0-pin` pass a similarity `threshold` (`searchThreshold`, default `0.3`; configurable in `mem0-config.json`, shown in `/mem0-status`), `top_k`, and `rerank` to the mem0 search API, matching the Claude Code and OpenClaw integrations. mem0 ranks results by similarity with no relevance floor, so without a threshold an unrelated query returns the closest (weak) memories — and `/mem0-forget` would offer them for deletion. The server-side threshold makes a query with no sufficiently similar memory report no match, and reranking orders the genuine matches by deeper relevance. Raise `searchThreshold` to be stricter; lower it if relevant results are missed.
|
||||
|
||||
### Improved
|
||||
|
||||
- **Richer feedback across every command** — results now show what actually happened: an action heading plus the relevant scope, query, match count, and affected memories.
|
||||
- `/mem0-remember` echoes the stored text and the scope it landed in, instead of a generic success line.
|
||||
- `/mem0-search` adds an `N matches for "<query>"` header.
|
||||
- `/mem0-forget` and `/mem0-pin` name the query when nothing matches, list the affected memory, and label the disambiguation dialog with the match count.
|
||||
- `/mem0-scope` explains where new memories will be saved.
|
||||
- **Cleaner `/mem0-dream`** — the consolidation protocol is now sent to the agent hidden (`display: false`, still included in LLM context) behind a concise "Dreaming…" status line, instead of dumping the raw protocol into the transcript.
|
||||
|
||||
## 0.1.1 (2026-06-10)
|
||||
|
||||
Maintenance release — no functional changes.
|
||||
|
||||
### Chores
|
||||
|
||||
- Version bump to validate the new release pipeline (`pi-agent-plugin-checks.yml` / `pi-agent-plugin-cd.yml`)
|
||||
|
||||
## 0.1.0 (2026-06-09)
|
||||
|
||||
Initial release of `@mem0/pi-agent-plugin` — persistent semantic memory for Pi Agent.
|
||||
|
||||
### Features
|
||||
|
||||
- **Extension entry point** — registers `mem0_memory` tool, 8 slash commands, and auto-capture hooks
|
||||
- **Agent tool** (`mem0_memory`) — search, add, get_all, delete, delete_all with scoped filters
|
||||
- **Auto-capture** — extracts and stores memories from both user and assistant messages on `agent_end`
|
||||
- **Dream consolidation** — automated memory maintenance: merge duplicates, resolve contradictions, prune stale entries. Gated by session count, time elapsed, and memory count thresholds
|
||||
- **System prompt injection** — appends `MEMORY_POLICY` to every agent turn via `before_agent_start`
|
||||
- **Monorepo-aware project scoping** — uses `git rev-parse --show-toplevel` for consistent app_id across subdirectories
|
||||
- **3 memory scopes** — project (default), session, global
|
||||
- **10 memory categories** — identity, preferences, goals, projects, decisions, technical, relationships, routines, lessons, work
|
||||
- **8 skills** — context-loader, remember, search, forget, dream, tour, pin, status
|
||||
- **Confirmation dialogs** — `/mem0-forget` and `/mem0-pin` ask for confirmation before destructive or mutating actions via `ctx.ui.confirm()`
|
||||
- **Pin preserves memory ID** — `/mem0-pin` uses `mem0.update()` instead of add+delete, keeping the original UUID
|
||||
- **Full memory IDs** — all displayed memory references show the complete UUID, not truncated short IDs
|
||||
- **Dream gate optimization** — `dreamChecked` flag prevents repeated `getAll` API calls when the memory gate fails
|
||||
- **Output truncation** — tool results capped at 200 lines / 50KB per Pi docs
|
||||
- **Signal cancellation** — all tool actions respect `AbortSignal`
|
||||
- **Session shutdown cleanup** — releases dream lock on `session_shutdown`
|
||||
- **PostHog telemetry** — batched event queue with PII-safe error payloads
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/mem0-remember` | Store a memory verbatim (no inference) |
|
||||
| `/mem0-forget` | Search and delete memories (with confirmation) |
|
||||
| `/mem0-search` | Semantic search across memories |
|
||||
| `/mem0-tour` | Browse all memories by category |
|
||||
| `/mem0-dream` | Trigger memory consolidation |
|
||||
| `/mem0-pin` | Pin a memory to protect from pruning (preserves ID) |
|
||||
| `/mem0-scope` | Change default scope for this session |
|
||||
| `/mem0-status` | Connection health and diagnostics |
|
||||
|
||||
### Hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|------|---------|
|
||||
| `session_start` | Detect project (git root), resolve session ID, increment dream counter |
|
||||
| `before_agent_start` | Inject memory policy into system prompt, auto-trigger dream if gates pass |
|
||||
| `agent_end` | Auto-capture conversation memories, check dream completion |
|
||||
| `session_shutdown` | Release dream lock, flush telemetry |
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2026] [Taranjeet Singh]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,147 +0,0 @@
|
||||
# @mem0/pi-agent-plugin
|
||||
|
||||
Persistent semantic memory for [Pi Agent](https://pi.dev), powered by [Mem0](https://mem0.ai).
|
||||
|
||||
This extension gives Pi Agent long-term memory that persists across sessions, projects, and devices. Memories are automatically captured from conversations and can be searched, managed, and consolidated through slash commands and an agent-accessible tool.
|
||||
|
||||
## Features
|
||||
|
||||
- **Automatic memory capture** — learns from every conversation (both user and assistant messages)
|
||||
- **Semantic search** — find memories by meaning, not just keywords
|
||||
- **Scoped memory** — project, session, or global scope
|
||||
- **Monorepo-aware** — uses git root for project detection, consistent app_id across subdirectories
|
||||
- **Dream consolidation** — merges duplicates, resolves contradictions, prunes stale entries
|
||||
- **Confirmation dialogs** — destructive commands ask before acting
|
||||
- **8 slash commands** — essential memory management from the command line
|
||||
- **Agent tool** — `mem0_memory` tool lets the agent search and store memories autonomously
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Get an API key
|
||||
|
||||
Sign up at [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys) and copy your API key.
|
||||
|
||||
### 2. Install
|
||||
|
||||
```bash
|
||||
pi install npm:@mem0/pi-agent-plugin
|
||||
```
|
||||
|
||||
### 3. Configure
|
||||
|
||||
Set the API key as an environment variable:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-key-here"
|
||||
```
|
||||
|
||||
Or create a config file at `~/.pi/agent/mem0-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "m0-your-key-here",
|
||||
"userId": "your-username",
|
||||
"autoCapture": true,
|
||||
"defaultScope": "project",
|
||||
"searchThreshold": 0.2,
|
||||
"dream": {
|
||||
"enabled": true,
|
||||
"auto": true,
|
||||
"minHours": 24,
|
||||
"minSessions": 5,
|
||||
"minMemories": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file.
|
||||
|
||||
`searchThreshold` (default `0.3`) is the minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search`, `/mem0-forget`, and `/mem0-pin`. It is passed to the mem0 search API (along with reranking for higher-precision ordering), so a query with no sufficiently similar memory reports no match instead of returning the closest unrelated memories. Raise it to be stricter; lower it if relevant results are missed.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/mem0-remember <text>` | Store a memory verbatim (no inference) |
|
||||
| `/mem0-forget <query>` | Search and delete memories (with confirmation) |
|
||||
| `/mem0-search <query>` | Semantic search across memories |
|
||||
| `/mem0-tour [scope]` | Browse all memories grouped by category |
|
||||
| `/mem0-dream` | Consolidate — merge duplicates, prune stale, resolve contradictions |
|
||||
| `/mem0-pin <query>` | Pin a memory to protect from dream pruning (preserves ID) |
|
||||
| `/mem0-scope <scope>` | Change default scope for this session |
|
||||
| `/mem0-status` | Connection health, identity, and memory count |
|
||||
|
||||
## Skills
|
||||
|
||||
The plugin includes 8 skills that guide the agent on how to use each capability:
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `context-loader` | Pre-fetch relevant memories at session start |
|
||||
| `remember` | Store facts with category classification |
|
||||
| `search` | Quick semantic search with compact results |
|
||||
| `forget` | Delete memories with confirmation |
|
||||
| `dream` | Memory consolidation workflow |
|
||||
| `tour` | Full memory walkthrough by category |
|
||||
| `pin` | Protect critical memories from pruning |
|
||||
| `status` | Health check and diagnostics |
|
||||
|
||||
## Memory Scopes
|
||||
|
||||
| Scope | Filters | Use case |
|
||||
|-------|---------|----------|
|
||||
| `project` | user + app_id (git root) | Default. Project-specific knowledge |
|
||||
| `session` | user + app_id + run_id | Ephemeral, session-only context |
|
||||
| `global` | user only | All memories across all your projects |
|
||||
|
||||
Project scoping uses `git rev-parse --show-toplevel` to detect the repository root, so all subdirectories within a monorepo share the same memory pool.
|
||||
|
||||
## Memory Categories
|
||||
|
||||
Memories are automatically classified into 10 general-purpose categories:
|
||||
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| `identity` | Personal details, background, self-descriptions |
|
||||
| `preferences` | Likes, dislikes, habits, preferred approaches |
|
||||
| `goals` | Objectives, aspirations, targets |
|
||||
| `projects` | Ongoing work, initiatives, areas of focus |
|
||||
| `decisions` | Choices made, rationale, trade-offs |
|
||||
| `technical` | Technical knowledge, tools, configurations |
|
||||
| `relationships` | People, teams, organizations |
|
||||
| `routines` | Recurring patterns, workflows, schedules |
|
||||
| `lessons` | Insights learned, mistakes to avoid |
|
||||
| `work` | Professional context, role, responsibilities |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
pi-agent-plugin/
|
||||
├── src/
|
||||
│ ├── entry.ts # Extension entry point
|
||||
│ ├── index.ts # Barrel exports
|
||||
│ ├── commands.ts # 8 slash commands
|
||||
│ ├── prompt.ts # System prompt injection (MEMORY_POLICY)
|
||||
│ ├── types.ts # Shared interfaces and categories
|
||||
│ ├── telemetry.ts # PostHog telemetry (batched, PII-safe)
|
||||
│ ├── config/ # Config loading (~/.pi/agent/mem0-config.json)
|
||||
│ ├── memory/ # Tool registration, scoping (git root), formatting
|
||||
│ ├── capture/ # Auto-capture from conversations (user + assistant)
|
||||
│ └── dream/ # Consolidation state, gating, locking, prompts
|
||||
├── skills/ # 8 SKILL.md files for Pi Agent
|
||||
├── tests/ # Vitest unit tests
|
||||
└── dist/ # Built output (ESM + DTS)
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm install # Install dependencies
|
||||
pnpm run typecheck # Type check
|
||||
pnpm run test # Run tests
|
||||
pnpm run build # Build (ESM + declarations)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](LICENSE)
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"name": "@mem0/pi-agent-plugin",
|
||||
"version": "0.1.2",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "integrations/pi-agent-plugin"
|
||||
},
|
||||
"keywords": [
|
||||
"pi-package",
|
||||
"pi-extension",
|
||||
"skills",
|
||||
"memory",
|
||||
"mem0",
|
||||
"semantic-memory",
|
||||
"persistent-memory",
|
||||
"agent-memory"
|
||||
],
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"skills",
|
||||
"CHANGELOG.md",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"pi": {
|
||||
"extensions": [
|
||||
"./src/entry.ts"
|
||||
],
|
||||
"skills": [
|
||||
"./skills"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-ai": "*",
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"typebox": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-ai": "^0.79.0",
|
||||
"@earendil-works/pi-coding-agent": "^0.79.0",
|
||||
"@types/node": "^25.9.2",
|
||||
"tsup": "^8.5.0",
|
||||
"typebox": "^1.2.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"mem0ai": "^3.0.7"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
-4932
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
overrides:
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
name: context-loader
|
||||
description: Searches and injects relevant memories into context before starting work on a task or topic. Use when beginning a new task, switching context, or when past decisions, preferences, or knowledge need to be loaded.
|
||||
---
|
||||
|
||||
# Context Loader
|
||||
|
||||
Pre-fetches relevant memories to prime context before working on a task or topic.
|
||||
|
||||
## When to use
|
||||
|
||||
- Session start (auto-triggered by the extension's `before_agent_start` event)
|
||||
- User starts work on a specific topic or area
|
||||
- User says "what do we know about X" or "context for X"
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract topics** from current message/task. Identify: subject areas, people mentioned, project names, goal references.
|
||||
|
||||
2. **Run 2-4 parallel searches** using `mem0_memory` tool with `action="search"` and different query angles:
|
||||
|
||||
| Query angle | Purpose |
|
||||
|---|---|
|
||||
| Topic/subject name | Relevant decisions and preferences |
|
||||
| People mentioned | Relationship context |
|
||||
| Project/goal references | Progress and background |
|
||||
| Broad context | Catch-all for anything relevant |
|
||||
|
||||
3. **Deduplicate** results by memory ID across all search responses.
|
||||
|
||||
4. **Output compact context block** (max 10 memories):
|
||||
|
||||
```
|
||||
context-loader: loaded <N> memories for "<task summary>"
|
||||
- [decisions] <content> [mem0:<short_id>]
|
||||
- [preferences] <content> [mem0:<short_id>]
|
||||
- [lessons] <content> [mem0:<short_id>]
|
||||
```
|
||||
|
||||
5. If **zero results**: output nothing. Don't announce empty context.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Read-only** — never modify or delete memories
|
||||
- **Max 10 memories** returned (most relevant only)
|
||||
- **Silent on empty** — only surfaces findings if relevant context exists
|
||||
- Skip memories already visible in current session context
|
||||
@@ -1,125 +0,0 @@
|
||||
---
|
||||
name: dream
|
||||
description: Consolidates stored memories by merging duplicates, resolving contradictions, and pruning stale entries. Use when memory count is high, search results feel noisy or repetitive, or periodic cleanup is needed to maintain memory quality.
|
||||
---
|
||||
|
||||
# Dream — Memory Consolidation
|
||||
|
||||
This skill performs a memory consolidation pass: it fetches all memories, identifies near-duplicates, flags contradictions, and prunes stale entries. All proposed changes are shown as a diff for user approval before anything is modified.
|
||||
|
||||
**IMPORTANT: Execute steps strictly in order (1 -> 2 -> 3 -> 4 -> 5). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead.**
|
||||
|
||||
## Step 1: Fetch ALL Memories
|
||||
|
||||
Use `mem0_memory` tool with `action="get_all"` to retrieve every memory.
|
||||
|
||||
If zero memories are found, print:
|
||||
|
||||
```
|
||||
No memories found. Nothing to consolidate.
|
||||
```
|
||||
|
||||
...and stop.
|
||||
|
||||
## Step 2: Analyze — Find Issues
|
||||
|
||||
Work entirely in-memory; do not modify anything yet.
|
||||
|
||||
Group memories by category. For each group, identify the following:
|
||||
|
||||
### 2a. Near-duplicate pairs (merge candidates)
|
||||
|
||||
Two memories are near-duplicates when they express the same fact but phrased differently (e.g., "Prefers morning meetings" and "Likes scheduling meetings early").
|
||||
|
||||
Heuristics — two memories are near-duplicates if **all** of these hold:
|
||||
- If >60% of significant nouns/keywords overlap, treat as near-duplicate.
|
||||
- Same category.
|
||||
- Neither memory is pinned (content does not start with `[PINNED]`).
|
||||
|
||||
For each qualifying pair, draft a merged version that is more complete than either original.
|
||||
|
||||
### 2b. Contradictions
|
||||
|
||||
Two memories contradict when they assert opposing facts about the same topic (e.g., "Prefers cats" vs. "Allergic to cats, prefers dogs").
|
||||
|
||||
Identify the likely winner: the more recent memory wins. Store both IDs and their content for user review.
|
||||
|
||||
### 2c. Prune candidates
|
||||
|
||||
A memory is a prune candidate when **any** of the following is true:
|
||||
|
||||
1. It is older than 180 days AND has not been accessed recently.
|
||||
2. Its content is extremely vague (fewer than 5 meaningful words).
|
||||
|
||||
**Always skip memories where content starts with `[PINNED]`**, regardless of age.
|
||||
|
||||
## Step 3: Print Diff Report
|
||||
|
||||
Print a structured diff before making any changes:
|
||||
|
||||
```
|
||||
## dream — consolidation report
|
||||
|
||||
Merges (<N>):
|
||||
[mem0:<id1>] + [mem0:<id2>] -> "<merged content, 100 chars>"
|
||||
|
||||
Conflicts (<N>):
|
||||
[mem0:<idA>] vs [mem0:<idB>] — "<topic>" [A/B/skip]
|
||||
|
||||
Prune (<N>):
|
||||
[mem0:<id>] — <category>, <age>d old
|
||||
|
||||
Proposed: <N> merges, <N> prunes, <N> conflicts. Apply? [Y/n]
|
||||
```
|
||||
|
||||
If there are zero total proposals, print:
|
||||
|
||||
```
|
||||
Dream complete. No duplicate, contradictory, or stale memories found.
|
||||
```
|
||||
|
||||
...and stop.
|
||||
|
||||
## Step 4: Wait for User Input and Apply
|
||||
|
||||
### 4a. Contradictions
|
||||
|
||||
For each conflict pair, wait for the user to choose A, B, or skip.
|
||||
|
||||
### 4b. Final confirmation
|
||||
|
||||
After all conflict resolutions are collected, prompt: `Apply? [Y/n]`
|
||||
|
||||
If the user declines, print `Cancelled. No changes made.` and stop.
|
||||
|
||||
If confirmed, apply all changes:
|
||||
|
||||
**Merges:** Delete both originals, add the merged version using `mem0_memory` with `action="add"`.
|
||||
|
||||
**Contradictions (resolved):** Delete the loser using `mem0_memory` with `action="delete"`.
|
||||
|
||||
**Prunes:** Delete each using `mem0_memory` with `action="delete"`.
|
||||
|
||||
## Step 5: Print Summary
|
||||
|
||||
```
|
||||
Dream complete — merged: <N>, pruned: <N>, conflicts resolved: <N>, skipped: <N>
|
||||
```
|
||||
|
||||
## Auto mode
|
||||
|
||||
When invoked with `--auto` (e.g., `/mem0-dream --auto`), run non-interactively:
|
||||
|
||||
- **Merges**: applied automatically.
|
||||
- **Prunes**: applied automatically.
|
||||
- **Contradictions**: skipped — they require human judgment.
|
||||
|
||||
Print a compact summary:
|
||||
```
|
||||
[mem0-dream --auto] merged=<N> pruned=<N> conflicts_skipped=<N>
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- `/mem0-forget` — targeted deletion of specific memories
|
||||
- `/mem0-status` — quick health check
|
||||
@@ -1,53 +0,0 @@
|
||||
---
|
||||
name: forget
|
||||
description: Deletes memories by search query or memory ID with confirmation before removal. Use when removing outdated information, incorrect memories, sensitive data, or cleaning up after experiments.
|
||||
---
|
||||
|
||||
# Forget
|
||||
|
||||
Delete specific memories from Mem0.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Parse input
|
||||
|
||||
The user provides either:
|
||||
- A search query: `/mem0-forget travel plans`
|
||||
- A memory ID: `/mem0-forget <memory_id>`
|
||||
|
||||
If no argument, ask: "What should I forget? Provide a search query or memory ID."
|
||||
|
||||
### Step 2: Find memories
|
||||
|
||||
**If memory ID provided** (looks like a UUID or hex string):
|
||||
- Use `mem0_memory` tool with `action="search"` and the ID as query, or look it up directly.
|
||||
- Show: `Found: "<memory content first 120 chars>" (created <date>)`
|
||||
|
||||
**If search query provided:**
|
||||
- Use `mem0_memory` tool with `action="search"`, `query=<user's query>`.
|
||||
- Show numbered list:
|
||||
```
|
||||
Found <N> memories matching "<query>":
|
||||
1. <content, 120 chars> [<category>] [ID: <short_id>]
|
||||
2. ...
|
||||
```
|
||||
|
||||
### Step 3: Confirm
|
||||
|
||||
Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'."
|
||||
|
||||
For a single memory ID, ask: "Delete this memory? [y/N]"
|
||||
|
||||
**Never delete without confirmation.** This is destructive.
|
||||
|
||||
### Step 4: Delete
|
||||
|
||||
For each confirmed memory, use `mem0_memory` tool with `action="delete"` and the memory ID.
|
||||
|
||||
### Step 5: Report
|
||||
|
||||
```
|
||||
Deleted <N> memories.
|
||||
```
|
||||
|
||||
If any deletions failed, report which ones and why.
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: pin
|
||||
description: Pins or unpins a memory to protect it from pruning during dream consolidation. Use when a memory is critical and must never be removed, such as core preferences, important decisions, or immutable personal facts.
|
||||
---
|
||||
|
||||
# Pin
|
||||
|
||||
Pin a memory to mark it as high-priority and protect from dream pruning.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Find the memory
|
||||
|
||||
The user provides either a search query or memory ID.
|
||||
|
||||
**If memory ID:** Look it up directly.
|
||||
|
||||
**If search query:**
|
||||
- Use `mem0_memory` tool with `action="search"`, `query=<query>`.
|
||||
- Show numbered list with content previews.
|
||||
- Ask: "Which memory to pin? Enter a number."
|
||||
|
||||
### Step 2: Pin it
|
||||
|
||||
Pinning works by prepending `[PINNED]` to the memory text. This marker tells the dream consolidation to skip it during pruning.
|
||||
|
||||
Use `mem0_memory` tool with `action="add"`, `content="[PINNED] <original memory text>"`.
|
||||
|
||||
Then delete the original using `mem0_memory` with `action="delete"` and the original memory ID.
|
||||
|
||||
**For new memories** (user wants to pin text that isn't stored yet):
|
||||
- Use `mem0_memory` tool with `action="add"`, `content="[PINNED] <the user's text>"`.
|
||||
|
||||
### Step 3: Confirm
|
||||
|
||||
```
|
||||
Pinned: "<memory content, first 80 chars>"
|
||||
```
|
||||
|
||||
Append `...` only if content exceeds 80 characters.
|
||||
|
||||
### Unpin
|
||||
|
||||
If the user says "unpin":
|
||||
1. Find the memory (search or by ID).
|
||||
2. Create a new memory without the `[PINNED]` prefix.
|
||||
3. Delete the pinned version.
|
||||
4. Print: `Unpinned: "<content>..."`
|
||||
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: remember
|
||||
description: Stores a memory verbatim from user input with appropriate category classification. Use when the user says remember this, save this, store this, note that, or explicitly asks to record a preference, decision, goal, or lesson.
|
||||
---
|
||||
|
||||
# Remember
|
||||
|
||||
Store a fact, preference, or learning directly into Mem0.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Extract the content
|
||||
|
||||
The user provides the content as an argument: `/mem0-remember <text>`
|
||||
|
||||
If no text was provided, ask: "What should I remember?"
|
||||
|
||||
### Step 2: Classify the memory
|
||||
|
||||
Based on the content, pick the best category:
|
||||
|
||||
| Content signal | Category |
|
||||
|---|---|
|
||||
| "I prefer...", "I like...", "use X instead of Y" | `preferences` |
|
||||
| "we decided...", "always use...", "never..." | `decisions` |
|
||||
| "I learned...", "figured out...", "don't try..." | `lessons` |
|
||||
| "my goal is...", "I want to...", "working toward..." | `goals` |
|
||||
| "I work at...", "my role is...", "my team..." | `work` |
|
||||
| "every day I...", "my workflow is..." | `routines` |
|
||||
| "I'm working on...", "the project involves..." | `projects` |
|
||||
| "John is...", "my manager...", "the team..." | `relationships` |
|
||||
| "my name is...", "I'm from...", "I studied..." | `identity` |
|
||||
| setup, tools, config, environment | `technical` |
|
||||
| anything else | `lessons` |
|
||||
|
||||
### Step 3: Store
|
||||
|
||||
Use the `mem0_memory` tool with:
|
||||
- `action="add"`
|
||||
- `content="<the user's text>"`
|
||||
|
||||
The `/mem0-remember` command stores verbatim — no inference. This is already handled by the command.
|
||||
|
||||
### Step 4: Confirm
|
||||
|
||||
```
|
||||
Remembered as <category>: "<content, first 80 chars>"
|
||||
```
|
||||
|
||||
Append `...` only if content was truncated (longer than 80 chars).
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
name: search
|
||||
description: Searches memories and displays compact one-liner results, or looks up a specific memory by ID. Use for quick memory lookups, checking if something was recorded, resolving [mem0:id] citations, or browsing memories without full category detail.
|
||||
---
|
||||
|
||||
# Search / Peek
|
||||
|
||||
Quick semantic search with compact output. Lighter than `/mem0-tour`.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Parse query
|
||||
|
||||
The user provides a search query: `/mem0-search favorite restaurants`
|
||||
|
||||
If no query provided, ask: "What should I search for?"
|
||||
|
||||
**Memory ID detection:** If the query matches a UUID pattern (`^[a-f0-9-]{20,}$`), treat it as a direct memory lookup instead of a search.
|
||||
|
||||
### Step 2: Search
|
||||
|
||||
Use `mem0_memory` tool with `action="search"`, `query=<user's query>`.
|
||||
|
||||
### Step 3: Display
|
||||
|
||||
Show compact results:
|
||||
|
||||
```
|
||||
## mem0 search: "<query>" (<N> results)
|
||||
|
||||
1. [preferences] Prefers window seats on flights (2026-05-15) [mem0:a3f8b2c1]
|
||||
2. [goals] Wants to visit Japan in 2027 (2026-05-10) [mem0:7e2d9f4a]
|
||||
3. [identity] Lives in San Francisco (2026-05-08) [mem0:c4d5e6f7]
|
||||
```
|
||||
|
||||
Format: `<number>. [<category>] <content, 80 chars> (<date>) [mem0:<short_id>]`
|
||||
|
||||
If no results:
|
||||
```
|
||||
No memories matching "<query>".
|
||||
```
|
||||
@@ -1,89 +0,0 @@
|
||||
---
|
||||
name: status
|
||||
description: Diagnoses Mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, or to verify the plugin is working correctly.
|
||||
---
|
||||
|
||||
# Health Check / Status
|
||||
|
||||
Run a diagnostic check on the Mem0 plugin. Useful for troubleshooting.
|
||||
|
||||
## Execution
|
||||
|
||||
Run ALL checks, then display a single summary. Do not stop on the first failure.
|
||||
|
||||
### Check 1: API key
|
||||
|
||||
Verify the API key is configured. The plugin loads it from `MEM0_API_KEY` env var or `~/.pi/agent/mem0-config.json`.
|
||||
|
||||
- If not set: FAIL — "No API key configured"
|
||||
- If set: PASS — show first 6 chars followed by `...`
|
||||
|
||||
### Check 2: Identity resolution
|
||||
|
||||
Report the resolved identity:
|
||||
- `user_id`: from config, env, or system user
|
||||
- `project_id`: auto-detected from current directory
|
||||
- `session_id`: current session identifier
|
||||
|
||||
PASS if user_id and project_id are non-empty. WARN if any falls back to defaults.
|
||||
|
||||
### Check 3: Connectivity
|
||||
|
||||
Use `mem0_memory` tool with `action="search"`, `query="health check"`.
|
||||
|
||||
- If returns successfully (even empty): PASS
|
||||
- If errors: FAIL — show the error message
|
||||
|
||||
### Check 4: Memory write capability
|
||||
|
||||
Use `mem0_memory` tool with `action="add"`, `content="Health check probe — safe to delete."`.
|
||||
|
||||
- If succeeds: PASS — then clean up by deleting the probe memory.
|
||||
- If errors: FAIL — show the error.
|
||||
|
||||
### Display
|
||||
|
||||
```
|
||||
## mem0 health
|
||||
|
||||
PASS API Key m0-dVe...
|
||||
PASS Identity user=kartik, project=my-app, session=abc123
|
||||
PASS Connectivity 142ms
|
||||
PASS Write/Read write + delete OK
|
||||
|
||||
All checks passed.
|
||||
```
|
||||
|
||||
If any check fails, add a `## Troubleshooting` section with specific fix steps.
|
||||
|
||||
## Extended mode: Memory Quality Analysis
|
||||
|
||||
When invoked with `--deep` (e.g., `/mem0-status --deep`), run the standard checks above **plus** a memory quality scan.
|
||||
|
||||
### Quality Check 1: Duplicates
|
||||
|
||||
Fetch all memories with `mem0_memory` `action="get_all"`. Compare pairs within the same category for high textual overlap (shared nouns > 60%). Report:
|
||||
|
||||
```
|
||||
Potential duplicates: <N> pairs
|
||||
[mem0:<id1>] ~ [mem0:<id2>] — both about "<shared topic>"
|
||||
```
|
||||
|
||||
### Quality Check 2: Stale memories
|
||||
|
||||
Flag memories older than 180 days that haven't been accessed recently.
|
||||
|
||||
### Quality Check 3: Contradictions
|
||||
|
||||
Within each category, flag pairs that assert opposing facts.
|
||||
|
||||
### Quality summary
|
||||
|
||||
```
|
||||
## Memory Quality
|
||||
|
||||
Duplicates: <N> · Stale: <N> · Contradictions: <N>
|
||||
```
|
||||
|
||||
If all counts are 0: `Memory quality: clean.`
|
||||
If any non-zero: append `Run /mem0-dream to fix.`
|
||||
@@ -1,88 +0,0 @@
|
||||
---
|
||||
name: tour
|
||||
description: Browses all stored memories grouped by category with full content display. Use when reviewing all memories, exploring stored knowledge, onboarding to a new session, or getting an overview of what the agent remembers.
|
||||
---
|
||||
|
||||
# Memory Tour
|
||||
|
||||
Show the user what Mem0 has stored — a full walkthrough of all memories grouped by category.
|
||||
|
||||
## Cross-project mode
|
||||
|
||||
When invoked with `--all-projects` (e.g., `/mem0-tour --all-projects`), search across ALL projects:
|
||||
|
||||
1. Use `mem0_memory` tool with `action="get_all"`, `scope="global"` — no project filter.
|
||||
2. Group results by project first, then by category within each project.
|
||||
3. Display:
|
||||
```
|
||||
## <project_1> (<N> memories) <- current
|
||||
**Goals** — <memory content>
|
||||
...
|
||||
|
||||
## <project_2> (<N> memories)
|
||||
...
|
||||
|
||||
<N> memories across <M> projects
|
||||
```
|
||||
4. Mark the current project with `<- current` in the heading.
|
||||
|
||||
If `--all-projects` is NOT present, use the standard single-project flow below.
|
||||
|
||||
## Search mode
|
||||
|
||||
When `/mem0-tour` receives a search query argument (e.g., `/mem0-tour cooking recipes`), run in **search mode** — compact one-liner results:
|
||||
|
||||
1. Use `mem0_memory` tool with `action="search"`, `query=<query>`.
|
||||
2. Display compact results (same format as the search skill).
|
||||
3. If no results: `No memories matching "<query>".`
|
||||
|
||||
If no query argument and no `--all-projects` flag, use the full tour flow below.
|
||||
|
||||
## Execution
|
||||
|
||||
### Step 1: Fetch ALL memories
|
||||
|
||||
Use `mem0_memory` tool with `action="get_all"`.
|
||||
|
||||
### Step 2: Group by category
|
||||
|
||||
Group memories using their `categories` field. Map to display names:
|
||||
|
||||
| Category | Display name |
|
||||
|---|---|
|
||||
| `identity` | Identity & Background |
|
||||
| `preferences` | Preferences |
|
||||
| `goals` | Goals & Aspirations |
|
||||
| `projects` | Projects & Initiatives |
|
||||
| `decisions` | Decisions |
|
||||
| `technical` | Technical Knowledge |
|
||||
| `relationships` | People & Relationships |
|
||||
| `routines` | Routines & Workflows |
|
||||
| `lessons` | Lessons Learned |
|
||||
| `work` | Work & Professional |
|
||||
| anything else | Other |
|
||||
|
||||
### Step 3: Display results
|
||||
|
||||
Sort groups by descending memory count. For each group:
|
||||
|
||||
```
|
||||
## <display_name> (<count> memories)
|
||||
- <full_memory_content> (<date>)
|
||||
- ...
|
||||
```
|
||||
|
||||
Show the **full memory text** for each entry — do NOT truncate. If a group has more than 10 entries, show top 10 by recency and note `... and <N> more`.
|
||||
|
||||
### Step 4: Print totals
|
||||
|
||||
```
|
||||
<N> memories across <M> categories
|
||||
```
|
||||
|
||||
### Step 5: Empty state
|
||||
|
||||
If zero memories found:
|
||||
```
|
||||
No memories stored yet. Start a conversation — Mem0 captures learnings automatically, or use /mem0-remember to store something manually.
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type MemoryClient from "mem0ai";
|
||||
import type { Mem0Config, ScopeContext } from "../types.ts";
|
||||
import { DEFAULT_CUSTOM_CATEGORIES } from "../types.ts";
|
||||
import { resolveAddParams } from "../memory/scoping.ts";
|
||||
import { captureEvent } from "../telemetry.ts";
|
||||
|
||||
interface MessageLike {
|
||||
role: string;
|
||||
content?: unknown;
|
||||
}
|
||||
|
||||
function extractText(content: unknown): string | null {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
const texts = content
|
||||
.filter((b: any) => b.type === "text" && typeof b.text === "string")
|
||||
.map((b: any) => b.text);
|
||||
return texts.length > 0 ? texts.join("\n") : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractConversation(
|
||||
messages: MessageLike[],
|
||||
): Array<{ role: "user" | "assistant"; content: string }> {
|
||||
const result: Array<{ role: "user" | "assistant"; content: string }> = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
||||
const text = extractText(msg.content);
|
||||
if (!text) continue;
|
||||
result.push({ role: msg.role as "user" | "assistant", content: text });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setupAutoCapture(
|
||||
pi: ExtensionAPI,
|
||||
mem0: MemoryClient,
|
||||
config: Mem0Config,
|
||||
getScopeCtx: () => ScopeContext,
|
||||
telemetryCtx?: { apiKey?: string },
|
||||
): void {
|
||||
if (!config.autoCapture) return;
|
||||
|
||||
pi.on("agent_end", async (event) => {
|
||||
const messages = event.messages ?? [];
|
||||
const conversation = extractConversation(messages);
|
||||
if (conversation.length === 0) return;
|
||||
|
||||
const scopeCtx = getScopeCtx();
|
||||
const addParams = resolveAddParams("project", scopeCtx);
|
||||
|
||||
try {
|
||||
await mem0.add(conversation, {
|
||||
...addParams,
|
||||
customCategories: DEFAULT_CUSTOM_CATEGORIES,
|
||||
});
|
||||
captureEvent("pi.capture.auto", { success: true, message_count: conversation.length }, telemetryCtx);
|
||||
} catch (err: unknown) {
|
||||
captureEvent("pi.capture.auto", {
|
||||
success: false,
|
||||
error_type: err instanceof Error ? err.name : "unknown",
|
||||
}, telemetryCtx);
|
||||
console.error("[mem0] auto-capture failed:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,510 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { registerCommands } from "./commands.ts";
|
||||
import type { Mem0Config, ScopeContext } from "./types.ts";
|
||||
|
||||
vi.mock("./telemetry.ts", () => ({
|
||||
captureCommandEvent: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./dream/index.ts", () => ({
|
||||
acquireDreamLock: vi.fn(() => true),
|
||||
}));
|
||||
|
||||
vi.mock("./dream/prompt.ts", () => ({
|
||||
DREAM_PROTOCOL: "dream protocol text",
|
||||
}));
|
||||
|
||||
function makeMem0() {
|
||||
return {
|
||||
search: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
add: vi.fn(),
|
||||
get: vi.fn(),
|
||||
getAll: vi.fn(),
|
||||
update: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
function makePi() {
|
||||
const commands = new Map<string, { handler: (args: string, ctx: any) => Promise<void> }>();
|
||||
return {
|
||||
registerCommand: vi.fn((name: string, opts: any) => {
|
||||
commands.set(name, opts);
|
||||
}),
|
||||
sendMessage: vi.fn(),
|
||||
_commands: commands,
|
||||
_invoke: (name: string, args: string, ctx: any) => commands.get(name)!.handler(args, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
function makeCtx(confirmResult = true) {
|
||||
return {
|
||||
hasUI: true,
|
||||
ui: {
|
||||
notify: vi.fn(),
|
||||
confirm: vi.fn(async () => confirmResult),
|
||||
select: vi.fn(),
|
||||
input: vi.fn(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConfig: Mem0Config = {
|
||||
apiKey: "test-key",
|
||||
userId: "test-user",
|
||||
autoCapture: false,
|
||||
defaultScope: "project",
|
||||
contextInjection: false,
|
||||
searchThreshold: 0.3,
|
||||
dream: { enabled: false, auto: false, minHours: 24, minSessions: 5, minMemories: 20 },
|
||||
};
|
||||
|
||||
const scopeCtx: ScopeContext = { userId: "test-user", appId: "test-app", runId: "test-run" };
|
||||
|
||||
describe("registerCommands", () => {
|
||||
let pi: ReturnType<typeof makePi>;
|
||||
let mem0: ReturnType<typeof makeMem0>;
|
||||
|
||||
beforeEach(() => {
|
||||
pi = makePi();
|
||||
mem0 = makeMem0();
|
||||
defaultConfig.defaultScope = "project";
|
||||
registerCommands(pi as any, mem0, defaultConfig, () => scopeCtx);
|
||||
});
|
||||
|
||||
it("registers all expected commands", () => {
|
||||
const names = [...pi._commands.keys()];
|
||||
expect(names).toContain("mem0-remember");
|
||||
expect(names).toContain("mem0-forget");
|
||||
expect(names).toContain("mem0-search");
|
||||
expect(names).toContain("mem0-tour");
|
||||
expect(names).toContain("mem0-dream");
|
||||
expect(names).toContain("mem0-pin");
|
||||
expect(names).toContain("mem0-scope");
|
||||
expect(names).toContain("mem0-status");
|
||||
});
|
||||
|
||||
describe("/mem0-forget", () => {
|
||||
it("shows warning when no query provided", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-forget", "", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-forget <query>", "warning");
|
||||
expect(mem0.search).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends a visible message naming the query when no memories match", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [] });
|
||||
await pi._invoke("mem0-forget", "old preference", ctx);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining('No matches for "old preference"'),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("asks for confirmation before deleting a single match", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
|
||||
mem0.delete.mockResolvedValue({ message: "Deleted" });
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(ctx.ui.confirm).toHaveBeenCalledWith(
|
||||
"Delete this memory?",
|
||||
expect.stringContaining("test mem"),
|
||||
);
|
||||
expect(mem0.delete).toHaveBeenCalledWith("abc-123");
|
||||
});
|
||||
|
||||
it("sends a visible confirmation showing what was forgotten", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
|
||||
mem0.delete.mockResolvedValue({ message: "Deleted" });
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining("Forgotten"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not delete when user cancels confirmation", async () => {
|
||||
const ctx = makeCtx(false);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] });
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(ctx.ui.confirm).toHaveBeenCalled();
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses select UI for multiple matches and deletes chosen memory", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "mem one" },
|
||||
{ id: "id-2", memory: "mem two" },
|
||||
],
|
||||
});
|
||||
mem0.delete.mockResolvedValue({ message: "Deleted" });
|
||||
ctx.ui.select = vi.fn(async (_title: string, options: string[]) => options[1]);
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(ctx.ui.select).toHaveBeenCalledWith(
|
||||
expect.stringContaining("which should I delete"),
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("mem one"),
|
||||
expect.stringContaining("mem two"),
|
||||
]),
|
||||
);
|
||||
expect(mem0.delete).toHaveBeenCalledWith("id-2");
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-forget",
|
||||
content: expect.stringContaining("Forgotten"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not delete when user cancels select", async () => {
|
||||
const ctx = makeCtx();
|
||||
ctx.ui.select = vi.fn(async () => undefined);
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "mem one" },
|
||||
{ id: "id-2", memory: "mem two" },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-forget", "test", ctx);
|
||||
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-pin", () => {
|
||||
it("uses update to pin in-place, preserving memory ID", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] });
|
||||
mem0.update.mockResolvedValue([]);
|
||||
|
||||
await pi._invoke("mem0-pin", "important", ctx);
|
||||
|
||||
expect(ctx.ui.confirm).toHaveBeenCalledWith(
|
||||
"Pin this memory?",
|
||||
expect.stringContaining("important fact"),
|
||||
);
|
||||
expect(mem0.update).toHaveBeenCalledWith("abc-123", { text: "[PINNED] important fact" });
|
||||
expect(mem0.add).not.toHaveBeenCalled();
|
||||
expect(mem0.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends a visible confirmation after pinning", async () => {
|
||||
const ctx = makeCtx(true);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] });
|
||||
mem0.update.mockResolvedValue([]);
|
||||
|
||||
await pi._invoke("mem0-pin", "important", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-pin",
|
||||
content: expect.stringContaining("Pinned"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not pin when user cancels", async () => {
|
||||
const ctx = makeCtx(false);
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "fact" }] });
|
||||
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(mem0.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("skips already-pinned memories with a visible message", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "[PINNED] fact" }] });
|
||||
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(ctx.ui.confirm).not.toHaveBeenCalled();
|
||||
expect(mem0.add).not.toHaveBeenCalled();
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Already pinned"), display: true }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses select UI for multiple matches and pins chosen memory", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "fact one" },
|
||||
{ id: "id-2", memory: "fact two" },
|
||||
],
|
||||
});
|
||||
mem0.update.mockResolvedValue([]);
|
||||
ctx.ui.select = vi.fn(async (_title: string, options: string[]) => options[1]);
|
||||
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(ctx.ui.select).toHaveBeenCalledWith(
|
||||
expect.stringContaining("which should I pin"),
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("fact one"),
|
||||
expect.stringContaining("fact two"),
|
||||
]),
|
||||
);
|
||||
expect(mem0.update).toHaveBeenCalledWith("id-2", { text: "[PINNED] fact two" });
|
||||
});
|
||||
|
||||
it("does not pin when user cancels select", async () => {
|
||||
const ctx = makeCtx();
|
||||
ctx.ui.select = vi.fn(async () => undefined);
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "fact one" },
|
||||
{ id: "id-2", memory: "fact two" },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-pin", "fact", ctx);
|
||||
|
||||
expect(mem0.update).not.toHaveBeenCalled();
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-search", () => {
|
||||
it("performs server-side semantic search with a relevance threshold", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [{ id: "id-1", memory: "result" }] });
|
||||
|
||||
await pi._invoke("mem0-search", "my preferences", ctx);
|
||||
|
||||
expect(mem0.search).toHaveBeenCalledWith(
|
||||
"my preferences",
|
||||
expect.objectContaining({ threshold: 0.3, topK: 10, rerank: true }),
|
||||
);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customType: "mem0-search" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses semantic search even for hex-looking strings", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [] });
|
||||
|
||||
await pi._invoke("mem0-search", "abcd1234", ctx);
|
||||
|
||||
expect(mem0.search).toHaveBeenCalledWith("abcd1234", expect.any(Object));
|
||||
expect(mem0.getAll).not.toHaveBeenCalled();
|
||||
expect(mem0.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a no-matches message naming the query", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({ results: [] });
|
||||
|
||||
await pi._invoke("mem0-search", "nonexistent", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("No matches") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a result count header when there are matches", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "one" },
|
||||
{ id: "id-2", memory: "two" },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-search", "stuff", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ content: expect.stringContaining("2 matches") }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows all results the API returns (relevance gating is server-side)", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.search.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "first match", score: 0.62 },
|
||||
{ id: "id-2", memory: "second match", score: 0.31 },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-search", "stuff", ctx);
|
||||
|
||||
const call = pi.sendMessage.mock.calls.find(([m]: any[]) => m.customType === "mem0-search");
|
||||
expect(call?.[0].content).toContain("first match");
|
||||
expect(call?.[0].content).toContain("second match");
|
||||
expect(call?.[0].content).toContain("2 matches");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-remember", () => {
|
||||
it("stores a memory verbatim", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.add.mockResolvedValue({ message: "Memory stored." });
|
||||
|
||||
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
|
||||
|
||||
expect(mem0.add).toHaveBeenCalledWith(
|
||||
[{ role: "user", content: "I prefer dark mode" }],
|
||||
expect.objectContaining({ infer: false }),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the stored text in a visible confirmation (infer:false status response)", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.add.mockResolvedValue({ message: "Memories stored successfully" });
|
||||
|
||||
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-remember",
|
||||
content: expect.stringContaining("I prefer dark mode"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("lists memory objects returned by the API when present", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.add.mockResolvedValue([{ id: "m1", memory: "Uses dark mode", event: "ADD" }]);
|
||||
|
||||
await pi._invoke("mem0-remember", "I prefer dark mode", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-remember",
|
||||
content: expect.stringContaining("Uses dark mode"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows warning when no text provided", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-remember", " ", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-remember <text>", "warning");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-scope", () => {
|
||||
it("sends a visible message showing the current scope when no arg is given", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "", ctx);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-scope",
|
||||
content: expect.stringContaining("Current scope:"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends a visible confirmation after changing scope", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "global", ctx);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-scope",
|
||||
content: expect.stringContaining("Scope changed to global"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("warns on an invalid scope", async () => {
|
||||
const ctx = makeCtx();
|
||||
await pi._invoke("mem0-scope", "bogus", ctx);
|
||||
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Invalid scope "bogus"'),
|
||||
"warning",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-tour", () => {
|
||||
it("shows an empty-state message when there are no memories", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.getAll.mockResolvedValue({ results: [] });
|
||||
|
||||
await pi._invoke("mem0-tour", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-tour",
|
||||
content: expect.stringContaining("No memories"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("groups memories by category with a count header", async () => {
|
||||
const ctx = makeCtx();
|
||||
mem0.getAll.mockResolvedValue({
|
||||
results: [
|
||||
{ id: "id-1", memory: "likes tea", categories: ["preferences"] },
|
||||
{ id: "id-2", memory: "uses vim", categories: ["technical"] },
|
||||
],
|
||||
});
|
||||
|
||||
await pi._invoke("mem0-tour", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-tour",
|
||||
content: expect.stringContaining("Memory tour"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("/mem0-dream", () => {
|
||||
it("feeds the protocol to the agent and shows a clean status line", async () => {
|
||||
const ctx = makeCtx();
|
||||
|
||||
await pi._invoke("mem0-dream", "", ctx);
|
||||
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ customType: "mem0-dream", display: false }),
|
||||
expect.objectContaining({ triggerTurn: true }),
|
||||
);
|
||||
expect(pi.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
customType: "mem0-dream",
|
||||
content: expect.stringContaining("Dreaming"),
|
||||
display: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,339 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import type MemoryClient from "mem0ai";
|
||||
import type { Mem0Config, ScopeContext, Scope } from "./types.ts";
|
||||
import { DEFAULT_CUSTOM_CATEGORIES } from "./types.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./memory/scoping.ts";
|
||||
import { formatMemoryList, formatMemoryCompact, groupByCategory } from "./memory/formatting.ts";
|
||||
import { DREAM_PROTOCOL } from "./dream/prompt.ts";
|
||||
import { acquireDreamLock } from "./dream/index.ts";
|
||||
import { CONFIG_DIR } from "./config/index.ts";
|
||||
import { captureCommandEvent } from "./telemetry.ts";
|
||||
|
||||
const SEARCH_TOP_K = 10;
|
||||
|
||||
export function registerCommands(
|
||||
pi: ExtensionAPI,
|
||||
mem0: MemoryClient,
|
||||
config: Mem0Config,
|
||||
getScopeCtx: () => ScopeContext,
|
||||
telemetryCtx?: { apiKey?: string },
|
||||
): void {
|
||||
const sendFeedback = (customType: string, content: string): void => {
|
||||
pi.sendMessage({ customType, content, display: true });
|
||||
};
|
||||
|
||||
const pluralize = (n: number, one: string, many: string): string =>
|
||||
`${n} ${n === 1 ? one : many}`;
|
||||
|
||||
const searchMemories = async (query: string, scope: Scope) => {
|
||||
const filters = resolveSearchFilters(scope, getScopeCtx());
|
||||
const result = await mem0.search(query, {
|
||||
filters,
|
||||
threshold: config.searchThreshold,
|
||||
topK: SEARCH_TOP_K,
|
||||
rerank: true,
|
||||
});
|
||||
return result.results ?? [];
|
||||
};
|
||||
|
||||
pi.registerCommand("mem0-remember", {
|
||||
description: "Store a memory verbatim (no inference)",
|
||||
handler: async (args, ctx) => {
|
||||
const text = args?.trim();
|
||||
if (!text) {
|
||||
ctx.ui.notify("Usage: /mem0-remember <text>", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const addParams = resolveAddParams(config.defaultScope, getScopeCtx());
|
||||
const result = await mem0.add(
|
||||
[{ role: "user", content: text }],
|
||||
{ ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES, infer: false },
|
||||
);
|
||||
captureCommandEvent("mem0-remember", {}, telemetryCtx);
|
||||
|
||||
const storedItems = (Array.isArray(result) ? result : [])
|
||||
.map((m) => (m as { memory?: string }).memory)
|
||||
.filter((m): m is string => Boolean(m));
|
||||
const items = storedItems.length > 0 ? storedItems : [text];
|
||||
sendFeedback(
|
||||
"mem0-remember",
|
||||
[`**Stored to ${config.defaultScope} memory**`, ...items.map((m) => `- ${m}`)].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-forget", {
|
||||
description: "Delete memories matching a natural language query",
|
||||
handler: async (args, ctx) => {
|
||||
const query = args?.trim();
|
||||
if (!query) {
|
||||
ctx.ui.notify("Usage: /mem0-forget <query>", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-forget", { result_count: 0 }, telemetryCtx);
|
||||
sendFeedback("mem0-forget", `**No matches for "${query}"** — nothing to forget.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const forgotten = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx);
|
||||
sendFeedback(
|
||||
"mem0-forget",
|
||||
[`**Forgotten from ${config.defaultScope} memory**`, `- ${formatMemoryCompact(mem)}`].join("\n"),
|
||||
);
|
||||
};
|
||||
|
||||
if (memories.length === 1) {
|
||||
const target = memories[0];
|
||||
const confirmed = await ctx.ui.confirm("Delete this memory?", formatMemoryCompact(target));
|
||||
if (!confirmed) {
|
||||
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
|
||||
return;
|
||||
}
|
||||
await mem0.delete(target.id);
|
||||
forgotten(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = memories.map((m) => formatMemoryCompact(m));
|
||||
const selected = await ctx.ui.select(
|
||||
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I delete?`,
|
||||
labels,
|
||||
);
|
||||
if (!selected) {
|
||||
sendFeedback("mem0-forget", "**Cancelled** — no memories deleted.");
|
||||
return;
|
||||
}
|
||||
const idx = labels.indexOf(selected);
|
||||
if (idx < 0) return;
|
||||
const target = memories[idx];
|
||||
await mem0.delete(target.id);
|
||||
forgotten(target);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-search", {
|
||||
description: "Semantic search across memories",
|
||||
handler: async (args, ctx) => {
|
||||
const query = args?.trim();
|
||||
if (!query) {
|
||||
ctx.ui.notify("Usage: /mem0-search <query>", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
captureCommandEvent("mem0-search", { result_count: memories.length }, telemetryCtx);
|
||||
|
||||
if (memories.length === 0) {
|
||||
sendFeedback("mem0-search", `**No matches for "${query}"** · ${config.defaultScope} scope`);
|
||||
return;
|
||||
}
|
||||
|
||||
sendFeedback(
|
||||
"mem0-search",
|
||||
[
|
||||
`**${pluralize(memories.length, "match", "matches")} for "${query}"** · ${config.defaultScope} scope`,
|
||||
"",
|
||||
formatMemoryList(memories),
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-tour", {
|
||||
description: "Browse all memories grouped by category",
|
||||
handler: async (args, ctx) => {
|
||||
const raw = args?.trim().toLowerCase();
|
||||
const validScopes: Scope[] = ["project", "session", "global"];
|
||||
if (raw && !validScopes.includes(raw as Scope)) {
|
||||
ctx.ui.notify(`Invalid scope "${raw}". Must be one of: ${validScopes.join(", ")}`, "warning");
|
||||
return;
|
||||
}
|
||||
const scope: Scope = (raw as Scope) || config.defaultScope;
|
||||
const filters = resolveSearchFilters(scope, getScopeCtx());
|
||||
const result = await mem0.getAll({ filters });
|
||||
const memories = result.results ?? [];
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-tour", { memory_count: 0, scope }, telemetryCtx);
|
||||
sendFeedback("mem0-tour", `**No memories in ${scope} scope yet** — store one with \`/mem0-remember\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const groups = groupByCategory(memories);
|
||||
const lines: string[] = [
|
||||
`**Memory tour** · ${pluralize(memories.length, "memory", "memories")} · ${scope} scope`,
|
||||
"",
|
||||
];
|
||||
|
||||
for (const [category, items] of groups) {
|
||||
lines.push(`### ${category} (${items.length})`);
|
||||
for (const m of items) {
|
||||
lines.push(`- ${formatMemoryCompact(m)}`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
captureCommandEvent("mem0-tour", { memory_count: memories.length, scope }, telemetryCtx);
|
||||
sendFeedback("mem0-tour", lines.join("\n"));
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-dream", {
|
||||
description: "Consolidate memories — merge duplicates, prune stale entries, resolve contradictions",
|
||||
handler: async (_args, ctx) => {
|
||||
if (!acquireDreamLock(CONFIG_DIR)) {
|
||||
ctx.ui.notify("A dream consolidation is already in progress.", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
captureCommandEvent("mem0-dream", {}, telemetryCtx);
|
||||
pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: false }, { triggerTurn: true });
|
||||
sendFeedback(
|
||||
"mem0-dream",
|
||||
"**Dreaming** — reviewing your memories to merge duplicates, resolve contradictions, and prune stale entries. I'll report what changed.",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-pin", {
|
||||
description: "Pin a memory to protect it from dream pruning",
|
||||
handler: async (args, ctx) => {
|
||||
const query = args?.trim();
|
||||
if (!query) {
|
||||
ctx.ui.notify("Usage: /mem0-pin <query>", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
const memories = await searchMemories(query, config.defaultScope);
|
||||
|
||||
if (memories.length === 0) {
|
||||
captureCommandEvent("mem0-pin", { result_count: 0 }, telemetryCtx);
|
||||
sendFeedback("mem0-pin", `**No matches for "${query}"** — nothing to pin.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const pinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx);
|
||||
sendFeedback(
|
||||
"mem0-pin",
|
||||
["**Pinned** — protected from dream pruning", `- ${formatMemoryCompact(mem)}`].join("\n"),
|
||||
);
|
||||
};
|
||||
const alreadyPinned = (mem: Parameters<typeof formatMemoryCompact>[0]) => {
|
||||
sendFeedback("mem0-pin", ["**Already pinned**", `- ${formatMemoryCompact(mem)}`].join("\n"));
|
||||
};
|
||||
|
||||
if (memories.length === 1) {
|
||||
const target = memories[0];
|
||||
const text = target.memory ?? "";
|
||||
if (text.startsWith("[PINNED]")) {
|
||||
alreadyPinned(target);
|
||||
return;
|
||||
}
|
||||
const confirmed = await ctx.ui.confirm("Pin this memory?", formatMemoryCompact(target));
|
||||
if (!confirmed) {
|
||||
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
|
||||
return;
|
||||
}
|
||||
await mem0.update(target.id, { text: `[PINNED] ${text}` });
|
||||
pinned(target);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = memories.map((m) => formatMemoryCompact(m));
|
||||
const selected = await ctx.ui.select(
|
||||
`Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I pin?`,
|
||||
labels,
|
||||
);
|
||||
if (!selected) {
|
||||
sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned.");
|
||||
return;
|
||||
}
|
||||
const idx = labels.indexOf(selected);
|
||||
if (idx < 0) return;
|
||||
const target = memories[idx];
|
||||
const selectedText = target.memory ?? "";
|
||||
if (selectedText.startsWith("[PINNED]")) {
|
||||
alreadyPinned(target);
|
||||
return;
|
||||
}
|
||||
await mem0.update(target.id, { text: `[PINNED] ${selectedText}` });
|
||||
pinned(target);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-scope", {
|
||||
description: "Change default memory scope for this session (project, session, global)",
|
||||
handler: async (args, ctx) => {
|
||||
const scope = args?.trim().toLowerCase();
|
||||
const valid: Scope[] = ["project", "session", "global"];
|
||||
|
||||
if (!scope) {
|
||||
sendFeedback(
|
||||
"mem0-scope",
|
||||
[
|
||||
`**Current scope: ${config.defaultScope}**`,
|
||||
`New memories save to the **${config.defaultScope}** pool. Switch with \`/mem0-scope <${valid.join(" | ")}>\`.`,
|
||||
].join("\n"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valid.includes(scope as Scope)) {
|
||||
ctx.ui.notify(`Invalid scope "${scope}". Must be one of: ${valid.join(", ")}`, "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
config.defaultScope = scope as Scope;
|
||||
captureCommandEvent("mem0-scope", { scope }, telemetryCtx);
|
||||
sendFeedback(
|
||||
"mem0-scope",
|
||||
[
|
||||
`**Scope changed to ${scope}**`,
|
||||
`New memories now save to the **${scope}** pool for this session.`,
|
||||
].join("\n"),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("mem0-status", {
|
||||
description: "Show connection health, identity, project, and memory count",
|
||||
handler: async (_args, _ctx) => {
|
||||
const scopeCtx = getScopeCtx();
|
||||
const filters = resolveSearchFilters("project", scopeCtx);
|
||||
|
||||
let count = 0;
|
||||
let connected = false;
|
||||
try {
|
||||
const result = await mem0.getAll({ filters });
|
||||
count = result.count ?? (result.results ?? []).length;
|
||||
connected = true;
|
||||
} catch {
|
||||
connected = false;
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"**Mem0 status**",
|
||||
"",
|
||||
`- Connection: ${connected ? "connected" : "disconnected"}`,
|
||||
`- User: ${scopeCtx.userId}`,
|
||||
`- Project: ${scopeCtx.appId}`,
|
||||
`- Session: ${scopeCtx.runId}`,
|
||||
`- Default scope: ${config.defaultScope}`,
|
||||
`- Search relevance threshold: ${config.searchThreshold}`,
|
||||
`- Project memories: ${count}`,
|
||||
`- Auto-capture: ${config.autoCapture ? "on" : "off"}`,
|
||||
`- Dream: ${config.dream.enabled ? "enabled" : "disabled"}`,
|
||||
];
|
||||
|
||||
captureCommandEvent("mem0-status", { connected, memory_count: count }, telemetryCtx);
|
||||
sendFeedback("mem0-status", lines.join("\n"));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { Mem0Config, DreamConfig } from "../types.ts";
|
||||
|
||||
const AGENT_ROOT = path.join(os.homedir(), ".pi", "agent");
|
||||
export const CONFIG_DIR = AGENT_ROOT;
|
||||
const CONFIG_PATH = path.join(AGENT_ROOT, "mem0-config.json");
|
||||
|
||||
const DEFAULT_DREAM: DreamConfig = {
|
||||
enabled: true,
|
||||
auto: true,
|
||||
minHours: 24,
|
||||
minSessions: 5,
|
||||
minMemories: 20,
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: Mem0Config = {
|
||||
apiKey: "",
|
||||
userId: "",
|
||||
autoCapture: true,
|
||||
defaultScope: "project",
|
||||
contextInjection: false,
|
||||
searchThreshold: 0.3,
|
||||
dream: DEFAULT_DREAM,
|
||||
};
|
||||
|
||||
export function loadConfig(): Mem0Config {
|
||||
let fileConfig: Partial<Mem0Config> = {};
|
||||
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
try {
|
||||
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
||||
fileConfig = JSON.parse(raw);
|
||||
} catch {
|
||||
// Corrupted config — use defaults
|
||||
}
|
||||
}
|
||||
|
||||
const dream: DreamConfig = {
|
||||
...DEFAULT_DREAM,
|
||||
...(fileConfig.dream ?? {}),
|
||||
};
|
||||
|
||||
const config: Mem0Config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...fileConfig,
|
||||
dream,
|
||||
};
|
||||
|
||||
if (process.env.MEM0_API_KEY) {
|
||||
config.apiKey = process.env.MEM0_API_KEY;
|
||||
}
|
||||
if (process.env.MEM0_USER_ID) {
|
||||
config.userId = process.env.MEM0_USER_ID;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { DreamState, DreamLock, DreamConfig } from "../types.ts";
|
||||
|
||||
const LOCK_STALE_MS = 60 * 60 * 1000;
|
||||
|
||||
const DEFAULTS: DreamConfig = {
|
||||
enabled: true,
|
||||
auto: true,
|
||||
minHours: 24,
|
||||
minSessions: 5,
|
||||
minMemories: 20,
|
||||
};
|
||||
|
||||
function statePath(stateDir: string): string {
|
||||
return path.join(stateDir, "mem0-dream-state.json");
|
||||
}
|
||||
|
||||
function lockPath(stateDir: string): string {
|
||||
return path.join(stateDir, "mem0-dream.lock");
|
||||
}
|
||||
|
||||
function ensureDir(dir: string): void {
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
} catch { /* exists */ }
|
||||
}
|
||||
|
||||
function readState(stateDir: string): DreamState {
|
||||
try {
|
||||
const raw = fs.readFileSync(statePath(stateDir), "utf-8");
|
||||
return JSON.parse(raw) as DreamState;
|
||||
} catch {
|
||||
return { lastConsolidatedAt: 0, sessionsSince: 0, lastSessionId: null };
|
||||
}
|
||||
}
|
||||
|
||||
function writeState(stateDir: string, state: DreamState): void {
|
||||
ensureDir(stateDir);
|
||||
fs.writeFileSync(statePath(stateDir), JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
export function incrementSessionCount(stateDir: string, sessionId: string): void {
|
||||
const state = readState(stateDir);
|
||||
if (state.lastSessionId !== sessionId) {
|
||||
state.sessionsSince++;
|
||||
state.lastSessionId = sessionId;
|
||||
writeState(stateDir, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkCheapGates(
|
||||
stateDir: string,
|
||||
config: Partial<DreamConfig>,
|
||||
): { proceed: boolean; reason?: string } {
|
||||
const minHours = config.minHours ?? DEFAULTS.minHours;
|
||||
const minSessions = config.minSessions ?? DEFAULTS.minSessions;
|
||||
const state = readState(stateDir);
|
||||
|
||||
const hoursSince = (Date.now() - state.lastConsolidatedAt) / 3_600_000;
|
||||
if (hoursSince < minHours) {
|
||||
return { proceed: false, reason: `time: ${hoursSince.toFixed(1)}h < ${minHours}h` };
|
||||
}
|
||||
|
||||
if (state.sessionsSince < minSessions) {
|
||||
return { proceed: false, reason: `sessions: ${state.sessionsSince} < ${minSessions}` };
|
||||
}
|
||||
|
||||
return { proceed: true };
|
||||
}
|
||||
|
||||
export function checkMemoryGate(
|
||||
memoryCount: number,
|
||||
config: Partial<DreamConfig>,
|
||||
): { pass: boolean; reason?: string } {
|
||||
const minMemories = config.minMemories ?? DEFAULTS.minMemories;
|
||||
if (memoryCount < minMemories) {
|
||||
return { pass: false, reason: `memories: ${memoryCount} < ${minMemories}` };
|
||||
}
|
||||
return { pass: true };
|
||||
}
|
||||
|
||||
export function acquireDreamLock(stateDir: string): boolean {
|
||||
ensureDir(stateDir);
|
||||
const lp = lockPath(stateDir);
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(lp, "utf-8");
|
||||
const lock = JSON.parse(raw) as DreamLock;
|
||||
if (Date.now() - lock.startedAt < LOCK_STALE_MS) {
|
||||
return false;
|
||||
}
|
||||
try { fs.unlinkSync(lp); } catch { /* race ok */ }
|
||||
} catch { /* no lock file */ }
|
||||
|
||||
const lock: DreamLock = { pid: process.pid, startedAt: Date.now() };
|
||||
try {
|
||||
fs.writeFileSync(lp, JSON.stringify(lock), { flag: "wx" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function releaseDreamLock(stateDir: string): void {
|
||||
try { fs.unlinkSync(lockPath(stateDir)); } catch { /* already gone */ }
|
||||
}
|
||||
|
||||
export function recordDreamCompletion(stateDir: string): void {
|
||||
const state = readState(stateDir);
|
||||
state.lastConsolidatedAt = Date.now();
|
||||
state.sessionsSince = 0;
|
||||
state.lastSessionId = null;
|
||||
writeState(stateDir, state);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export const DREAM_PROTOCOL = `<mem0-dream>
|
||||
You are running memory consolidation. Complete these steps using the mem0_memory tool:
|
||||
|
||||
1. ORIENT — Call mem0_memory with action "get_all" to list all memories. Count by category. Note oldest/newest.
|
||||
|
||||
2. GATHER TARGETS — Review each memory. Classify as:
|
||||
- DELETE: sensitive information (API keys, passwords, tokens), expired/stale entries, noise, redundant operational details
|
||||
- MERGE: near-duplicates (same fact stated differently). Keep the better-worded one, delete the other.
|
||||
- REWRITE: vague, first-person, or poorly-categorized entries. Use mem0_memory "add" with improved text, then "delete" the old one.
|
||||
- KEEP: everything else.
|
||||
Skip any memory starting with "[PINNED]".
|
||||
|
||||
3. CONSOLIDATE — Execute the changes:
|
||||
- Delete stale/duplicate entries
|
||||
- For merges: add the merged text, delete both originals
|
||||
- For rewrites: add improved version, delete original
|
||||
|
||||
4. REPORT — Summarize: how many reviewed, deleted, merged, rewritten, final count.
|
||||
|
||||
Quality targets: zero sensitive data stored, zero duplicates, all entries are atomic (one fact each), 15-50 words each.
|
||||
After consolidation, respond to the user's message normally.
|
||||
</mem0-dream>`;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { resolveUserId } from "./entry.ts";
|
||||
|
||||
describe("resolveUserId", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
it("returns config userId when set", () => {
|
||||
expect(resolveUserId("config-user")).toBe("config-user");
|
||||
});
|
||||
|
||||
it("falls back to USER env var", () => {
|
||||
process.env.USER = "env-user";
|
||||
delete process.env.USERNAME;
|
||||
expect(resolveUserId("")).toBe("env-user");
|
||||
});
|
||||
|
||||
it("falls back to USERNAME env var on Windows", () => {
|
||||
delete process.env.USER;
|
||||
process.env.USERNAME = "win-user";
|
||||
expect(resolveUserId("")).toBe("win-user");
|
||||
});
|
||||
|
||||
it("falls back to os.userInfo() when env vars are missing", () => {
|
||||
delete process.env.USER;
|
||||
delete process.env.USERNAME;
|
||||
const result = resolveUserId("");
|
||||
expect(typeof result).toBe("string");
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import MemoryClient from "mem0ai";
|
||||
import { loadConfig, CONFIG_DIR } from "./config/index.ts";
|
||||
import { detectAppId, detectRunId, resolveSearchFilters } from "./memory/scoping.ts";
|
||||
import { registerMemoryTool } from "./memory/tools.ts";
|
||||
import { registerCommands } from "./commands.ts";
|
||||
import { setupAutoCapture } from "./capture/index.ts";
|
||||
import { MEMORY_POLICY } from "./prompt.ts";
|
||||
import { DREAM_PROTOCOL } from "./dream/prompt.ts";
|
||||
import {
|
||||
incrementSessionCount,
|
||||
checkCheapGates,
|
||||
checkMemoryGate,
|
||||
acquireDreamLock,
|
||||
releaseDreamLock,
|
||||
recordDreamCompletion,
|
||||
} from "./dream/index.ts";
|
||||
import { captureEvent } from "./telemetry.ts";
|
||||
import * as os from "node:os";
|
||||
import type { ScopeContext } from "./types.ts";
|
||||
|
||||
export function resolveUserId(configUserId: string): string {
|
||||
if (configUserId) return configUserId;
|
||||
if (process.env.USER) return process.env.USER;
|
||||
if (process.env.USERNAME) return process.env.USERNAME;
|
||||
try { return os.userInfo().username; } catch { return "default"; }
|
||||
}
|
||||
|
||||
export default function mem0Extension(pi: ExtensionAPI): void {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!config.apiKey) {
|
||||
console.warn("[mem0] No API key found. Set MEM0_API_KEY or add apiKey to ~/.pi/agent/mem0-config.json. Extension disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const mem0 = new MemoryClient({ apiKey: config.apiKey });
|
||||
|
||||
const scopeCtx: ScopeContext = {
|
||||
userId: resolveUserId(config.userId),
|
||||
appId: "",
|
||||
runId: "unknown",
|
||||
};
|
||||
|
||||
function getScopeCtx(): ScopeContext {
|
||||
return scopeCtx;
|
||||
}
|
||||
|
||||
const telemetryCtx = { apiKey: config.apiKey };
|
||||
|
||||
// ── Register tool + commands + auto-capture ─────────────────────────
|
||||
registerMemoryTool(pi, mem0, config, getScopeCtx, telemetryCtx);
|
||||
registerCommands(pi, mem0, config, getScopeCtx, telemetryCtx);
|
||||
setupAutoCapture(pi, mem0, config, getScopeCtx, telemetryCtx);
|
||||
|
||||
captureEvent("pi.plugin.registered", {
|
||||
auto_capture: config.autoCapture,
|
||||
dream_enabled: config.dream.enabled,
|
||||
default_scope: config.defaultScope,
|
||||
}, telemetryCtx);
|
||||
|
||||
// ── session_start: detect project + session, reconstruct scope ──────
|
||||
pi.on("session_start", async (_event, ctx) => {
|
||||
scopeCtx.appId = detectAppId(ctx.cwd);
|
||||
|
||||
const sessionFile = ctx.sessionManager?.getSessionFile?.();
|
||||
scopeCtx.runId = detectRunId(sessionFile);
|
||||
|
||||
if (config.userId) {
|
||||
scopeCtx.userId = config.userId;
|
||||
}
|
||||
|
||||
if (config.dream.enabled) {
|
||||
incrementSessionCount(CONFIG_DIR, scopeCtx.runId);
|
||||
}
|
||||
|
||||
captureEvent("pi.session.start", {}, telemetryCtx);
|
||||
});
|
||||
|
||||
// ── before_agent_start: append memory policy + auto-dream trigger ───
|
||||
let dreamTriggered = false;
|
||||
let dreamChecked = false;
|
||||
|
||||
pi.on("before_agent_start", async (event, _ctx) => {
|
||||
let extra = MEMORY_POLICY;
|
||||
|
||||
if (config.dream.enabled && config.dream.auto && !dreamTriggered && !dreamChecked) {
|
||||
const gates = checkCheapGates(CONFIG_DIR, config.dream);
|
||||
if (gates.proceed) {
|
||||
try {
|
||||
const filters = resolveSearchFilters("project", scopeCtx);
|
||||
const result = await mem0.getAll({ filters });
|
||||
const count = result.count ?? (result.results ?? []).length;
|
||||
dreamChecked = true;
|
||||
const memGate = checkMemoryGate(count, config.dream);
|
||||
|
||||
if (memGate.pass && acquireDreamLock(CONFIG_DIR)) {
|
||||
dreamTriggered = true;
|
||||
extra += "\n\n" + DREAM_PROTOCOL;
|
||||
captureEvent("pi.dream.triggered", { memory_count: count }, telemetryCtx);
|
||||
}
|
||||
} catch {
|
||||
// Transient error — retry next turn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
systemPrompt: (event.systemPrompt ?? "") + "\n\n" + extra,
|
||||
};
|
||||
});
|
||||
|
||||
// ── agent_end: dream completion check ───────────────────────────────
|
||||
pi.on("agent_end", async (event) => {
|
||||
if (!dreamTriggered) return;
|
||||
|
||||
const messages = event.messages ?? [];
|
||||
const hadWriteAction = messages.some((m) => {
|
||||
if (m.role !== "assistant") return false;
|
||||
const content = Array.isArray(m.content) ? m.content : [];
|
||||
return content.some(
|
||||
(block: any) =>
|
||||
block.type === "tool_use" &&
|
||||
block.name === "mem0_memory" &&
|
||||
["add", "delete", "delete_all"].includes(block.input?.action),
|
||||
);
|
||||
});
|
||||
|
||||
if (hadWriteAction) {
|
||||
recordDreamCompletion(CONFIG_DIR);
|
||||
captureEvent("pi.dream.completed", {}, telemetryCtx);
|
||||
}
|
||||
|
||||
releaseDreamLock(CONFIG_DIR);
|
||||
dreamTriggered = false;
|
||||
});
|
||||
|
||||
// ── session_shutdown: release dream lock if still held ──────────────
|
||||
pi.on("session_shutdown", async () => {
|
||||
captureEvent("pi.session.stop", {}, telemetryCtx);
|
||||
if (dreamTriggered) {
|
||||
releaseDreamLock(CONFIG_DIR);
|
||||
dreamTriggered = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
export type {
|
||||
Scope,
|
||||
Mem0Config,
|
||||
DreamConfig,
|
||||
ScopeContext,
|
||||
CustomCategory,
|
||||
} from "./types.ts";
|
||||
export { DEFAULT_CUSTOM_CATEGORIES } from "./types.ts";
|
||||
|
||||
export { loadConfig, CONFIG_DIR } from "./config/index.ts";
|
||||
|
||||
export { registerMemoryTool, buildToolExecute } from "./memory/tools.ts";
|
||||
export { detectAppId, detectRunId, resolveSearchFilters, resolveAddParams } from "./memory/scoping.ts";
|
||||
export { formatAge, formatMemoryCompact, formatMemoryList, groupByCategory } from "./memory/formatting.ts";
|
||||
|
||||
export { setupAutoCapture, extractConversation } from "./capture/index.ts";
|
||||
|
||||
export {
|
||||
incrementSessionCount,
|
||||
checkCheapGates,
|
||||
checkMemoryGate,
|
||||
acquireDreamLock,
|
||||
releaseDreamLock,
|
||||
recordDreamCompletion,
|
||||
} from "./dream/index.ts";
|
||||
export { DREAM_PROTOCOL } from "./dream/prompt.ts";
|
||||
|
||||
export { MEMORY_POLICY } from "./prompt.ts";
|
||||
|
||||
export { registerCommands } from "./commands.ts";
|
||||
|
||||
export { captureEvent, captureToolEvent, captureCommandEvent, _getEventQueue, _resetForTesting } from "./telemetry.ts";
|
||||
|
||||
export { default as mem0Extension } from "./entry.ts";
|
||||
@@ -1,43 +0,0 @@
|
||||
interface MemoryLike {
|
||||
id: string;
|
||||
memory?: string;
|
||||
categories?: string[];
|
||||
createdAt?: Date | string;
|
||||
}
|
||||
|
||||
export function formatAge(date: Date | string): string {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const ms = Date.now() - d.getTime();
|
||||
const minutes = Math.floor(ms / 60_000);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
export function formatMemoryCompact(mem: MemoryLike): string {
|
||||
const cat = mem.categories?.[0] ?? "uncategorized";
|
||||
const age = mem.createdAt ? ` (${formatAge(mem.createdAt)})` : "";
|
||||
return `[${cat}] ${mem.memory ?? "(empty)"}${age} [mem0:${mem.id}]`;
|
||||
}
|
||||
|
||||
export function formatMemoryList(memories: MemoryLike[]): string {
|
||||
if (memories.length === 0) return "No memories found.";
|
||||
return memories
|
||||
.map((m, i) => `${i + 1}. ${formatMemoryCompact(m)}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function groupByCategory(
|
||||
memories: MemoryLike[],
|
||||
): Map<string, MemoryLike[]> {
|
||||
const groups = new Map<string, MemoryLike[]>();
|
||||
for (const m of memories) {
|
||||
const cat = m.categories?.[0] ?? "uncategorized";
|
||||
const list = groups.get(cat) ?? [];
|
||||
list.push(m);
|
||||
groups.set(cat, list);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { detectRunId, resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
|
||||
const mockExecFileSync = vi.fn();
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
execFileSync: (...args: any[]) => mockExecFileSync(...args),
|
||||
}));
|
||||
|
||||
const { detectAppId } = await import("./scoping.ts");
|
||||
|
||||
describe("detectAppId", () => {
|
||||
beforeEach(() => {
|
||||
mockExecFileSync.mockReset();
|
||||
});
|
||||
|
||||
it("uses git root basename for a git repo", () => {
|
||||
mockExecFileSync.mockReturnValue("/home/user/projects/my-app\n");
|
||||
expect(detectAppId("/home/user/projects/my-app")).toBe("my-app");
|
||||
});
|
||||
|
||||
it("returns same app_id from any subdirectory in a monorepo", () => {
|
||||
mockExecFileSync.mockReturnValue("/home/user/projects/monorepo\n");
|
||||
const root = detectAppId("/home/user/projects/monorepo");
|
||||
const sub = detectAppId("/home/user/projects/monorepo/packages/core");
|
||||
expect(root).toBe("monorepo");
|
||||
expect(sub).toBe("monorepo");
|
||||
});
|
||||
|
||||
it("falls back to basename when not in a git repo", () => {
|
||||
mockExecFileSync.mockImplementation(() => {
|
||||
throw new Error("fatal: not a git repository");
|
||||
});
|
||||
expect(detectAppId("/home/user/scratch")).toBe("scratch");
|
||||
});
|
||||
});
|
||||
|
||||
describe("detectRunId", () => {
|
||||
it("returns 'unknown' when no session file", () => {
|
||||
expect(detectRunId(undefined)).toBe("unknown");
|
||||
});
|
||||
|
||||
it("returns a 12-char hex hash for a session file", () => {
|
||||
const id = detectRunId("/tmp/session-abc.json");
|
||||
expect(id).toMatch(/^[0-9a-f]{12}$/);
|
||||
});
|
||||
|
||||
it("produces different IDs for different session files", () => {
|
||||
const a = detectRunId("/tmp/session-a.json");
|
||||
const b = detectRunId("/tmp/session-b.json");
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSearchFilters", () => {
|
||||
const ctx = { userId: "u1", appId: "a1", runId: "r1" };
|
||||
|
||||
it("includes user_id and app_id for project scope", () => {
|
||||
expect(resolveSearchFilters("project", ctx)).toEqual({ user_id: "u1", app_id: "a1" });
|
||||
});
|
||||
|
||||
it("includes run_id for session scope", () => {
|
||||
expect(resolveSearchFilters("session", ctx)).toEqual({ user_id: "u1", app_id: "a1", run_id: "r1" });
|
||||
});
|
||||
|
||||
it("uses wildcard app_id for global scope", () => {
|
||||
expect(resolveSearchFilters("global", ctx)).toEqual({ user_id: "u1", app_id: "*" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveAddParams", () => {
|
||||
const ctx = { userId: "u1", appId: "a1", runId: "r1" };
|
||||
|
||||
it("includes userId and appId for project scope", () => {
|
||||
expect(resolveAddParams("project", ctx)).toEqual({ userId: "u1", appId: "a1" });
|
||||
});
|
||||
|
||||
it("includes runId for session scope", () => {
|
||||
expect(resolveAddParams("session", ctx)).toEqual({ userId: "u1", appId: "a1", runId: "r1" });
|
||||
});
|
||||
|
||||
it("only includes userId for global scope", () => {
|
||||
expect(resolveAddParams("global", ctx)).toEqual({ userId: "u1" });
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import * as path from "node:path";
|
||||
import * as crypto from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import type { Scope, ScopeContext } from "../types.ts";
|
||||
|
||||
export function detectAppId(cwd: string): string {
|
||||
try {
|
||||
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
||||
cwd,
|
||||
encoding: "utf-8",
|
||||
timeout: 3000,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
return path.basename(root);
|
||||
} catch {
|
||||
return path.basename(cwd);
|
||||
}
|
||||
}
|
||||
|
||||
export function detectRunId(sessionFile: string | undefined): string {
|
||||
if (!sessionFile) return "unknown";
|
||||
return crypto.createHash("sha256").update(sessionFile).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
export function resolveSearchFilters(
|
||||
scope: Scope,
|
||||
ctx: ScopeContext,
|
||||
): Record<string, string> {
|
||||
switch (scope) {
|
||||
case "project":
|
||||
return { user_id: ctx.userId, app_id: ctx.appId };
|
||||
case "session":
|
||||
return { user_id: ctx.userId, app_id: ctx.appId, run_id: ctx.runId };
|
||||
case "global":
|
||||
return { user_id: ctx.userId, app_id: "*" };
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveAddParams(
|
||||
scope: Scope,
|
||||
ctx: ScopeContext,
|
||||
): Record<string, string> {
|
||||
switch (scope) {
|
||||
case "project":
|
||||
return { userId: ctx.userId, appId: ctx.appId };
|
||||
case "session":
|
||||
return { userId: ctx.userId, appId: ctx.appId, runId: ctx.runId };
|
||||
case "global":
|
||||
return { userId: ctx.userId };
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { StringEnum } from "@earendil-works/pi-ai";
|
||||
import type MemoryClient from "mem0ai";
|
||||
import type { Scope, ScopeContext, Mem0Config } from "../types.ts";
|
||||
import { DEFAULT_CUSTOM_CATEGORIES } from "../types.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
import { formatMemoryList } from "./formatting.ts";
|
||||
import { captureToolEvent } from "../telemetry.ts";
|
||||
|
||||
interface MemoryResult {
|
||||
message?: string;
|
||||
eventId?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const MAX_OUTPUT_LINES = 200;
|
||||
const MAX_OUTPUT_BYTES = 50_000;
|
||||
|
||||
function truncateOutput(text: string): string {
|
||||
const lines = text.split("\n");
|
||||
if (lines.length <= MAX_OUTPUT_LINES && text.length <= MAX_OUTPUT_BYTES) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const kept = lines.slice(0, MAX_OUTPUT_LINES);
|
||||
let result = kept.join("\n");
|
||||
if (result.length > MAX_OUTPUT_BYTES) {
|
||||
result = result.slice(0, MAX_OUTPUT_BYTES);
|
||||
}
|
||||
|
||||
const dropped = lines.length - kept.length;
|
||||
if (dropped > 0 || text.length > MAX_OUTPUT_BYTES) {
|
||||
result += `\n\n[Output truncated: showing ${kept.length} of ${lines.length} lines]`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface ToolParams {
|
||||
action: "search" | "add" | "get_all" | "update" | "delete" | "delete_all";
|
||||
query?: string;
|
||||
content?: string;
|
||||
memory_id?: string;
|
||||
scope?: Scope;
|
||||
}
|
||||
|
||||
export function buildToolExecute(
|
||||
mem0: MemoryClient,
|
||||
scopeCtx: ScopeContext,
|
||||
defaultScope: Scope,
|
||||
) {
|
||||
return async (params: ToolParams, signal?: AbortSignal) => {
|
||||
const scope = params.scope ?? defaultScope;
|
||||
|
||||
switch (params.action) {
|
||||
case "search": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
if (!params.query) throw new Error("query is required for search");
|
||||
const filters = resolveSearchFilters(scope, scopeCtx);
|
||||
const result = await mem0.search(params.query, { filters });
|
||||
const memories = result.results ?? [];
|
||||
return {
|
||||
content: [{ type: "text" as const, text: truncateOutput(formatMemoryList(memories)) }],
|
||||
details: { matchCount: memories.length },
|
||||
};
|
||||
}
|
||||
|
||||
case "add": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
if (!params.content) throw new Error("content is required for add");
|
||||
const addParams = resolveAddParams(scope, scopeCtx);
|
||||
const result = await mem0.add(
|
||||
[{ role: "user", content: params.content }],
|
||||
{ ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES },
|
||||
);
|
||||
const res = result as MemoryResult;
|
||||
const msg = res.message ?? "Memory stored.";
|
||||
return {
|
||||
content: [{ type: "text" as const, text: msg }],
|
||||
details: { eventId: res.eventId ?? null, status: res.status ?? null },
|
||||
};
|
||||
}
|
||||
|
||||
case "get_all": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
const filters = resolveSearchFilters(scope, scopeCtx);
|
||||
const result = await mem0.getAll({ filters });
|
||||
const memories = result.results ?? [];
|
||||
return {
|
||||
content: [{ type: "text" as const, text: truncateOutput(formatMemoryList(memories)) }],
|
||||
details: { totalCount: result.count ?? memories.length },
|
||||
};
|
||||
}
|
||||
|
||||
case "update": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
if (!params.memory_id) throw new Error("memory_id is required for update");
|
||||
if (!params.content) throw new Error("content is required for update");
|
||||
const updateResult = await mem0.update(params.memory_id, { text: params.content });
|
||||
const res = updateResult as MemoryResult;
|
||||
return {
|
||||
content: [{ type: "text" as const, text: res.status ?? "Memory updated." }],
|
||||
details: { memoryId: params.memory_id },
|
||||
};
|
||||
}
|
||||
|
||||
case "delete": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
if (!params.memory_id) throw new Error("memory_id is required for delete");
|
||||
const result = await mem0.delete(params.memory_id);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: result.message ?? "Memory deleted." }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
|
||||
case "delete_all": {
|
||||
if (signal?.aborted) throw new Error("Cancelled");
|
||||
const delParams = resolveAddParams(scope, scopeCtx);
|
||||
const result = await mem0.deleteAll(delParams);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: result.message ?? "All memories deleted." }],
|
||||
details: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function registerMemoryTool(
|
||||
pi: ExtensionAPI,
|
||||
mem0: MemoryClient,
|
||||
config: Mem0Config,
|
||||
getScopeCtx: () => ScopeContext,
|
||||
telemetryCtx?: { apiKey?: string },
|
||||
): void {
|
||||
pi.registerTool({
|
||||
name: "mem0_memory",
|
||||
label: "Mem0 Memory",
|
||||
description:
|
||||
"Search, add, update, and manage persistent semantic memories powered by Mem0. Memories persist across sessions and devices. Output is truncated to 200 lines / 50KB.",
|
||||
promptSnippet: "Semantic memory search and storage via Mem0",
|
||||
promptGuidelines: [
|
||||
'Use mem0_memory with action "search" when the user asks about past conversations, preferences, or decisions',
|
||||
'Use mem0_memory with action "add" to save important facts, preferences, goals, decisions, or lessons the user shares',
|
||||
'Use mem0_memory with action "update" to modify an existing memory — requires memory_id and content. Preserves the memory ID',
|
||||
"Always use the default project scope unless the user EXPLICITLY asks to search across all projects — only then use scope \"global\"",
|
||||
"Do NOT pass scope at all for normal queries — omitting it uses the project default automatically",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
action: StringEnum([
|
||||
"search",
|
||||
"add",
|
||||
"get_all",
|
||||
"update",
|
||||
"delete",
|
||||
"delete_all",
|
||||
] as const),
|
||||
query: Type.Optional(
|
||||
Type.String({ description: "Search query or memory text" }),
|
||||
),
|
||||
content: Type.Optional(
|
||||
Type.String({ description: "Memory content to store or updated text" }),
|
||||
),
|
||||
memory_id: Type.Optional(
|
||||
Type.String({ description: "Memory ID for update or delete" }),
|
||||
),
|
||||
scope: Type.Optional(
|
||||
StringEnum(["project", "session", "global"] as const),
|
||||
),
|
||||
}),
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const scopeCtx = getScopeCtx();
|
||||
const exec = buildToolExecute(mem0, scopeCtx, config.defaultScope);
|
||||
const start = Date.now();
|
||||
try {
|
||||
const result = await exec(params as ToolParams, signal);
|
||||
const details = (result as any).details ?? {};
|
||||
captureToolEvent((params as ToolParams).action, {
|
||||
success: true,
|
||||
latency_ms: Date.now() - start,
|
||||
result_count: details.matchCount ?? details.totalCount ?? undefined,
|
||||
}, telemetryCtx);
|
||||
return result;
|
||||
} catch (err) {
|
||||
captureToolEvent((params as ToolParams).action, {
|
||||
success: false,
|
||||
latency_ms: Date.now() - start,
|
||||
error_type: err instanceof Error ? err.name : "unknown",
|
||||
}, telemetryCtx);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export const MEMORY_POLICY = `<mem0-memory-policy>
|
||||
You have persistent semantic memory via the mem0_memory tool, powered by Mem0.
|
||||
|
||||
Memory is scoped to the current project by default. Do not change the scope unless explicitly asked.
|
||||
- "project" (default): memories for this project — use this for all normal queries
|
||||
- "session": memories from this session only
|
||||
- "global": all memories across projects — ONLY use when the user explicitly asks for cross-project search
|
||||
|
||||
When to use memory:
|
||||
- Search when the user references past conversations, preferences, or decisions
|
||||
- Save important facts, user preferences, key decisions, and lessons learned
|
||||
- Check memory before asking the user something they may have already told you
|
||||
- Save identity information, goals, relationships, and routines the user shares
|
||||
|
||||
Memory persists across sessions and devices via Mem0's cloud.
|
||||
</mem0-memory-policy>`;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
_getEventQueue,
|
||||
_resetForTesting,
|
||||
captureCommandEvent,
|
||||
captureEvent,
|
||||
captureToolEvent,
|
||||
} from "./telemetry.ts";
|
||||
|
||||
const CTX = { apiKey: "m0-testkey123" };
|
||||
|
||||
function findEvent(name: string): Record<string, unknown> | undefined {
|
||||
return _getEventQueue().find((e) => (e as Record<string, unknown>).event === name) as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
_resetForTesting();
|
||||
});
|
||||
|
||||
describe("pi-agent telemetry", () => {
|
||||
it.each(["add", "search", "update", "delete"])(
|
||||
"captureToolEvent tracks the %s operation as pi.tool.mem0_memory",
|
||||
(action) => {
|
||||
captureToolEvent(action, { success: true }, CTX);
|
||||
const ev = findEvent("pi.tool.mem0_memory");
|
||||
expect(ev).toBeDefined();
|
||||
const props = ev!.properties as Record<string, unknown>;
|
||||
expect(props.action).toBe(action);
|
||||
expect(props.success).toBe(true);
|
||||
expect(props.source).toBe("PI_AGENT_PLUGIN");
|
||||
expect(props.$process_person_profile).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("captureCommandEvent emits a namespaced pi.command.* event", () => {
|
||||
captureCommandEvent("mem0-search", { result_count: 3 }, CTX);
|
||||
const ev = findEvent("pi.command.mem0-search");
|
||||
expect(ev).toBeDefined();
|
||||
expect((ev!.properties as Record<string, unknown>).result_count).toBe(3);
|
||||
});
|
||||
|
||||
it("respects the MEM0_TELEMETRY opt-out", () => {
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
captureEvent("pi.session.start", {}, CTX);
|
||||
captureToolEvent("add", {}, CTX);
|
||||
expect(_getEventQueue()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,240 +0,0 @@
|
||||
/**
|
||||
* Plugin telemetry — anonymous usage tracking via PostHog.
|
||||
*
|
||||
* Sends fire-and-forget events to PostHog using native fetch().
|
||||
* Events are batched and flushed every 5 seconds or when the queue
|
||||
* reaches 10 events, whichever comes first.
|
||||
*
|
||||
* Disable with: MEM0_TELEMETRY=false
|
||||
*/
|
||||
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { CONFIG_DIR } from "./config/index.ts";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
|
||||
|
||||
const FLUSH_INTERVAL_MS = 5_000;
|
||||
const FLUSH_THRESHOLD = 10;
|
||||
|
||||
let eventQueue: Record<string, unknown>[] = [];
|
||||
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
function _loadPluginVersion(): string {
|
||||
try {
|
||||
const pkgUrl = new URL("../package.json", import.meta.url);
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgUrl, "utf-8"));
|
||||
return pkg.version ?? "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
const PLUGIN_VERSION = _loadPluginVersion();
|
||||
|
||||
// ── Opt-out ──────────────────────────────────────────────────────────────
|
||||
|
||||
function isTelemetryEnabled(): boolean {
|
||||
try {
|
||||
const val = process.env.MEM0_TELEMETRY;
|
||||
if (val !== undefined) {
|
||||
const s = val.toLowerCase();
|
||||
return s !== "false" && s !== "0" && s !== "no" && s !== "off";
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Identity ─────────────────────────────────────────────────────────────
|
||||
|
||||
const TELEMETRY_ID_PATH = path.join(CONFIG_DIR, "mem0-telemetry-id.json");
|
||||
|
||||
let _cachedAnonymousId: string | undefined;
|
||||
|
||||
function getOrCreateAnonymousId(): string {
|
||||
if (_cachedAnonymousId) return _cachedAnonymousId;
|
||||
try {
|
||||
if (fs.existsSync(TELEMETRY_ID_PATH)) {
|
||||
const data = JSON.parse(fs.readFileSync(TELEMETRY_ID_PATH, "utf-8"));
|
||||
if (data.anonymousId) {
|
||||
_cachedAnonymousId = data.anonymousId;
|
||||
return _cachedAnonymousId!;
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const newId = `pi-mem0-anon-${randomUUID().replace(/-/g, "")}`;
|
||||
try {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
fs.writeFileSync(TELEMETRY_ID_PATH, JSON.stringify({ anonymousId: newId }), "utf-8");
|
||||
} catch { /* ignore */ }
|
||||
_cachedAnonymousId = newId;
|
||||
return newId;
|
||||
}
|
||||
|
||||
function getDistinctId(apiKey?: string): string {
|
||||
if (apiKey) {
|
||||
return createHash("sha256").update(apiKey).digest("hex");
|
||||
}
|
||||
return getOrCreateAnonymousId();
|
||||
}
|
||||
|
||||
let _identifyDone = false;
|
||||
|
||||
function maybeBuildIdentifyEvent(distinctId: string): Record<string, unknown> | null {
|
||||
if (_identifyDone) return null;
|
||||
if (!distinctId || distinctId.startsWith("pi-mem0-anon-")) return null;
|
||||
try {
|
||||
if (!fs.existsSync(TELEMETRY_ID_PATH)) {
|
||||
_identifyDone = true;
|
||||
return null;
|
||||
}
|
||||
const data = JSON.parse(fs.readFileSync(TELEMETRY_ID_PATH, "utf-8"));
|
||||
const storedAnon = data.anonymousId;
|
||||
if (!storedAnon) {
|
||||
_identifyDone = true;
|
||||
return null;
|
||||
}
|
||||
const identifyEvent = {
|
||||
event: "$identify",
|
||||
distinct_id: distinctId,
|
||||
properties: { $anon_distinct_id: storedAnon, $lib: "posthog-node" },
|
||||
};
|
||||
try {
|
||||
fs.unlinkSync(TELEMETRY_ID_PATH);
|
||||
} catch { /* ignore */ }
|
||||
_identifyDone = true;
|
||||
_cachedAnonymousId = undefined;
|
||||
return identifyEvent;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flush machinery ──────────────────────────────────────────────────────
|
||||
|
||||
function ensureFlushTimer(): void {
|
||||
if (flushTimer) return;
|
||||
flushTimer = setInterval(flushEvents, FLUSH_INTERVAL_MS);
|
||||
if (typeof flushTimer === "object" && "unref" in flushTimer) {
|
||||
flushTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
let _exitHandlerInstalled = false;
|
||||
|
||||
function ensureExitHandler(): void {
|
||||
if (_exitHandlerInstalled) return;
|
||||
_exitHandlerInstalled = true;
|
||||
process.on("beforeExit", async () => {
|
||||
if (eventQueue.length === 0) return;
|
||||
const batch = eventQueue;
|
||||
eventQueue = [];
|
||||
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
|
||||
try {
|
||||
await fetch(POSTHOG_HOST, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": String(Buffer.byteLength(body)),
|
||||
},
|
||||
body,
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
});
|
||||
} catch { /* silently swallow */ }
|
||||
});
|
||||
}
|
||||
|
||||
function flushEvents(): void {
|
||||
if (eventQueue.length === 0) return;
|
||||
const batch = eventQueue;
|
||||
eventQueue = [];
|
||||
|
||||
const body = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
|
||||
fetch(POSTHOG_HOST, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": String(Buffer.byteLength(body)),
|
||||
},
|
||||
body,
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
}).catch(() => { /* silently swallow */ });
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
export function captureEvent(
|
||||
eventName: string,
|
||||
properties: Record<string, unknown> = {},
|
||||
ctx?: { apiKey?: string },
|
||||
): void {
|
||||
if (!isTelemetryEnabled()) return;
|
||||
|
||||
try {
|
||||
const distinctId = getDistinctId(ctx?.apiKey);
|
||||
|
||||
const identifyEvent = maybeBuildIdentifyEvent(distinctId);
|
||||
if (identifyEvent) {
|
||||
eventQueue.push(identifyEvent);
|
||||
}
|
||||
|
||||
eventQueue.push({
|
||||
event: eventName,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
source: "PI_AGENT_PLUGIN",
|
||||
language: "node",
|
||||
plugin_version: PLUGIN_VERSION,
|
||||
node_version: process.version,
|
||||
os: process.platform,
|
||||
$process_person_profile: false,
|
||||
$lib: "posthog-node",
|
||||
...properties,
|
||||
},
|
||||
});
|
||||
|
||||
ensureFlushTimer();
|
||||
ensureExitHandler();
|
||||
|
||||
if (eventQueue.length >= FLUSH_THRESHOLD) {
|
||||
flushEvents();
|
||||
}
|
||||
} catch { /* silently swallow */ }
|
||||
}
|
||||
|
||||
export function captureToolEvent(
|
||||
action: string,
|
||||
properties: Record<string, unknown> = {},
|
||||
ctx?: { apiKey?: string },
|
||||
): void {
|
||||
captureEvent("pi.tool.mem0_memory", { action, ...properties }, ctx);
|
||||
}
|
||||
|
||||
export function captureCommandEvent(
|
||||
command: string,
|
||||
properties: Record<string, unknown> = {},
|
||||
ctx?: { apiKey?: string },
|
||||
): void {
|
||||
captureEvent(`pi.command.${command}`, properties, ctx);
|
||||
}
|
||||
|
||||
// ── Test helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
export function _getEventQueue(): Record<string, unknown>[] {
|
||||
return eventQueue;
|
||||
}
|
||||
|
||||
export function _resetForTesting(): void {
|
||||
eventQueue = [];
|
||||
if (flushTimer) {
|
||||
clearInterval(flushTimer);
|
||||
flushTimer = undefined;
|
||||
}
|
||||
_cachedAnonymousId = undefined;
|
||||
_identifyDone = false;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
export type Scope = "project" | "session" | "global";
|
||||
|
||||
export interface DreamConfig {
|
||||
enabled: boolean;
|
||||
auto: boolean;
|
||||
minHours: number;
|
||||
minSessions: number;
|
||||
minMemories: number;
|
||||
}
|
||||
|
||||
export interface Mem0Config {
|
||||
apiKey: string;
|
||||
userId: string;
|
||||
autoCapture: boolean;
|
||||
defaultScope: Scope;
|
||||
contextInjection: boolean;
|
||||
searchThreshold: number;
|
||||
dream: DreamConfig;
|
||||
}
|
||||
|
||||
export interface DreamState {
|
||||
lastConsolidatedAt: number;
|
||||
sessionsSince: number;
|
||||
lastSessionId: string | null;
|
||||
}
|
||||
|
||||
export interface DreamLock {
|
||||
pid: number;
|
||||
startedAt: number;
|
||||
}
|
||||
|
||||
export interface ScopeContext {
|
||||
userId: string;
|
||||
appId: string;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
export interface CustomCategory {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CUSTOM_CATEGORIES: CustomCategory[] = [
|
||||
{ identity: "Personal details, background, and self-descriptions" },
|
||||
{ preferences: "Likes, dislikes, habits, and preferred ways of doing things" },
|
||||
{ goals: "Objectives, aspirations, and targets the user is working toward" },
|
||||
{ projects: "Ongoing work, initiatives, and areas of focus" },
|
||||
{ decisions: "Choices made, rationale, and trade-offs considered" },
|
||||
{ technical: "Technical knowledge, tools, configurations, and environment details" },
|
||||
{ relationships: "People, teams, organizations, and their roles" },
|
||||
{ routines: "Recurring patterns, workflows, schedules, and processes" },
|
||||
{ lessons: "Insights learned, mistakes to avoid, and best practices discovered" },
|
||||
{ work: "Professional context, role, responsibilities, and work environment" },
|
||||
];
|
||||
@@ -1,62 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { extractConversation } from "../src/capture/index.ts";
|
||||
|
||||
describe("extractConversation", () => {
|
||||
it("extracts user and assistant text messages", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there!" },
|
||||
];
|
||||
const result = extractConversation(messages);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ role: "user", content: "Hello" });
|
||||
expect(result[1]).toEqual({ role: "assistant", content: "Hi there!" });
|
||||
});
|
||||
|
||||
it("skips assistant tool_use blocks, keeps text blocks", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: "Search" },
|
||||
{ role: "assistant", content: [{ type: "tool_use", id: "x", name: "mem0" }] },
|
||||
{ role: "tool", content: "results" },
|
||||
{ role: "assistant", content: "Here are the results" },
|
||||
];
|
||||
const result = extractConversation(messages);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({ role: "user", content: "Search" });
|
||||
expect(result[1]).toEqual({ role: "assistant", content: "Here are the results" });
|
||||
});
|
||||
|
||||
it("extracts text from content arrays for both roles", () => {
|
||||
const messages = [
|
||||
{ role: "user", content: [{ type: "text", text: "Hello world" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "Response" }, { type: "tool_use", id: "x" }] },
|
||||
];
|
||||
const result = extractConversation(messages);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].content).toBe("Hello world");
|
||||
expect(result[1].content).toBe("Response");
|
||||
});
|
||||
|
||||
it("skips tool and system messages", () => {
|
||||
const messages = [
|
||||
{ role: "system", content: "You are helpful" },
|
||||
{ role: "user", content: "Hi" },
|
||||
{ role: "tool", content: "tool output" },
|
||||
];
|
||||
const result = extractConversation(messages);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual({ role: "user", content: "Hi" });
|
||||
});
|
||||
|
||||
it("skips assistant messages with only tool_use (no text)", () => {
|
||||
const messages = [
|
||||
{ role: "assistant", content: [{ type: "tool_use", id: "x", name: "bash" }] },
|
||||
];
|
||||
const result = extractConversation(messages);
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns empty array for empty input", () => {
|
||||
expect(extractConversation([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user