Compare commits

..
Author SHA1 Message Date
kartik-mem0 f91f0e6c71 Merge branch 'main' into doc/changelog-mdx
# Conflicts:
#	openclaw/CHANGELOG.md
2026-04-09 15:02:26 +05:30
KartikandGitHub 88f696a60a refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734) 2026-04-09 14:57:33 +05:30
kartik-mem0 b624b8704f docs: update SDK changelog 2026-04-08 22:56:37 +05:30
kartik-mem0 5429cd22ee docs: update SDK changelog 2026-04-08 22:09:24 +05:30
081eca6d8f fix: guard temp_uuid_mapping lookups against LLM-hallucinated IDs (fixes #3931) (#4674)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-08 21:55:52 +05:30
KartikandGitHub 2434b9d550 docs: add ChatDev integration guide and update integrations list (#4751) 2026-04-08 20:16:04 +05:30
Rakhee SinghandGitHub 3ffea554bc fix(azure_openai): forward response_format to Azure OpenAI API (#4689) 2026-04-08 19:22:02 +05:30
Rakhee SinghandGitHub 1ad8a59b0c fix(deepseek): forward response_format to OpenAI-compatible API (#4688) 2026-04-08 19:21:11 +05:30
kartik-mem0 918059fd45 chore: update the highlight section 2026-04-08 15:05:36 +05:30
kartik-mem0 e36fa26eb1 docs: removing the changelog.mdx file adn replacing it with new changelog system 2026-04-08 14:52:32 +05:30
Saket AryanandGitHub a670333d67 feat: add AGENTS.md for AI coding agent instructions (#4726) 2026-04-06 21:32:43 +05:30
Saket AryanandGitHub 4c2db3e68b feat(skills): introduce Mem0 skill graph with dedicated CLI and Vercel AI SDK skills (#4725) 2026-04-06 20:41:29 +05:30
07f0d4f1e0 fix: use npx npm@latest for OIDC trusted publishing (#4724)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:22:00 +05:30
Saket AryanandGitHub 3565404eef fix: remove npm self-upgrade from CD workflows (#4723) 2026-04-06 17:11:28 +05:30
KartikandGitHub 144627c4ce fix(docs): change the position of the openclaw to agnet plugin and fix the integrations overview and sidebar list view (#4722) 2026-04-06 16:57:24 +05:30
KartikandGitHub 6984958138 chore: sat release (#4702) 2026-04-06 16:55:08 +05:30
KartikandGitHub b13748c446 feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704) 2026-04-06 16:54:27 +05:30
Saket AryanandGitHub 4642a1d6e3 feat(cli): validate API key upfront via ping and unify telemetry identity resolution (#4701) 2026-04-04 23:03:27 +05:30
686d5e987d fix: openclaw plugin and fix the login section there (#4696)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-04 22:21:46 +05:30
DEVAN CHAUHANandGitHub c55447c1e4 [fix] groq model (#4700) 2026-04-04 21:36:17 +05:30
Saket AryanandGitHub ee67602c58 feat(cli): add PostHog telemetry and source tracking to Python & Node CLIs (#4699) 2026-04-04 20:47:38 +05:30
100 changed files with 12214 additions and 2701 deletions
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: mem0-ts/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+2 -5
View File
@@ -30,9 +30,6 @@ jobs:
cache: 'pnpm'
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Upgrade npm for OIDC trusted publishing
run: npm install -g npm@latest
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -43,7 +40,7 @@ jobs:
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npm publish --provenance --access public --tag "$PREID"
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npm publish --provenance --access public
npx npm@latest publish --provenance --access public
fi
+580
View File
@@ -0,0 +1,580 @@
# AGENTS.md
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
## Project Overview
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
- **Repository**: https://github.com/mem0ai/mem0
- **Documentation**: https://docs.mem0.ai
- **License**: Apache-2.0
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
### Key Directories
| Directory | Description |
|-----------|-------------|
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
| `skills/` | Claude Code skill definitions — `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/` |
| `docs/` | Documentation site (Mintlify) |
| `tests/` | Python SDK tests (pytest) |
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
| `pr-reviews/` | Pull request review materials |
### Core Package Dependencies
```
mem0 (Python SDK) mem0-ts (TypeScript SDK)
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
├── mem0/embeddings/ ├── src/llms/
├── mem0/vector_stores/ ├── src/embeddings/
├── mem0/graphs/ ├── src/vector_stores/
└── mem0/reranker/ └── src/graphs/
cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls)
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
openclaw/ ──▶ mem0ai (npm)
```
## Development Setup
### Requirements
- **Python**: 3.9+ (3.10+ for CLI)
- **Node.js**: v18+ (v20 or v22 recommended)
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
- **Hatch**: Python build/environment tool (`pip install hatch`)
- **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup
```bash
# Python SDK
hatch shell dev_py_3_11 # creates environment with all deps
pre-commit install # install git hooks
# TypeScript packages
cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI
cd vercel-ai-sdk && pnpm install # Vercel AI provider
cd openclaw && pnpm install # OpenClaw plugin
```
## Build, Lint, and Test Commands
### Python SDK (`mem0/`)
```bash
# Environment setup (uses Hatch)
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
# Linting and formatting
make lint # ruff check
make format # ruff format
make sort # isort mem0/
# Tests
make test # pytest tests/
make test-py-3.9 # test specific Python version (3.9–3.12)
# Build and publish
make build # hatch build
make publish # hatch publish
```
- **Python:** 3.9, 3.10, 3.11, 3.12
- **Linter/formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
### TypeScript SDK (`mem0-ts/`)
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup
pnpm run test # jest (all tests)
pnpm run test:unit # jest --coverage (unit tests only)
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
pnpm run test:ci # jest --coverage --ci (CI mode)
pnpm run test:watch # jest watch mode
```
- **Node:** 20, 22 (CI-tested)
- **Build:** tsup (CJS + ESM)
- **Test:** jest
- **Formatter:** prettier
### Python CLI (`cli/python/`)
```bash
cd cli/python
pip install -e ".[dev]" # dev install with ruff + pytest
ruff check . # lint
ruff format . # format
pytest # test
hatch build # build
```
- **Python:** 3.10+ (not 3.9)
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
- **Framework:** Typer + Rich + httpx
- **Entry point:** `mem0 = "mem0_cli.app:main"`
- **Source layout:** `src/mem0_cli/`
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
### Node CLI (`cli/node/`)
```bash
cd cli/node
pnpm install
pnpm run build # tsup
pnpm run lint # biome check src/
pnpm run lint:fix # biome check --write src/
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest run
pnpm run test:watch # vitest (watch mode)
pnpm run dev # tsx src/index.ts (development)
```
- **Node:** 18+ required
- **Build:** tsup (ESM)
- **Linter:** Biome (not ESLint, not Ruff)
- **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
```bash
cd vercel-ai-sdk
pnpm install
pnpm run build # tsup
pnpm run lint # eslint
pnpm run type-check # tsc --noEmit
pnpm run prettier-check # prettier --check
pnpm run test # jest
pnpm run test:edge # vitest (edge runtime)
pnpm run test:node # vitest (node runtime)
```
- **Build:** tsup (CJS + ESM)
- **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`openclaw/`)
```bash
cd openclaw
pnpm install
pnpm run build # tsup
pnpm run test # vitest run
```
- **Build:** tsup (ESM)
- **Test:** vitest (with Codecov in CI)
- **Plugin manifest:** `openclaw.plugin.json`
### Server (`server/`)
```bash
# Docker production build
cd server
make build # docker build -t mem0-api-server .
make run_local # docker run -p 8000:8000 with .env
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
cd server
docker-compose up # starts all 3 services
# mem0 API: localhost:8888
# PostgreSQL: localhost:8432
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
```
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
### OpenMemory (`openmemory/`)
```bash
# Full stack via Docker Compose
cd openmemory
docker-compose up
# Qdrant: localhost:6333
# API (MCP): localhost:8765
# UI: localhost:3000
# Individual development
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
cd openmemory/ui && npm run dev # Next.js frontend
# Tests
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
```
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
- **Vector store:** Qdrant
### Documentation (`docs/`)
```bash
make docs # or: cd docs && mintlify dev
```
- **Framework:** Mintlify
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation (`evaluation/`)
```bash
cd evaluation
make run-mem0-add # Run mem0 add experiments
make run-mem0-search # Run mem0 search experiments
make run-mem0-plus-add # With graph memory
make run-mem0-plus-search # With graph memory
make run-rag # RAG baseline
make run-full-context # Full context baseline
make run-langmem # LangMem comparison
make run-openai # OpenAI comparison
```
## Core APIs
### Python
| Function / Class | Purpose | Import |
|-----------------|---------|--------|
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
**Key `Memory` / `MemoryClient` methods:**
| Method | Purpose |
|--------|---------|
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
| `get(memory_id)` | Retrieve a single memory by ID |
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
| `update(memory_id, data)` | Update a memory |
| `delete(memory_id)` | Delete a memory |
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
| `history(memory_id)` | Get change history for a memory |
### TypeScript
| Export | Purpose | Import |
|--------|---------|--------|
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
## Import Patterns
### Python
| What | Import |
|------|--------|
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
| Configuration | `from mem0.configs.base import MemoryConfig` |
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
### TypeScript
| What | Import |
|------|--------|
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
## Coding Standards
### File Naming Conventions
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
### Python Conventions
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
- **Pydantic v2** for all data models and configuration.
- **Ruff** is the single linting and formatting tool — no black, no flake8.
- Root SDK: line length **120**
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
- **isort** with `profile = "black"` for import sorting.
- Ruff excludes `embedchain/` and `openmemory/` from root config.
### TypeScript Conventions
- **Build:** tsup across all packages.
- **Package manager:** pnpm everywhere (no npm, no yarn).
- **TypeScript strict mode** across all packages.
- **Linting varies by package:**
| Package | Linter | Formatter | Test Framework |
|---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest |
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `openclaw/` | — | — | vitest |
### Type Checking
Always run type checking after modifying TypeScript code:
```bash
cd <package> && pnpm run typecheck # or: tsc --noEmit
```
## Architecture
### Provider Pattern
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
| Category | Count | Examples |
|----------|-------|---------|
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
### Two Usage Modes
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
### Graph Memory
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
### MCP Integration
Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
- **Plugin:** MCP tools in `mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
### Plugin & Skills System
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `skills/` contains structured skill definitions for AI agents, covering SDK usage, CLI workflows, and Vercel AI SDK patterns.
### Adding a New Provider
To add a new LLM, embedding, vector store, or reranker provider:
1. Create `mem0/<category>/<provider_name>.py`
2. Inherit from the abstract base class in `mem0/<category>/base.py`
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
4. Register the provider in `mem0/<category>/__init__.py`
5. Add tests in `tests/<category>/<provider_name>/`
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
## CI/CD
### CI Workflows (automated testing)
| Workflow | File | Triggers | Tests |
|----------|------|----------|-------|
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
### CD Workflows (automated publishing)
| Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------|
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
### Utility Workflows
| Workflow | File | Purpose |
|----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
## Task Completion Guidelines
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
### Bug Fixes
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
2. **Implementation**: Fix the bug
3. **Manual verification**: Run the relevant test suite to confirm the fix
4. **Lint**: Run the appropriate linter for the package you modified
### New Features
1. **Implementation**: Build the feature following existing patterns
2. **Unit tests**: Comprehensive test coverage for new functionality
3. **Documentation**: Update relevant docs in `docs/` for public APIs
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
### New Provider (LLM / Embedding / Vector Store / Reranker)
1. **Implementation**: Follow the "Adding a New Provider" steps above
2. **Tests**: Add unit tests matching the pattern of existing providers
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
5. **Documentation**: Add an integration guide in `docs/integrations/`
### Refactoring / Internal Changes
- Unit tests for any changed behavior
- No documentation needed for internal-only changes
- Ensure all existing tests still pass
### When to Deviate
These are guidelines, not rigid rules. Adjust based on:
- **Scope**: Trivial fixes (typos, comments) may not need tests
- **Visibility**: Internal changes may not need documentation
- **Context**: Some changes span multiple categories — use judgment
When uncertain about expected artifacts, ask for clarification.
## Contributing Guidelines
### Workflow
1. Fork and clone the repository.
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
3. Make your changes — add tests, docs, and examples as appropriate.
4. Run linting and tests for every package you modified (see commands above).
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
7. Push and open a Pull Request against `main`.
### Pull Request Requirements
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
2. **Description** — Explain what the PR does and why it's needed.
3. **Type of Change** — Check the appropriate box:
- Bug fix / New feature / Breaking change / Refactor / Documentation update
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
5. **Test Coverage** — Check what applies:
- Added/updated unit tests
- Added/updated integration tests
- Tested manually (describe how)
- No tests needed (explain why)
6. **Checklist** — All must be checked before merge:
- [ ] Code follows the project's style guidelines
- [ ] Self-review performed
- [ ] Tests added that prove the fix/feature works
- [ ] New and existing tests pass locally
- [ ] Documentation updated if needed
### PR Description Template
```markdown
## Linked Issue
Closes #<!-- issue number -->
## Description
<!-- What does this PR do? Why is it needed? -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Refactor (no functional changes)
- [ ] Documentation update
## Breaking Changes
N/A
## Test Coverage
- [ ] I added/updated unit tests
- [ ] I added/updated integration tests
- [ ] I tested manually (describe below)
- [ ] No tests needed (explain why)
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added tests that prove my fix/feature works
- [ ] New and existing tests pass locally
- [ ] I have updated documentation if needed
```
### General Rules
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
- For `server/` and `openmemory/` work, use Docker Compose for local development.
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
### Contributing Guides
| Task | Guide |
|------|-------|
| Code contributions | `docs/contributing/development.mdx` |
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
## Do NOT
- Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials.
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
- Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
- Change public APIs without updating documentation in `docs/`.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.1",
"version": "0.2.2",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
+2
View File
@@ -89,6 +89,8 @@ export interface Backend {
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
ping(): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<
Record<string, unknown>
>;
+16 -2
View File
@@ -3,6 +3,8 @@
*/
import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
APIError,
type AddOptions,
@@ -24,6 +26,9 @@ export class PlatformBackend implements Backend {
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
};
}
@@ -38,9 +43,14 @@ export class PlatformBackend implements Backend {
url += `?${qs}`;
}
const headers = {
...this.headers,
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
};
const fetchOpts: RequestInit = {
method,
headers: this.headers,
headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
@@ -286,11 +296,15 @@ export class PlatformBackend implements Backend {
return result;
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
await this._request("GET", "/v1/ping/");
await this.ping();
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
+20 -2
View File
@@ -41,10 +41,16 @@ async function emailLogin(
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
const sourceHeaders = {
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
};
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: sourceHeaders,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
@@ -85,7 +91,7 @@ async function emailLogin(
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: sourceHeaders,
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
@@ -215,6 +221,16 @@ async function validatePlatform(config: Mem0Config): Promise<void> {
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
// Cache user_email from ping response for telemetry distinct_id
try {
const pingData = (await backend.ping()) as Record<string, unknown>;
const userEmail = pingData?.user_email as string | undefined;
if (userEmail) {
config.platform.userEmail = userEmail;
}
} catch {
/* ignore — telemetry ID will fall back to API key hash */
}
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
@@ -307,6 +323,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
@@ -385,6 +402,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
+6
View File
@@ -20,6 +20,7 @@ export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
userEmail: string;
}
export interface DefaultsConfig {
@@ -49,6 +50,7 @@ export function createDefaultConfig(): Mem0Config {
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
},
};
}
@@ -70,6 +72,7 @@ export function loadConfig(): Mem0Config {
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
config.platform.userEmail = plat.user_email ?? "";
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
@@ -114,6 +117,7 @@ export function saveConfig(config: Mem0Config): void {
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
},
};
@@ -131,6 +135,7 @@ export function redactKey(key: string): string {
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"platform.user_email": ["platform", "userEmail"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
@@ -139,6 +144,7 @@ const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
// Short-form aliases
api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"],
user_email: ["platform", "userEmail"],
user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"],
+106 -22
View File
@@ -8,22 +8,27 @@ import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import { type Backend, getBackend } from "./backend/index.js";
import { colors, printError } from "./branding.js";
import { AuthError, type Backend, getBackend } from "./backend/index.js";
import { colors, printError, printWarning } from "./branding.js";
import type { Mem0Config } from "./config.js";
import { loadConfig } from "./config.js";
import { loadConfig, saveConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js";
const program = new Command();
// ── Validated user identity (set by getBackendAndConfig) ─────────────────
let _validatedUserEmail: string | undefined;
// ── Helpers ──────────────────────────────────────────────────────────────
function getBackendAndConfig(
async function getBackendAndConfig(
apiKey?: string,
baseUrl?: string,
): { backend: Backend; config: Mem0Config } {
): Promise<{ backend: Backend; config: Mem0Config }> {
const config = loadConfig();
if (apiKey) config.platform.apiKey = apiKey;
@@ -37,11 +42,51 @@ function getBackendAndConfig(
process.exit(1);
}
return { backend: getBackend(config), config };
const backend = getBackend(config);
// Validate the API key upfront with a fast timeout
try {
const pingData = (await Promise.race([
backend.ping(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("timeout")), 5000),
),
])) as Record<string, unknown>;
const email = pingData?.user_email as string | undefined;
if (email) {
_validatedUserEmail = email;
if (config.platform.userEmail !== email) {
config.platform.userEmail = email;
try {
saveConfig(config);
} catch {
/* ignore */
}
}
}
} catch (e) {
if (e instanceof AuthError) {
printError(
"Invalid or expired API key.",
"Run 'mem0 init' or set MEM0_API_KEY environment variable.",
);
process.exit(1);
}
// Network error / timeout — warn but proceed
printWarning(
"Could not validate API key (network issue). Proceeding anyway.",
);
}
return { backend, config };
}
function getBackendOnly(apiKey?: string, baseUrl?: string): Backend {
return getBackendAndConfig(apiKey, baseUrl).backend;
async function getBackendOnly(
apiKey?: string,
baseUrl?: string,
): Promise<Backend> {
return (await getBackendAndConfig(apiKey, baseUrl)).backend;
}
function checkAgentMode(): boolean {
@@ -123,6 +168,30 @@ program
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
// ── Telemetry hook ───────────────────────────────────────────────────────
program.hook("preAction", (_thisCommand, actionCommand) => {
try {
const commandName = actionCommand.name();
const parentName = actionCommand.parent?.name();
const fullCommand =
parentName && parentName !== "mem0"
? `${parentName}.${commandName}`
: commandName;
const isAgent = !!(program.opts().json || program.opts().agent);
captureEvent(
`cli.${fullCommand}`,
{
command: fullCommand,
is_agent: isAgent,
},
_validatedUserEmail,
);
} catch {
/* silently swallow */
}
});
// ── Init ──────────────────────────────────────────────────────────────────
program
@@ -179,7 +248,10 @@ program
.action(async (text, opts) => {
const { cmdAdd } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
@@ -233,7 +305,10 @@ program
}
const { cmdSearch } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
@@ -265,7 +340,7 @@ program
.action(async (memoryId, opts) => {
const { cmdGet } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdGet(backend, memoryId, { output });
});
@@ -301,7 +376,10 @@ program
.action(async (opts) => {
const { cmdList } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts);
const enableGraph = resolveGraph(config, opts);
const output = isAgent ? "agent" : opts.output;
@@ -337,7 +415,7 @@ program
}
const { cmdUpdate } = await import("./commands/memory.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata,
@@ -407,7 +485,7 @@ program
// ── Dispatch: single memory ──
if (memoryId) {
const { cmdDelete } = await import("./commands/memory.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdDelete(backend, memoryId, {
output,
dryRun: opts.dryRun,
@@ -419,7 +497,7 @@ program
// ── Dispatch: --all ──
if (opts.all) {
const { cmdDeleteAll } = await import("./commands/memory.js");
const { backend, config } = getBackendAndConfig(
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
@@ -444,7 +522,7 @@ program
// ── Dispatch: --entity ──
if (opts.entity) {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
await cmdEntitiesDelete(backend, { ...opts, output });
return;
}
@@ -519,7 +597,7 @@ entityCmd
.action(async (entityType, opts) => {
const { cmdEntitiesList } = await import("./commands/entities.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesList(backend, entityType, { output });
});
@@ -543,7 +621,7 @@ entityCmd
.action(async (opts) => {
const { cmdEntitiesDelete } = await import("./commands/entities.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEntitiesDelete(backend, { ...opts, output });
});
@@ -569,7 +647,7 @@ eventCmd
.action(async (opts) => {
const { cmdEventList } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventList(backend, { output });
});
@@ -587,7 +665,7 @@ eventCmd
.action(async (eventId, opts) => {
const { cmdEventStatus } = await import("./commands/events.js");
const isAgent = checkAgentMode();
const backend = getBackendOnly(opts.apiKey, opts.baseUrl);
const backend = await getBackendOnly(opts.apiKey, opts.baseUrl);
const output = isAgent ? "agent" : opts.output;
await cmdEventStatus(backend, eventId, { output });
});
@@ -604,7 +682,10 @@ program
.action(async (opts) => {
const { cmdStatus } = await import("./commands/utils.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const output = isAgent ? "agent" : opts.output;
await cmdStatus(backend, {
userId: config.defaults.userId || undefined,
@@ -628,7 +709,10 @@ program
.action(async (filePath, opts) => {
const { cmdImport } = await import("./commands/utils.js");
const isAgent = checkAgentMode();
const { backend, config } = getBackendAndConfig(opts.apiKey, opts.baseUrl);
const { backend, config } = await getBackendAndConfig(
opts.apiKey,
opts.baseUrl,
);
const ids = resolveIds(config, opts);
const output = isAgent ? "agent" : opts.output;
await cmdImport(backend, filePath, {
+106
View File
@@ -0,0 +1,106 @@
/**
* CLI telemetry — anonymous usage tracking via PostHog.
*
* Sends fire-and-forget events by spawning a detached child process
* (telemetry-sender.cjs). The parent CLI process exits immediately;
* the child handles email resolution, caching, and the HTTP POST.
*
* Disable with: MEM0_TELEMETRY=false
*/
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { CONFIG_FILE, loadConfig } from "./config.js";
import { CLI_VERSION } from "./version.js";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
function isTelemetryEnabled(): boolean {
try {
return process.env.MEM0_TELEMETRY !== "false";
} catch {
return true;
}
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
* Matches the SDK pattern in mem0-ts/src/client/mem0.ts.
*/
function getDistinctId(): string {
try {
const config = loadConfig();
if (config.platform.userEmail) {
return config.platform.userEmail;
}
if (config.platform.apiKey) {
return createHash("md5").update(config.platform.apiKey).digest("hex");
}
} catch {
/* ignore */
}
return "anonymous-cli";
}
/**
* Fire a PostHog event (non-blocking, returns void, never throws).
* Spawns telemetry-sender.cjs as a detached subprocess.
*
* When `preResolvedEmail` is provided (e.g. from an upfront ping
* validation), it is used directly as the PostHog distinct ID and the
* subprocess skips its own `/v1/ping/` call.
*/
export function captureEvent(
eventName: string,
properties: Record<string, unknown> = {},
preResolvedEmail?: string,
): void {
if (!isTelemetryEnabled()) return;
try {
const config = loadConfig();
const distinctId = preResolvedEmail || getDistinctId();
const payload = {
api_key: POSTHOG_API_KEY,
distinct_id: distinctId,
event: eventName,
properties: {
source: "CLI",
language: "node",
cli_version: CLI_VERSION,
node_version: process.version,
os: process.platform,
...properties,
$process_person_profile: false,
$lib: "posthog-node",
},
};
const context = {
payload,
posthogHost: POSTHOG_HOST,
needsEmail: !distinctId || !distinctId.includes("@"),
mem0ApiKey: config.platform.apiKey || "",
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
configPath: CONFIG_FILE,
};
const child = spawn(
process.execPath,
[SENDER_SCRIPT, JSON.stringify(context)],
{ detached: true, stdio: "ignore" },
);
child.unref();
} catch {
/* silently swallow */
}
}
+108
View File
@@ -0,0 +1,108 @@
/**
* Standalone telemetry sender — runs as a detached child process.
*
* Usage: node telemetry-sender.cjs '<json context>'
*
* This script is spawned by telemetry.captureEvent() and runs independently
* of the parent CLI process. It:
*
* 1. Resolves the user's email via /v1/ping/ if not already cached
* 2. Caches the email in ~/.mem0/config.json for future runs
* 3. Sends the PostHog event
*
* All errors are silently swallowed — this process must never produce output
* or affect the user experience.
*/
"use strict";
const https = require("https");
const fs = require("fs");
function httpsRequest(url, method, headers, body) {
return new Promise((resolve, reject) => {
const u = new URL(url);
const opts = {
hostname: u.hostname,
path: u.pathname + u.search,
method,
headers,
timeout: 10000,
};
const req = https.request(opts, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch {
resolve({});
}
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("timeout"));
});
if (body) {
req.end(body);
} else {
req.end();
}
});
}
async function resolveAndCacheEmail(ctx, payload) {
try {
const pingUrl = ctx.mem0BaseUrl.replace(/\/+$/, "") + "/v1/ping/";
const data = await httpsRequest(pingUrl, "GET", {
Authorization: "Token " + ctx.mem0ApiKey,
"Content-Type": "application/json",
});
if (data.user_email) {
payload.distinct_id = data.user_email;
cacheEmail(ctx.configPath, data.user_email);
}
} catch {
// silently swallow
}
}
function cacheEmail(configPath, email) {
if (!configPath) return;
try {
const raw = fs.readFileSync(configPath, "utf-8");
const cfg = JSON.parse(raw);
if (!cfg.platform) cfg.platform = {};
cfg.platform.user_email = email;
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
} catch {
// silently swallow
}
}
async function sendPosthogEvent(posthogHost, payload) {
try {
const body = JSON.stringify(payload);
await httpsRequest(posthogHost, "POST", {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
}, body);
} catch {
// silently swallow
}
}
async function main() {
const ctx = JSON.parse(process.argv[2]);
const payload = ctx.payload;
if (ctx.needsEmail && ctx.mem0ApiKey) {
await resolveAndCacheEmail(ctx, payload);
}
await sendPosthogEvent(ctx.posthogHost, payload);
}
main().catch(() => {});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.1"
version = "0.2.2"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.1"
__version__ = "0.2.2"
+78 -4
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
import json as _json
import os
import stat as _stat_mod
@@ -12,7 +13,7 @@ import typer
from rich.console import Console
from mem0_cli import __version__
from mem0_cli.branding import BRAND_COLOR, print_error
from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
console = Console()
err_console = Console(stderr=True)
@@ -55,6 +56,44 @@ event_app = typer.Typer(
# entity_app and event_app registered after Memory commands to control panel ordering
# ── Validated user identity (set by _get_backend_and_config) ──────────────
_validated_user_email: str | None = None
# ── Telemetry helper ─────────────────────────────────────────────────────
def _fire_telemetry(command_name: str, extra: dict | None = None) -> None:
"""Fire a PostHog telemetry event (non-blocking, never fails)."""
try:
from mem0_cli.telemetry import capture_event
props = {"command": command_name}
if extra:
props.update(extra)
capture_event(f"cli.{command_name}", props, pre_resolved_email=_validated_user_email)
except Exception:
pass
@config_app.callback(invoke_without_command=True)
def _config_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"config.{ctx.invoked_subcommand}")
@entity_app.callback(invoke_without_command=True)
def _entity_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"entity.{ctx.invoked_subcommand}")
@event_app.callback(invoke_without_command=True)
def _event_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"event.{ctx.invoked_subcommand}")
# ── Helpers ───────────────────────────────────────────────────────────────
@@ -62,9 +101,16 @@ def _get_backend_and_config(
api_key: str | None = None,
base_url: str | None = None,
):
"""Build and return the Platform backend plus the loaded config."""
"""Build and return the Platform backend plus the loaded config.
Validates the API key upfront via ``/v1/ping/`` and caches the
resolved user email for telemetry.
"""
global _validated_user_email
from mem0_cli.backend import get_backend
from mem0_cli.config import load_config
from mem0_cli.backend.platform import AuthError
from mem0_cli.config import load_config, save_config
config = load_config()
@@ -81,7 +127,29 @@ def _get_backend_and_config(
)
raise typer.Exit(1)
return get_backend(config), config
backend = get_backend(config)
# Validate the API key upfront with a fast timeout
try:
ping_data = backend.ping(timeout=5.0)
email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
if email:
_validated_user_email = email
if config.platform.user_email != email:
config.platform.user_email = email
with contextlib.suppress(Exception):
save_config(config)
except AuthError:
print_error(
err_console,
"Invalid or expired API key.",
hint="Run 'mem0 init' or set MEM0_API_KEY environment variable.",
)
raise typer.Exit(1) from None
except Exception:
print_warning(err_console, "Could not validate API key (network issue). Proceeding anyway.")
return backend, config
def _get_backend(
@@ -165,8 +233,11 @@ def main_callback(
if version:
from mem0_cli.commands.utils import cmd_version
_fire_telemetry("version")
cmd_version()
raise typer.Exit()
if ctx.invoked_subcommand:
_fire_telemetry(ctx.invoked_subcommand)
# ── Memory: add ───────────────────────────────────────────────────────────
@@ -571,12 +642,14 @@ def delete(
# ── Dispatch ─────────────────────────────────────────────────────
if memory_id is not None:
_fire_telemetry("delete", {"delete_mode": "single"})
from mem0_cli.commands.memory import cmd_delete
backend = _get_backend(api_key, base_url)
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
elif all_:
_fire_telemetry("delete", {"delete_mode": "all"})
from mem0_cli.commands.memory import cmd_delete_all
backend, config = _get_backend_and_config(api_key, base_url)
@@ -584,6 +657,7 @@ def delete(
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
else: # --entity
_fire_telemetry("delete", {"delete_mode": "entity"})
from mem0_cli.commands.entities import cmd_entities_delete
backend = _get_backend(api_key, base_url)
+22 -1
View File
@@ -6,6 +6,7 @@ from typing import Any
import httpx
from mem0_cli import __version__
from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
@@ -21,11 +22,17 @@ class PlatformBackend(Backend):
headers={
"Authorization": f"Token {config.api_key}",
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Client-Version": __version__,
},
timeout=30.0,
)
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
from mem0_cli.state import is_agent_mode
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
resp = self._client.request(method, path, **kwargs)
if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
@@ -281,6 +288,20 @@ class PlatformBackend(Backend):
result = self._request("DELETE", f"/v2/entities/{entity_type}/{entity_id}/")
return result
def ping(self, timeout: float | None = None) -> dict:
"""Call the ping endpoint and return the raw response.
When *timeout* is given it overrides the client-level timeout so that
validation pings can fail fast without blocking the user.
"""
if timeout is not None:
resp = self._client.get("/v1/ping/", timeout=timeout)
if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
resp.raise_for_status()
return resp.json()
return self._request("GET", "/v1/ping/")
def status(
self,
*,
@@ -289,7 +310,7 @@ class PlatformBackend(Backend):
) -> dict[str, Any]:
"""Check connectivity using the ping endpoint."""
try:
self._request("GET", "/v1/ping/")
self.ping()
return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)}
@@ -108,6 +108,10 @@ def _email_login(
The caller expects at minimum an ``api_key`` field.
"""
url = base_url.rstrip("/")
_source_headers = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
}
with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code
@@ -116,6 +120,7 @@ def _email_login(
resp = client.post(
f"{url}/api/v1/auth/email_code/",
json={"email": email},
headers=_source_headers,
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -148,6 +153,7 @@ def _email_login(
resp = client.post(
f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()},
headers=_source_headers,
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
@@ -229,6 +235,7 @@ def run_init(
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.platform.user_email = email
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
@@ -299,6 +306,7 @@ def run_init(
raise typer.Exit(1)
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.platform.user_email = email_addr
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
@@ -384,6 +392,14 @@ def _validate_platform(config: Mem0Config) -> None:
)
if status.get("connected"):
print_success(console, "Connected to mem0 Platform!")
# Cache user_email from ping response for telemetry distinct_id
try:
ping_data = backend.ping()
user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
if user_email:
config.platform.user_email = user_email
except Exception:
pass
else:
print_error(
err_console,
+4
View File
@@ -27,6 +27,7 @@ CONFIG_VERSION = 1
class PlatformConfig:
api_key: str = ""
base_url: str = DEFAULT_BASE_URL
user_email: str = ""
@dataclass
@@ -48,6 +49,7 @@ class Mem0Config:
SHORT_KEY_ALIASES: dict[str, str] = {
"api_key": "platform.api_key",
"base_url": "platform.base_url",
"user_email": "platform.user_email",
"user_id": "defaults.user_id",
"agent_id": "defaults.agent_id",
"app_id": "defaults.app_id",
@@ -76,6 +78,7 @@ def load_config() -> Mem0Config:
plat = data.get("platform", {})
config.platform.api_key = plat.get("api_key", "")
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
config.platform.user_email = plat.get("user_email", "")
defaults = data.get("defaults", {})
config.defaults.user_id = defaults.get("user_id", "")
@@ -132,6 +135,7 @@ def save_config(config: Mem0Config) -> None:
"platform": {
"api_key": config.platform.api_key,
"base_url": config.platform.base_url,
"user_email": config.platform.user_email,
},
}
+105
View File
@@ -0,0 +1,105 @@
"""CLI telemetry — anonymous usage tracking via PostHog.
Sends fire-and-forget events to PostHog by spawning a detached subprocess
(telemetry_sender.py). The parent CLI process exits immediately; the
subprocess handles email resolution, caching, and the HTTP POST.
Disable with: MEM0_TELEMETRY=false
"""
from __future__ import annotations
import hashlib
import json
import os
import platform
import subprocess
import sys
from typing import Any
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
def _is_telemetry_enabled() -> bool:
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
return val not in ("false", "0", "no")
def _get_distinct_id() -> str:
"""Return a stable anonymous identifier for the current user.
Priority: cached user_email (from /v1/ping/) > MD5(api_key) > fallback.
Matches the SDK pattern in mem0/client/main.py.
"""
try:
from mem0_cli.config import load_config
config = load_config()
if config.platform.user_email:
return config.platform.user_email
if config.platform.api_key:
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
except Exception:
pass
return "anonymous-cli"
def capture_event(
event_name: str,
properties: dict[str, Any] | None = None,
pre_resolved_email: str | None = None,
) -> None:
"""Fire a PostHog event via a detached subprocess (non-blocking).
When *pre_resolved_email* is provided (e.g. from an upfront ping
validation), it is used directly as the PostHog distinct ID and the
subprocess skips its own ``/v1/ping/`` call.
"""
if not _is_telemetry_enabled():
return
try:
from mem0_cli import __version__
from mem0_cli.config import CONFIG_FILE, load_config
from mem0_cli.state import is_agent_mode
config = load_config()
distinct_id = pre_resolved_email or _get_distinct_id()
payload = {
"api_key": POSTHOG_API_KEY,
"distinct_id": distinct_id,
"event": event_name,
"properties": {
"source": "CLI",
"language": "python",
"cli_version": __version__,
"agent_mode": is_agent_mode(),
"python_version": sys.version,
"os": sys.platform,
"os_version": platform.version(),
"$process_person_profile": False,
"$lib": "posthog-python",
**(properties or {}),
},
}
context = {
"payload": payload,
"posthog_host": POSTHOG_HOST,
"needs_email": not distinct_id or "@" not in distinct_id,
"mem0_api_key": config.platform.api_key or "",
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
"config_path": str(CONFIG_FILE),
}
subprocess.Popen(
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
close_fds=True,
)
except Exception:
pass
@@ -0,0 +1,86 @@
"""Standalone telemetry sender — runs as a detached subprocess.
Usage: python -m mem0_cli.telemetry_sender '<json context>'
This module is spawned by telemetry.capture_event() and runs independently
of the parent CLI process. It:
1. Resolves the user's email via /v1/ping/ if not already cached
2. Caches the email in ~/.mem0/config.json for future runs
3. Sends the PostHog event
All errors are silently swallowed — this process must never produce output
or affect the user experience.
"""
from __future__ import annotations
import json
import sys
import urllib.request
def main() -> None:
ctx = json.loads(sys.argv[1])
payload = ctx["payload"]
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
_resolve_and_cache_email(ctx, payload)
_send_posthog_event(ctx["posthog_host"], payload)
def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
"""Call /v1/ping/ to get the user's email, update the payload, and cache it."""
try:
ping_url = ctx["mem0_base_url"].rstrip("/") + "/v1/ping/"
req = urllib.request.Request(
ping_url,
headers={
"Authorization": "Token " + ctx["mem0_api_key"],
"Content-Type": "application/json",
},
)
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
email = data.get("user_email")
if email:
payload["distinct_id"] = email
_cache_email(ctx.get("config_path"), email)
except Exception:
pass
def _cache_email(config_path: str | None, email: str) -> None:
"""Write user_email into the config file for future runs."""
if not config_path:
return
try:
with open(config_path) as f:
cfg = json.load(f)
cfg.setdefault("platform", {})["user_email"] = email
with open(config_path, "w") as f:
json.dump(cfg, f, indent=2)
except Exception:
pass
def _send_posthog_event(posthog_host: str, payload: dict) -> None:
"""POST the event to PostHog."""
try:
body = json.dumps(payload).encode()
req = urllib.request.Request(
posthog_host,
data=body,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=10)
except Exception:
pass
if __name__ == "__main__":
import contextlib
with contextlib.suppress(Exception):
main()
+82
View File
@@ -0,0 +1,82 @@
---
title: "Highlights"
description: "Major product launches, headline features, and milestones for Mem0."
mode: "wide"
---
<Update label="2026-04-06" description="Mem0 Skill Graph">
**Mem0 Skill Graph — In-Context Documentation for AI Agents**
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow — no doc searching required. Three interconnected skills launched:
- **mem0 Core Skill** — Complete Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more
- **mem0-cli Skill** — Terminal command reference, configuration walkthroughs, and CI/CD recipes
- **mem0-vercel-ai-sdk Skill** — Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup
</Update>
<Update label="2026-04-06" description="Mem0 CLI v0.2.2">
**Official Mem0 CLI — Now on PyPI and npm**
A full-featured command-line interface for Mem0, available in both Python and Node.js:
- **Install:** `pip install mem0-cli` or `npm install -g @mem0/cli`
- **Full command suite** — `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`
- **Interactive setup** — `mem0 init` with email verification or direct API key entry
- **Works everywhere** — Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable** — `--json` flag for CI/CD pipelines and automation
- **Dual SDK** — Same commands, same experience across Python and Node.js
</Update>
<Update label="2026-04-04" description="OpenClaw v1.0.4">
**OpenClaw Plugin — Production-Ready**
The OpenClaw Mem0 plugin went from initial release to production-ready in one week (v1.0.0 → v1.0.4):
- **Skills-based memory architecture** — New extraction pipeline with skill-loader, batched extraction, and domain-aware memory triage
- **Dream gate** — Automatic memory consolidation during idle periods for higher-quality long-term recall
- **Interactive CLI** — `openclaw mem0 init`, `status`, `config`, `import`, and `event` commands
- **Unified tool naming** — `memory_add` and `memory_delete` replace 4 legacy tools, matching the platform API
- **Security hardened** — Path traversal protection, pinned dependencies, 329 tests across 10 files
</Update>
<Update label="2026-04-02" description="Mem0 Plugin for AI Editors">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
Launched a unified Mem0 plugin across three major AI development environments — Claude Code and Cursor first (March 25), then Codex (April 2):
- **9 MCP memory tools** — add, search, get, update, delete, bulk delete, entity management via `mcp.mem0.ai`
- **Lifecycle hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server** — Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport** — New MCP transport protocol for real-time streaming
- **Codex-specific skill** — Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
</Update>
<Update label="2026-03-21" description="New Providers">
**Apache AGE, Turbopuffer, MiniMax, and pgvector for Node.js**
Major expansion of the provider ecosystem:
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
- **Turbopuffer** — New vector database provider for Python SDK
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
- **Reasoning models** — `reasoning_effort` parameter for OpenAI o1/o3-style models
</Update>
<Update label="2026-03-14" description="Mem0 Platform Skill">
**Mem0 Platform Skill on skills.sh**
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
</Update>
+185
View File
@@ -0,0 +1,185 @@
---
title: "OpenClaw"
description: "Release notes for the OpenClaw plugin and agent harness."
mode: "wide"
---
<Update label="2026-04-07" description="v1.0.5">
**Bug Fixes:**
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
- **OSS pgvector crash** ([#4727](https://github.com/mem0ai/mem0/issues/4727)): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
**Removed:**
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
</Update>
<Update label="2026-04-04" description="v1.0.4">
**New Features:**
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status`, `openclaw mem0 config show`, `openclaw mem0 config set`
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
- **`event list` / `event status` CLI commands**: Monitor background processing events
- **`fs-safe.ts` module**: Isolated filesystem wrappers in a separate entry point
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
- **Test suite**: 329 tests across 10 test files
**Changes:**
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts`
- **Code splitting**: tsup builds with `splitting: true` and two entry points
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`)
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race`
- **Auto-capture fire-and-forget**: `provider.add()` runs in background via `.then()/.catch()`
- **Auto-capture minimum content gate**: Skips extraction when total user content is fewer than 50 chars
**Removed:**
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- `memory_history` tool and `history` CLI command — deprecated
</Update>
<Update label="2026-04-03" description="v1.0.3">
**Bug Fixes:**
- **Security**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal
- **Noise filter**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction`
**Changes:**
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
**Tests:**
- 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
</Update>
<Update label="2026-04-02" description="v1.0.2">
**Bug Fixes:**
- **Security**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — plugin-side env resolution was redundant and triggered static analysis warnings ([#4676](https://github.com/mem0ai/mem0/pull/4676))
</Update>
<Update label="2026-04-02" description="v1.0.1">
**New Features:**
- **CD workflow**: Added continuous deployment workflow with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file ([#4667](https://github.com/mem0ai/mem0/pull/4667))
**Bug Fixes:**
- **Dream gate**: Fixed cheap-first ordering, session isolation, and verified completion ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup**: Plugin now starts gracefully when no API key is configured ([#4669](https://github.com/mem0ai/mem0/pull/4669))
</Update>
<Update label="2026-04-01" description="v1.0.0">
**New Features:**
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Memory consolidation and dream-cycle processing during idle periods
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
**Changes:**
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
</Update>
<Update label="2026-03-26" description="v0.4.1">
**New Features:**
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions
**Bug Fixes:**
- **Credential detection**: Improved detection of credentials, API keys, and secrets in extraction instructions (#4552)
- **Standalone timestamps**: Prevented extraction of standalone timestamps as memories (#4550)
</Update>
<Update label="2026-03-16" description="v0.4.0">
**New Features:**
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers
- **Subagent hallucination prevention**: Detects ephemeral subagent sessions and routes recall to parent namespace
- **Dynamic recall thresholding**: Memories scoring less than 50% of top result are dropped
- **SQLite resilience**: Init error recovery with automatic retry for OSS mode
- **`disableHistory` config option**: New `oss.disableHistory` flag
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
**Changes:**
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Relaxed extraction instructions: related facts kept together to preserve context
**Bug Fixes:**
- **Concurrent session race condition**: Lifecycle hooks now use `ctx.sessionKey` directly instead of a shared mutable variable
</Update>
<Update label="2026-03-12" description="v0.3.1">
**New Features:**
- **Message filtering pipeline**: Multi-stage noise removal before extraction
- **Broad recall for new sessions**: Short or new-session prompts trigger secondary broad search
- **Client-side threshold filtering**: Safety net that drops low-relevance results
- **Temporal anchoring**: Extraction instructions now include current date
- 55 unit tests covering filtering and isolation helpers
**Changes:**
- Extraction window expanded from last 10 to last 20 messages
- Rewritten custom extraction instructions for conciseness and deduplication
- Refactored monolithic `index.ts` (1772 lines) into 6 focused modules
</Update>
<Update label="2026-03-10" description="v0.3.0">
**Bug Fixes:**
- Updated `mem0ai` dependency with sqlite3 to better-sqlite3 migration (#4270)
</Update>
<Update label="2026-03-09" description="v0.2.0">
**New Features:**
- Per-agent memory isolation for multi-agent setups via `agentId`
- "Understanding userId" section in docs
**Changes:**
- Updated config examples to use concrete `userId` values instead of placeholders
**Bug Fixes:**
- Migrated platform search to Mem0 v2 API
</Update>
<Update label="2026-02-19" description="v0.1.2">
**New Features:**
- Source field for openclaw memory entries
**Bug Fixes:**
- Auto-recall injection and auto-capture message drop
</Update>
<Update label="2026-02-02" description="v0.1.0">
**New Features:**
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
</Update>
+290
View File
@@ -0,0 +1,290 @@
---
title: "Platform"
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
mode: "wide"
---
<Update label="2025-07-23" description="">
**Bug Fixes:**
- **Memory:** Fixed ADD functionality
</Update>
<Update label="2025-07-19" description="">
**New Features:**
- **UI:** Added Settings UI and latency display
- **Performance:** Neo4j query optimization
**Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
</Update>
<Update label="2025-07-18" description="">
**Improvements:**
- **UI:** Updated Event UI
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error
</Update>
<Update label="2025-07-17" description="">
**New Features:**
- **UI:** New Settings Page
- **Memory:** Duplicate memories entities support
**Improvements:**
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
</Update>
<Update label="2025-07-16" description="">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
**Bug Fixes:**
- **API:** Fixed pagination for get all API
</Update>
<Update label="2025-07-12" description="">
**Bug Fixes:**
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
**Improvements:**
- **Rate Limiting:** New rate limit for V2 Search
**Bug Fixes:**
- **Slack:** Fixed Slack rate limit error with backend improvements
</Update>
<Update label="2025-07-10" description="">
**Improvements:**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
</Update>
<Update label="2025-07-09" description="">
**Improvements:**
- **Graph:** Graph Optimizations V2 and memory improvements
</Update>
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
</Update>
+100 -293
View File
@@ -1,13 +1,25 @@
---
title: "Product Updates"
description: "Latest releases, bug fixes, and improvements for the Mem0 Python and TypeScript SDKs."
title: "SDK & Tools"
description: "Release notes for the Mem0 Python SDK, TypeScript SDK, Vercel AI SDK, CLI, and editor plugins."
mode: "wide"
---
<Tabs>
<Tab title="Python">
<Update label="2026-04-04" description="v1.0.11">
**New Features & Updates:**
- **SDK:** Added `multilingual` parameter to project update ([#4314](https://github.com/mem0ai/mem0/pull/4314))
**Bug Fixes:**
- **LLMs:** Fixed Groq model configuration ([#4700](https://github.com/mem0ai/mem0/pull/4700))
- **Core:** Prevented thread and memory leaks from PostHog telemetry ([#4535](https://github.com/mem0ai/mem0/pull/4535))
- **Vector Stores:** Used `DatetimeRange` for datetime string values in Qdrant range filters ([#4659](https://github.com/mem0ai/mem0/pull/4659))
- **Configs:** Added missing `ConfigDict` to vector store configs (Elasticsearch, MongoDB, Neptune, OpenSearch, PGVector, Supabase, Valkey) ([#4656](https://github.com/mem0ai/mem0/pull/4656))
</Update>
<Update label="2026-04-01" description="v1.0.10">
**New Features & Updates:**
@@ -831,6 +843,12 @@ mode: "wide"
</Tab>
<Tab title="TypeScript">
<Update label="2026-04-04" description="v2.4.6">
**New Features & Updates:**
- **Client:** Added `multilingual` parameter to project update types ([#4314](https://github.com/mem0ai/mem0/pull/4314))
</Update>
<Update label="2026-04-01" description="v2.4.5">
@@ -1140,352 +1158,141 @@ mode: "wide"
</Tab>
<Tab title="Platform">
<Tab title="CLI">
<Update label="2025-07-23" description="">
<Update label="2026-04-06" description="Python v0.2.2 / Node v0.2.2">
**New Features:**
- **Telemetry:** Added PostHog telemetry and source tracking to both Python and Node CLIs ([#4699](https://github.com/mem0ai/mem0/pull/4699))
- **Validation:** API key validated upfront via `/v1/ping/` on startup — fail-fast with a helpful error instead of cryptic 401s ([#4701](https://github.com/mem0ai/mem0/pull/4701))
**Bug Fixes:**
- **Memory:** Fixed ADD functionality
- **CD:** Fixed OIDC trusted publishing with `npx npm@latest` ([#4724](https://github.com/mem0ai/mem0/pull/4724))
- **CD:** Removed npm self-upgrade from CD workflows ([#4723](https://github.com/mem0ai/mem0/pull/4723))
</Update>
<Update label="2025-07-19" description="">
<Update label="2026-04-03" description="Python v0.2.1 / Node v0.2.1">
**New Features:**
- **UI:** Added Settings UI and latency display
- **Performance:** Neo4j query optimization
- **Docs:** Comprehensive README with installation, usage examples, and purple branding ([#4680](https://github.com/mem0ai/mem0/pull/4680))
**Bug Fixes:**
- **OpenMemory:** Fixed OMM raising unnecessary exceptions
- **npm:** Added `repository` field to Node packages for npm provenance ([#4671](https://github.com/mem0ai/mem0/pull/4671))
- **CD:** Added CD workflows for Node SDK packages with OIDC trusted publishing ([#4670](https://github.com/mem0ai/mem0/pull/4670))
</Update>
<Update label="2025-07-18" description="">
<Update label="2026-04-02" description="Python v0.2.0 / Node v0.1.1">
**Improvements:**
- **UI:** Updated Event UI
- **Performance:** Fixed N+1 query issue in semantic_search_v2 by optimizing MemorySerializer field selection
**New Features:**
- **`event` commands:** `mem0 event list` shows recent background processing events in a table; `mem0 event status <id>` shows full detail including nested memory results ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`--json` / `--agent` flag:** Root-level flag switches all command output to a structured JSON envelope for programmatic/agent consumption. Envelope format: `{"status", "command", "duration_ms", "scope", "count", "data"}` ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Agent output sanitization:** Raw API responses projected to only relevant fields per command (e.g., `add` → `{id, memory, event}`, `search` → `{id, memory, score, created_at, categories}`) ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **Email login:** Added email verification code login to `mem0 init` ([#4623](https://github.com/mem0ai/mem0/pull/4623))
- **Brand update:** Updated color palette from purple to golden ([#4664](https://github.com/mem0ai/mem0/pull/4664))
- **CI/CD:** Added CI pipelines and CD workflows for both CLIs ([#4640](https://github.com/mem0ai/mem0/pull/4640), [#4653](https://github.com/mem0ai/mem0/pull/4653))
**Bug Fixes:**
- **Memory:** Fixed duplicate memory index sentry error
</Update>
<Update label="2025-07-17" description="">
**New Features:**
- **UI:** New Settings Page
- **Memory:** Duplicate memories entities support
- **Node:** Fixed critical `MODULE_NOT_FOUND` crash on `status`, `import`, and all commands when installed globally — replaced runtime `createRequire` with build-time version injection ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Node:** API errors now show full response detail instead of bare "Bad Request" ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **Python:** Fixed double error printing on all commands ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- **`status` command:** Replaced heavyweight `/v1/entities/` check with dedicated `GET /v1/ping/` endpoint ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`add` command:** Deduplicated PENDING results from API; changed misleading count message ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`init` command:** Partial flags now work in non-TTY; warns before overwriting existing config; added `--force` flag ([#4649](https://github.com/mem0ai/mem0/pull/4649))
- **`delete` command:** Fixed entity delete via v2 API for all entity types ([#4649](https://github.com/mem0ai/mem0/pull/4649))
**Improvements:**
- **Performance:** Optimized semantic search and get_all APIs by eliminating N+1 queries
- Tables now show full UUIDs (was truncated to 8 chars, making `mem0 get <id>` fail) ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- Search table includes Score column ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- `config get api_key` short-form aliases added ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- Client-side validation for `--expires`, `--page-size`, `--page`, `--top-k`, `--threshold`, and empty content ([#4636](https://github.com/mem0ai/mem0/pull/4636))
- `printInfo` / `printScope` moved to stderr to avoid contaminating JSON piping ([#4636](https://github.com/mem0ai/mem0/pull/4636))
</Update>
<Update label="2025-07-16" description="">
<Update label="2026-03-26" description="Python v0.1.0 / Node v0.1.0">
**New Features:**
- **Database:** Implemented read replica routing with enhanced logging and app-specific DB routing
**Initial Release — Official Mem0 CLI**
**Improvements:**
- **Performance:** Improved query performance in search v2 and get all v2 endpoints
A full-featured command-line interface for Mem0, available in both Python and Node.js:
**Bug Fixes:**
- **API:** Fixed pagination for get all API
</Update>
<Update label="2025-07-12" description="">
**Bug Fixes:**
- **Graph:** Fixed social graph bugs and connection issues
</Update>
<Update label="2025-07-11" description="">
**Improvements:**
- **Rate Limiting:** New rate limit for V2 Search
**Bug Fixes:**
- **Slack:** Fixed Slack rate limit error with backend improvements
</Update>
<Update label="2025-07-10" description="">
**Improvements:**
- **Performance:**
- Changed connection pooling time to 5 minutes
- Separated graph lambdas for better performance
</Update>
<Update label="2025-07-09" description="">
**Improvements:**
- **Graph:** Graph Optimizations V2 and memory improvements
</Update>
<Update label="2025-07-08" description="">
**New Features:**
- **Database:** Added read replica support for improved database performance
- **UI:** Implemented UI changes for Users Page
- **Feedback:** Enabled feedback functionality
**Bug Fixes:**
- **Serializer:** Fixed GET ALL Serializer
</Update>
<Update label="2025-07-05" description="">
**New Features:**
- **UI:** User Page Revamp and New Users Page
</Update>
<Update label="2025-07-04" description="">
**New Features:**
- **Users:** New Users Page implementation
- **Tools:** Added script to backfill memory categories
**Bug Fixes:**
- **Filters:** Fixed Filters Get All functionality
</Update>
<Update label="2025-07-03" description="">
**Improvements:**
- **Graph:** Graph Memory optimization
- **Memory:** Fixed exact memories and semantically similar memories retrieval
</Update>
<Update label="2025-07-02" description="">
**Improvements:**
- **Categorization:** Refactored categorization logic to utilize Gemini 2.5 Flash and improve message handling
</Update>
<Update label="2025-07-01" description="">
**Bug Fixes:**
- **Memory:** Fixed old_memory issue in Async memory addition lambda
- **Events:** Fixed missing events
</Update>
<Update label="2025-06-30" description="">
**Improvements:**
- **Graph:** Improvements to graph memory and added user to LTM-STM
</Update>
<Update label="2025-06-28" description="">
**New Features:**
- **Graph:** Added support for SQS in graph memory addition
- **Testing:** Added Locust load testing script and Grafana Dashboard
</Update>
<Update label="2025-06-27" description="">
**Improvements:**
- **Rate Limiting:** Updated rate limiting for ADD API to 1000/min
- **Performance:** Improved Neo4j performance
</Update>
<Update label="2025-06-26" description="">
**New Features:**
- **Memory:** Edit Memory From Drawer functionality
- **API:** Added Topic Suggestions API Endpoint
</Update>
<Update label="2025-06-25" description="">
**New Features:**
- **Group Chat:** Group-Chat v2 with Actor-Aware Memories
- **Memory:** Editable Metadata in Memories
- **UI:** Memory Actions Badges
</Update>
<Update label="2025-06-19" description="">
**New Features:**
- **Rate Limiting:** Implemented comprehensive rate limiting system
**Improvements:**
- **Performance:** Added performance indexes for memory stats query
**Bug Fixes:**
- **Search:** Fixed search events not respecting top-k parameter
</Update>
<Update label="2025-06-18" description="">
**New Features:**
- **Memory Management:** Implemented OpenAI Batch API for Memory Cleaning with fallback
- **Playground:** Added Claude 4 support on Playground
**Improvements:**
- **Memory:** Added ability to update memory metadata
</Update>
<Update label="2025-06-17" description="">
**New Features:**
- **UI:** New Memories Page UI design
</Update>
<Update label="2025-06-16" description="">
**Improvements:**
- **Infrastructure:** Migrated to Application Load Balancer (ALB)
</Update>
<Update label="2025-06-13" description="">
**Improvements:**
- **Memory Management:** Enhanced Memory Management with Cosine Similarity Fallback
</Update>
<Update label="2025-06-11" description="">
**New Features:**
- **OMM:** Added OMM Script and UI functionality
**Improvements:**
- **API:** Added filters validation to semantic_search_v2 endpoint
</Update>
<Update label="2025-06-09" description="">
**New Features:**
- **Intercom:** Set Intercom events for ADD and SEARCH operations
- **OpenMemory:** Added Posthog integration and feedback functionality
- **MCP:** New JavaScript MCP Server with feedback support
**Improvements:**
- **Structured Data:** Enhanced structured data handling in memory management
</Update>
<Update label="2025-06-06" description="">
**New Features:**
- **OAuth:** Added Mem0 OAuth integration
- **OMM:** Added OMM-Mem0 sync for deleted memories
</Update>
<Update label="2025-06-05" description="">
**New Features:**
- **Filters:** Implemented Wildcard Filters and refactored filter logic in V2 Views
</Update>
<Update label="2025-06-02" description="">
**New Features:**
- **OpenMemory Cloud:** Added OpenMemory Cloud support
- **Structured Data:** Added 'structured_attributes' field to Memory model
</Update>
<Update label="2025-05-30" description="">
**New Features:**
- **Projects:** Added version and enable_graph to project views
- **OpenMemory:** Added Postgres support for OpenMemory
</Update>
<Update label="2025-05-19" description="">
**Bug Fixes:**
- **Core:** Fixed unicode error in user_id, agent_id, run_id and app_id
- **Install:** `pip install mem0-cli` (Python) or `npm install -g @mem0/cli` (Node.js)
- **Full command suite:** `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`
- **Interactive setup:** `mem0 init` with API key entry and user ID configuration
- **Works everywhere:** Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable:** `-o json` flag for CI/CD pipelines and automation
- **Dual SDK:** Same commands, same experience across Python and Node.js
- **Shared spec:** Both implementations driven by a single `cli-spec.json` ensuring identical behavior ([#4575](https://github.com/mem0ai/mem0/pull/4575))
</Update>
</Tab>
<Tab title="Vercel AI SDK">
<Tab title="Plugins">
<Update label="2025-12-26" description="v2.0.5">
<Update label="2026-04-02" description="mem0-plugin v1.0.0">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
The unified Mem0 plugin for AI development environments:
- **9 MCP memory tools:** `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities` — all via `mcp.mem0.ai`
- **Lifecycle hooks:** Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server:** Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport:** New MCP transport protocol for real-time streaming
- **Codex-specific skill:** Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
- **Supported editors:** Claude Code, Claude Cowork, Cursor, Codex
</Update>
<Update label="2025-12-26" description="Vercel AI SDK v2.0.5">
**Bug Fix:**
- **Vercel AI SDK:** Removed unnecessary dependencies to make the package lighter.
- Removed unnecessary dependencies to make the package lighter.
</Update>
<Update label="2025-09-25" description="v2.0.4">
<Update label="2025-09-25" description="Vercel AI SDK v2.0.3 – v2.0.4">
**New Features:**
- Added file support for multimodal capabilities with memory context (v2.0.3)
**Bug Fix:**
- **Vercel AI SDK:** Fixed version parameter in the AI SDK to use V2 for addition.
- Fixed version parameter to use V2 for addition (v2.0.4)
</Update>
<Update label="2025-09-25" description="v2.0.3">
**New Features:**
- **Vercel AI SDK:** Added file support for multimodal capabilities with memory context
</Update>
<Update label="2025-09-03" description="v2.0.2">
<Update label="2025-09-03" description="Vercel AI SDK v2.0.2">
**Bug Fix:**
- **Vercel AI SDK:** Fixed streaming response in the AI SDK.
- Fixed streaming response in the AI SDK.
</Update>
<Update label="2025-08-05" description="v2.0.1">
<Update label="2025-08-05" description="Vercel AI SDK v2.0.0 – v2.0.1">
**New Features:**
- **Vercel AI SDK:** Added a new param `host` to the config.
- Migration to AI SDK V5 (v2.0.0)
- Added `host` param to the config (v2.0.1)
</Update>
<Update label="2025-08-05" description="v2.0.0">
<Update label="2025-06-15" description="Vercel AI SDK v1.0.6">
**New Features:**
- **Vercel AI SDK:** Migration to AI SDK V5.
- Added `filter_memories` param.
</Update>
<Update label="2025-06-15" description="v1.0.6">
<Update label="2025-05-23" description="Vercel AI SDK v1.0.5">
**New Features:**
- **Vercel AI SDK:** Added param `filter_memories`.
- Added support for Google provider.
</Update>
<Update label="2025-05-23" description="v1.0.5">
<Update label="2025-05-10" description="Vercel AI SDK v1.0.3 – v1.0.4">
**New Features:**
- **Vercel AI SDK:** Added support for Google provider.
</Update>
- Added support for `output_format` param (v1.0.4)
<Update label="2025-05-10" description="v1.0.4">
**New Features:**
- **Vercel AI SDK:** Added support for new param `output_format`.
</Update>
<Update label="2025-05-08" description="v1.0.3">
**Improvements:**
- **Vercel AI SDK:** Added support for graceful failure in cases services are down.
- Added graceful failure handling when services are down (v1.0.3)
</Update>
<Update label="2025-05-01" description="v1.0.1">
<Update label="2025-05-01" description="Vercel AI SDK v1.0.1">
**New Features:**
- **Vercel AI SDK:** Added support for graph memories
- Added support for graph memories.
</Update>
</Tab>
</Tabs>
+22 -6
View File
@@ -138,7 +138,10 @@
"group": "Release Notes",
"icon": "rocket",
"pages": [
"changelog"
"changelog/highlights",
"changelog/sdk",
"changelog/platform",
"changelog/openclaw"
]
}
]
@@ -147,9 +150,11 @@
"tab": "OpenClaw",
"groups": [
{
"group": "OpenClaw",
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw"
"integrations/openclaw",
"integrations/hermes"
]
}
]
@@ -411,12 +416,11 @@
"integrations/autogen",
"integrations/agno",
"integrations/camel-ai",
"integrations/openclaw",
"integrations/hermes",
"integrations/openai-agents-sdk",
"integrations/google-ai-adk",
"integrations/mastra",
"integrations/vercel-ai-sdk"
"integrations/vercel-ai-sdk",
"integrations/chatdev"
]
},
{
@@ -460,6 +464,14 @@
"integrations/cursor",
"integrations/codex"
]
},
{
"group": "Agent Harness",
"icon": "robot",
"pages": [
"integrations/openclaw",
"integrations/hermes"
]
}
]
},
@@ -600,6 +612,10 @@
]
},
"redirects": [
{
"source": "/changelog",
"destination": "/changelog/highlights"
},
{
"source": "/api-reference/memory/v2-search-memories",
"destination": "/api-reference/memory/search-memories"
+20 -49
View File
@@ -382,67 +382,38 @@ Here are the available integrations for Mem0:
Build AI agents with persistent memory using Mastra's framework and tools.
</Card>
<Card
title="Claude Code"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.92888 16.2181L9.86008 14.0122L9.92585 13.8199L9.86008 13.7137H9.66782L9.01009 13.6732L6.76369 13.6125L4.81581 13.5315L2.92863 13.4303L2.45304 13.3292L2.00781 12.7423L2.05335 12.4488L2.45304 12.1807L3.02476 12.2313L4.28962 12.3173L6.18692 12.4488L7.56309 12.5298L9.60204 12.7423H9.92585L9.97138 12.6107L9.86008 12.5298L9.77407 12.4488L7.811 11.1182L5.68603 9.71165L4.57295 8.90214L3.97088 8.49233L3.66731 8.10781L3.53577 7.26794L4.08219 6.66587L4.81581 6.71646L5.00301 6.76706L5.74674 7.33877L7.33541 8.56822L9.40979 10.0962L9.71335 10.3491L9.83478 10.2631L9.84996 10.2024L9.71335 9.97475L8.5851 7.93579L7.38095 5.86141L6.84465 5.00131L6.70298 4.48524C6.65239 4.27275 6.61697 4.09567 6.61697 3.87811L7.23928 3.03318L7.58332 2.92188L8.41307 3.03318L8.76218 3.33675L9.27824 4.5156L10.113 6.37242L11.4083 8.89708L11.7877 9.64588L11.9901 10.339L12.066 10.5515H12.1975V10.4301L12.3038 9.00839L12.5011 7.26288L12.6934 5.01649L12.7591 4.38406L13.0728 3.62514L13.6951 3.21532L14.1808 3.44806L14.5805 4.01978L14.5249 4.38912L14.2871 5.93225L13.8216 8.35066L13.5181 9.96969H13.6951L13.8975 9.76731L14.7171 8.67953L16.0933 6.95931L16.7005 6.27629L17.4088 5.52243L17.8641 5.16321H18.7242L19.3567 6.10427L19.0733 7.07568L18.1879 8.19888L17.4543 9.15006L16.4019 10.5667L15.7442 11.7L15.8049 11.7911L15.9618 11.7759L18.3397 11.27L19.6248 11.0372L21.1578 10.7741L21.851 11.0979L21.9269 11.4268L21.6537 12.0997L20.0144 12.5045L18.0918 12.889L15.2282 13.567L15.1927 13.5923L15.2332 13.6428L16.5234 13.7643L17.0749 13.7946H18.4257L20.9403 13.9818L21.598 14.4169L21.9926 14.9482L21.9269 15.3529L20.915 15.869L19.5489 15.5452L16.3615 14.7863L15.2686 14.5131H15.1168V14.6041L16.0275 15.4946L17.6972 17.0023L19.7867 18.9451L19.893 19.4258L19.6248 19.8053L19.3415 19.7648L17.5049 18.3835L16.7966 17.7612L15.1927 16.4104H15.0865V16.552L15.4558 17.0934L17.4088 20.0279L17.51 20.9285L17.3683 21.2219L16.8624 21.399L16.3058 21.2978L15.1624 19.6939L13.9835 17.8877L13.0324 16.2687L12.916 16.3345L12.3544 22.3805L12.0913 22.6891L11.4842 22.9219L10.9782 22.5374L10.7101 21.915L10.9782 20.6856L11.302 19.0818L11.5651 17.8068L11.8029 16.2232L11.9446 15.697L11.9345 15.6616L11.8181 15.6767L10.6241 17.316L8.80771 19.7698L7.37083 21.3079L7.02679 21.4445L6.42977 21.1359L6.48542 20.5844L6.81935 20.0936L8.80771 17.5639L10.0068 15.9955L10.7809 15.0898L10.7758 14.9583H10.7303L5.44824 18.3886L4.50718 18.51L4.10242 18.1306L4.15302 17.5083L4.34528 17.3059L5.93394 16.213L5.92888 16.2181Z" fill="currentColor"/>
</svg>
}
href="/integrations/claude-code"
title="OpenAI Agents SDK"
icon="robot"
href="/integrations/openai-agents-sdk"
>
Add persistent memory to Claude Code and Cowork with MCP server, lifecycle hooks, and SDK skill.
Integrate Mem0 with the OpenAI Agents SDK for persistent memory across multi-agent workflows.
</Card>
<Card
title="Cursor"
icon={
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4216 6.73383L12.4151 2.11135C12.158 1.96288 11.8408 1.96288 11.5837 2.11135L3.57759 6.73383C3.36146 6.85862 3.22803 7.08941 3.22803 7.33937V16.6606C3.22803 16.9106 3.36146 17.1414 3.57759 17.2662L11.5841 21.8886C11.8412 22.0371 12.1584 22.0371 12.4155 21.8886L20.4219 17.2662C20.6381 17.1414 20.7715 16.9106 20.7715 16.6606V7.33937C20.7715 7.08941 20.6377 6.85862 20.4216 6.73383ZM19.9187 7.71298L12.1896 21.1001C12.1373 21.1903 11.9994 21.1534 11.9994 21.0489V12.2832C11.9994 12.1081 11.9058 11.9461 11.7539 11.8581L4.16282 7.47543C4.07261 7.42319 4.10945 7.28524 4.21394 7.28524H19.6721C19.8916 7.28524 20.0284 7.52279 19.9187 7.71298Z" fill="currentColor"/>
</svg>
}
href="/integrations/cursor"
title="Google ADK"
icon="google"
href="/integrations/google-ai-adk"
>
Add persistent memory to Cursor with MCP server, lifecycle hooks, and context-aware coding.
Integrate Mem0 with Google Agent Development Kit for persistent memory across multi-agent workflows.
</Card>
<Card
title="Codex"
icon={
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.5565 10.6338C21.0009 9.27575 20.8528 7.76958 20.1367 6.53501C19.0503 4.63378 16.8528 3.67081 14.7046 4.11526C13.7663 3.05353 12.3836 2.46094 10.9515 2.46094C8.75399 2.46094 6.82807 3.86835 6.13671 5.94242C4.7293 6.23872 3.51943 7.10291 2.80338 8.36217C1.71696 10.2634 1.96387 12.6338 3.42066 14.2634C2.97622 15.6461 3.14906 17.1276 3.8651 18.3622C4.95152 20.2634 7.14906 21.2511 9.2972 20.7819C10.2602 21.8437 11.6182 22.4609 13.0503 22.4609C15.2478 22.4609 17.1737 21.0535 17.8651 18.9795C19.2725 18.6832 20.4824 17.819 21.1984 16.5597C22.2849 14.6585 22.0379 12.2634 20.5565 10.6338ZM13.0503 21.1523C12.1614 21.1523 11.3219 20.856 10.6552 20.2881C10.6799 20.2634 10.754 20.2387 10.7787 20.214L14.754 17.9177C14.9515 17.7943 15.075 17.5967 15.075 17.3498V11.7449L16.754 12.7079C16.7787 12.7079 16.7787 12.7325 16.7787 12.7572V17.3992C16.8034 19.4733 15.1244 21.1523 13.0503 21.1523ZM5.00091 17.7202C4.55646 16.9548 4.40831 16.0659 4.55646 15.2017C4.58115 15.2264 4.63054 15.2511 4.67992 15.2758L8.65523 17.572C8.85276 17.6955 9.09967 17.6955 9.2972 17.572L14.1614 14.7572V16.7079C14.1614 16.7325 14.1614 16.7572 14.1367 16.7572L10.112 19.0782C8.33424 20.1153 6.03794 19.498 5.00091 17.7202ZM3.96387 9.02884C4.40831 8.26341 5.09967 7.69551 5.91449 7.37452V12.1153C5.91449 12.3375 6.03794 12.5597 6.23548 12.6832L11.0997 15.498L9.42066 16.4609C9.39597 16.4609 9.37128 16.4856 9.37128 16.4609L5.34659 14.1399C3.51943 13.1029 2.92683 10.8066 3.96387 9.02884ZM17.791 12.2387L12.9268 9.4239L14.6058 8.46094C14.6305 8.46094 14.6552 8.43625 14.6552 8.46094L18.6799 10.7819C20.4824 11.819 21.075 14.1153 20.0379 15.893C19.5935 16.6585 18.9021 17.2264 18.0873 17.5227V12.8066C18.112 12.5844 17.9886 12.3622 17.791 12.2387ZM19.4454 9.7202C19.4207 9.69551 19.3713 9.67081 19.3219 9.64612L15.3466 7.34983C15.1491 7.22637 14.9021 7.22637 14.7046 7.34983L9.84041 10.1646V8.21402C9.84041 8.18933 9.84041 8.16464 9.86511 8.16464L13.8898 5.84365C15.6923 4.80662 17.9639 5.4239 19.0009 7.22637C19.4454 7.96711 19.5935 8.856 19.4454 9.7202ZM8.92683 13.177L7.24782 12.214C7.22313 12.214 7.22313 12.1893 7.22313 12.1646V7.52267C7.22313 5.44859 8.90214 3.76958 10.9762 3.76958C11.8651 3.76958 12.7046 4.06588 13.3713 4.63378C13.3466 4.65847 13.2972 4.68316 13.2478 4.70785L9.27251 7.00415C9.07498 7.1276 8.95152 7.32514 8.95152 7.57205V13.177H8.92683ZM9.84041 11.2017L12.0133 9.94242L14.1861 11.2017V13.6955L12.0133 14.9548L9.84041 13.6955V11.2017Z" fill="currentColor"/>
</svg>
}
href="/integrations/codex"
title="Flowise"
icon="diagram-project"
href="/integrations/flowise"
>
Add persistent memory to OpenAI Codex with MCP server and skill-based memory protocol.
Add persistent Mem0 memory to Flowise chatflows for context-aware conversations in the low-code builder.
</Card>
<Card
title="OpenClaw"
icon={
<svg width="24" height="24" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="m153.5 173.5q24.62 1.46 46 13.5 12.11 8.1 17.5 21.5 0.74 2.45 0.5 5 0.09 0.81 1 1 1.48-4.9 1-10 5.04 10.48 1.5 22-9.81 27.86-35.5 42.5-26.17 14.97-56 19.5-2.77-0.4-2 1 2.86 1.27 6 1 25.64 1.53 48.5-10 0.34 10.08 2 20 1.08 5.76 5 10 1 1.5 0 3-31.11 20.84-68.5 17.5-23.7-5.7-32.5-28.5-4.39-9.18-3.5-19 15.41 6.23 32 4.5-20.68-6.39-39-18-34.81-27.22-12.5-65.5 11.84-14.83 29-23 4.21 7.66 11.5 12.5 3 1 6 0-26.04-34.62-29-78-0.13-8.46 2-16.5 1 6.5 2 13 3.43 39.53 24.5 73 2.03 2.28 4.5 4 0.5-1.25 1-2.5-1.27-6.54-5-12 0.5-0.75 1-1.5 9.72-3.43 20-4 0.55 10.34 8 17.5 1.94 0.74 4 0.5-17.8-64.6 16.5-122 0.98-1.79 1.5 0-28.21 56.64-13.5 118 1.08 1.43 2.5 0.5 2.21-4.98 2-10.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m454.5 97.5q-1.33 11.18-8.5 20-21.81 26.28-55.5 32-1.11-0.2-2 0.5 2.31 2.82 5.5 4.5 1 2 0 4-9.56 11.3-19.5 20 19.71-8.72 31-27 2.68-0.43 5 1-14.24 30.97-48 36.5-9.93 1.71-20 1.5-6.8-0.48-13 1 5.81 6.92 14 11-10.78 16.03-27 26.5 27.16-7.4 38-33.5 4.34 1.35 9 1-9.08 23.84-33 33.5-18.45 6.41-38 7 22.59 8.92 45-1 12.05-5.52 24-11 9.01-1.79 17 2.5 5.28-4.38 11-8 12.8-6.07 27-5 0 0.5 0 1-19.34 2.69-34 15.5 0.5 0.25 1 0.5 17.79-8.09 36-15 2.71-0.79 5-2 2.5-1 5-2 5.53-4.04 11-8 11.7-4.18 24-6.5 7.78-1.36 15 1.5-2.97 18.45-13.5 34-34.92 49.37-94.5 62.5-59.27 12.45-108-23-15.53-12.52-21.5-31.5-2.47-14.26 4-27-3.15 24.41 14 42-4.92-10.28-7-22-1.97-17.63 7-33 47.28-69.5 125.5-100 15.86-3.42 32-5.5 18.63-1.47 37 1.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m231.5 238.5q1.31-0.2 2 1-3.13 28.62 15 51-16.25 6.75-27-7.5-1-1-2 0 14.73 29.34 46 18.5 1.79 0.52 0 1.5-37.63 16.82-50.5-22.5-5.1-26.48 16.5-42z" fill="currentColor"/>
<path fill-rule="evenodd" d="m203.5 266.5q1.31-0.2 2 1-2.48 22.08 12 39-6.99 1.35-14 0.5 4.59 4.08 10 7-8.71 0.28-14.5-6.5-16.98-22.76 4.5-41z" fill="currentColor"/>
<path fill-rule="evenodd" d="m58.5 284.5q9.6-2.17 14.5 6 5.15 14.18-1 28-11.05-13.14-27.5-17.5 5.15-9.9 14-16.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m56.5 313.5q3.43 5.43 8 10-4.88 0.44-8 4-1.11-0.2-2 0.5 28.91 1.65 38 28.5 0.45 3.16-1 6-11.02-7.01-23-12.5-4.75-3.75-9.5-7.5 1.47 7.42 7 13 8.34 27.18 32 43 0.99 2.41-1.5 3.5-40.25 5.58-66.5-25.5-15.67-22.01-8-48 10.46-23.87 34.5-15z" fill="currentColor"/>
<path fill-rule="evenodd" d="m198.5 319.5q1.44 0.68 2.5 2 2.41 8.23 6 16 1.2 2.64-0.5 5-30.65 21.41-68 18.5-25.16-6.17-32.5-30.5 6.96 4.99 15.5 6.5 8.99 0.75 18 0.5 16.25 2.38 32-2.5 15.9-3.94 27-15.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m239.5 342.5q7.02-0.25 14 0.5 4.46 1.06 8 3.5-5.2 2.35-10 5.5-3.88 4.65-9 7.5-9.89-3.09-9.5-13 2.36-3.63 6.5-4z" fill="currentColor"/>
<path fill-rule="evenodd" d="m214.5 349.5q5.96 7.2 13.5 13 1 1 0 2-28.58 23.34-65.5 20.5-18.15-4.24-27.5-19.5 1.13 0.94 2.5 1.5 14.7 1.42 29-1.5 26.57-0.52 48-16z" fill="currentColor"/>
<path fill-rule="evenodd" d="m302.5 373.5q0.21 2.44-2 3.5-28.69 7.6-50.5-12.5-0.06-6.71 6.5-9 4.45-0.75 9-1 22.26 2.27 37 19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m232.5 365.5q17.6 6.19 10.5 23-10.6 10.42-25.5 11.5-25.94 3.21-49-9 36.75-1.65 64-25.5z" fill="currentColor"/>
<path fill-rule="evenodd" d="m113.5 367.5q7.7-0.01 9.5 7-9.69 7.19-18.5 15.5-7.23 5.76-5.5-3.5 3.12-12.84 14.5-19z" fill="currentColor"/>
<path fill-rule="evenodd" d="m126.5 380.5q7.88-0.4 12 6.5-8.5 7.25-17 14.5-5.62-12.55 5-21z" fill="currentColor"/>
<path fill-rule="evenodd" d="m283.5 385.5q3.22 2.95 7 5.5 2.8 4.03 6 7.5 0.42 2.77-2 4-15.5-9.75-31-19.5-1.79-0.98 0-1.5 9.96 2.49 20 4z" fill="currentColor"/>
</svg>
}
href="/integrations/openclaw"
title="AWS Bedrock"
icon="cloud"
href="/integrations/aws-bedrock"
>
Add long-term memory to OpenClaw agents with auto-recall and auto-capture support.
Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage.
</Card>
<Card
title="Hermes Agent"
icon="bolt"
href="/integrations/hermes"
title="ChatDev"
icon="comments"
href="/integrations/chatdev"
>
Add long-term memory to Hermes agents with automatic background sync and zero-latency prefetch.
Add persistent cloud-managed memory to ChatDev multi-agent workflows with zero-code YAML configuration.
</Card>
</CardGroup>
+243
View File
@@ -0,0 +1,243 @@
---
title: ChatDev
description: "Add persistent, cloud-managed memory to ChatDev multi-agent workflows with Mem0 — no code required, just YAML configuration."
---
Build multi-agent workflows in [ChatDev](https://github.com/OpenBMB/ChatDev) with persistent memory powered by Mem0. ChatDev is a zero-code multi-agent platform where agents, tools, and workflows are defined entirely in YAML. Mem0 integrates as a built-in memory store (`type: mem0`), giving your agents cloud-managed semantic search and cross-session persistence — all without writing any code.
## Overview
In this guide, you'll:
1. Set up ChatDev with the Mem0 memory store
2. Configure agents with persistent memory using YAML
3. Enable automatic memory retrieval and storage across conversations
4. Leverage cross-session persistence for personalized multi-agent interactions
## Prerequisites
- **Python 3.12+**
- **[uv](https://docs.astral.sh/uv/)** — Python package manager
- **Node.js 18+** and **npm** — only needed if using the web console
- A **Mem0 API key** from [app.mem0.ai](https://app.mem0.ai)
- An **OpenAI API key** (or another LLM provider supported by ChatDev)
## Setup and Configuration
Install ChatDev and its dependencies (includes `mem0ai`):
```bash
git clone https://github.com/OpenBMB/ChatDev.git
cd ChatDev
uv sync
```
If you plan to use the web console, also install the frontend:
```bash
cd frontend && npm install && cd ..
```
Set up your environment variables in a `.env` file:
<Note>Get your Mem0 API key from [Mem0 Platform](https://app.mem0.ai).</Note>
```bash
MEM0_API_KEY=your-mem0-api-key
API_KEY=your-openai-api-key
BASE_URL=https://api.openai.com/v1
```
## Configure Mem0 Memory Store
In your ChatDev workflow YAML, add a Mem0 memory store in the `memory` section:
```yaml
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: my-user-123 # optional: scope memories to a user
agent_id: my-agent # optional: scope memories to an agent
```
Mem0 handles all storage, embeddings, and search server-side — no local vector databases or embedding models are needed.
## Attach Memory to an Agent
Reference the memory store in your agent node's `memories` list:
```yaml
nodes:
- id: writer
type: agent
config:
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions.
memories:
- name: mem0_store
top_k: 5
similarity_threshold: 0.5 # minimum relevance score (0.0–1.0); set to -1.0 to disable
retrieve_stage:
- gen
read: true
write: true
```
- **`read: true`** — Agent retrieves relevant memories before generating a response
- **`write: true`** — Agent stores new memories from user input after each interaction
- **`top_k`** — Number of memories to retrieve per query
- **`similarity_threshold`** — Minimum relevance score for retrieved memories. Set to `-1.0` to return all results regardless of score
- **`retrieve_stage`** — When to retrieve memories. Options: `pre_gen_thinking` (before generation), `gen` (during generation), `post_gen_thinking` (after generation), `finished` (after completion)
## Full Example Workflow
Here's a complete workflow YAML that creates a memory-backed conversational agent:
```yaml
version: 0.4.0
graph:
description: Memory-backed conversation using Mem0
nodes:
- id: writer
type: agent
config:
base_url: ${BASE_URL}
api_key: ${API_KEY}
provider: openai
name: gpt-5.4
role: |
You are a knowledgeable writer. Use your memories to build
on past interactions. If memory sections are provided
(wrapped by ===== Related Memories =====), incorporate
relevant context from those memories into your response.
params:
temperature: 0.7
max_tokens: 2000
memories:
- name: mem0_store
top_k: 5
retrieve_stage:
- gen
read: true
write: true
memory:
- name: mem0_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: project-user-123
agent_id: writer-agent
start:
- writer
end: []
```
Run the workflow:
```bash
# Option 1: CLI (recommended for quick testing)
uv run python run.py --path yaml_instance/demo_mem0_memory.yaml --name my_project
# Option 2: Web console
make dev
# Backend starts at http://localhost:6400, frontend at http://localhost:5173
```
To use the web console, open `http://localhost:5173`, create a new workflow, and paste your YAML configuration into the editor. The web console provides a visual chat interface for interacting with your memory-backed agents.
## How It Works
When an agent with Mem0 memory receives input, the following cycle runs automatically:
**1. Retrieve** — Before generating a response, ChatDev queries Mem0 with the user's input using semantic search. Relevant memories are injected into the agent's context in this format:
```
===== Related Memories =====
--- mem0_store ---
1. User's favorite language is Rust
2. User lives in San Francisco
===== End of Memory =====
```
This is why the role prompt in the example references `===== Related Memories =====` — the agent needs to know how to use this injected context.
**2. Generate** — The agent produces a response using the retrieved memories as additional context.
**3. Store** — After generation, the user's input is sent to Mem0 via `client.add()`. Mem0's extraction model automatically identifies and stores facts, preferences, and key information. Only user input is stored — agent output is excluded to keep memories clean.
Memories persist in Mem0's cloud across all sessions. The next time the same `user_id` or `agent_id` is used, previous memories are automatically retrieved.
## Dual-Scope Memory (User + Agent)
When both `user_id` and `agent_id` are configured, Mem0 uses an OR filter to search across both scopes in a single query:
```yaml
memory:
- name: shared_store
type: mem0
config:
api_key: ${MEM0_API_KEY}
user_id: alice # stores user preferences ("Alice prefers dark mode")
agent_id: support-bot # stores agent-learned context ("Resolved Alice's billing issue")
```
This means retrieval returns memories from **both** the user's scope and the agent's scope. Writes include both IDs, so each memory is accessible from either dimension. Use this when you want an agent to remember both what the user told it *and* what the agent learned across sessions.
## Configuration Reference
### Memory Store Config
| Field | Required | Description |
|-------|----------|-------------|
| `api_key` | Yes | Mem0 API key from [app.mem0.ai](https://app.mem0.ai) |
| `user_id` | No | Scope memories to a specific user |
| `agent_id` | No | Scope memories to a specific agent |
### Memory Attachment Config
| Field | Default | Description |
|-------|---------|-------------|
| `top_k` | `3` | Number of memories to retrieve |
| `similarity_threshold` | `-1.0` (disabled) | Minimum relevance score. Set a value between `0.0` and `1.0` to filter low-relevance results. Default (`-1.0`) returns all matches without filtering |
| `retrieve_stage` | `["gen"]` | When to retrieve: `pre_gen_thinking`, `gen`, `post_gen_thinking`, or `finished` |
| `read` | `true` | Whether the agent retrieves memories |
| `write` | `true` | Whether the agent stores new memories |
## Tips and Common Pitfalls
<Info>
**Indexing delay** — Freshly stored memories may take a few seconds to become searchable. If a memory isn't retrieved immediately after being stored, wait a moment and try again.
</Info>
- **No memories returned on first run** — This is expected. Memories are stored *after* the agent responds, so the first interaction has no prior context. Memories appear starting from the second interaction onward.
- **`mem0ai` not installed** — If you see `ImportError: mem0ai is required for Mem0Memory`, run `uv add mem0ai` or `pip install mem0ai` to add the dependency.
- **Invalid API key** — A wrong or expired `MEM0_API_KEY` will log errors like `Mem0 search failed` or `Mem0 add failed` but won't crash the agent. Check your key at [app.mem0.ai](https://app.mem0.ai).
- **Pipeline headers in memories** — ChatDev automatically strips internal pipeline headers (e.g., `=== INPUT FROM TASK (user) ===`) before sending text to Mem0, so your memories stay clean.
- **Clearing test memories** — To delete memories created during testing, use the Mem0 dashboard at [app.mem0.ai](https://app.mem0.ai) or the Python SDK: `MemoryClient().delete_all(user_id="your-test-user")`.
## Key Features
1. **Zero-Code Integration** — Configure Mem0 entirely through YAML, no Python code required
2. **Cloud-Managed Storage** — Mem0 handles embeddings, persistence, and search server-side
3. **Semantic Search** — Retrieve contextually relevant memories, not just keyword matches
4. **Cross-Session Persistence** — Memories survive across runs, sessions, and restarts
5. **Multi-Agent Memory Sharing** — Multiple agents can share memories through common `user_id` or `agent_id` scopes
6. **Intelligent Input Processing** — Only user input is stored; agent output is excluded to prevent noisy memories
## Conclusion
By adding Mem0 as a memory store in ChatDev, your multi-agent workflows gain persistent, intelligent memory with zero code changes. Agents automatically remember past interactions and use that context to provide personalized, coherent responses across sessions.
<CardGroup cols={2}>
<Card title="CrewAI Integration" icon="users" href="/integrations/crewai">
Build multi-agent systems with CrewAI and Mem0
</Card>
<Card title="AutoGen Integration" icon="robot" href="/integrations/autogen">
Build conversational agents with AutoGen and Mem0
</Card>
</CardGroup>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.4.5",
"version": "2.4.6",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+2
View File
@@ -134,6 +134,8 @@ class AzureOpenAILLM(LLMBase):
"messages": messages,
})
if response_format:
params["response_format"] = response_format
if tools:
params["tools"] = tools
params["tool_choice"] = tool_choice
+1 -1
View File
@@ -17,7 +17,7 @@ class GroqLLM(LLMBase):
super().__init__(config)
if not self.config.model:
self.config.model = "llama3-70b-8192"
self.config.model = "llama-3.3-70b-versatile"
api_key = self.config.api_key or os.getenv("GROQ_API_KEY")
self.client = Groq(api_key=api_key)
+33 -8
View File
@@ -63,6 +63,19 @@ def _normalize_iso_timestamp_to_utc(timestamp: Optional[str]) -> Optional[str]:
return parsed.astimezone(timezone.utc).isoformat()
def _resolve_mapped_id(temp_uuid_mapping, resp, event_type):
"""Resolve a temp integer ID from the LLM response to a real UUID.
Returns the UUID if found, or None (with a warning log) if the LLM
hallucinated an ID that doesn't exist in the mapping.
"""
raw_id = resp.get("id")
memory_id = temp_uuid_mapping.get(raw_id)
if memory_id is None:
logger.warning(f"{event_type} skipped: LLM returned unknown id {raw_id!r}")
return memory_id
# Fields that hold runtime auth/connection objects and must be preserved.
# These are non-serializable objects (e.g. AWSV4SignerAuth, RequestsHttpConnection)
# needed by clients like OpenSearch — not sensitive strings to redact.
@@ -639,28 +652,34 @@ class Memory(MemoryBase):
)
returned_memories.append({"id": memory_id, "memory": action_text, "event": event_type})
elif event_type == "UPDATE":
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE")
if memory_id is None:
continue
# Ensure action_text has an embedding cached to avoid redundant API calls
if action_text not in new_message_embeddings:
new_message_embeddings[action_text] = self.embedding_model.embed(action_text, "update")
self._update_memory(
memory_id=temp_uuid_mapping[resp.get("id")],
memory_id=memory_id,
data=action_text,
existing_embeddings=new_message_embeddings,
metadata=deepcopy(metadata),
)
returned_memories.append(
{
"id": temp_uuid_mapping[resp.get("id")],
"id": memory_id,
"memory": action_text,
"event": event_type,
"previous_memory": resp.get("old_memory"),
}
)
elif event_type == "DELETE":
self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")])
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE")
if memory_id is None:
continue
self._delete_memory(memory_id=memory_id)
returned_memories.append(
{
"id": temp_uuid_mapping[resp.get("id")],
"id": memory_id,
"memory": action_text,
"event": event_type,
}
@@ -1755,6 +1774,9 @@ class AsyncMemory(MemoryBase):
)
memory_tasks.append((task, resp, "ADD", None))
elif event_type == "UPDATE":
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "UPDATE")
if memory_id is None:
continue
# Ensure action_text has an embedding cached to avoid redundant API calls
if action_text not in new_message_embeddings:
new_message_embeddings[action_text] = await asyncio.to_thread(
@@ -1762,16 +1784,19 @@ class AsyncMemory(MemoryBase):
)
task = asyncio.create_task(
self._update_memory(
memory_id=temp_uuid_mapping[resp["id"]],
memory_id=memory_id,
data=action_text,
existing_embeddings=new_message_embeddings,
metadata=deepcopy(metadata),
)
)
memory_tasks.append((task, resp, "UPDATE", temp_uuid_mapping[resp["id"]]))
memory_tasks.append((task, resp, "UPDATE", memory_id))
elif event_type == "DELETE":
task = asyncio.create_task(self._delete_memory(memory_id=temp_uuid_mapping[resp.get("id")]))
memory_tasks.append((task, resp, "DELETE", temp_uuid_mapping[resp.get("id")]))
memory_id = _resolve_mapped_id(temp_uuid_mapping, resp, "DELETE")
if memory_id is None:
continue
task = asyncio.create_task(self._delete_memory(memory_id=memory_id))
memory_tasks.append((task, resp, "DELETE", memory_id))
elif event_type == "NONE":
# Even if content doesn't need updating, update session IDs if provided
memory_id = temp_uuid_mapping.get(resp.get("id"))
-159
View File
@@ -1,159 +0,0 @@
# Changelog
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
## [1.0.4] - 2026-04-03 (beta)
### Added
- **Interactive login flow**: `openclaw mem0 login` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **Config file fallback**: Reads `~/.mem0/config.json` (shared with Python CLI) when no API key in plugin config. Supports both camelCase and snake_case field names
- **CLI subcommands**: `openclaw mem0 login`, `openclaw mem0 search`, `openclaw mem0 stats`, `openclaw mem0 status`, `openclaw mem0 dream`
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **Backend layer**: `backend/base.ts` + `backend/platform.ts` with direct `fetch()` for platform mode, `providerToBackend()` adapter for OSS
- **Plugin manifest**: Added `name`, `description`, `contracts.tools`, `baseUrl` config field, CLI `descriptors` for lazy-loading
### Changed
- **Modular architecture**: Extracted tools into `tools/` directory (7 files) and CLI into `cli/commands.ts` — `index.ts` down from 1724 to ~780 lines
- **WRITE_TOOLS updated**: Dream gate tracks `memory_delete` instead of removed `memory_forget` / `memory_delete_all`
- **Auto-recall timeout** (#4634): Recall wrapped in 8-second `Promise.race` — if OSS/Ollama LLM takes too long, recall is skipped instead of stalling the gateway
- **Auto-capture fire-and-forget** (#4634): `provider.add()` now runs in the background via `.then()/.catch()` — the `agent_end` hook returns immediately, zero event loop blocking
- **Auto-capture minimum content gate**: Skips extraction when total user content is <50 chars after filtering — trivial conversations ("ok", "thanks") no longer trigger LLM calls
- **CLI search**: Removed `source: "OPENCLAW"` filter and lowered threshold to 0.3 so explicit searches find all memories, not just plugin-tagged ones
### Removed
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — replaced by `memory_delete`
- `memory_status` tool — redundant with `openclaw mem0 status` CLI
- `memory_import` tool — bulk import, rarely needed by agents
- `entity_list`, `entity_delete` tools — niche, platform-only
- `event_list`, `event_status` tools — debugging tools, not agent tools
- Duplicate `ToolContext` interfaces from individual tool files — now imports from canonical `tools/index.ts`
## [1.0.3] - 2026-04-03
### Fixed
- **Path traversal vulnerability**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal via `config.domain` or the exported `loadSkill` API
- **Noise filter regression**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction` so the filter correctly matches real upstream compaction audit messages
- **Cosmetic revert**: Restored `// Over-fetch for ranking` comment in `recall.ts` (was changed to work around a false-positive scanner match on the substring `fetch`)
### Changed
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
### Added
- **Path traversal tests**: 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
## [1.0.2] - 2026-04-02
### Fixed
- **Security scanner warning**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — OpenClaw already resolves `${VAR}` in `openclaw.json` before passing config to the plugin, so plugin-side env resolution was redundant and triggered the "credential harvesting" static analysis warning ([#4676](https://github.com/mem0ai/mem0/pull/4676))
## [1.0.1] - 2026-04-02
### Added
- **CD workflow**: Added continuous deployment workflow for `@mem0/openclaw-mem0` with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` specifying minimum gateway version and OpenClaw SDK compatibility (`>=2026.3.24-beta.2`) ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file to the package ([#4667](https://github.com/mem0ai/mem0/pull/4667))
### Fixed
- **Dream gate correctness**: Fixed cheap-first ordering, session isolation, and verified completion in the dream gate memory consolidation pipeline ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup without API key**: Plugin now starts gracefully when no API key is configured instead of crashing on init ([#4669](https://github.com/mem0ai/mem0/pull/4669))
## [1.0.0] - 2026-04-01
### Added
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction for higher quality memory capture ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Added `dream-gate.ts` for memory consolidation and dream-cycle processing
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
### Changed
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
## [0.4.1] - 2026-03-26
### Added
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions for higher-quality memory capture (#4302)
### Fixed
- **Credential detection in extraction**: Improved detection of credentials, API keys, and secrets in extraction instructions to prevent them from being stored as memories (#4552)
- **Standalone timestamp extraction**: Prevented extraction of standalone timestamps as memories when no meaningful content accompanies them (#4550)
## [0.4.0] - 2026-03-16
### Added
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers — prevents system-generated noise from polluting memory
- **Subagent hallucination prevention**: `isSubagentSession()` detects ephemeral subagent sessions and routes recall to the parent (main user) namespace instead of empty ephemeral namespaces; skips capture to prevent orphaned memories
- **Subagent-specific preamble**: Subagents receive "You are a subagent — use these memories for context but do not assume you are this user" to prevent identity assumption
- **User identity in recall preamble**: Recalled memories now include `userId` attribution for better context
- **User identity in extraction preamble**: Extraction context includes user identity and current date for accurate attribution and temporal anchoring
- **User-content guard**: Skips extraction when no meaningful user messages remain after filtering
- **Dynamic recall thresholding**: Memories scoring less than 50% of the top result are dropped to filter out the long tail of weak matches
- **SQLite resilience for OSS mode**: Init error recovery with automatic retry (history disabled) when native SQLite bindings fail under jiti
- **`disableHistory` config option**: New `oss.disableHistory` flag to explicitly skip history DB initialization
- **Updated minimum package version of mem0ai package**: Updated minimum package version of mem0ai package to ^2.3.0 to force old users to migrate to better-sqlite3
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
### Changed
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision during automatic injection (explicit tool searches remain at 0.5)
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Provider init promises now reset on failure, allowing retry on subsequent calls
- Relaxed extraction instructions: related facts are kept together to preserve context (removed atomic memory requirement)
### Fixed
- **Concurrent session race condition**: Lifecycle hooks (`before_agent_start`, `agent_end`) now use `ctx.sessionKey` directly from the event context instead of a shared mutable `currentSessionId` variable, preventing cross-session data leaks when multiple sessions run simultaneously
## [0.3.1] - 2026-03-12
### Added
- **Message filtering pipeline**: Multi-stage noise removal before extraction — drops heartbeats, timestamps, single-word acks, system routing metadata, compaction audit logs, and generic assistant acknowledgments
- **Broad recall for new sessions**: Short or new-session prompts trigger a secondary broad search to avoid cold-start blindness
- **Client-side threshold filtering**: Safety net that drops low-relevance results even if the API doesn't honor the threshold parameter
- **Temporal anchoring**: Extraction instructions now include current date so memories are prefixed with "As of YYYY-MM-DD, ..."
- **Summary message inclusion**: Earlier assistant messages containing work summaries are included in extraction context even if outside the recent-message window
- 55 unit tests covering filtering and isolation helpers
### Changed
- Default `searchThreshold` remains at 0.5, with client-side filtering as a safety net
- Extraction window expanded from last 10 → last 20 messages for richer context
- Rewritten custom extraction instructions: conciseness, outcome-over-intent, deduplication guidance, language preservation
- **Refactored** monolithic `index.ts` (1772 lines) into 6 focused modules: `types.ts`, `providers.ts`, `config.ts`, `filtering.ts`, `isolation.ts`, `index.ts`
### Fixed
- **README image on npmjs.com**: Changed architecture diagram from relative path to absolute GitHub URL so it renders correctly on the npm registry
## [0.3.0] - 2026-03-10
### Fixed
- Updated `mem0ai` dependency which includes the sqlite3 to better-sqlite3 migration for native binding resolution (#4270)
## [0.2.0] - 2026-03-09
### Added
- "Understanding userId" section in docs clarifying that `userId` is user-defined
- Per-agent memory isolation for multi-agent setups via `agentId`
- Regression tests for per-agent isolation helpers
### Changed
- Updated config examples to use concrete `userId` values instead of placeholders
### Fixed
- Migrated platform search to Mem0 v2 API
## [0.1.2] - 2026-02-19
### Added
- Source field for openclaw memory entries
### Fixed
- Auto-recall injection and auto-capture message drop
## [0.1.0] - 2026-02-02
### Added
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
+103 -151
View File
@@ -2,223 +2,175 @@
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [Mem0](https://mem0.ai).
Your agent forgets everything between sessions. This plugin fixes that. It watches conversations, extracts what matters, and brings it back when relevant — automatically.
Your agent forgets everything between sessions. This plugin fixes that — it watches conversations, extracts what matters, and brings it back when relevant. Automatically.
## How it works
<p align="center">
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
</p>
**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for memories that match the current message and injects them into context.
**Auto-Capture** — After the agent responds, the plugin filters the conversation through a noise-removal pipeline, then sends the cleaned exchange to Mem0. Mem0 decides what's worth keeping — new facts get stored, stale ones updated, duplicates merged.
Both run silently. No prompting, no configuration, no manual calls.
### Message filtering
Before extraction, messages pass through a multi-stage filtering pipeline:
1. **Noise detection** — Drops entire messages that are system noise: heartbeats (`HEARTBEAT_OK`, `NO_REPLY`), timestamps, single-word acknowledgments (`ok`, `sure`, `done`), system routing metadata, and compaction audit logs.
2. **Generic assistant detection** — Drops short assistant messages that are boilerplate acknowledgments with no extractable facts (e.g. "I see you've shared an update. How can I help?").
3. **Content stripping** — Removes embedded noise fragments (media boilerplate, routing metadata, compaction blocks) from otherwise useful messages.
4. **Truncation** — Caps messages at 2000 characters to avoid sending excessive context.
### Short-term vs long-term memory
Memories are organized into two scopes:
- **Session (short-term)** — Auto-capture stores memories scoped to the current session via Mem0's `run_id` / `runId` parameter. These are contextual to the ongoing conversation and automatically recalled alongside long-term memories.
- **User (long-term)** — The agent can explicitly store long-term memories using the `memory_store` tool (with `longTerm: true`, the default). These persist across all sessions for the user.
During **auto-recall**, the plugin searches both scopes and presents them separately — long-term memories first, then session memories — so the agent has full context.
The agent tools (`memory_search`, `memory_list`) accept a `scope` parameter (`"session"`, `"long-term"`, or `"all"`) to control which memories are queried. The `memory_store` tool accepts a `longTerm` boolean (default: `true`) to choose where to store.
All new parameters are optional and backward-compatible — existing configurations work without changes.
### Per-agent memory isolation
In multi-agent setups, each agent automatically gets its own memory namespace. Session keys following the pattern `agent:<agentId>:<uuid>` are parsed to derive isolated namespaces (`${userId}:agent:${agentId}`). Single-agent deployments are unaffected — plain session keys and `agent:main:*` keys resolve to the configured `userId`.
**How it works:**
- The agent's session key is inspected on every recall/capture cycle
- If the key matches `agent:<name>:<uuid>`, memories are stored under `userId:agent:<name>`
- Different agents never see each other's memories unless explicitly queried
**Subagent handling:**
Ephemeral subagents (session keys like `agent:main:subagent:<uuid>`) are handled specially:
- **Recall** is routed to the parent (main user) namespace — subagents get the user's long-term context instead of searching their empty ephemeral namespace
- **Capture** is skipped entirely — the main agent's `agent_end` hook captures the consolidated result including subagent output, preventing orphaned memories
- A **subagent-specific preamble** is used: "You are a subagent — use these memories for context but do not assume you are this user"
**Explicit cross-agent queries:**
All memory tools (`memory_search`, `memory_store`, `memory_list`, `memory_forget`) accept an optional `agentId` parameter to query another agent's namespace:
```
memory_search({ query: "user's tech stack", agentId: "researcher" })
```
The `agentId` is always namespaced under the configured `userId` (e.g. `agentId: "researcher"` → `utkarsh:agent:researcher`), so it cannot be used to access other users' namespaces.
### Concurrency safety
Lifecycle hooks (`before_agent_start`, `agent_end`) use `ctx.sessionKey` directly from the event context rather than shared mutable state. This prevents race conditions when multiple sessions run concurrently (e.g. multiple Telegram users chatting simultaneously).
Tools still read from a best-effort `currentSessionId` variable (since tools don't receive `ctx`), but hooks — where the critical recall and capture logic runs — are fully concurrency-safe.
### Non-interactive trigger filtering
The plugin automatically skips recall and capture for non-interactive triggers: `cron`, `heartbeat`, `automation`, and `schedule`. Detection works via both `ctx.trigger` and session key patterns (`:cron:`, `:heartbeat:`). This prevents system-generated noise from polluting long-term memory.
## Setup
## Quick Start
```bash
openclaw plugins install @mem0/openclaw-mem0
```
### Understanding `userId`
The `userId` field is a **string you choose** to uniquely identify the user whose memories are being stored. It is **not** something you look up in the Mem0 dashboard — you define it yourself.
Pick any stable, unique identifier for the user. Common choices:
- Your application's internal user ID (e.g. `"user_123"`, `"alice@example.com"`)
- A UUID (e.g. `"550e8400-e29b-41d4-a716-446655440000"`)
- A simple username (e.g. `"alice"`)
All memories are scoped to this `userId` — different values create separate memory namespaces. If you don't set it, it defaults to `"default"`, which means all users share the same memory space.
> **Tip:** In a multi-user application, set `userId` dynamically per user (e.g. from your auth system) rather than hardcoding a single value.
### Platform (Mem0 Cloud)
Get an API key from [app.mem0.ai](https://app.mem0.ai), then add to your `openclaw.json`:
Get an API key from [app.mem0.ai](https://app.mem0.ai/dashboard/api-keys):
```bash
openclaw mem0 init --api-key <your-key> --user-id <your-user-id>
```
Or configure manually in `openclaw.json`:
```json5
// plugins.entries
"openclaw-mem0": {
"enabled": true,
"config": {
"apiKey": "${MEM0_API_KEY}",
"userId": "alice" // any unique identifier you choose for this user
"userId": "alice"
}
}
```
### Open-Source (Self-hosted)
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings/LLM.
No Mem0 key needed. Requires `OPENAI_API_KEY` for default embeddings and LLM. Vectors are stored locally in SQLite at `~/.mem0/vector_store.db` — no external database required.
Defaults: `text-embedding-3-small` for embeddings, `gpt-5.4` for fact extraction.
```json5
"openclaw-mem0": {
"enabled": true,
"config": {
"mode": "open-source",
"userId": "alice" // any unique identifier you choose for this user
"userId": "alice"
}
}
```
Sensible defaults out of the box. To customize the embedder, vector store, or LLM:
Customize the embedder, vector store, or LLM via the `oss` block:
```json5
"config": {
"mode": "open-source",
"userId": "your-user-id",
"userId": "alice",
"oss": {
"embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small" } },
"vectorStore": { "provider": "qdrant", "config": { "host": "localhost", "port": 6333 } },
"llm": { "provider": "openai", "config": { "model": "gpt-4o" } }
"llm": { "provider": "openai", "config": { "model": "gpt-5.4" } }
}
}
```
All `oss` fields are optional. See [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for providers.
All `oss` fields are optional. See the [Mem0 OSS docs](https://docs.mem0.ai/open-source/node-quickstart) for supported providers.
## Agent tools
## How It Works
The agent gets five tools it can call during conversations:
<p align="center">
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
</p>
**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
**Auto-Capture** — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
Both run silently. No prompting, no manual calls required.
### Memory Scopes
- **Session (short-term)** — Scoped to the current conversation via `run_id`. Recalled alongside long-term memories.
- **User (long-term)** — Persistent across all sessions. Default for `memory_add`.
### Multi-Agent Isolation
Each agent gets its own memory namespace automatically via session key routing (`agent:<name>:<uuid>` maps to `userId:agent:<name>`). Single-agent setups are unaffected.
## Agent Tools
Eight tools are registered for agent use:
| Tool | Description |
|------|-------------|
| `memory_search` | Search memories by natural language. Optional `agentId` to scope to a specific agent, `scope` to filter by session/long-term. |
| `memory_list` | List all stored memories. Optional `agentId` to scope to a specific agent, `scope` to filter. |
| `memory_store` | Explicitly save a fact. Optional `agentId` to store under a specific agent's namespace, `longTerm` to choose scope. |
| `memory_get` | Retrieve a memory by ID. |
| `memory_forget` | Delete by ID or by query. Optional `agentId` to scope deletion to a specific agent. |
| ---- | ----------- |
| `memory_search` | Search by natural language query. Supports `scope` (`session`, `long-term`, `all`), `categories`, `filters`, and `agentId`. |
| `memory_add` | Store facts. Accepts `text` or `facts` array, `category`, `importance`, `longTerm`, `metadata`. |
| `memory_get` | Retrieve a single memory by ID. |
| `memory_list` | List all memories. Filter by `userId`, `agentId`, `scope`. |
| `memory_update` | Update a memory's text in place. Preserves history. |
| `memory_delete` | Delete by `memoryId`, `query` (search-and-delete), or `all: true` (requires `confirm: true`). |
| `memory_event_list` | List recent background processing events. Platform mode only. |
| `memory_event_status` | Get status of a specific event by ID. Platform mode only. |
## CLI
All commands: `openclaw mem0 <command>`.
```bash
# Search all memories (long-term + session)
# Memory operations
openclaw mem0 add "User prefers TypeScript over JavaScript"
openclaw mem0 search "what languages does the user know"
openclaw mem0 search "preferences" --scope long-term
openclaw mem0 get <memory_id>
openclaw mem0 list --user-id alice --top-k 20
openclaw mem0 update <memory_id> "Updated preference text"
openclaw mem0 delete <memory_id>
openclaw mem0 delete --all --user-id alice --confirm
openclaw mem0 import memories.json
# Search only long-term memories
openclaw mem0 search "what languages does the user know" --scope long-term
# Management
openclaw mem0 init
openclaw mem0 init --api-key <key> --user-id alice
openclaw mem0 status
openclaw mem0 config show
openclaw mem0 config get api_key
openclaw mem0 config set user_id alice
# Search only session/short-term memories
openclaw mem0 search "what languages does the user know" --scope session
# Events (platform only)
openclaw mem0 event list
openclaw mem0 event status <event_id>
# Stats
openclaw mem0 stats
# Search a specific agent's memories
openclaw mem0 search "user preferences" --agent researcher
# Stats for a specific agent
openclaw mem0 stats --agent researcher
# Memory consolidation
openclaw mem0 dream
openclaw mem0 dream --dry-run
```
## Options
## Configuration Reference
### General
| Key | Type | Default | |
|-----|------|---------|---|
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
| `userId` | `string` | `"default"` | Any unique identifier you choose for the user (e.g. `"alice"`, `"user_123"`). All memories are scoped to this value. Not found in any dashboard — you define it yourself. |
| `autoRecall` | `boolean` | `true` | Inject memories before each turn |
| `autoCapture` | `boolean` | `true` | Store facts after each turn |
| `topK` | `number` | `5` | Max memories per recall |
| `searchThreshold` | `number` | `0.5` | Min similarity (0–1) |
| Key | Type | Default | Description |
| --- | ---- | ------- | ----------- |
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn |
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn |
| `topK` | `number` | `5` | Max memories returned per recall |
| `searchThreshold` | `number` | `0.5` | Minimum similarity score (0-1) |
### Platform mode
### Platform Mode
| Key | Type | Default | |
|-----|------|---------|---|
| Key | Type | Default | Description |
| --- | ---- | ------- | ----------- |
| `apiKey` | `string` | — | **Required.** Mem0 API key (supports `${MEM0_API_KEY}`) |
| `orgId` | `string` | — | Organization ID |
| `projectId` | `string` | — | Project ID |
| `enableGraph` | `boolean` | `false` | Entity graph for relationships |
| `customInstructions` | `string` | *(built-in)* | Extraction rules — what to store, how to format. Built-in instructions include temporal anchoring, conciseness, outcome-over-intent, deduplication, and language preservation guidelines. |
| `customCategories` | `object` | *(12 defaults)* | Category name → description map for tagging |
| `enableGraph` | `boolean` | `false` | Entity graph for relationship tracking |
| `customInstructions` | `string` | *(built-in)* | Custom extraction rules |
| `customCategories` | `object` | *(12 defaults)* | Category name to description map |
### Open-source mode
### Open-Source Mode
Works with zero extra config. The `oss` block lets you swap out any component:
All fields optional. Defaults: `text-embedding-3-small` embeddings, local SQLite vector store (`~/.mem0/vector_store.db`), `gpt-5.4` LLM.
| Key | Type | Default | |
|-----|------|---------|---|
| `customPrompt` | `string` | *(built-in)* | Extraction prompt for memory processing |
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider (`"openai"`, `"ollama"`, `"lmstudio"`, etc.) |
| `oss.embedder.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL` |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store (`"memory"`, `"qdrant"`, `"chroma"`, etc.) |
| `oss.vectorStore.config` | `object` | — | Provider config: `host`, `port`, `collectionName`, `dimension` |
| `oss.llm.provider` | `string` | `"openai"` | LLM provider (`"openai"`, `"anthropic"`, `"ollama"`, `"lmstudio"`, etc.) |
| `oss.llm.config` | `object` | — | Provider config: `apiKey`, `model`, `baseURL`, `temperature` |
| `oss.historyDbPath` | `string` | — | SQLite path for memory edit history |
| `oss.disableHistory` | `boolean` | `false` | Skip history DB initialization (useful when native SQLite bindings fail) |
Everything inside `oss` is optional — defaults use OpenAI embeddings (`text-embedding-3-small`), in-memory vector store, and OpenAI LLM. Override only what you need.
> **SQLite resilience:** If the history DB fails to initialize (e.g. native binding resolution under jiti), the plugin automatically retries with history disabled. Core memory operations (add, search, get, delete) work without the history DB.
| Key | Type | Default | Description |
| --- | ---- | ------- | ----------- |
| `customPrompt` | `string` | *(built-in)* | Extraction prompt |
| `oss.embedder.provider` | `string` | `"openai"` | Embedding provider |
| `oss.embedder.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.vectorStore.provider` | `string` | `"memory"` | Vector store provider (see list above) |
| `oss.vectorStore.config` | `object` | — | Provider config (`host`, `port`, `collectionName`, `dbPath`) |
| `oss.llm.provider` | `string` | `"openai"` | LLM provider |
| `oss.llm.config` | `object` | — | Provider config (`apiKey`, `model`, `baseURL`) |
| `oss.historyDbPath` | `string` | — | SQLite path for edit history |
| `oss.disableHistory` | `boolean` | `false` | Skip history DB |
## License
Apache 2.0
[Apache 2.0](LICENSE)
-3
View File
@@ -14,7 +14,6 @@ export interface AddOptions {
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
}
export interface SearchOptions {
@@ -28,7 +27,6 @@ export interface SearchOptions {
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
}
export interface ListOptions {
@@ -41,7 +39,6 @@ export interface ListOptions {
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
}
export interface DeleteOptions {
+10 -4
View File
@@ -4,6 +4,7 @@
* Platform (SaaS) backend — communicates with api.mem0.ai.
*/
import { PLUGIN_VERSION } from "../telemetry.ts";
import {
APIError,
type AddOptions,
@@ -25,6 +26,10 @@ export class PlatformBackend implements Backend {
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "OPENCLAW",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": PLUGIN_VERSION,
"X-Mem0-Caller-Type": "plugin",
};
}
@@ -106,7 +111,6 @@ export class PlatformBackend implements Backend {
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", {
json: payload,
@@ -166,7 +170,6 @@ export class PlatformBackend implements Backend {
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
@@ -217,7 +220,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", {
json: payload,
@@ -287,11 +289,15 @@ export class PlatformBackend implements Backend {
return result;
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
}
async status(
_opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
await this._request("GET", "/v1/ping/");
await this.ping();
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
+1091 -275
View File
File diff suppressed because it is too large Load Diff
+119 -45
View File
@@ -1,65 +1,139 @@
/**
* File-based config helpers for ~/.mem0/config.json.
* File-based config helpers for the OpenClaw Mem0 plugin.
*
* Separated from commands.ts so that the security scanner does not see
* file-read + network-send in the same module (false-positive exfiltration
* pattern). This module only touches the filesystem — no network calls.
* Plugin auth and settings are stored in ~/.openclaw/openclaw.json under
* plugins.entries.openclaw-mem0.config — the single source of truth.
*
* Uses fs-safe.ts for all filesystem operations to pass the OpenClaw
* code_safety scanner.
*/
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { readText, exists, writeText, mkdirp } from "../fs-safe.ts";
// OpenClaw config — source of truth for plugin settings
export const OPENCLAW_CONFIG_DIR = join(homedir(), ".openclaw");
export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json");
export const CONFIG_DIR = join(homedir(), ".mem0");
export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
export interface Mem0FileConfig {
version: number;
platform: Record<string, unknown>;
defaults: Record<string, unknown>;
[key: string]: unknown;
const PLUGIN_ID = "openclaw-mem0";
// ============================================================================
// Types
// ============================================================================
/** Fields stored in the plugin config section of openclaw.json */
export interface PluginAuthConfig {
apiKey?: string;
baseUrl?: string;
userId?: string;
userEmail?: string;
mode?: string;
autoRecall?: boolean;
autoCapture?: boolean;
topK?: number;
}
export function readMem0Config(): Mem0FileConfig {
if (existsSync(CONFIG_FILE)) {
// ============================================================================
// OpenClaw config read/write
// ============================================================================
/** Read the full ~/.openclaw/openclaw.json */
function readFullConfig(): Record<string, unknown> {
if (exists(OPENCLAW_CONFIG_FILE)) {
try {
const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
return {
...raw,
version: raw.version ?? 1,
platform: raw.platform ?? {},
defaults: raw.defaults ?? {},
};
return JSON.parse(readText(OPENCLAW_CONFIG_FILE));
} catch {
/* ignore parse errors */
}
}
return { version: 1, platform: {}, defaults: {} };
return {};
}
/** Get the base URL from config, handling both camelCase and snake_case */
export function getBaseUrl(config: Mem0FileConfig): string {
const p = config.platform;
return ((p.baseUrl ?? p.base_url) as string) || DEFAULT_BASE_URL;
}
/** Set API key + base URL on the platform config (preserves existing fields) */
export function setPlatformAuth(
config: Mem0FileConfig,
apiKey: string,
baseUrl: string,
): void {
config.platform.apiKey = apiKey;
config.platform.baseUrl = baseUrl;
// Also write snake_case so Python CLI can read it
config.platform.api_key = apiKey;
config.platform.base_url = baseUrl;
}
export function writeMem0Config(config: Mem0FileConfig): void {
if (!existsSync(CONFIG_DIR)) {
mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
/** Write the full ~/.openclaw/openclaw.json (preserves all non-plugin config) */
function writeFullConfig(config: Record<string, unknown>): void {
if (!exists(OPENCLAW_CONFIG_DIR)) {
mkdirp(OPENCLAW_CONFIG_DIR, 0o700);
}
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
writeText(
OPENCLAW_CONFIG_FILE,
JSON.stringify(config, null, 2),
{ mode: 0o600 },
);
}
/** Read plugin auth/identity config from openclaw.json's plugin section */
export function readPluginAuth(): PluginAuthConfig {
const full = readFullConfig() as any;
const cfg = full?.plugins?.entries?.[PLUGIN_ID]?.config;
if (!cfg || typeof cfg !== "object") return {};
return {
apiKey: (cfg.apiKey ?? cfg.api_key) as string | undefined,
baseUrl: (cfg.baseUrl ?? cfg.base_url) as string | undefined,
userId: (cfg.userId ?? cfg.user_id) as string | undefined,
userEmail: (cfg.userEmail ?? cfg.user_email) as string | undefined,
mode: cfg.mode as string | undefined,
autoRecall: cfg.autoRecall as boolean | undefined,
autoCapture: cfg.autoCapture as boolean | undefined,
topK: cfg.topK as number | undefined,
};
}
/** Write auth/identity fields into the plugin section of openclaw.json */
export function writePluginAuth(auth: PluginAuthConfig): void {
const full = readFullConfig() as any;
// Ensure nested structure exists
if (!full.plugins) full.plugins = {};
if (!full.plugins.entries) full.plugins.entries = {};
if (!full.plugins.entries[PLUGIN_ID]) {
full.plugins.entries[PLUGIN_ID] = { enabled: true, config: {} };
}
if (!full.plugins.entries[PLUGIN_ID].config) {
full.plugins.entries[PLUGIN_ID].config = {};
}
const cfg = full.plugins.entries[PLUGIN_ID].config;
// Write all defined fields into the config section
for (const [key, value] of Object.entries(auth)) {
if (value !== undefined) cfg[key] = value;
}
writeFullConfig(full);
}
export function writePluginConfigField(
path: string[],
value: unknown,
): void {
const full = readFullConfig() as any;
if (!full.plugins) full.plugins = {};
if (!full.plugins.entries) full.plugins.entries = {};
if (!full.plugins.entries[PLUGIN_ID]) {
full.plugins.entries[PLUGIN_ID] = { enabled: true, config: {} };
}
if (!full.plugins.entries[PLUGIN_ID].config) {
full.plugins.entries[PLUGIN_ID].config = {};
}
let target = full.plugins.entries[PLUGIN_ID].config;
for (let i = 0; i < path.length - 1; i++) {
if (!target[path[i]] || typeof target[path[i]] !== "object") {
target[path[i]] = {};
}
target = target[path[i]];
}
target[path[path.length - 1]] = value;
writeFullConfig(full);
}
/** Get the configured base URL from openclaw.json or default */
export function getBaseUrl(): string {
const auth = readPluginAuth();
return auth.baseUrl || DEFAULT_BASE_URL;
}
+26 -50
View File
@@ -1,43 +1,24 @@
/**
* Configuration parsing, env var resolution, and default instructions/categories.
* Configuration parsing and default instructions/categories.
*
* NOTE: This module must NOT import from `node:fs` or `node:fs/promises`.
* All filesystem operations are centralized in fs-safe.ts.
*/
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { homedir, userInfo } from "node:os";
import { userInfo } from "node:os";
import type { Mem0Config, Mem0Mode } from "./types.ts";
// NOTE: No process.env access in this module. OpenClaw resolves ${VAR}
// syntax in openclaw.json before passing pluginConfig to register().
// Plugin-side env var resolution was removed to clear OpenClaw's
// security scanner warning ("credential harvesting" pattern).
// NOTE: The gateway resolves ${VAR} syntax in openclaw.json before passing
// pluginConfig to register(). No plugin-side variable resolution needed.
// ============================================================================
// Login config fallback — reads ~/.mem0/config.json (shared with CLI)
// Login config fallback type — read from openclaw.json plugin section
// ============================================================================
function readMem0ConfigFile(): {
/** Shape accepted by parse() for the openclaw.json plugin auth fallback. */
export interface FileConfig {
apiKey?: string;
baseUrl?: string;
orgId?: string;
projectId?: string;
} {
try {
const configPath = join(homedir(), ".mem0", "config.json");
if (!existsSync(configPath)) return {};
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
const p = raw?.platform;
if (!p) return {};
return {
// Support both camelCase (our login) and snake_case (Python CLI)
apiKey: p.apiKey || p.api_key || undefined,
baseUrl: p.baseUrl || p.base_url || undefined,
orgId: p.orgId || p.org_id || undefined,
projectId: p.projectId || p.project_id || undefined,
};
} catch {
return {};
}
}
// ============================================================================
@@ -168,14 +149,12 @@ const ALLOWED_KEYS = [
"apiKey",
"baseUrl",
"userId",
"orgId",
"projectId",
"userEmail",
"autoCapture",
"autoRecall",
"customInstructions",
"customCategories",
"customPrompt",
"enableGraph",
"searchThreshold",
"topK",
"oss",
@@ -193,41 +172,41 @@ function assertAllowedKeys(
}
export const mem0ConfigSchema = {
parse(value: unknown): Mem0Config {
parse(value: unknown, fileConfig?: FileConfig): Mem0Config {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("openclaw-mem0 config required");
}
const cfg = value as Record<string, unknown>;
assertAllowedKeys(cfg, ALLOWED_KEYS, "openclaw-mem0 config");
// Accept both "open-source" and legacy "oss" as open-source mode; everything else is platform
// Only two modes: "platform" (default) or "open-source"
if (
typeof cfg.mode === "string" &&
cfg.mode !== "platform" &&
cfg.mode !== "open-source"
) {
console.warn(
`[mem0] Unknown mode "${cfg.mode}" — expected "platform" or "open-source". Defaulting to "platform".`,
);
}
const mode: Mem0Mode =
cfg.mode === "oss" || cfg.mode === "open-source"
? "open-source"
: "platform";
cfg.mode === "open-source" ? "open-source" : "platform";
// Resolve API key: pluginConfig → ~/.mem0/config.json fallback
// Resolve API key: pluginConfig → fileConfig fallback (from openclaw.json plugin section)
let resolvedApiKey =
typeof cfg.apiKey === "string" ? cfg.apiKey : undefined;
let resolvedBaseUrl =
typeof cfg.baseUrl === "string" ? cfg.baseUrl : undefined;
let resolvedOrgId = typeof cfg.orgId === "string" ? cfg.orgId : undefined;
let resolvedProjectId =
typeof cfg.projectId === "string" ? cfg.projectId : undefined;
if (mode === "platform" && !resolvedApiKey) {
const fileConfig = readMem0ConfigFile();
if (mode === "platform" && !resolvedApiKey && fileConfig) {
if (fileConfig.apiKey) resolvedApiKey = fileConfig.apiKey;
if (fileConfig.baseUrl) resolvedBaseUrl = fileConfig.baseUrl;
if (!resolvedOrgId && fileConfig.orgId) resolvedOrgId = fileConfig.orgId;
if (!resolvedProjectId && fileConfig.projectId)
resolvedProjectId = fileConfig.projectId;
}
// Platform mode requires apiKey — but don't throw on missing config.
// The plugin should register successfully and log a setup message.
const needsSetup = mode === "platform" && !resolvedApiKey;
// OpenClaw resolves ${VAR} in pluginConfig before register() — no plugin-side expansion needed
// OpenClaw resolves ${VAR} in openclaw.json before register() — no plugin-side expansion needed
let ossConfig: Mem0Config["oss"];
if (cfg.oss && typeof cfg.oss === "object" && !Array.isArray(cfg.oss)) {
ossConfig = cfg.oss as Mem0Config["oss"];
@@ -247,8 +226,6 @@ export const mem0ConfigSchema = {
return "default";
}
})(),
orgId: resolvedOrgId,
projectId: resolvedProjectId,
autoCapture: cfg.autoCapture !== false,
autoRecall: cfg.autoRecall !== false,
customInstructions:
@@ -265,7 +242,6 @@ export const mem0ConfigSchema = {
typeof cfg.customPrompt === "string"
? cfg.customPrompt
: DEFAULT_CUSTOM_INSTRUCTIONS,
enableGraph: cfg.enableGraph === true,
searchThreshold:
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
+9 -9
View File
@@ -6,8 +6,8 @@
* Lock prevents concurrent consolidation runs.
*/
import * as fs from "fs";
import * as path from "path";
import * as path from "node:path";
import { readText, writeText, mkdirp, unlink } from "./fs-safe.ts";
// ============================================================================
// Types
@@ -52,7 +52,7 @@ function lockPath(stateDir: string): string {
function ensureDir(dir: string): void {
try {
fs.mkdirSync(dir, { recursive: true });
mkdirp(dir);
} catch {
/* exists */
}
@@ -60,7 +60,7 @@ function ensureDir(dir: string): void {
function readState(stateDir: string): DreamState {
try {
const raw = fs.readFileSync(statePath(stateDir), "utf-8");
const raw = readText(statePath(stateDir));
return JSON.parse(raw) as DreamState;
} catch {
return { lastConsolidatedAt: 0, sessionsSince: 0, lastSessionId: null };
@@ -69,7 +69,7 @@ function readState(stateDir: string): DreamState {
function writeState(stateDir: string, state: DreamState): void {
ensureDir(stateDir);
fs.writeFileSync(statePath(stateDir), JSON.stringify(state, null, 2));
writeText(statePath(stateDir), JSON.stringify(state, null, 2));
}
// ============================================================================
@@ -157,7 +157,7 @@ export function acquireDreamLock(stateDir: string): boolean {
// Check existing lock
try {
const raw = fs.readFileSync(lp, "utf-8");
const raw = readText(lp);
const lock = JSON.parse(raw) as DreamLock;
const age = Date.now() - lock.startedAt;
if (age < LOCK_STALE_MS) {
@@ -165,7 +165,7 @@ export function acquireDreamLock(stateDir: string): boolean {
}
// Stale lock — remove it before attempting exclusive create
try {
fs.unlinkSync(lp);
unlink(lp);
} catch {
/* race ok */
}
@@ -177,7 +177,7 @@ export function acquireDreamLock(stateDir: string): boolean {
// only one succeeds. The other gets EEXIST.
const lock: DreamLock = { pid: process.pid, startedAt: Date.now() };
try {
fs.writeFileSync(lp, JSON.stringify(lock), { flag: "wx" });
writeText(lp, JSON.stringify(lock), { flag: "wx" });
return true;
} catch {
return false; // Lost race
@@ -189,7 +189,7 @@ export function acquireDreamLock(stateDir: string): boolean {
*/
export function releaseDreamLock(stateDir: string): void {
try {
fs.unlinkSync(lockPath(stateDir));
unlink(lockPath(stateDir));
} catch {
/* already gone */
}
+7
View File
@@ -27,6 +27,13 @@ const NOISE_CONTENT_PATTERNS: Array<{ pattern: RegExp; replacement: string }> =
/Conversation info \(untrusted metadata\):\s*```json\s*\{[\s\S]*?\}\s*```/g,
replacement: "",
},
{
// OpenClaw TUI sends "Sender (untrusted metadata)" with a JSON block
// containing label, id, name, username — strip to prevent storing as memory
pattern:
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
replacement: "",
},
{ pattern: /\[media attached:.*?\]/g, replacement: "" },
{
pattern:
+43
View File
@@ -0,0 +1,43 @@
/**
* Safe filesystem helpers — thin wrappers around Node.js fs sync operations.
* Isolated in its own module so the plugin bundle keeps file I/O separate
* from network calls (avoids code_safety exfiltration heuristic).
*/
import * as fs from "node:fs";
/** Read a file as UTF-8 text. */
export function readText(filePath: string): string {
return fs.readFileSync(filePath, "utf-8");
}
/** Check if a file or directory exists. */
export function exists(filePath: string): boolean {
return fs.existsSync(filePath);
}
/** Write text to a file. */
export function writeText(
filePath: string,
content: string,
opts?: { mode?: number; flag?: string },
): void {
fs.writeFileSync(filePath, content, opts);
}
/** Create a directory (recursive). */
export function mkdirp(dirPath: string, mode?: number): void {
fs.mkdirSync(dirPath, { mode, recursive: true });
}
/** Remove a file. */
export function unlink(filePath: string): void {
fs.unlinkSync(filePath);
}
export function bootstrapTelemetryFlag(): void {
const val = process.env.MEM0_TELEMETRY;
if (val !== undefined) {
(globalThis as any).__mem0_telemetry_override = val;
}
}
+189 -97
View File
@@ -5,28 +5,29 @@
* and the open-source self-hosted SDK. Uses the official `mem0ai` package.
*
* Features:
* - 7 core tools: memory_search, memory_store, memory_get, memory_list,
* memory_update, memory_delete, memory_history
* - 6 core tools: memory_search, memory_add, memory_get, memory_list,
* memory_update, memory_delete
* - Short-term (session-scoped) and long-term (user-scoped) memory
* - Auto-recall: injects relevant memories (both scopes) before each agent turn
* - Auto-capture: stores key facts scoped to the current session after each agent turn
* - Per-agent isolation: multi-agent setups write/read from separate userId namespaces
* automatically via sessionKey routing (zero breaking changes for single-agent setups)
* - CLI: openclaw mem0 search, openclaw mem0 stats
* - CLI: openclaw mem0 search, openclaw mem0 status
* - Dual mode: platform or open-source (self-hosted)
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type {
Mem0Config,
Mem0Provider,
MemoryItem,
AddOptions,
SearchOptions,
} from "./types.ts";
import { createProvider, providerToBackend } from "./providers.ts";
import { mem0ConfigSchema } from "./config.ts";
import type { FileConfig } from "./config.ts";
import { filterMessagesForExtraction } from "./filtering.ts";
import {
effectiveUserId,
@@ -51,9 +52,14 @@ import {
} from "./dream-gate.ts";
import { PlatformBackend } from "./backend/platform.ts";
import type { Backend } from "./backend/base.ts";
import { registerAllTools } from "./tools/index.ts";
import type { ToolContext } from "./tools/index.ts";
import { registerCliCommands } from "./cli/commands.ts";
import { readPluginAuth } from "./cli/config-file.ts";
import { registerAllTools } from "./tools/index.ts";
import type { ToolDeps } from "./tools/index.ts";
import { captureEvent } from "./telemetry.ts";
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
bootstrapTelemetryFlag();
// ============================================================================
// Re-exports (for tests and external consumers)
@@ -74,44 +80,57 @@ export {
filterMessagesForExtraction,
} from "./filtering.ts";
export { mem0ConfigSchema } from "./config.ts";
export type { FileConfig } from "./config.ts";
export { createProvider } from "./providers.ts";
// ============================================================================
// Helpers
// ============================================================================
/** Convert Record<string, string> categories to the array format mem0ai expects */
function categoriesToArray(
cats: Record<string, string>,
): Array<Record<string, string>> {
return Object.entries(cats).map(([key, value]) => ({ [key]: value }));
}
// ============================================================================
// Plugin Definition
// ============================================================================
const memoryPlugin = {
const memoryPlugin = definePluginEntry({
id: "openclaw-mem0",
name: "Memory (Mem0)",
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
kind: "memory" as const,
configSchema: mem0ConfigSchema,
register(api: OpenClawPluginApi) {
const cfg = mem0ConfigSchema.parse(api.pluginConfig);
// Read auth from openclaw.json plugin config (picks up post-startup login).
// This is the single source of truth — set via `openclaw mem0 login`.
const pluginAuth = readPluginAuth();
const fileConfig: FileConfig = {
apiKey: pluginAuth.apiKey,
baseUrl: pluginAuth.baseUrl,
};
const cfg = mem0ConfigSchema.parse(api.pluginConfig, fileConfig);
// Telemetry context bound to this plugin instance's config
const telemetryCtx = {
apiKey: cfg.apiKey,
mode: cfg.mode,
skillsActive: false,
};
const _captureEvent = (event: string, props?: Record<string, unknown>) => {
try {
captureEvent(event, props, telemetryCtx);
} catch {
/* silently swallow */
}
};
if (cfg.needsSetup) {
api.logger.warn(
"openclaw-mem0: API key not configured. Memory features are disabled.\n" +
" To set up, run:\n" +
" openclaw mem0 login\n" +
" openclaw mem0 init\n" +
" Get your key at: https://app.mem0.ai/dashboard/api-keys",
);
// Register CLI even without API key — login command must be available
// Register CLI even without API key — init command must be available
// to bootstrap configuration. Pass nulls for backend/provider since
// only the login subcommand works without auth.
// only the init subcommand works without auth.
registerCliCommands(
api,
null as any,
@@ -119,8 +138,9 @@ const memoryPlugin = {
cfg,
() => cfg.userId,
(id: string) => `${cfg.userId}:agent:${id}`,
() => ({ user_id: cfg.userId, top_k: cfg.topK, source: "OPENCLAW" }),
() => ({ user_id: cfg.userId, top_k: cfg.topK }),
() => undefined,
(cmd: string) => _captureEvent(`openclaw.${cmd}`, { command: cmd }),
);
api.registerService({
@@ -160,8 +180,15 @@ const memoryPlugin = {
resolveUserId(cfg.userId, opts, currentSessionId);
const skillsActive = isSkillsMode(cfg.skills);
telemetryCtx.skillsActive = skillsActive;
_captureEvent("openclaw.plugin.registered", {
auto_recall: cfg.autoRecall,
auto_capture: cfg.autoCapture,
});
api.logger.info(
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, graph: ${cfg.enableGraph}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`,
`openclaw-mem0: registered (mode: ${cfg.mode}, user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, skills: ${skillsActive})`,
);
// Helper: build add options
@@ -176,9 +203,6 @@ const memoryPlugin = {
};
if (runId) opts.run_id = runId;
if (cfg.mode === "platform") {
opts.custom_instructions = cfg.customInstructions;
opts.custom_categories = categoriesToArray(cfg.customCategories);
opts.enable_graph = cfg.enableGraph;
opts.output_format = "v1.1";
}
return opts;
@@ -207,23 +231,29 @@ const memoryPlugin = {
}
// ========================================================================
// Tools
// Tools (modular — each tool in its own file under tools/)
// ========================================================================
const toolCtx: ToolContext = {
const toolDeps: ToolDeps = {
api,
backend,
cfg,
provider,
cfg,
backend,
resolveUserId: _resolveUserId,
effectiveUserId: _effectiveUserId,
agentUserId: _agentUserId,
getCurrentSessionId: () => currentSessionId,
skillsActive,
buildAddOptions,
buildSearchOptions,
getCurrentSessionId: () => currentSessionId,
skillsActive,
captureToolEvent: (toolName: string, props: Record<string, unknown>) => {
_captureEvent(`openclaw.tool.${toolName}`, {
tool_name: toolName,
...props,
});
},
};
registerAllTools(toolCtx);
registerAllTools(toolDeps);
// ========================================================================
// CLI Commands
@@ -238,6 +268,7 @@ const memoryPlugin = {
_agentUserId,
buildSearchOptions,
() => currentSessionId,
(cmd: string) => _captureEvent(`openclaw.cli.${cmd}`, { command: cmd }),
);
// ========================================================================
@@ -258,6 +289,7 @@ const memoryPlugin = {
getStateDir: () => pluginStateDir,
},
skillsActive,
_captureEvent,
);
// ========================================================================
@@ -277,7 +309,7 @@ const memoryPlugin = {
},
});
},
};
});
// ============================================================================
// Lifecycle Hook Registration
@@ -304,6 +336,10 @@ function registerHooks(
getStateDir: () => string | undefined;
},
skillsActive: boolean = false,
_captureEvent: (
event: string,
props?: Record<string, unknown>,
) => void = () => {},
) {
// ========================================================================
// SKILLS MODE: Agentic memory via before_prompt_build
@@ -330,16 +366,17 @@ function registerHooks(
return;
}
// Skip recall for system/bootstrap prompts. These are OpenClaw internal
// commands (/new, /reset) that contain system instructions, not user queries.
// Sending them to mem0 search wastes API calls and returns noise.
const promptLower = event.prompt.toLowerCase();
const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt =
promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session");
!isChannelSystemEvent &&
(promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) {
api.logger.info(
"openclaw-mem0: skills-mode skipping recall for system/bootstrap prompt",
@@ -372,6 +409,7 @@ function registerHooks(
const recallStrategy = cfg.skills?.recall?.strategy ?? "smart";
if (recallEnabled && recallStrategy !== "manual") {
const recallStart = Date.now();
try {
const query = sanitizeQuery(event.prompt);
@@ -395,6 +433,12 @@ function registerHooks(
`openclaw-mem0: skills-mode recall (strategy=${recallStrategy}) injecting ${recallResult.memories.length} memories (~${recallResult.tokenEstimate} tokens)`,
);
_captureEvent("openclaw.hook.recall", {
strategy: recallStrategy,
memory_count: recallResult.memories.length,
latency_ms: Date.now() - recallStart,
});
recallContext = recallResult.context;
} catch (err) {
api.logger.warn(
@@ -440,6 +484,10 @@ function registerHooks(
"\n</auto-dream>";
// Track which session triggered dream (session-keyed, not global)
dreamSessionId = sessionId;
_captureEvent("openclaw.hook.dream", {
phase: "triggered",
memory_count: memCount,
});
api.logger.info(
`openclaw-mem0: auto-dream triggered (${memCount} memories, gate passed)`,
);
@@ -487,12 +535,12 @@ function registerHooks(
}
// Verify the model actually performed WRITE operations (not just reads).
// Only count memory_store, memory_update, memory_delete.
// Only count memory_add, memory_update, memory_delete.
// Exclude memory_list and memory_search (read-only, orient-only pass).
// Scan only the LAST assistant message (this turn), not the full session
// snapshot, to avoid matching earlier tool calls from prior turns.
const WRITE_TOOLS = new Set([
"memory_store",
"memory_add",
"memory_update",
"memory_delete",
]);
@@ -512,6 +560,10 @@ function registerHooks(
if (writeToolUsed) {
releaseDreamLock(stateDir);
recordDreamCompletion(stateDir);
_captureEvent("openclaw.hook.dream", {
phase: "completed",
write_tools_used: true,
});
api.logger.info(
"openclaw-mem0: auto-dream completed (verified write tool usage), lock released",
);
@@ -545,11 +597,14 @@ function registerHooks(
// LEGACY MODE: Original auto-recall + auto-capture behavior
// ========================================================================
// Auto-recall: inject relevant memories before agent starts
// Track last seen session ID to detect actual new sessions (not every turn)
let lastRecallSessionId: string | undefined;
// Auto-recall: inject relevant memories before prompt is built
if (cfg.autoRecall) {
const RECALL_TIMEOUT_MS = 8_000;
api.on("before_agent_start", async (event, ctx) => {
api.on("before_prompt_build", async (event: any, ctx: any) => {
if (!event.prompt || event.prompt.length < 5) return;
// Skip non-interactive triggers (cron, heartbeat, automation)
@@ -562,24 +617,54 @@ function registerHooks(
return;
}
const promptLower = event.prompt.toLowerCase();
const isChannelSystemEvent = /^system(?:\s*\(untrusted\))?:\s*\[/i.test(
event.prompt,
);
const isSystemPrompt =
!isChannelSystemEvent &&
(promptLower.includes("a new session was started") ||
promptLower.includes("session startup sequence") ||
promptLower.includes("/new or /reset") ||
promptLower.startsWith("system:") ||
promptLower.startsWith("run your session"));
if (isSystemPrompt) {
api.logger.info(
"openclaw-mem0: skipping recall for system/bootstrap prompt",
);
return;
}
// Update shared state for tools (best-effort — tools don't have ctx)
if (sessionId) session.setCurrentSessionId(sessionId);
// Detect new session for cold-start broadening
const isNewSession = true; // treat every hook invocation as potentially new
// Detect actual new session (first turn with a different sessionKey)
const isNewSession =
sessionId !== undefined && sessionId !== lastRecallSessionId;
if (sessionId) lastRecallSessionId = sessionId;
// Subagents have ephemeral UUIDs — their namespace is always empty.
// Search the parent (main) user namespace instead so subagents get
// the user's long-term context.
const isSubagent = isSubagentSession(sessionId);
const recallSessionKey = isSubagent ? undefined : sessionId;
// Strip OpenClaw sender metadata from the prompt before searching
const cleanPrompt = event.prompt
.replace(
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
"",
)
.trim();
const recallStart = Date.now();
const recallWork = async () => {
// Use a larger candidate pool for recall, then filter down
// Single search with a reasonable candidate pool
const recallTopK = Math.max((cfg.topK ?? 5) * 2, 10);
// Search long-term memories (user-scoped; subagents read from parent namespace)
let longTermResults = await provider.search(
event.prompt,
cleanPrompt,
buildSearchOptions(
undefined,
recallTopK,
@@ -607,11 +692,9 @@ function registerHooks(
}
}
// For short/generic prompts or new sessions, broaden recall
// with a general query to avoid cold-start blindness.
// Use a lower threshold (0.5) since the generic query is
// intentionally broad and strict thresholds defeat the purpose.
if (event.prompt.length < 100 || isNewSession) {
// Only broaden for genuinely new sessions with short prompts
// (cold-start blindness). Skip on subsequent turns to save API calls.
if (isNewSession && cleanPrompt.length < 100) {
const broadOpts = buildSearchOptions(
undefined,
5,
@@ -634,53 +717,24 @@ function registerHooks(
// Cap at configured topK after filtering
longTermResults = longTermResults.slice(0, cfg.topK);
// Search session memories (session-scoped) if we have a session ID
let sessionResults: MemoryItem[] = [];
if (sessionId) {
sessionResults = await provider.search(
event.prompt,
buildSearchOptions(
undefined,
undefined,
sessionId,
recallSessionKey,
),
);
sessionResults = sessionResults.filter(
(r) => (r.score ?? 0) >= cfg.searchThreshold,
);
}
// Deduplicate session results against long-term
const longTermIds = new Set(longTermResults.map((r) => r.id));
const uniqueSessionResults = sessionResults.filter(
(r) => !longTermIds.has(r.id),
);
if (longTermResults.length === 0 && uniqueSessionResults.length === 0)
return undefined;
if (longTermResults.length === 0) return undefined;
// Build context with clear labels
let memoryContext = "";
if (longTermResults.length > 0) {
memoryContext += longTermResults
.map(
(r) =>
`- ${r.memory}${r.categories?.length ? ` [${r.categories.join(", ")}]` : ""}`,
)
.join("\n");
}
if (uniqueSessionResults.length > 0) {
if (memoryContext) memoryContext += "\n";
memoryContext += "\nSession memories:\n";
memoryContext += uniqueSessionResults
.map((r) => `- ${r.memory}`)
.join("\n");
}
const memoryContext = longTermResults
.map(
(r) =>
`- ${r.memory}${r.categories?.length ? ` [${r.categories.join(", ")}]` : ""}`,
)
.join("\n");
_captureEvent("openclaw.hook.recall", {
strategy: "legacy",
memory_count: longTermResults.length,
latency_ms: Date.now() - recallStart,
});
const totalCount = longTermResults.length + uniqueSessionResults.length;
api.logger.info(
`openclaw-mem0: injecting ${totalCount} memories into context (${longTermResults.length} long-term, ${uniqueSessionResults.length} session)`,
`openclaw-mem0: injecting ${longTermResults.length} memories into context`,
);
const preamble = isSubagent
@@ -742,6 +796,26 @@ function registerHooks(
// Update shared state for tools (best-effort — tools don't have ctx)
if (sessionId) session.setCurrentSessionId(sessionId);
const MEMORY_MUTATE_TOOLS = new Set([
"memory_add",
"memory_update",
"memory_delete",
]);
const agentUsedMemoryTool = event.messages.some((msg: any) => {
if (msg?.role !== "assistant" || !Array.isArray(msg?.content))
return false;
return msg.content.some(
(block: any) =>
block?.type === "tool_use" && MEMORY_MUTATE_TOOLS.has(block.name),
);
});
if (agentUsedMemoryTool) {
api.logger.info(
"openclaw-mem0: skipping auto-capture — agent already used memory tools this turn",
);
return;
}
// --- Build capture payload synchronously (cheap), then fire-and-forget ---
// Patterns indicating an assistant message contains a summary of
@@ -800,6 +874,19 @@ function registerHooks(
.trim();
if (!textContent) continue;
}
// Strip OpenClaw sender metadata prefix (prevents storing TUI identity as memory)
if (
textContent.includes("Sender") &&
textContent.includes("untrusted metadata")
) {
textContent = textContent
.replace(
/Sender\s*\(untrusted metadata\):\s*```json[\s\S]*?```\s*/gi,
"",
)
.trim();
if (!textContent) continue;
}
const isSummary =
role === "assistant" &&
@@ -873,10 +960,15 @@ function registerHooks(
});
const addOpts = buildAddOptions(undefined, sessionId, sessionId);
const captureStart = Date.now();
provider
.add(formattedMessages, addOpts)
.then((result) => {
const capturedCount = result.results?.length ?? 0;
_captureEvent("openclaw.hook.capture", {
captured_count: capturedCount,
latency_ms: Date.now() - captureStart,
});
if (capturedCount > 0) {
api.logger.info(
`openclaw-mem0: auto-captured ${capturedCount} memories`,
+30 -3
View File
@@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" {
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
definition: {
name: string;
description: string;
parameters: unknown;
execute: (
toolCallId: string,
params: Record<string, unknown>,
) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
[key: string]: unknown;
},
metadata?: { optional?: boolean; [key: string]: unknown },
): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerCommand?(definition: Record<string, unknown>): void;
registerService(service: {
id: string;
start: () => void;
start: (...args: any[]) => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
declare module "openclaw/plugin-sdk/plugin-entry" {
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
export interface PluginEntry {
id: string;
name: string;
description?: string;
register(api: OpenClawPluginApi): void;
}
export function definePluginEntry<T extends PluginEntry>(entry: T): T;
}
declare module "openclaw/plugin-sdk/core" {
export * from "openclaw/plugin-sdk";
}
+17 -20
View File
@@ -2,14 +2,20 @@
"id": "openclaw-mem0",
"name": "Memory (Mem0)",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"version": "1.0.5",
"kind": "memory",
"skills": ["skills"],
"contracts": {
"tools": [
"memory_search", "memory_store", "memory_get", "memory_list",
"memory_update", "memory_delete", "memory_history"
"memory_search", "memory_add", "memory_get", "memory_list",
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
]
},
"providerAuthEnvVars": {
"mem0": ["MEM0_API_KEY"],
"mem0-oss-openai": ["OPENAI_API_KEY"],
"mem0-oss-anthropic": ["ANTHROPIC_API_KEY"]
},
"uiHints": {
"mode": {
"label": "Mode",
@@ -19,13 +25,7 @@
"label": "Mem0 API Key",
"sensitive": true,
"placeholder": "m0-...",
"help": "API key from app.mem0.ai (or use ${MEM0_API_KEY}). Only needed for platform mode."
},
"baseUrl": {
"label": "API Base URL",
"placeholder": "https://api.mem0.ai",
"advanced": true,
"help": "Override the Mem0 API base URL. Only needed for custom deployments."
"help": "Platform mode only. Use a SecretRef ({\"source\":\"env\",\"provider\":\"default\",\"id\":\"MEM0_API_KEY\"}) or ${MEM0_API_KEY} instead of storing the key directly."
},
"userId": {
"label": "Default User ID",
@@ -82,7 +82,7 @@
"oss": {
"label": "Open-Source Configuration",
"advanced": true,
"help": "Optional. Configure custom embedder, vector store, LLM, or history DB for open-source mode. Has sensible defaults — only override what you need."
"help": "Optional. Configure custom embedder, vector store, LLM, or history DB for open-source mode. For API keys in sub-provider configs, use SecretRef objects or ${VAR} syntax instead of plaintext values."
},
"skills": {
"label": "Agentic Memory Skills",
@@ -98,17 +98,16 @@
"type": "string",
"enum": [
"platform",
"open-source",
"oss"
"open-source"
]
},
"apiKey": {
"type": "string"
},
"baseUrl": {
"userId": {
"type": "string"
},
"userId": {
"userEmail": {
"type": "string"
},
"orgId": {
@@ -216,12 +215,10 @@
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"schedule": { "type": "string" },
"mergeThreshold": { "type": "number" },
"maxMemoriesPerUser": { "type": "number" },
"preserveImmutable": { "type": "boolean" },
"credentialScan": { "type": "boolean" },
"expireStaleAfterDays": { "type": "number" }
"auto": { "type": "boolean" },
"minHours": { "type": "number" },
"minSessions": { "type": "number" },
"minMemories": { "type": "number" }
}
},
"domain": { "type": "string" },
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "1.0.4-beta.0",
"version": "1.0.5",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -42,12 +42,12 @@
"./dist/index.js"
],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
"pluginApi": ">=2026.3.28 <=2026.4.2",
"minGatewayVersion": "2026.3.28"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
"openclawVersion": "2026.4.1",
"pluginSdkVersion": "2026.4.1"
}
},
"devDependencies": {
+118 -57
View File
@@ -81,8 +81,7 @@ class PlatformProvider implements Mem0Provider {
constructor(
private readonly apiKey: string,
private readonly orgId?: string,
private readonly projectId?: string,
private readonly baseUrl?: string,
) {}
private async ensureClient(): Promise<void> {
@@ -97,11 +96,13 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: { apiKey: string; org_id?: string; project_id?: string } = {
const opts: {
apiKey: string;
host?: string;
} = {
apiKey: this.apiKey,
};
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
if (this.baseUrl) opts.host = this.baseUrl;
this.client = new MemoryClient(opts);
}
@@ -116,7 +117,6 @@ class PlatformProvider implements Mem0Provider {
opts.custom_instructions = options.custom_instructions;
if (options.custom_categories)
opts.custom_categories = options.custom_categories;
if (options.enable_graph) opts.enable_graph = options.enable_graph;
if (options.output_format) opts.output_format = options.output_format;
if (options.source) opts.source = options.source;
// Agentic harness: direct storage bypass
@@ -133,20 +133,11 @@ class PlatformProvider implements Mem0Provider {
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
await this.ensureClient();
// Base filters: always scope by user_id, optionally by run_id
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
// Merge agent-provided filters (created_at ranges, metadata, etc.)
// with base filters. Agent filters extend, never override user scoping.
const mergedFilters = options.filters
? { AND: [baseFilters, options.filters] }
: baseFilters;
const opts: Record<string, unknown> = {
api_version: "v2",
filters: mergedFilters,
user_id: options.user_id,
};
if (options.run_id) opts.run_id = options.run_id;
if (options.top_k != null) opts.top_k = options.top_k;
if (options.threshold != null) opts.threshold = options.threshold;
if (options.keyword_search != null)
@@ -155,6 +146,14 @@ class PlatformProvider implements Mem0Provider {
if (options.filter_memories != null)
opts.filter_memories = options.filter_memories;
if (options.categories != null) opts.categories = options.categories;
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
if (options.filters) {
opts.filters = { AND: [baseFilters, options.filters] };
} else {
opts.filters = baseFilters;
}
const results = await this.client.search(query, opts);
return normalizeSearchResults(results);
@@ -168,10 +167,16 @@ class PlatformProvider implements Mem0Provider {
async getAll(options: ListOptions): Promise<MemoryItem[]> {
await this.ensureClient();
const opts: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) opts.run_id = options.run_id;
const opts: Record<string, unknown> = {
api_version: "v2",
user_id: options.user_id,
filters: { user_id: options.user_id },
};
if (options.run_id) {
opts.run_id = options.run_id;
(opts.filters as Record<string, unknown>).run_id = options.run_id;
}
if (options.page_size != null) opts.page_size = options.page_size;
if (options.source) opts.source = options.source;
const results = await this.client.getAll(opts);
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
@@ -196,9 +201,7 @@ class PlatformProvider implements Mem0Provider {
await this.client.deleteAll({ user_id: userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -237,15 +240,53 @@ class OSSProvider implements Mem0Provider {
return this.initPromise;
}
private async _init(): Promise<void> {
const { Memory } = await import("mem0ai/oss");
private _buildConfig(disableHistory = false): Record<string, unknown> {
const config: Record<string, unknown> = { version: "v1.1" };
if (this.ossConfig?.embedder) config.embedder = this.ossConfig.embedder;
const defaultEmbedder = {
provider: "openai",
config: { model: "text-embedding-3-small" },
};
const defaultLlm = { provider: "openai", config: { model: "gpt-5.4" } };
const stripEmpty = (obj: Record<string, unknown>) => {
const out = { ...obj };
for (const k of Object.keys(out)) {
if (out[k] === "") delete out[k];
}
return out;
};
if (this.ossConfig?.embedder) {
const ec = stripEmpty(this.ossConfig.embedder.config ?? {});
if (ec.host && !ec.url) {
ec.url = ec.host;
delete ec.host;
}
config.embedder = {
provider: this.ossConfig.embedder.provider || defaultEmbedder.provider,
config: { ...defaultEmbedder.config, ...ec },
};
} else {
config.embedder = defaultEmbedder;
}
if (this.ossConfig?.llm) {
const lc = stripEmpty(this.ossConfig.llm.config ?? {});
if (lc.host && !lc.url) {
lc.url = lc.host;
delete lc.host;
}
config.llm = {
provider: this.ossConfig.llm.provider || defaultLlm.provider,
config: { ...defaultLlm.config, ...lc },
};
} else {
config.llm = defaultLlm;
}
if (this.ossConfig?.vectorStore)
config.vectorStore = this.ossConfig.vectorStore;
if (this.ossConfig?.llm) config.llm = this.ossConfig.llm;
config.vectorStore = { ...this.ossConfig.vectorStore };
if (this.ossConfig?.historyDbPath) {
const dbPath = this.resolvePath
@@ -254,42 +295,61 @@ class OSSProvider implements Mem0Provider {
config.historyDbPath = dbPath;
}
if (this.ossConfig?.disableHistory) {
if (disableHistory || this.ossConfig?.disableHistory) {
config.disableHistory = true;
}
if (this.customPrompt) config.customPrompt = this.customPrompt;
return config;
}
private async _init(): Promise<void> {
const mod = await import("mem0ai/oss");
const Memory = mod.Memory;
for (const cls of ["PGVector", "RedisDB", "Qdrant"]) {
const VectorCls = (mod as any)[cls];
if (!VectorCls || VectorCls.prototype.__patched) continue;
const origInit = VectorCls.prototype.initialize;
VectorCls.prototype.initialize = function (this: any) {
if (!this.config?.embeddingModelDims && this.config?.dimension) {
this.config.embeddingModelDims = this.config.dimension;
}
// Qdrant reads this.dimension directly
if (!this.dimension && this.config?.dimension) {
this.dimension = this.config.dimension;
}
// Skip premature constructor call when dimensions unknown
const dims = this.config?.embeddingModelDims ?? this.dimension;
if (!dims) return Promise.resolve();
// Run the real initialize only once
if (!this._initializePromise) {
this._initializePromise = origInit.call(this);
}
return this._initializePromise;
};
VectorCls.prototype.__patched = true;
}
let mem: any;
try {
this.memory = new Memory(config);
mem = new Memory(this._buildConfig());
} catch (err) {
// If initialization fails (e.g. native SQLite binding resolution under
// jiti), retry with history disabled — the history DB is the most common
// source of native-binding failures and is not required for core
// memory operations.
if (!config.disableHistory) {
// If constructor fails (e.g. native SQLite binding under jiti/Docker),
// retry with a FRESH config that has history disabled.
if (!this.ossConfig?.disableHistory) {
console.warn(
"[mem0] Memory initialization failed, retrying with history disabled:",
err instanceof Error ? err.message : err,
);
config.disableHistory = true;
this.memory = new Memory(config);
mem = new Memory(this._buildConfig(true));
} else {
throw err;
}
}
// Force the SDK's internal auto-initialization to complete now.
// Without this, concurrent method calls (e.g. auto-recall + stats)
// both trigger _autoInitialize() simultaneously, causing PGVector's
// pg client to call connect() twice → "Client has already been
// connected" crash. (#4638)
try {
await this.memory.getAll({ userId: "__mem0_warmup__" });
} catch {
// Warmup errors are non-fatal — the SDK may still work for
// subsequent calls once its internal state settles.
}
await mem.getAll({ userId: "__mem0_warmup__" });
this.memory = mem;
}
async add(
@@ -370,7 +430,7 @@ class OSSProvider implements Mem0Provider {
async update(memoryId: string, text: string): Promise<void> {
await this.ensureMemory();
await this.memory.update(memoryId, { data: text });
await this.memory.update(memoryId, text);
}
async delete(memoryId: string): Promise<void> {
@@ -383,9 +443,7 @@ class OSSProvider implements Mem0Provider {
await this.memory.deleteAll({ userId });
}
async history(
memoryId: string,
): Promise<
async history(memoryId: string): Promise<
Array<{
id: string;
old_memory: string;
@@ -398,8 +456,12 @@ class OSSProvider implements Mem0Provider {
try {
const result = await this.memory.history(memoryId);
return Array.isArray(result) ? result : [];
} catch {
// OSS may not support history depending on config
} catch (err) {
// OSS may not support history depending on config (e.g. disableHistory)
console.warn(
"[mem0] OSS history() failed:",
err instanceof Error ? err.message : err,
);
return [];
}
}
@@ -419,7 +481,7 @@ export function createProvider(
);
}
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl);
}
// ============================================================================
@@ -449,7 +511,6 @@ export function providerToBackend(
...(opts.immutable && { immutable: true }),
...(opts.infer === false && { infer: false }),
...(opts.expires && { expiration_date: opts.expires }),
...(opts.enableGraph && { enable_graph: true }),
},
);
return result as unknown as Record<string, unknown>;
+13 -13
View File
@@ -3,10 +3,10 @@
* injects user config, and produces the final injectable prompt string.
*/
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import type { SkillsConfig, CategoryConfig } from "./types.ts";
import { readText, exists } from "./fs-safe.ts";
// ============================================================================
// Defaults
@@ -111,7 +111,7 @@ function resolveSkillsDir(): string {
// Validate: must contain the expected subdirectory structure
for (const dir of candidates) {
if (fs.existsSync(path.join(dir, "memory-triage", "SKILL.md"))) {
if (exists(path.join(dir, "memory-triage", "SKILL.md"))) {
return dir;
}
}
@@ -143,7 +143,7 @@ function readSkillFile(skillName: string): string | null {
const filePath = safePath(skillName, "SKILL.md");
if (!filePath) return null;
try {
return fs.readFileSync(filePath, "utf-8");
return readText(filePath);
} catch {
return null;
}
@@ -158,7 +158,7 @@ function readDomainOverlay(domain: string, targetSkill: string): string | null {
const filePath = safePath(targetSkill, "domains", `${domain}.md`);
if (!filePath) return null;
try {
const content = fs.readFileSync(filePath, "utf-8");
const content = readText(filePath);
const parsed = parseSkillFile(content);
// Check applies_to for backward compat (skip if targeting a different skill)
const appliesTo = parsed.frontmatter.applies_to;
@@ -318,7 +318,7 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
const parts: string[] = [];
parts.push("<memory-system>");
parts.push(
"IMPORTANT: Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
"IMPORTANT: Use `memory_add` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
);
parts.push("");
parts.push(triage.prompt);
@@ -326,19 +326,19 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
parts.push("## Tool Usage");
parts.push("");
parts.push(
"Batch facts by CATEGORY. All facts in one memory_store call must share the same category because category determines retention policy (TTL, immutability). If a turn has facts in different categories, make one call per category.",
"Batch facts by CATEGORY. All facts in one memory_add call must share the same category because category determines retention policy (TTL, immutability). If a turn has facts in different categories, make one call per category.",
);
parts.push("");
parts.push("FORMAT (single category):");
parts.push(
' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
' memory_add(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
);
parts.push("FORMAT (mixed categories in one turn, separate calls):");
parts.push(
' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
' memory_add(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
);
parts.push(
' memory_store(facts: ["As of 2026-04-01, migrating from Postgres to CockroachDB"], category: "decision")',
' memory_add(facts: ["As of 2026-04-01, migrating from Postgres to CockroachDB"], category: "decision")',
);
// Only include search instructions if recall is enabled
if (config.recall?.enabled !== false) {
@@ -434,7 +434,7 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
"You have persistent long-term memory via mem0. After EVERY response, evaluate the turn for facts worth storing.",
);
parts.push(
"Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
"Use `memory_add` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
);
parts.push("Most turns produce ZERO memory operations. That is correct.");
parts.push(
@@ -444,7 +444,7 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
"Batch facts by CATEGORY. All facts in one call must share the same category.",
);
parts.push(
'Format: memory_store(facts: ["fact text"], category: "identity")',
'Format: memory_add(facts: ["fact text"], category: "identity")',
);
parts.push(
"NEVER store credentials (sk-, m0-, ghp_, AKIA, Bearer tokens, passwords).",
+3 -6
View File
@@ -7,7 +7,7 @@ description: >
Also triggers automatically after sufficient activity (configurable).
user-invocable: true
metadata:
{"openclaw": {"emoji": "💤"}}
{"openclaw": {"emoji": "💤", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Consolidation
@@ -34,9 +34,6 @@ Identify which memories need action. Use the tools to investigate.
**Search for recent additions:**
Call `memory_search` with a `created_at` filter to find memories added since the last consolidation. These are the most likely to need merging or cleanup.
**Check edit history on suspicious entries:**
If a memory looks like it was updated multiple times or has contradictory content, call `memory_history` on it to understand how it evolved.
**Classify each target into one of these actions:**
- DELETE: contains credentials, expired by TTL, pure noise, raw tool output, standalone timestamps
- MERGE: two or more memories express the same fact in different words, or a series tracks incremental changes to the same entity
@@ -48,7 +45,7 @@ Execute the actions identified in Phase 2. Work in this priority order:
### 3a. Delete dangerous and expired entries
Delete immediately using `memory_forget`:
Delete immediately using `memory_delete`:
- Credentials, API keys, tokens, passwords, secrets (patterns: sk-, m0-, ghp_, AKIA, Bearer, password=, token=, secret=)
- Pure timestamps with no context
- Raw tool output stored as memory
@@ -62,7 +59,7 @@ Delete immediately using `memory_forget`:
When two or more memories express the same fact:
1. Pick the most complete version as the base
2. Call `memory_update` on the best version to incorporate missing details from the others
3. Call `memory_forget` on the redundant entries
3. Call `memory_delete` on the redundant entries
`memory_update` is preferred over forget-then-store because it is atomic and preserves edit history.
+26 -26
View File
@@ -3,12 +3,12 @@ name: memory-triage
description: >
ALWAYS USE THIS SKILL. Persistent long-term memory protocol powered by mem0.
After EVERY response, evaluate the conversation for durable facts worth storing
via memory_store. Handles identity, preferences, decisions, configurations, rules,
projects, and relationships. Use memory_store (NOT workspace files) for all user facts.
via memory_add. Handles identity, preferences, decisions, configurations, rules,
projects, and relationships. Use memory_add (NOT workspace files) for all user facts.
Read this skill at the start of every session.
user-invocable: false
metadata:
{"openclaw": {"always": true, "emoji": "🧠"}}
{"openclaw": {"always": true, "emoji": "🧠", "requires": {"env": ["MEM0_API_KEY"], "bins": []}}}
---
# Memory Protocol
@@ -108,26 +108,26 @@ Each memory you store must be a **self-contained, independently understandable f
**ALWAYS group all information about the same entity, concept, event, or subject into a SINGLE unified memory.** If multiple pieces of information refer to the same entity (e.g., a conference, a project, a person, a system), they MUST be combined into one comprehensive memory.
**DO NOT split requirements, specifications, or details about the same entity across multiple memory_store calls.** Even if information is phrased differently ("Budget for X", "X requires Y", "X needs Z"), if they all refer to the same entity, combine ALL into ONE call.
**DO NOT split requirements, specifications, or details about the same entity across multiple memory_add calls.** Even if information is phrased differently ("Budget for X", "X requires Y", "X needs Z"), if they all refer to the same entity, combine ALL into ONE call.
**WRONG** — fragmented into separate facts:
```
memory_store(facts: ["Conference requires at least 4 breakout rooms", "Conference requires vegan options", "Conference requires parking"], category: "project")
memory_add(facts: ["Conference requires at least 4 breakout rooms", "Conference requires vegan options", "Conference requires parking"], category: "project")
```
**CORRECT** — grouped into one self-contained fact:
```
memory_store(facts: ["Conference requires at least 4 breakout rooms for 30-40 people each, robust vegan and vegetarian options with allergen-free alternatives, parking for at least 100 vehicles, venue within walking distance of transit"], category: "project")
memory_add(facts: ["Conference requires at least 4 breakout rooms for 30-40 people each, robust vegan and vegetarian options with allergen-free alternatives, parking for at least 100 vehicles, venue within walking distance of transit"], category: "project")
```
**WRONG** — same entity split into separate facts:
```
memory_store(facts: ["Budget is $150-175 per person for TechForward event", "TechForward event requires strong WiFi", "TechForward event requires hybrid capabilities"], category: "project")
memory_add(facts: ["Budget is $150-175 per person for TechForward event", "TechForward event requires strong WiFi", "TechForward event requires hybrid capabilities"], category: "project")
```
**CORRECT** — combined into one fact about TechForward:
```
memory_store(facts: ["TechForward event has a budget of $150-175 per person per day including venue rental, standard AV setup, and catering. Requires strong WiFi and hybrid event capabilities for remote attendees."], category: "project")
memory_add(facts: ["TechForward event has a budget of $150-175 per person per day including venue rental, standard AV setup, and catering. Requires strong WiFi and hybrid event capabilities for remote attendees."], category: "project")
```
**Only create separate memories when information refers to genuinely different entities, concepts, or unrelated topics** (e.g., "TechForward event" vs "Marketing campaign" are separate).
@@ -151,10 +151,10 @@ Do not store characterizations from assistant messages (e.g., "user seems excite
## How to Store
Use `memory_store` with the `facts` array. All facts in one call MUST share the same category because category determines retention policy (TTL, immutability).
Use `memory_add` with the `facts` array. All facts in one call MUST share the same category because category determines retention policy (TTL, immutability).
```
memory_store(
memory_add(
facts: ["fact one in third person", "fact two in third person"],
category: "identity"
)
@@ -163,8 +163,8 @@ memory_store(
If a turn produces facts in different categories, make one call per category:
```
memory_store(facts: ["User is Alex, senior engineer at Stripe, PST timezone"], category: "identity")
memory_store(facts: ["As of 2026-04-01, user decided to migrate from Postgres to CockroachDB"], category: "decision")
memory_add(facts: ["User is Alex, senior engineer at Stripe, PST timezone"], category: "identity")
memory_add(facts: ["As of 2026-04-01, user decided to migrate from Postgres to CockroachDB"], category: "decision")
```
Categories: `identity`, `configuration`, `rule`, `preference`, `decision`, `technical`, `relationship`, `project`
@@ -197,8 +197,8 @@ Categories: `identity`, `configuration`, `rule`, `preference`, `decision`, `tech
When a recalled memory needs updating (fact changed, status changed, new detail added):
1. `memory_search` to find the existing memory
2. `memory_forget` on the old memory's ID
3. `memory_store` with the corrected/expanded fact
2. `memory_delete` on the old memory's ID
3. `memory_add` with the corrected/expanded fact
**Choose the MORE COMPLETE version.** When both old and new have unique context, COMBINE them into a unified memory using the user's stated words.
@@ -240,7 +240,7 @@ When a recalled memory needs updating (fact changed, status changed, new detail
```
User: "I set up the research agent on Claude Sonnet with a 30-min cron. It checks HackerNews and sends summaries to #research-feed in Slack."
Agent: [responds helpfully]
→ memory_store(facts: ["User's research agent runs on Claude Sonnet, cron every 30 minutes, monitors HackerNews and posts summaries to Slack #research-feed"], category: "configuration")
→ memory_add(facts: ["User's research agent runs on Claude Sonnet, cron every 30 minutes, monitors HackerNews and posts summaries to Slack #research-feed"], category: "configuration")
```
### Example 2: NOOP — tool output
@@ -260,7 +260,7 @@ User: "Hey Chris here again"
### Example 4: Rule with rationale (preserving user's words)
```
User: "Never use Docker for local dev, it ate 40GB of disk last time and my Mac mini only has 256GB"
→ memory_store(facts: ["User rule: avoid Docker for local dev. Reason: ate 40GB of disk on 256GB Mac mini"], category: "rule")
→ memory_add(facts: ["User rule: avoid Docker for local dev. Reason: ate 40GB of disk on 256GB Mac mini"], category: "rule")
```
### Example 5: UPDATE — combining contexts from both versions
@@ -268,22 +268,22 @@ User: "Never use Docker for local dev, it ate 40GB of disk last time and my Mac
Recalled: ["As of 2026-03-15, user is planning trip to Paris in September with friend Jack"]
User: "Can't wait for the Paris trip, definitely want to hit the Eiffel Tower and try authentic French pastries"
→ memory_search("Paris trip planning")
→ memory_forget("mem-id-of-old")
→ memory_store(facts: ["As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries"], category: "project")
→ memory_delete(memoryId: "mem-id-of-old")
→ memory_add(facts: ["As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries"], category: "project")
```
### Example 6: Outcome over intent
```
User: "Update the call scripts sheet with the new truth-based templates"
Agent: [updates the sheet successfully]
→ memory_store(facts: ["Call scripts sheet (ID: 146Qbb...) was updated with truth-based templates (as of 2026-03-30)"], category: "configuration")
→ memory_add(facts: ["Call scripts sheet (ID: 146Qbb...) was updated with truth-based templates (as of 2026-03-30)"], category: "configuration")
```
### Example 7: Credential — store the fact, not the value
```
User: "Use this API key for the new service: sk-proj-abc123def456"
Agent: [configures the service]
→ memory_store(facts: ["API key was configured for the new service (as of 2026-03-30)"], category: "configuration")
→ memory_add(facts: ["API key was configured for the new service (as of 2026-03-30)"], category: "configuration")
```
### Example 8: NOOP — cosmetic difference, not material
@@ -296,7 +296,7 @@ User: "Yeah me and Poppy love our daily walks"
### Example 9: Entity grouping — single call, not fragmented
```
User: "The budget for the offsite is $200 per head. We need a venue with WiFi, parking for 50 cars, and a projector."
→ memory_store(facts: ["Team offsite budget is $200 per person. Venue requirements: WiFi, parking for 50 vehicles, and projector setup."], category: "project")
→ memory_add(facts: ["Team offsite budget is $200 per person. Venue requirements: WiFi, parking for 50 vehicles, and projector setup."], category: "project")
All details about the same entity (offsite) go in one fact, one call.
```
@@ -304,15 +304,15 @@ All details about the same entity (offsite) go in one fact, one call.
```
Recalled: ["User enjoys hiking on weekends and finds it therapeutic"]
User: "I hurt my knee last week, can't hike for a while"
→ memory_store(facts: ["As of 2026-03-30, user has temporarily paused hiking due to knee injury"], category: "project")
→ memory_add(facts: ["As of 2026-03-30, user has temporarily paused hiking due to knee injury"], category: "project")
DO NOT delete the hiking preference. It is temporarily paused, not contradicted.
```
### Example 11: Mixed categories in one turn — separate calls
```
User: "I'm Sarah, I work at Cloudflare. I just decided to switch our monitoring from Datadog to Grafana because of cost."
→ memory_store(facts: ["User is Sarah, works at Cloudflare"], category: "identity")
→ memory_store(facts: ["As of 2026-03-30, user decided to switch monitoring from Datadog to Grafana due to cost"], category: "decision")
→ memory_add(facts: ["User is Sarah, works at Cloudflare"], category: "identity")
→ memory_add(facts: ["As of 2026-03-30, user decided to switch monitoring from Datadog to Grafana due to cost"], category: "decision")
Two calls because identity and decision have different retention policies.
```
@@ -328,8 +328,8 @@ Agent: "Hello! How can I help?"
Recalled: ["User has a dog", "Dog's name is Poppy", "User walks dog daily"]
User: "Poppy learned fetch! Our walks are even better now, honestly it's the best part of my day"
→ memory_search("dog Poppy walks") → find all three old memory IDs
→ memory_forget(id-1), memory_forget(id-2), memory_forget(id-3)
→ memory_store(facts: ["User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable."], category: "preference")
→ memory_delete(memoryId: "id-1"), memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3")
→ memory_add(facts: ["User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable."], category: "preference")
```
### Example 12: NOOP — generic greeting, nothing to store
+387 -9
View File
@@ -4,9 +4,18 @@
* 2. initPromise poisoning fix (retry after failure)
* 3. Graceful SQLite fallback in OSSProvider
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { mem0ConfigSchema, createProvider } from "./index.ts";
/** Stub vector-store classes required by OSSProvider._init's patching loop. */
function vectorStubs() {
return {
PGVector: class { initialize() { return Promise.resolve(); } },
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
};
}
// ---------------------------------------------------------------------------
// 1. Config: disableHistory passthrough
// ---------------------------------------------------------------------------
@@ -60,6 +69,7 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
beforeEach(() => {
capturedConfig = undefined;
memoryCallCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -81,9 +91,14 @@ describe("OSSProvider — disableHistory passthrough to Memory", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes disableHistory: true to Memory when configured", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -130,6 +145,7 @@ describe("OSSProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
@@ -154,9 +170,14 @@ describe("OSSProvider — initPromise retry after failure", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -184,14 +205,21 @@ describe("OSSProvider — initPromise retry after failure", () => {
// ---------------------------------------------------------------------------
describe("OSSProvider — graceful SQLite fallback", () => {
let capturedConfigs: Record<string, unknown>[];
/** When set, the mock Memory constructor always throws with this message. */
let forceConstructorError: string | null;
beforeEach(() => {
capturedConfigs = [];
forceConstructorError = null;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfigs.push({ ...config });
if (forceConstructorError) {
throw new Error(forceConstructorError);
}
if (!config.disableHistory) {
throw new Error("Could not locate the bindings file");
}
@@ -211,9 +239,14 @@ describe("OSSProvider — graceful SQLite fallback", () => {
}
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries with disableHistory: true when initial construction fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
@@ -242,14 +275,8 @@ describe("OSSProvider — graceful SQLite fallback", () => {
});
it("does not retry when disableHistory is already true", async () => {
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
// Fail even with disableHistory (e.g. vector store issue)
throw new Error("vector store connection refused");
}
},
}));
// Force the constructor to always throw, regardless of disableHistory
forceConstructorError = "vector store connection refused";
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -274,6 +301,7 @@ describe("PlatformProvider — initPromise retry after failure", () => {
beforeEach(() => {
callCount = 0;
vi.resetModules();
vi.doMock("mem0ai", () => ({
default: class MockMemoryClient {
@@ -300,6 +328,10 @@ describe("PlatformProvider — initPromise retry after failure", () => {
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("retries initialization after a transient failure", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
@@ -320,3 +352,349 @@ describe("PlatformProvider — initPromise retry after failure", () => {
expect(callCount).toBe(2);
});
});
// ---------------------------------------------------------------------------
// 6. OSSProvider: _buildConfig covers all branches
// ---------------------------------------------------------------------------
describe("OSSProvider — _buildConfig branch coverage", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("builds config with custom embedder, llm, vectorStore, and historyDbPath", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "sk-e", model: "text-embedding-3-small" } },
llm: { provider: "openai", config: { apiKey: "sk-l", model: "gpt-4" } },
vectorStore: { provider: "qdrant", config: { host: "localhost", port: 6333 } },
historyDbPath: "/tmp/history.db",
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => `/resolved${p}` } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.embedder).toEqual({
provider: "openai",
config: { model: "text-embedding-3-small", apiKey: "sk-e" },
});
expect(capturedConfig!.llm).toEqual({
provider: "openai",
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
});
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db");
expect(capturedConfig!.disableHistory).toBe(true);
});
it("strips empty-string values from embedder and llm config", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "openai", config: { apiKey: "", model: "custom-model" } },
llm: { provider: "openai", config: { apiKey: "", model: "" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty apiKey should be stripped, leaving only the non-empty model
const embedderCfg = (capturedConfig!.embedder as any).config;
expect(embedderCfg.apiKey).toBeUndefined();
expect(embedderCfg.model).toBe("custom-model");
// Both empty keys in llm should be stripped, defaults applied
const llmCfg = (capturedConfig!.llm as any).config;
expect(llmCfg.apiKey).toBeUndefined();
});
it("falls back to default provider when embedder/llm provider is empty", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: {
embedder: { provider: "", config: { apiKey: "sk-e" } },
llm: { provider: "", config: { apiKey: "sk-l" } },
disableHistory: true,
},
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
// Empty provider should fall back to "openai" default
expect((capturedConfig!.embedder as any).provider).toBe("openai");
expect((capturedConfig!.llm as any).provider).toBe("openai");
});
});
// ---------------------------------------------------------------------------
// 7. OSSProvider: vector store dimension patching
// ---------------------------------------------------------------------------
describe("OSSProvider — vector store dimension patching", () => {
let capturedModule: any;
beforeEach(() => {
vi.resetModules();
vi.doMock("mem0ai/oss", () => {
const mod = {
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("pg-initialized"); }
},
RedisDB: class {
config: any;
_initializePromise: any;
initialize() { return Promise.resolve("redis-initialized"); }
},
Qdrant: class {
config: any;
dimension: any;
_initializePromise: any;
initialize() { return Promise.resolve("qdrant-initialized"); }
},
};
capturedModule = mod;
return mod;
});
});
afterEach(() => {
vi.restoreAllMocks();
});
async function triggerInit() {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
await provider.search("test", { user_id: "u1" });
}
it("copies config.dimension to embeddingModelDims and this.dimension", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = { dimension: 1536 };
await pg.initialize();
expect(pg.config.embeddingModelDims).toBe(1536);
expect(pg.dimension).toBe(1536);
});
it("returns resolved promise when no dimensions are known", async () => {
await triggerInit();
const pg = new capturedModule.PGVector();
pg.config = {};
const result = await pg.initialize();
expect(result).toBeUndefined();
});
it("runs original initialize only once via cached promise", async () => {
await triggerInit();
const q = new capturedModule.Qdrant();
q.config = { dimension: 768 };
const first = await q.initialize();
const second = await q.initialize();
expect(first).toBe("qdrant-initialized");
expect(second).toBe("qdrant-initialized");
expect(q._initializePromise).toBeDefined();
});
it("skips missing vector store classes without crashing", async () => {
// Override with a mock that omits PGVector entirely
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
PGVector: undefined, // explicitly absent — tests the !VectorCls guard
RedisDB: class { initialize() { return Promise.resolve(); } },
Qdrant: class { initialize() { return Promise.resolve(); } },
}));
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const provider = createProvider(cfg, { resolvePath: (p: string) => p } as any);
// Should not throw even though PGVector is missing
const results = await provider.search("test", { user_id: "u1" });
expect(results).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// 8. OSSProvider: history() error handler
// ---------------------------------------------------------------------------
describe("OSSProvider — history error handling", () => {
/** When set, the mock history() throws this value instead of an Error. */
let historyThrowValue: unknown;
beforeEach(() => {
historyThrowValue = new Error("history not available");
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor() {}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
async history() { throw historyThrowValue; }
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("returns empty array and warns when history() throws an Error", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-123");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"history not available",
);
warnSpy.mockRestore();
});
it("handles non-Error thrown values in history()", async () => {
historyThrowValue = "raw string error";
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
const result = await provider.history("mem-456");
expect(result).toEqual([]);
expect(warnSpy).toHaveBeenCalledWith(
"[mem0] OSS history() failed:",
"raw string error",
);
warnSpy.mockRestore();
});
});
// ---------------------------------------------------------------------------
// 9. OSSProvider: customPrompt passthrough
// ---------------------------------------------------------------------------
describe("OSSProvider — customPrompt passthrough", () => {
let capturedConfig: Record<string, unknown> | undefined;
beforeEach(() => {
capturedConfig = undefined;
vi.resetModules();
vi.doMock("mem0ai/oss", () => ({
Memory: class MockMemory {
constructor(config: Record<string, unknown>) {
capturedConfig = { ...config };
}
async search() { return { results: [] }; }
async get() { return {}; }
async getAll() { return []; }
async add() { return { results: [] }; }
async delete() {}
},
...vectorStubs(),
}));
});
afterEach(() => {
vi.restoreAllMocks();
});
it("passes customPrompt to Memory config when provided", async () => {
const { createProvider } = await import("./index.ts");
const cfg = mem0ConfigSchema.parse({
mode: "open-source",
oss: { disableHistory: true },
customPrompt: "Extract only user preferences.",
});
const api = { resolvePath: (p: string) => p } as any;
const provider = createProvider(cfg, api);
await provider.search("test", { user_id: "u1" });
expect(capturedConfig).toBeDefined();
expect(capturedConfig!.customPrompt).toBe("Extract only user preferences.");
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* 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 } from "node:crypto";
import { readPluginAuth } from "./cli/config-file.ts";
export const PLUGIN_VERSION = "1.0.4";
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;
let _telemetryEnabled: boolean | undefined;
function isTelemetryEnabled(): boolean {
if (_telemetryEnabled !== undefined) return _telemetryEnabled;
try {
const val = (globalThis as any).__mem0_telemetry_override;
if (val !== undefined) {
const s = String(val).toLowerCase();
_telemetryEnabled = s !== "false" && s !== "0" && s !== "no";
} else {
_telemetryEnabled = true;
}
} catch {
_telemetryEnabled = true;
}
return _telemetryEnabled;
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached userEmail (from /v1/ping/) > MD5(apiKey) > fallback.
*/
function getDistinctId(apiKey?: string): string {
try {
const auth = readPluginAuth();
if (auth.userEmail) return auth.userEmail;
} catch {
/* ignore */
}
if (apiKey) {
return createHash("md5").update(apiKey).digest("hex");
}
return "anonymous-openclaw";
}
function ensureFlushTimer(): void {
if (flushTimer) return;
flushTimer = setInterval(flushEvents, FLUSH_INTERVAL_MS);
if (typeof flushTimer === "object" && "unref" in flushTimer) {
flushTimer.unref();
}
}
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 */
});
}
/**
* Capture a PostHog event (non-blocking, never throws).
*/
export function captureEvent(
eventName: string,
properties: Record<string, unknown> = {},
ctx?: { apiKey?: string; mode?: string; skillsActive?: boolean },
): void {
if (!isTelemetryEnabled()) return;
try {
const distinctId = getDistinctId(ctx?.apiKey);
eventQueue.push({
event: eventName,
distinct_id: distinctId,
properties: {
source: "OPENCLAW",
language: "node",
plugin_version: PLUGIN_VERSION,
node_version: process.version,
os: process.platform,
mode: ctx?.mode,
skills_active: ctx?.skillsActive,
$process_person_profile: false,
$lib: "posthog-node",
...properties,
},
});
ensureFlushTimer();
if (eventQueue.length >= FLUSH_THRESHOLD) {
flushEvents();
}
} catch {
/* silently swallow */
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* Test shim for openclaw/plugin-sdk/plugin-entry.
* At runtime this is resolved from the OpenClaw gateway.
*/
export function definePluginEntry<T>(entry: T): T {
return entry;
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Test shim for openclaw/plugin-sdk.
* At runtime this is resolved from the OpenClaw gateway.
*/
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(definition: Record<string, unknown>, metadata?: Record<string, unknown>): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(handler: (context: { program: any }) => void, options?: Record<string, unknown>): void;
registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void;
[key: string]: unknown;
}
File diff suppressed because it is too large Load Diff
+233
View File
@@ -0,0 +1,233 @@
/**
* Tests for cli/config-file.ts — file-based config helpers.
*
* All filesystem operations are mocked via fs-safe.ts so tests never
* touch the real disk.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("../fs-safe.ts", () => ({
readText: vi.fn(),
exists: vi.fn(),
writeText: vi.fn(),
mkdirp: vi.fn(),
unlink: vi.fn(),
}));
import { readText, exists, writeText, mkdirp } from "../fs-safe.ts";
import {
readPluginAuth,
writePluginAuth,
getBaseUrl,
DEFAULT_BASE_URL,
} from "../cli/config-file.ts";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const mockExists = exists as ReturnType<typeof vi.fn>;
const mockReadText = readText as ReturnType<typeof vi.fn>;
const mockWriteText = writeText as ReturnType<typeof vi.fn>;
const mockMkdirp = mkdirp as ReturnType<typeof vi.fn>;
function setConfigFile(obj: Record<string, unknown>): void {
mockExists.mockReturnValue(true);
mockReadText.mockReturnValue(JSON.stringify(obj));
}
function setNoFile(): void {
mockExists.mockReturnValue(false);
}
beforeEach(() => {
vi.resetAllMocks();
});
// ---------------------------------------------------------------------------
// readPluginAuth
// ---------------------------------------------------------------------------
describe("readPluginAuth", () => {
it("returns empty object when config file does not exist", () => {
setNoFile();
expect(readPluginAuth()).toEqual({});
});
it("returns empty object when config has no plugins section", () => {
setConfigFile({ someOtherKey: true });
expect(readPluginAuth()).toEqual({});
});
it("reads all fields correctly from nested config", () => {
setConfigFile({
plugins: {
entries: {
"openclaw-mem0": {
enabled: true,
config: {
apiKey: "sk-test-123",
baseUrl: "https://custom.api.com",
userId: "user-1",
mode: "platform",
autoRecall: true,
autoCapture: false,
topK: 10,
},
},
},
},
});
const auth = readPluginAuth();
expect(auth).toEqual({
apiKey: "sk-test-123",
baseUrl: "https://custom.api.com",
userId: "user-1",
mode: "platform",
autoRecall: true,
autoCapture: false,
topK: 10,
});
});
it("handles snake_case aliases (api_key, base_url, user_id)", () => {
setConfigFile({
plugins: {
entries: {
"openclaw-mem0": {
enabled: true,
config: {
api_key: "sk-snake",
base_url: "https://snake.api.com",
user_id: "user-snake",
},
},
},
},
});
const auth = readPluginAuth();
expect(auth.apiKey).toBe("sk-snake");
expect(auth.baseUrl).toBe("https://snake.api.com");
expect(auth.userId).toBe("user-snake");
});
it("returns empty object when JSON is invalid", () => {
mockExists.mockReturnValue(true);
mockReadText.mockReturnValue("not valid json {{{");
expect(readPluginAuth()).toEqual({});
});
});
// ---------------------------------------------------------------------------
// writePluginAuth
// ---------------------------------------------------------------------------
describe("writePluginAuth", () => {
it("creates nested structure from scratch when no config exists", () => {
setNoFile();
// exists returns false for both the file (readFullConfig) and the dir (writeFullConfig)
mockExists.mockReturnValue(false);
writePluginAuth({ apiKey: "sk-new", userId: "u1" });
expect(mockMkdirp).toHaveBeenCalled();
expect(mockWriteText).toHaveBeenCalledOnce();
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
expect(written.plugins.entries["openclaw-mem0"].enabled).toBe(true);
expect(written.plugins.entries["openclaw-mem0"].config.apiKey).toBe(
"sk-new",
);
expect(written.plugins.entries["openclaw-mem0"].config.userId).toBe("u1");
});
it("merges into existing config preserving other data", () => {
setConfigFile({
otherSetting: "keep-me",
plugins: {
entries: {
"openclaw-mem0": {
enabled: true,
config: {
apiKey: "sk-old",
mode: "platform",
},
},
},
},
});
writePluginAuth({ baseUrl: "https://new.api.com" });
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
// Existing fields preserved
expect(written.otherSetting).toBe("keep-me");
expect(written.plugins.entries["openclaw-mem0"].config.apiKey).toBe(
"sk-old",
);
expect(written.plugins.entries["openclaw-mem0"].config.mode).toBe(
"platform",
);
// New field added
expect(written.plugins.entries["openclaw-mem0"].config.baseUrl).toBe(
"https://new.api.com",
);
});
it("creates directory if missing", () => {
// File doesn't exist (readFullConfig returns {}), dir doesn't exist
mockExists.mockReturnValue(false);
writePluginAuth({ apiKey: "sk-test" });
expect(mockMkdirp).toHaveBeenCalledWith(
expect.stringContaining(".openclaw"),
0o700,
);
});
it("skips undefined values", () => {
setNoFile();
mockExists.mockReturnValue(false);
writePluginAuth({
apiKey: "sk-set",
baseUrl: undefined,
userId: undefined,
});
const written = JSON.parse(mockWriteText.mock.calls[0][1]);
const cfg = written.plugins.entries["openclaw-mem0"].config;
expect(cfg.apiKey).toBe("sk-set");
expect(cfg).not.toHaveProperty("baseUrl");
expect(cfg).not.toHaveProperty("userId");
});
});
// ---------------------------------------------------------------------------
// getBaseUrl
// ---------------------------------------------------------------------------
describe("getBaseUrl", () => {
it("returns configured URL when baseUrl is set", () => {
setConfigFile({
plugins: {
entries: {
"openclaw-mem0": {
enabled: true,
config: { baseUrl: "https://custom.example.com" },
},
},
},
});
expect(getBaseUrl()).toBe("https://custom.example.com");
});
it("returns default URL when baseUrl is not configured", () => {
setNoFile();
expect(getBaseUrl()).toBe(DEFAULT_BASE_URL);
});
});
+10 -35
View File
@@ -64,11 +64,6 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
expect(cfg.searchThreshold).toBe(0.5);
});
it("enableGraph defaults to false", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.enableGraph).toBe(false);
});
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
expect(cfg.customInstructions).toBe(DEFAULT_CUSTOM_INSTRUCTIONS);
@@ -99,9 +94,9 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
// mem0ConfigSchema.parse() — mode parsing
// ---------------------------------------------------------------------------
describe("mem0ConfigSchema.parse() — mode parsing", () => {
it('"oss" is normalized to "open-source"', () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss" });
expect(cfg.mode).toBe("open-source");
it('"oss" is not a valid mode and defaults to "platform"', () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", apiKey: "k" });
expect(cfg.mode).toBe("platform");
});
it('"open-source" stays as "open-source"', () => {
@@ -184,8 +179,8 @@ describe("mem0ConfigSchema.parse() — needsSetup", () => {
expect(cfg.needsSetup).toBe(false);
});
it("is false when mode is open-source (no apiKey needed)", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss" });
it("is false when mode is 'open-source' via explicit string (no apiKey needed)", () => {
const cfg = mem0ConfigSchema.parse({ mode: "open-source" });
expect(cfg.needsSetup).toBe(false);
});
@@ -274,14 +269,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.autoRecall).toBe(false);
});
it("enableGraph can be set to true", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
enableGraph: true,
});
expect(cfg.enableGraph).toBe(true);
});
it("custom topK is used when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", topK: 20 });
expect(cfg.topK).toBe(20);
@@ -330,18 +317,6 @@ describe("mem0ConfigSchema.parse() — explicit overrides", () => {
expect(cfg.baseUrl).toBe("https://custom.api.com");
});
it("orgId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({ apiKey: "k", orgId: "org-123" });
expect(cfg.orgId).toBe("org-123");
});
it("projectId is passed through when provided", () => {
const cfg = mem0ConfigSchema.parse({
apiKey: "k",
projectId: "proj-456",
});
expect(cfg.projectId).toBe("proj-456");
});
});
// ---------------------------------------------------------------------------
@@ -359,22 +334,23 @@ describe("mem0ConfigSchema.parse() — oss config", () => {
historyDbPath: "/tmp/history.db",
disableHistory: false,
};
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: ossConfig });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: ossConfig });
expect(cfg.mode).toBe("open-source");
expect(cfg.oss).toEqual(ossConfig);
});
it("ignores oss when it is not a plain object", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: "not-an-object" });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: "not-an-object" });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is an array", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: [1, 2, 3] });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: [1, 2, 3] });
expect(cfg.oss).toBeUndefined();
});
it("ignores oss when it is null", () => {
const cfg = mem0ConfigSchema.parse({ mode: "oss", oss: null });
const cfg = mem0ConfigSchema.parse({ mode: "open-source", oss: null });
expect(cfg.oss).toBeUndefined();
});
});
@@ -388,7 +364,6 @@ describe("mem0ConfigSchema.parse() — skills config", () => {
triage: {
enabled: true,
importanceThreshold: 3,
enableGraph: false,
credentialPatterns: ["sk-", "ghp_"],
},
recall: {
+376
View File
@@ -0,0 +1,376 @@
/**
* Tests for dream-gate.ts — activity tracking, gate logic, and lock mechanism
* for automatic memory consolidation.
*
* All filesystem operations are mocked via fs-safe.ts.
* Time-dependent tests use vi.useFakeTimers().
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("../fs-safe.ts", () => ({
readText: vi.fn(),
writeText: vi.fn(),
mkdirp: vi.fn(),
unlink: vi.fn(),
}));
import { readText, writeText, mkdirp, unlink } from "../fs-safe.ts";
import {
incrementSessionCount,
checkCheapGates,
checkMemoryGate,
acquireDreamLock,
releaseDreamLock,
recordDreamCompletion,
getDreamState,
} from "../dream-gate.ts";
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const mockReadText = readText as ReturnType<typeof vi.fn>;
const mockWriteText = writeText as ReturnType<typeof vi.fn>;
const mockMkdirp = mkdirp as ReturnType<typeof vi.fn>;
const mockUnlink = unlink as ReturnType<typeof vi.fn>;
const STATE_DIR = "/tmp/test-state";
interface DreamState {
lastConsolidatedAt: number;
sessionsSince: number;
lastSessionId: string | null;
}
function setDreamState(state: DreamState): void {
mockReadText.mockImplementation((filePath: string) => {
if (filePath.endsWith("dream-state.json")) {
return JSON.stringify(state);
}
throw new Error("ENOENT");
});
}
function setNoState(): void {
mockReadText.mockImplementation(() => {
throw new Error("ENOENT");
});
}
function getWrittenState(): DreamState {
const call = mockWriteText.mock.calls.find((c: unknown[]) =>
(c[0] as string).endsWith("dream-state.json"),
);
if (!call) throw new Error("No state file written");
return JSON.parse(call[1] as string);
}
beforeEach(() => {
vi.resetAllMocks();
mockMkdirp.mockReturnValue(undefined);
mockUnlink.mockReturnValue(undefined);
});
// ---------------------------------------------------------------------------
// incrementSessionCount
// ---------------------------------------------------------------------------
describe("incrementSessionCount", () => {
it("increments counter for a new session", () => {
setDreamState({
lastConsolidatedAt: 0,
sessionsSince: 3,
lastSessionId: "session-old",
});
incrementSessionCount(STATE_DIR, "session-new");
const written = getWrittenState();
expect(written.sessionsSince).toBe(4);
expect(written.lastSessionId).toBe("session-new");
});
it("deduplicates same session (no increment)", () => {
setDreamState({
lastConsolidatedAt: 0,
sessionsSince: 3,
lastSessionId: "session-same",
});
incrementSessionCount(STATE_DIR, "session-same");
// writeText should NOT have been called for the state file
const stateWrites = mockWriteText.mock.calls.filter((c: unknown[]) =>
(c[0] as string).endsWith("dream-state.json"),
);
expect(stateWrites).toHaveLength(0);
});
});
// ---------------------------------------------------------------------------
// checkCheapGates
// ---------------------------------------------------------------------------
describe("checkCheapGates", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("fails time gate when consolidation was too recent", () => {
const now = Date.now();
vi.setSystemTime(now);
// Last consolidated 1 hour ago, but minHours is 24
setDreamState({
lastConsolidatedAt: now - 1 * 3_600_000,
sessionsSince: 100,
lastSessionId: null,
});
const result = checkCheapGates(STATE_DIR, { minHours: 24, minSessions: 5 });
expect(result.proceed).toBe(false);
expect(result.reason).toContain("time");
});
it("fails session gate when too few sessions", () => {
const now = Date.now();
vi.setSystemTime(now);
// Last consolidated 48 hours ago (passes time gate), but only 2 sessions
setDreamState({
lastConsolidatedAt: now - 48 * 3_600_000,
sessionsSince: 2,
lastSessionId: null,
});
const result = checkCheapGates(STATE_DIR, {
minHours: 24,
minSessions: 5,
});
expect(result.proceed).toBe(false);
expect(result.reason).toContain("sessions");
});
it("passes both gates when conditions are met", () => {
const now = Date.now();
vi.setSystemTime(now);
// 48 hours ago, 10 sessions — both gates pass
setDreamState({
lastConsolidatedAt: now - 48 * 3_600_000,
sessionsSince: 10,
lastSessionId: null,
});
const result = checkCheapGates(STATE_DIR, {
minHours: 24,
minSessions: 5,
});
expect(result.proceed).toBe(true);
expect(result.reason).toBeUndefined();
});
it("uses defaults when config is empty", () => {
const now = Date.now();
vi.setSystemTime(now);
// Never consolidated (0), 100 sessions — should pass with defaults (24h, 5 sessions)
setDreamState({
lastConsolidatedAt: 0,
sessionsSince: 100,
lastSessionId: null,
});
const result = checkCheapGates(STATE_DIR, {});
expect(result.proceed).toBe(true);
});
});
// ---------------------------------------------------------------------------
// checkMemoryGate
// ---------------------------------------------------------------------------
describe("checkMemoryGate", () => {
it("fails when too few memories", () => {
const result = checkMemoryGate(5, { minMemories: 20 });
expect(result.pass).toBe(false);
expect(result.reason).toContain("memories");
expect(result.reason).toContain("5");
});
it("passes when enough memories", () => {
const result = checkMemoryGate(25, { minMemories: 20 });
expect(result.pass).toBe(true);
expect(result.reason).toBeUndefined();
});
});
// ---------------------------------------------------------------------------
// acquireDreamLock
// ---------------------------------------------------------------------------
describe("acquireDreamLock", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("succeeds when no lock exists", () => {
const now = Date.now();
vi.setSystemTime(now);
// readText throws for lock file (not found), writeText succeeds for wx create
mockReadText.mockImplementation(() => {
throw new Error("ENOENT");
});
mockWriteText.mockReturnValue(undefined);
const result = acquireDreamLock(STATE_DIR);
expect(result).toBe(true);
// Verify it wrote a lock file with wx flag
const lockWrite = mockWriteText.mock.calls.find((c: unknown[]) =>
(c[0] as string).endsWith("dream.lock"),
);
expect(lockWrite).toBeDefined();
const lockData = JSON.parse(lockWrite![1] as string);
expect(lockData.pid).toBe(process.pid);
expect(lockData.startedAt).toBe(now);
expect(lockWrite![2]).toEqual({ flag: "wx" });
});
it("fails when lock exists and is fresh", () => {
const now = Date.now();
vi.setSystemTime(now);
// Lock was created 10 minutes ago — still fresh (< 1 hour)
mockReadText.mockImplementation((filePath: string) => {
if (filePath.endsWith("dream.lock")) {
return JSON.stringify({
pid: 12345,
startedAt: now - 10 * 60 * 1000,
});
}
throw new Error("ENOENT");
});
const result = acquireDreamLock(STATE_DIR);
expect(result).toBe(false);
// Should NOT have written a new lock
const lockWrites = mockWriteText.mock.calls.filter((c: unknown[]) =>
(c[0] as string).endsWith("dream.lock"),
);
expect(lockWrites).toHaveLength(0);
});
it("succeeds when lock is stale (>1hr old)", () => {
const now = Date.now();
vi.setSystemTime(now);
// Lock was created 2 hours ago — stale
mockReadText.mockImplementation((filePath: string) => {
if (filePath.endsWith("dream.lock")) {
return JSON.stringify({
pid: 99999,
startedAt: now - 2 * 60 * 60 * 1000,
});
}
throw new Error("ENOENT");
});
mockWriteText.mockReturnValue(undefined);
const result = acquireDreamLock(STATE_DIR);
expect(result).toBe(true);
// Should have unlinked the stale lock
expect(mockUnlink).toHaveBeenCalled();
// Should have written a new lock
const lockWrite = mockWriteText.mock.calls.find((c: unknown[]) =>
(c[0] as string).endsWith("dream.lock"),
);
expect(lockWrite).toBeDefined();
});
});
// ---------------------------------------------------------------------------
// releaseDreamLock
// ---------------------------------------------------------------------------
describe("releaseDreamLock", () => {
it("removes lock file", () => {
releaseDreamLock(STATE_DIR);
expect(mockUnlink).toHaveBeenCalledWith(
expect.stringContaining("dream.lock"),
);
});
});
// ---------------------------------------------------------------------------
// recordDreamCompletion
// ---------------------------------------------------------------------------
describe("recordDreamCompletion", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("resets session counter and records timestamp", () => {
const now = 1700000000000;
vi.setSystemTime(now);
setDreamState({
lastConsolidatedAt: 0,
sessionsSince: 15,
lastSessionId: "session-xyz",
});
recordDreamCompletion(STATE_DIR);
const written = getWrittenState();
expect(written.lastConsolidatedAt).toBe(now);
expect(written.sessionsSince).toBe(0);
expect(written.lastSessionId).toBeNull();
});
});
// ---------------------------------------------------------------------------
// getDreamState
// ---------------------------------------------------------------------------
describe("getDreamState", () => {
it("returns default state when no file exists", () => {
setNoState();
const state = getDreamState(STATE_DIR);
expect(state).toEqual({
lastConsolidatedAt: 0,
sessionsSince: 0,
lastSessionId: null,
});
});
it("returns persisted state when file exists", () => {
const persisted = {
lastConsolidatedAt: 1700000000000,
sessionsSince: 7,
lastSessionId: "session-abc",
};
setDreamState(persisted);
const state = getDreamState(STATE_DIR);
expect(state).toEqual(persisted);
});
});
+37
View File
@@ -0,0 +1,37 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { bootstrapTelemetryFlag } from "../fs-safe.ts";
describe("bootstrapTelemetryFlag", () => {
const originalEnv = process.env.MEM0_TELEMETRY;
beforeEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
delete process.env.MEM0_TELEMETRY;
});
afterEach(() => {
delete (globalThis as any).__mem0_telemetry_override;
if (originalEnv !== undefined) {
process.env.MEM0_TELEMETRY = originalEnv;
} else {
delete process.env.MEM0_TELEMETRY;
}
});
it("sets globalThis override when MEM0_TELEMETRY is set", () => {
process.env.MEM0_TELEMETRY = "false";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("false");
});
it("does not set globalThis override when MEM0_TELEMETRY is unset", () => {
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBeUndefined();
});
it("passes through truthy values", () => {
process.env.MEM0_TELEMETRY = "true";
bootstrapTelemetryFlag();
expect((globalThis as any).__mem0_telemetry_override).toBe("true");
});
});
+285
View File
@@ -0,0 +1,285 @@
/**
* Tests for providers.ts — providerToBackend adapter layer.
*
* Verifies that the Backend wrapper correctly delegates to the
* underlying Mem0Provider methods with proper argument mapping.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { providerToBackend } from "../providers.ts";
// ---------------------------------------------------------------------------
// Mock provider factory
// ---------------------------------------------------------------------------
function createMockProvider() {
return {
search: vi
.fn()
.mockResolvedValue([{ id: "m1", memory: "found", score: 0.9 }]),
add: vi.fn().mockResolvedValue({
results: [{ id: "m1", event: "ADD", memory: "stored" }],
}),
get: vi
.fn()
.mockResolvedValue({ id: "m1", memory: "test", created_at: "2026-01-01" }),
getAll: vi.fn().mockResolvedValue([{ id: "m1", memory: "listed" }]),
update: vi.fn().mockResolvedValue(undefined),
delete: vi.fn().mockResolvedValue(undefined),
deleteAll: vi.fn().mockResolvedValue(undefined),
history: vi.fn().mockResolvedValue([]),
};
}
const DEFAULT_USER = "test-user";
beforeEach(() => {
vi.resetAllMocks();
});
// ---------------------------------------------------------------------------
// search
// ---------------------------------------------------------------------------
describe("providerToBackend — search", () => {
it("delegates to provider.search with correct options", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const results = await backend.search("hello world", {
topK: 10,
threshold: 0.5,
keyword: true,
rerank: true,
filters: { category: "preference" },
});
expect(provider.search).toHaveBeenCalledWith("hello world", {
user_id: DEFAULT_USER,
top_k: 10,
threshold: 0.5,
keyword_search: true,
reranking: true,
filters: { category: "preference" },
});
expect(results).toHaveLength(1);
expect((results[0] as any).id).toBe("m1");
});
it("uses default userId when opts.userId is not provided", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
await backend.search("query");
expect(provider.search).toHaveBeenCalledWith("query", {
user_id: DEFAULT_USER,
top_k: undefined,
threshold: undefined,
keyword_search: undefined,
reranking: undefined,
filters: undefined,
});
});
});
// ---------------------------------------------------------------------------
// add
// ---------------------------------------------------------------------------
describe("providerToBackend — add", () => {
it("delegates to provider.add with content as message", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.add("Remember this fact");
expect(provider.add).toHaveBeenCalledWith(
[{ role: "user", content: "Remember this fact" }],
expect.objectContaining({ user_id: DEFAULT_USER }),
);
expect(result).toBeDefined();
});
it("passes messages array when provided", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const messages = [
{ role: "user", content: "Hi" },
{ role: "assistant", content: "Hello" },
];
await backend.add(undefined, messages);
expect(provider.add).toHaveBeenCalledWith(
messages,
expect.objectContaining({ user_id: DEFAULT_USER }),
);
});
it("forwards optional add options", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
await backend.add("fact", undefined, {
runId: "run-1",
metadata: { source: "test" },
immutable: true,
infer: false,
expires: "2027-01-01",
});
expect(provider.add).toHaveBeenCalledWith(
[{ role: "user", content: "fact" }],
expect.objectContaining({
user_id: DEFAULT_USER,
run_id: "run-1",
metadata: { source: "test" },
immutable: true,
infer: false,
expiration_date: "2027-01-01",
}),
);
});
});
// ---------------------------------------------------------------------------
// get
// ---------------------------------------------------------------------------
describe("providerToBackend — get", () => {
it("delegates to provider.get", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.get("mem-123");
expect(provider.get).toHaveBeenCalledWith("mem-123");
expect((result as any).id).toBe("m1");
});
});
// ---------------------------------------------------------------------------
// listMemories
// ---------------------------------------------------------------------------
describe("providerToBackend — listMemories", () => {
it("delegates to provider.getAll", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const results = await backend.listMemories({ pageSize: 50 });
expect(provider.getAll).toHaveBeenCalledWith({
user_id: DEFAULT_USER,
page_size: 50,
});
expect(results).toHaveLength(1);
});
it("uses default userId when opts.userId is not provided", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
await backend.listMemories();
expect(provider.getAll).toHaveBeenCalledWith({
user_id: DEFAULT_USER,
page_size: undefined,
});
});
});
// ---------------------------------------------------------------------------
// update
// ---------------------------------------------------------------------------
describe("providerToBackend — update", () => {
it("calls provider.update with content", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.update("mem-123", "updated text");
expect(provider.update).toHaveBeenCalledWith("mem-123", "updated text");
expect((result as any).id).toBe("mem-123");
expect((result as any).updated).toBe(true);
});
it("warns and skips when only metadata is provided (no content)", async () => {
const provider = createMockProvider();
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.update("mem-123", undefined, {
tag: "important",
});
expect(provider.update).not.toHaveBeenCalled();
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("metadata updates are not supported"),
);
expect((result as any).id).toBe("mem-123");
warnSpy.mockRestore();
});
});
// ---------------------------------------------------------------------------
// delete
// ---------------------------------------------------------------------------
describe("providerToBackend — delete", () => {
it("calls provider.delete with memoryId", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.delete("mem-456");
expect(provider.delete).toHaveBeenCalledWith("mem-456");
expect((result as any).deleted).toBe("mem-456");
});
it("calls provider.deleteAll when opts.all is true", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.delete(undefined, {
all: true,
userId: "custom-user",
});
expect(provider.deleteAll).toHaveBeenCalledWith("custom-user");
expect((result as any).deleted).toBe("all");
});
});
// ---------------------------------------------------------------------------
// deleteEntities (platform-only)
// ---------------------------------------------------------------------------
describe("providerToBackend — deleteEntities", () => {
it("throws platform-only error", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
await expect(
backend.deleteEntities({ userId: DEFAULT_USER }),
).rejects.toThrow("platform mode");
});
});
// ---------------------------------------------------------------------------
// status
// ---------------------------------------------------------------------------
describe("providerToBackend — status", () => {
it("returns connected: true", async () => {
const provider = createMockProvider();
const backend = providerToBackend(provider as any, DEFAULT_USER);
const result = await backend.status();
expect((result as any).connected).toBe(true);
expect((result as any).backend).toBe("oss");
});
});
+65
View File
@@ -0,0 +1,65 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock config-file before importing telemetry
vi.mock("../cli/config-file.ts", () => ({
readPluginAuth: vi.fn().mockReturnValue({}),
}));
import { captureEvent, PLUGIN_VERSION } from "../telemetry.ts";
import { readPluginAuth } from "../cli/config-file.ts";
describe("telemetry", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
beforeEach(() => {
// Reset telemetry enabled state
(globalThis as any).__mem0_telemetry_override = undefined;
fetchSpy = vi.fn().mockResolvedValue({ ok: true });
vi.stubGlobal("fetch", fetchSpy);
});
afterEach(() => {
vi.restoreAllMocks();
delete (globalThis as any).__mem0_telemetry_override;
});
it("exports PLUGIN_VERSION", () => {
expect(PLUGIN_VERSION).toBe("1.0.4");
});
it("captureEvent does not throw", () => {
expect(() => captureEvent("test_event")).not.toThrow();
});
it("captureEvent accepts properties and context", () => {
expect(() =>
captureEvent("test_event", { key: "val" }, { apiKey: "m0-key", mode: "platform" }),
).not.toThrow();
});
it("captureEvent is silent when telemetry disabled", () => {
(globalThis as any).__mem0_telemetry_override = "false";
// Force re-evaluation by resetting cached value
// Since _telemetryEnabled is module-level, we test indirectly
expect(() => captureEvent("test_event")).not.toThrow();
});
it("uses userEmail as distinct ID when available", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({
userEmail: "test@example.com",
});
expect(() => captureEvent("test_event")).not.toThrow();
});
it("falls back to anonymous-openclaw when no apiKey", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockReturnValueOnce({});
expect(() => captureEvent("test_event", {}, {})).not.toThrow();
});
it("handles readPluginAuth errors gracefully", () => {
(readPluginAuth as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
throw new Error("config read failed");
});
expect(() => captureEvent("test_event")).not.toThrow();
});
});
+164 -91
View File
@@ -7,35 +7,31 @@
*/
import { describe, it, expect, vi } from "vitest";
import type { ToolContext } from "../tools/index.ts";
import type { ToolDeps } from "../tools/index.ts";
import { registerAllTools } from "../tools/index.ts";
import { createMemorySearchTool } from "../tools/memory-search.ts";
import { createMemoryStoreTool } from "../tools/memory-store.ts";
import { createMemoryAddTool } from "../tools/memory-add.ts";
import { createMemoryGetTool } from "../tools/memory-get.ts";
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
import { createMemoryListTool } from "../tools/memory-list.ts";
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
import { createMemoryEventListTool } from "../tools/memory-event-list.ts";
import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts";
// ---------------------------------------------------------------------------
// Mock helper
// ---------------------------------------------------------------------------
function createMockToolContext(overrides = {}): ToolContext {
function createMockToolDeps(overrides = {}): ToolDeps {
return {
api: {
registerTool: vi.fn(),
logger: { info: vi.fn(), warn: vi.fn() },
} as any,
backend: {
get: vi.fn().mockResolvedValue({ id: "test-id", memory: "test memory" }),
delete: vi.fn().mockResolvedValue(undefined),
status: vi.fn().mockResolvedValue({ connected: true }),
deleteEntities: vi.fn().mockResolvedValue(undefined),
} as any,
cfg: {
mode: "platform",
userId: "testuser",
topK: 5,
enableGraph: false,
autoCapture: true,
autoRecall: true,
searchThreshold: 0.5,
@@ -68,6 +64,7 @@ function createMockToolContext(overrides = {}): ToolContext {
agentUserId: vi.fn().mockReturnValue("testuser:agent:test"),
getCurrentSessionId: vi.fn().mockReturnValue(undefined),
skillsActive: false,
captureToolEvent: vi.fn(),
buildAddOptions: vi
.fn()
.mockReturnValue({ user_id: "testuser", source: "OPENCLAW" }),
@@ -83,30 +80,43 @@ function createMockToolContext(overrides = {}): ToolContext {
// ---------------------------------------------------------------------------
describe("registerAllTools", () => {
it("calls api.registerTool exactly 7 times", () => {
const ctx = createMockToolContext();
it("calls api.registerTool exactly 8 times", () => {
const ctx = createMockToolDeps();
registerAllTools(ctx);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(7);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(8);
});
it("registers tools with the correct names", () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
registerAllTools(ctx);
// Tools are registered as required (single argument — no metadata object).
// The name comes from the tool definition itself (call[0]).
const names = (
ctx.api.registerTool as ReturnType<typeof vi.fn>
).mock.calls.map((call: unknown[]) => (call[1] as { name: string }).name);
).mock.calls.map((call: unknown[]) => (call[0] as { name: string }).name);
expect(names).toEqual([
"memory_search",
"memory_store",
"memory_add",
"memory_get",
"memory_list",
"memory_update",
"memory_delete",
"memory_history",
"memory_event_list",
"memory_event_status",
]);
});
it("registers tools without a second argument (required, not optional)", () => {
const ctx = createMockToolDeps();
registerAllTools(ctx);
const calls = (ctx.api.registerTool as ReturnType<typeof vi.fn>).mock.calls;
for (const call of calls) {
expect(call).toHaveLength(1);
}
});
});
// ---------------------------------------------------------------------------
@@ -116,16 +126,18 @@ describe("registerAllTools", () => {
describe("tool factory shape", () => {
const factories = [
{ fn: createMemorySearchTool, expectedName: "memory_search" },
{ fn: createMemoryStoreTool, expectedName: "memory_store" },
{ fn: createMemoryAddTool, expectedName: "memory_add" },
{ fn: createMemoryGetTool, expectedName: "memory_get" },
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
{ fn: createMemoryListTool, expectedName: "memory_list" },
{ fn: createMemoryEventListTool, expectedName: "memory_event_list" },
{ fn: createMemoryEventStatusTool, expectedName: "memory_event_status" },
];
for (const { fn, expectedName } of factories) {
describe(expectedName, () => {
it("returns an object with name, label, description, parameters, and execute", () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = fn(ctx);
expect(tool.name).toBe(expectedName);
@@ -146,7 +158,7 @@ describe("tool factory shape", () => {
describe("memory_search execute", () => {
it("returns formatted results when provider returns matches", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemorySearchTool(ctx);
const result = await tool.execute("call-1", {
@@ -163,7 +175,7 @@ describe("memory_search execute", () => {
});
it("returns 'no relevant memories' when provider returns empty", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn().mockResolvedValue([]),
add: vi.fn(),
@@ -183,7 +195,7 @@ describe("memory_search execute", () => {
});
it("handles errors gracefully", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn().mockRejectedValue(new Error("network failure")),
add: vi.fn(),
@@ -204,7 +216,7 @@ describe("memory_search execute", () => {
});
it("calls resolveUserId with provided agentId and userId", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemorySearchTool(ctx);
await tool.execute("call-4", {
@@ -220,7 +232,7 @@ describe("memory_search execute", () => {
});
it("passes limit to buildSearchOptions", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemorySearchTool(ctx);
await tool.execute("call-5", { query: "test", limit: 10 });
@@ -232,7 +244,7 @@ describe("memory_search execute", () => {
const searchMock = vi
.fn()
.mockResolvedValue([{ id: "s1", memory: "session mem", score: 0.8 }]);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi.fn().mockReturnValue("session-abc"),
provider: {
search: searchMock,
@@ -273,7 +285,7 @@ describe("memory_search execute", () => {
{ id: "m2", memory: "session only", score: 0.7 },
]);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi.fn().mockReturnValue("session-xyz"),
provider: {
search: searchMock,
@@ -300,13 +312,13 @@ describe("memory_search execute", () => {
});
// ---------------------------------------------------------------------------
// memory_store execute
// memory_add execute
// ---------------------------------------------------------------------------
describe("memory_store execute", () => {
describe("memory_add execute", () => {
it("calls provider.add with the text and returns stored result", async () => {
const ctx = createMockToolContext();
const tool = createMemoryStoreTool(ctx);
const ctx = createMockToolDeps();
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-1", {
text: "User prefers dark mode",
@@ -323,8 +335,8 @@ describe("memory_store execute", () => {
});
it("returns error when no text or facts are provided", async () => {
const ctx = createMockToolContext();
const tool = createMemoryStoreTool(ctx);
const ctx = createMockToolDeps();
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-2", {});
@@ -333,8 +345,8 @@ describe("memory_store execute", () => {
});
it("supports facts array", async () => {
const ctx = createMockToolContext();
const tool = createMemoryStoreTool(ctx);
const ctx = createMockToolDeps();
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-3", {
facts: ["fact one", "fact two"],
@@ -350,7 +362,7 @@ describe("memory_store execute", () => {
});
it("handles errors gracefully", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn().mockResolvedValue([]),
add: vi.fn().mockRejectedValue(new Error("API error")),
@@ -361,11 +373,11 @@ describe("memory_store execute", () => {
history: vi.fn(),
},
});
const tool = createMemoryStoreTool(ctx);
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-4", { text: "test" });
expect(result.content[0].text).toContain("Memory store failed");
expect(result.content[0].text).toContain("Memory add failed");
expect(result.details.error).toContain("API error");
});
@@ -373,7 +385,7 @@ describe("memory_store execute", () => {
const addMock = vi.fn().mockResolvedValue({
results: [{ event: "ADD", memory: "stored in skills mode" }],
});
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
skillsActive: true,
provider: {
search: vi.fn().mockResolvedValue([]),
@@ -385,7 +397,7 @@ describe("memory_store execute", () => {
history: vi.fn(),
},
});
const tool = createMemoryStoreTool(ctx);
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-5", {
text: "skills fact",
@@ -395,18 +407,17 @@ describe("memory_store execute", () => {
expect(addMock).toHaveBeenCalledOnce();
const addOpts = addMock.mock.calls[0][1];
expect(addOpts.infer).toBe(false);
expect(addOpts.source).toBe("OPENCLAW");
expect(result.details.mode).toBe("skills");
expect(result.details.category).toBe("preference");
});
it("blocks subagent sessions from storing", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi
.fn()
.mockReturnValue("agent:main:subagent:uuid-123"),
});
const tool = createMemoryStoreTool(ctx);
const tool = createMemoryAddTool(ctx);
const result = await tool.execute("call-6", { text: "subagent fact" });
@@ -419,7 +430,7 @@ describe("memory_store execute", () => {
const addMock = vi.fn().mockResolvedValue({
results: [{ event: "ADD", memory: "stored" }],
});
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
skillsActive: false,
provider: {
search: searchMock,
@@ -431,7 +442,7 @@ describe("memory_store execute", () => {
history: vi.fn(),
},
});
const tool = createMemoryStoreTool(ctx);
const tool = createMemoryAddTool(ctx);
await tool.execute("call-7", { text: "new fact" });
@@ -447,7 +458,7 @@ describe("memory_store execute", () => {
describe("memory_get execute", () => {
it("calls provider.get with the memoryId and returns formatted result", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemoryGetTool(ctx);
const result = await tool.execute("call-1", { memoryId: "test-id" });
@@ -461,7 +472,7 @@ describe("memory_get execute", () => {
});
it("handles errors gracefully", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
@@ -486,11 +497,11 @@ describe("memory_get execute", () => {
// ---------------------------------------------------------------------------
describe("memory_delete execute", () => {
it("deletes by memory_id via provider.delete", async () => {
const ctx = createMockToolContext();
it("deletes by memoryId via provider.delete", async () => {
const ctx = createMockToolDeps();
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-1", { memory_id: "mem-abc" });
const result = await tool.execute("call-1", { memoryId: "mem-abc" });
expect(ctx.provider!.delete).toHaveBeenCalledWith("mem-abc");
expect(result.content[0].text).toBe("Memory mem-abc deleted.");
@@ -503,7 +514,7 @@ describe("memory_delete execute", () => {
.fn()
.mockResolvedValue([{ id: "m1", memory: "match", score: 0.95 }]);
const deleteMock = vi.fn().mockResolvedValue(undefined);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: searchMock,
add: vi.fn(),
@@ -532,7 +543,7 @@ describe("memory_delete execute", () => {
{ id: "m2", memory: "candidate two", score: 0.6 },
]);
const deleteMock = vi.fn();
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: searchMock,
add: vi.fn(),
@@ -557,7 +568,7 @@ describe("memory_delete execute", () => {
});
it("returns no matching memories when query yields empty results", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn().mockResolvedValue([]),
add: vi.fn(),
@@ -577,7 +588,7 @@ describe("memory_delete execute", () => {
});
it("requires confirm:true for bulk delete (all)", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-5", { all: true });
@@ -588,7 +599,7 @@ describe("memory_delete execute", () => {
it("performs bulk delete when all:true and confirm:true", async () => {
const deleteAllMock = vi.fn().mockResolvedValue(undefined);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
@@ -612,59 +623,34 @@ describe("memory_delete execute", () => {
expect(result.details.action).toBe("deleted_all");
});
it("requires confirm:true for entity cascade delete", async () => {
const ctx = createMockToolContext();
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-7", { entity: true });
expect(result.details.error).toBe("confirmation_required");
});
it("performs entity cascade delete when entity:true and confirm:true", async () => {
const ctx = createMockToolContext();
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-8", {
entity: true,
confirm: true,
user_id: "alice",
});
expect(ctx.backend.deleteEntities).toHaveBeenCalledWith({
userId: "alice",
});
expect(result.details.action).toBe("entity_deleted");
});
it("returns error when no mode param is specified", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-9", {});
expect(result.content[0].text).toContain(
"Provide memory_id, query, all, or entity",
"Provide memoryId, query, or all:true",
);
expect(result.details.error).toBe("missing_param");
});
it("blocks subagent sessions from deleting", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi
.fn()
.mockReturnValue("agent:main:subagent:uuid-456"),
});
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-10", { memory_id: "m1" });
const result = await tool.execute("call-10", { memoryId: "m1" });
expect(ctx.provider!.delete).not.toHaveBeenCalled();
expect(result.details.error).toBe("subagent_blocked");
});
it("handles errors gracefully", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
@@ -677,7 +663,7 @@ describe("memory_delete execute", () => {
});
const tool = createMemoryDeleteTool(ctx);
const result = await tool.execute("call-11", { memory_id: "m1" });
const result = await tool.execute("call-11", { memoryId: "m1" });
expect(result.content[0].text).toContain("Memory delete failed");
expect(result.details.error).toContain("delete failed");
@@ -690,7 +676,7 @@ describe("memory_delete execute", () => {
describe("memory_list execute", () => {
it("calls provider.getAll and returns formatted list", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemoryListTool(ctx);
const result = await tool.execute("call-1", {});
@@ -703,7 +689,7 @@ describe("memory_list execute", () => {
});
it("returns 'no memories stored' when provider returns empty", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
@@ -723,7 +709,7 @@ describe("memory_list execute", () => {
});
it("handles errors gracefully", async () => {
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
@@ -743,7 +729,7 @@ describe("memory_list execute", () => {
});
it("resolves userId from agentId", async () => {
const ctx = createMockToolContext();
const ctx = createMockToolDeps();
const tool = createMemoryListTool(ctx);
await tool.execute("call-4", { agentId: "researcher" });
@@ -765,7 +751,7 @@ describe("memory_list execute", () => {
{ id: "m2", memory: "session only" },
]);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi.fn().mockReturnValue("session-123"),
provider: {
search: vi.fn(),
@@ -790,7 +776,7 @@ describe("memory_list execute", () => {
const getAllMock = vi
.fn()
.mockResolvedValue([{ id: "s1", memory: "session mem" }]);
const ctx = createMockToolContext({
const ctx = createMockToolDeps({
getCurrentSessionId: vi.fn().mockReturnValue("sess-abc"),
provider: {
search: vi.fn(),
@@ -813,3 +799,90 @@ describe("memory_list execute", () => {
expect(result.details.count).toBe(1);
});
});
// ---------------------------------------------------------------------------
// memory_update execute
// ---------------------------------------------------------------------------
describe("memory_update execute", () => {
it("calls provider.update and returns success", async () => {
const ctx = createMockToolDeps();
const tool = createMemoryUpdateTool(ctx);
const result = await tool.execute("call-1", {
memoryId: "mem-123",
text: "Updated preference",
});
expect(ctx.provider!.update).toHaveBeenCalledWith(
"mem-123",
"Updated preference",
);
expect(result.content[0].text).toContain("Updated memory mem-123");
expect(result.content[0].text).toContain("Updated preference");
expect(result.details.action).toBe("updated");
expect(result.details.id).toBe("mem-123");
});
it("truncates long text in response", async () => {
const ctx = createMockToolDeps();
const tool = createMemoryUpdateTool(ctx);
const longText = "A".repeat(120);
const result = await tool.execute("call-2", {
memoryId: "mem-456",
text: longText,
});
expect(ctx.provider!.update).toHaveBeenCalledWith("mem-456", longText);
// The response text should contain the first 80 chars followed by "..."
expect(result.content[0].text).toContain("A".repeat(80) + "...");
expect(result.content[0].text).not.toContain("A".repeat(81));
expect(result.details.action).toBe("updated");
});
it("blocks subagent sessions", async () => {
const ctx = createMockToolDeps({
getCurrentSessionId: vi
.fn()
.mockReturnValue("agent:main:subagent:uuid-789"),
});
const tool = createMemoryUpdateTool(ctx);
const result = await tool.execute("call-3", {
memoryId: "mem-123",
text: "should not update",
});
expect(ctx.provider!.update).not.toHaveBeenCalled();
expect(result.content[0].text).toContain(
"not available in subagent sessions",
);
expect(result.details.error).toBe("subagent_blocked");
});
it("handles errors gracefully", async () => {
const ctx = createMockToolDeps({
provider: {
search: vi.fn(),
add: vi.fn(),
getAll: vi.fn(),
update: vi.fn().mockRejectedValue(new Error("update conflict")),
delete: vi.fn(),
get: vi.fn(),
history: vi.fn(),
},
});
const tool = createMemoryUpdateTool(ctx);
const result = await tool.execute("call-4", {
memoryId: "mem-123",
text: "new text",
});
expect(result.content[0].text).toContain("Memory update failed");
expect(result.content[0].text).toContain("update conflict");
expect(result.details.error).toContain("update conflict");
});
});
+28 -64
View File
@@ -1,76 +1,40 @@
/**
* Tool aggregator — single entry-point for registering all Mem0 tools.
*
* Re-exports the canonical `ToolContext` interface and provides
* `registerAllTools(ctx)` which wires up every tool in one call.
*/
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts";
import type { Backend } from "../backend/base.ts";
import type {
Mem0Config,
Mem0Provider,
AddOptions,
SearchOptions,
} from "../types.ts";
// ---------------------------------------------------------------------------
// Canonical ToolContext definition
// ---------------------------------------------------------------------------
export interface ToolContext {
api: OpenClawPluginApi;
backend: Backend;
cfg: Mem0Config;
provider?: Mem0Provider;
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
effectiveUserId: (sessionKey?: string) => string;
agentUserId: (id: string) => string;
getCurrentSessionId: () => string | undefined;
skillsActive: boolean;
buildAddOptions: (
userIdOverride?: string,
runId?: string,
sessionKey?: string,
) => AddOptions;
buildSearchOptions: (
userIdOverride?: string,
limit?: number,
runId?: string,
sessionKey?: string,
) => SearchOptions;
}
// ---------------------------------------------------------------------------
// Tool factory imports
// ---------------------------------------------------------------------------
import { createMemorySearchTool } from "./memory-search.ts";
import { createMemoryStoreTool } from "./memory-store.ts";
import { createMemoryAddTool } from "./memory-add.ts";
import { createMemoryGetTool } from "./memory-get.ts";
import { createMemoryListTool } from "./memory-list.ts";
import { createMemoryUpdateTool } from "./memory-update.ts";
import { createMemoryDeleteTool } from "./memory-delete.ts";
import { createMemoryHistoryTool } from "./memory-history.ts";
import { createMemoryEventListTool } from "./memory-event-list.ts";
import { createMemoryEventStatusTool } from "./memory-event-status.ts";
// ---------------------------------------------------------------------------
// Aggregator
// ---------------------------------------------------------------------------
export interface ToolDeps {
api: OpenClawPluginApi;
provider: Mem0Provider;
cfg: Mem0Config;
backend?: Backend;
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
effectiveUserId: (sessionKey?: string) => string;
agentUserId: (id: string) => string;
buildAddOptions: (userIdOverride?: string, runId?: string, sessionKey?: string) => AddOptions;
buildSearchOptions: (userIdOverride?: string, limit?: number, runId?: string, sessionKey?: string) => SearchOptions;
getCurrentSessionId: () => string | undefined;
skillsActive: boolean;
captureToolEvent: (toolName: string, properties: Record<string, unknown>) => void;
}
/**
* Registers all Mem0 tools with the plugin API.
*
* 7 core tools for memory management:
* search, store, get, list, update, delete, history
*/
export function registerAllTools(ctx: ToolContext): void {
const { api } = ctx;
export function registerAllTools(deps: ToolDeps): void {
const { api } = deps;
api.registerTool(createMemorySearchTool(ctx), { name: "memory_search" });
api.registerTool(createMemoryStoreTool(ctx), { name: "memory_store" });
api.registerTool(createMemoryGetTool(ctx), { name: "memory_get" });
api.registerTool(createMemoryListTool(ctx), { name: "memory_list" });
api.registerTool(createMemoryUpdateTool(ctx), { name: "memory_update" });
api.registerTool(createMemoryDeleteTool(ctx), { name: "memory_delete" });
api.registerTool(createMemoryHistoryTool(ctx), { name: "memory_history" });
api.registerTool(createMemorySearchTool(deps));
api.registerTool(createMemoryAddTool(deps));
api.registerTool(createMemoryGetTool(deps));
api.registerTool(createMemoryListTool(deps));
api.registerTool(createMemoryUpdateTool(deps));
api.registerTool(createMemoryDeleteTool(deps));
api.registerTool(createMemoryEventListTool(deps));
api.registerTool(createMemoryEventStatusTool(deps));
}
+107
View File
@@ -0,0 +1,107 @@
import { Type } from "@sinclair/typebox";
import type { AddOptions } from "../types.ts";
import { isSubagentSession } from "../isolation.ts";
import { resolveCategories, ttlToExpirationDate } from "../skill-loader.ts";
import type { ToolDeps } from "./index.ts";
export function createMemoryAddTool(deps: ToolDeps) {
const { api, cfg, provider, resolveUserId, getCurrentSessionId, buildAddOptions, buildSearchOptions, skillsActive } = deps;
return {
name: "memory_add",
label: "Memory Add",
description: "Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
parameters: Type.Object({
text: Type.Optional(Type.String({ description: "Single fact to remember" })),
facts: Type.Optional(Type.Array(Type.String(), { description: "Array of facts to store. ALL must share the same category." })),
category: Type.Optional(Type.String({ description: 'Category: "identity", "preference", "decision", "rule", "project", "configuration", "technical", "relationship"' })),
importance: Type.Optional(Type.Number({ description: "Importance (0.0-1.0), omit for category default" })),
userId: Type.Optional(Type.String({ description: "User ID to scope this memory" })),
agentId: Type.Optional(Type.String({ description: "Agent ID namespace" })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Additional metadata" })),
longTerm: Type.Optional(Type.Boolean({ description: "Long-term (default: true). Set false for session-scoped." })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const p = params as {
text?: string; facts?: string[]; category?: string; importance?: number;
userId?: string; agentId?: string; metadata?: Record<string, unknown>; longTerm?: boolean;
};
const allFacts: string[] = p.facts?.length ? p.facts : (p.text ? [p.text] : []);
if (allFacts.length === 0) {
return { content: [{ type: "text", text: "No facts provided. Pass 'text' or 'facts' array." }], details: { error: "missing_facts" } };
}
const start = Date.now();
try {
const currentSessionId = getCurrentSessionId();
if (isSubagentSession(currentSessionId)) {
return { content: [{ type: "text", text: "Memory storage is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
const uid = resolveUserId({ agentId: p.agentId, userId: p.userId });
const runId = !(p.longTerm ?? true) && currentSessionId ? currentSessionId : undefined;
if (skillsActive) {
const rawMetadata = p.metadata;
const category = p.category ?? rawMetadata?.category as string | undefined;
const importance = p.importance ?? rawMetadata?.importance as number | undefined;
const parsedMetadata: Record<string, unknown> = {
...(rawMetadata ?? {}),
...(category && { category }),
...(importance !== undefined && { importance }),
};
const categories = resolveCategories(cfg.skills);
const catConfig = category ? categories[category] : undefined;
const expirationDate = catConfig ? ttlToExpirationDate(catConfig.ttl) : undefined;
const isImmutable = catConfig?.immutable ?? false;
const addOpts: AddOptions = {
user_id: uid, source: "OPENCLAW", infer: false,
deduced_memories: allFacts, metadata: parsedMetadata ?? {},
...(expirationDate && { expiration_date: expirationDate }),
...(isImmutable && { immutable: true }),
};
if (runId) addOpts.run_id = runId;
if (cfg.mode === "platform") {
addOpts.output_format = "v1.1";
}
const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts);
const count = result.results?.length ?? 0;
api.logger.info(`openclaw-mem0: stored ${count} memor${count === 1 ? "y" : "ies"} (infer=false, category=${category ?? "none"})`);
deps.captureToolEvent("memory_add", { success: true, latency_ms: Date.now() - start, fact_count: allFacts.length, mode: "skills" });
return {
content: [{ type: "text", text: `Stored ${allFacts.length} fact(s) [${category ?? "uncategorized"}]: ${allFacts.map(f => `"${f.slice(0, 60)}${f.length > 60 ? "..." : ""}"`).join(", ")}` }],
details: { action: "stored", mode: "skills", category, factCount: allFacts.length, results: result.results },
};
}
const combinedText = allFacts.join("\n");
const dedupOpts = buildSearchOptions(uid, 3);
dedupOpts.threshold = 0.85;
await provider.search(combinedText.slice(0, 200), dedupOpts);
const result = await provider.add([{ role: "user", content: combinedText }], buildAddOptions(uid, runId, currentSessionId));
const added = result.results?.filter((r) => r.event === "ADD") ?? [];
const updated = result.results?.filter((r) => r.event === "UPDATE") ?? [];
const summary = [];
if (added.length > 0) summary.push(`${added.length} added`);
if (updated.length > 0) summary.push(`${updated.length} updated`);
if (summary.length === 0) summary.push("No new memories extracted");
deps.captureToolEvent("memory_add", { success: true, latency_ms: Date.now() - start, fact_count: allFacts.length });
return {
content: [{ type: "text", text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}` }],
details: { action: "stored", results: result.results },
};
} catch (err) {
deps.captureToolEvent("memory_add", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory add failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
+40 -221
View File
@@ -1,256 +1,75 @@
/**
* memory_delete tool — unified delete tool replacing memory_forget and memory_delete_all.
*
* Supports four modes:
* 1. By memory_id — direct deletion of a specific memory
* 2. By query — search-and-delete (auto-deletes high-confidence match, otherwise lists candidates)
* 3. all:true — bulk-delete all memories for a user (requires confirm:true)
* 4. entity:true — cascade-delete an entity and all its memories (requires confirm:true, platform only)
*/
import { Type } from "@sinclair/typebox";
import { isSubagentSession } from "../isolation.ts";
import type { ToolDeps } from "./index.ts";
import type { ToolContext } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_delete` tool config object suitable for
* `api.registerTool(config, { name })`.
*
* This replaces both `memory_forget` and `memory_delete_all` from the
* original index.ts implementation.
*/
export function createMemoryDeleteTool(ctx: ToolContext) {
const {
api,
provider,
resolveUserId,
getCurrentSessionId,
buildSearchOptions,
backend,
} = ctx;
export function createMemoryDeleteTool(deps: ToolDeps) {
const { api, provider, resolveUserId, getCurrentSessionId, buildSearchOptions } = deps;
return {
name: "memory_delete",
label: "Memory Delete",
description:
"Delete memories from Mem0. Provide a specific memoryId for direct deletion, a query to search and delete, all:true for bulk deletion, or entity:true to cascade-delete an entity. Bulk operations require confirm:true. GDPR-compliant.",
description: "Delete memories. Provide memoryId, query to search-and-delete, or all:true for bulk deletion (requires confirm:true).",
parameters: Type.Object({
memory_id: Type.Optional(
Type.String({ description: "Specific memory ID to delete" }),
),
query: Type.Optional(
Type.String({
description:
"Search query to find memory to delete (searches and deletes best match)",
}),
),
all: Type.Optional(
Type.Boolean({
description:
"Delete ALL memories matching scope filters. Requires confirm: true",
}),
),
entity: Type.Optional(
Type.Boolean({
description:
"Delete entity and all its memories (cascade). Requires confirm: true",
}),
),
confirm: Type.Optional(
Type.Boolean({
description:
"Must be true for bulk operations (all/entity). Safety gate.",
}),
),
user_id: Type.Optional(Type.String({ description: "Scope to user" })),
agent_id: Type.Optional(Type.String({ description: "Scope to agent" })),
memoryId: Type.Optional(Type.String({ description: "Specific memory ID to delete" })),
query: Type.Optional(Type.String({ description: "Search query to find and delete" })),
agentId: Type.Optional(Type.String({ description: "Agent ID to scope deletion" })),
all: Type.Optional(Type.Boolean({ description: "Delete ALL memories. Requires confirm: true." })),
confirm: Type.Optional(Type.Boolean({ description: "Safety gate for bulk operations" })),
userId: Type.Optional(Type.String({ description: "User ID scope" })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memory_id, query, all, entity, confirm, user_id, agent_id } =
params as {
memory_id?: string;
query?: string;
all?: boolean;
entity?: boolean;
confirm?: boolean;
user_id?: string;
agent_id?: string;
};
const { memoryId, query, agentId, all, confirm, userId } = params as {
memoryId?: string; query?: string; agentId?: string;
all?: boolean; confirm?: boolean; userId?: string;
};
const start = Date.now();
try {
// Block subagent deletes at the tool level.
const currentSessionId = getCurrentSessionId();
if (isSubagentSession(currentSessionId)) {
api.logger.warn(
"openclaw-mem0: blocked memory_delete from subagent session",
);
return {
content: [
{
type: "text",
text: "Memory deletion is not available in subagent sessions. The main agent handles memory.",
},
],
details: { error: "subagent_blocked" },
};
if (isSubagentSession(getCurrentSessionId())) {
return { content: [{ type: "text", text: "Memory deletion is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
// Mode 1: Delete by specific memory ID
if (memory_id) {
await provider!.delete(memory_id);
return {
content: [{ type: "text", text: `Memory ${memory_id} deleted.` }],
details: { action: "deleted", id: memory_id },
};
if (memoryId) {
await provider.delete(memoryId);
deps.captureToolEvent("memory_delete", { success: true, latency_ms: Date.now() - start, delete_mode: "single" });
return { content: [{ type: "text", text: `Memory ${memoryId} deleted.` }], details: { action: "deleted", id: memoryId } };
}
// Mode 2: Search-and-delete by query
if (query) {
const uid = resolveUserId({ agentId: agent_id, userId: user_id });
const results = await provider!.search(
query,
buildSearchOptions(uid, 5),
);
const uid = resolveUserId({ agentId, userId });
const results = await provider.search(query, buildSearchOptions(uid, 5));
if (!results || results.length === 0) {
return {
content: [{ type: "text", text: "No matching memories found." }],
details: { found: 0 },
};
return { content: [{ type: "text", text: "No matching memories found." }], details: { found: 0 } };
}
// If single high-confidence match, delete directly
if (results.length === 1 || (results[0].score ?? 0) > 0.9) {
await provider!.delete(results[0].id);
return {
content: [
{
type: "text",
text: `Deleted: "${results[0].memory}"`,
},
],
details: { action: "deleted", id: results[0].id },
};
await provider.delete(results[0].id);
return { content: [{ type: "text", text: `Deleted: "${results[0].memory}"` }], details: { action: "deleted", id: results[0].id } };
}
// Multiple ambiguous results — return candidates for user to pick
const list = results
.map(
(r) =>
`- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (score: ${((r.score ?? 0) * 100).toFixed(0)}%)`,
)
.join("\n");
const candidates = results.map((r) => ({
id: r.id,
memory: r.memory,
score: r.score,
}));
const list = results.map((r) =>
`- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (${((r.score ?? 0) * 100).toFixed(0)}%)`
).join("\n");
return {
content: [
{
type: "text",
text: `Found ${results.length} candidates. Specify memory_id to delete:\n${list}`,
},
],
details: { action: "candidates", candidates },
content: [{ type: "text", text: `Found ${results.length} candidates. Specify memoryId:\n${list}` }],
details: { action: "candidates", candidates: results.map((r) => ({ id: r.id, memory: r.memory, score: r.score })) },
};
}
// Mode 3: Bulk-delete all memories for a user
if (all) {
if (!confirm) {
return {
content: [
{
type: "text",
text: "Bulk deletion requires confirm: true. Ask the user to confirm before proceeding.",
},
],
details: { error: "confirmation_required" },
};
return { content: [{ type: "text", text: "Bulk deletion requires confirm: true." }], details: { error: "confirmation_required" } };
}
const uid = resolveUserId({ agentId: agent_id, userId: user_id });
await provider!.deleteAll(uid);
api.logger.info(
`openclaw-mem0: deleted all memories for user ${uid}`,
);
return {
content: [
{
type: "text",
text: `All memories deleted for user "${uid}".`,
},
],
details: { action: "deleted_all", user_id: uid },
};
const uid = resolveUserId({ agentId, userId });
await provider.deleteAll(uid);
deps.captureToolEvent("memory_delete", { success: true, latency_ms: Date.now() - start, delete_mode: "all" });
api.logger.info(`openclaw-mem0: deleted all memories for user ${uid}`);
return { content: [{ type: "text", text: `All memories deleted for user "${uid}".` }], details: { action: "deleted_all", user_id: uid } };
}
// Mode 4: Cascade-delete entity (platform only)
if (entity) {
if (!confirm) {
return {
content: [
{
type: "text",
text: "Entity deletion requires confirm: true. Ask the user to confirm before proceeding.",
},
],
details: { error: "confirmation_required" },
};
}
const entityOpts: { userId?: string; agentId?: string } = {};
if (user_id) entityOpts.userId = user_id;
if (agent_id) entityOpts.agentId = agent_id;
await backend.deleteEntities(entityOpts);
api.logger.info(
`openclaw-mem0: deleted entity (userId=${user_id ?? "default"}, agentId=${agent_id ?? "none"})`,
);
return {
content: [
{
type: "text",
text: `Entity and all associated memories deleted (userId=${user_id ?? "default"}, agentId=${agent_id ?? "none"}).`,
},
],
details: {
action: "entity_deleted",
user_id: user_id ?? "default",
agent_id: agent_id,
},
};
}
// No valid mode specified
return {
content: [
{
type: "text",
text: "Provide memory_id, query, all, or entity to specify what to delete.",
},
],
details: { error: "missing_param" },
};
return { content: [{ type: "text", text: "Provide memoryId, query, or all:true." }], details: { error: "missing_param" } };
} catch (err) {
return {
content: [
{
type: "text",
text: `Memory delete failed: ${String(err)}`,
},
],
details: { error: String(err) },
};
deps.captureToolEvent("memory_delete", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory delete failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
+60
View File
@@ -0,0 +1,60 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventListTool(deps: ToolDeps) {
return {
name: "memory_event_list",
label: "Memory Event List",
description:
"List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.",
parameters: Type.Object({}),
async execute(_toolCallId: string, _params: Record<string, unknown>) {
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const results = await deps.backend.listEvents();
if (!results.length) {
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 });
return {
content: [{ type: "text", text: "No events found." }],
details: { count: 0 },
};
}
const rows = results.map((ev) => {
const evId = String(ev.id ?? "");
const evType = String(ev.event_type ?? "—");
const status = String(ev.status ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
return { id: evId, type: evType, status, latency, created };
});
const text = rows
.map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`)
.join("\n");
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length });
return {
content: [{ type: "text", text: `${results.length} event(s):\n${text}` }],
details: { count: results.length, events: rows },
};
} catch (err) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to list events: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
+63
View File
@@ -0,0 +1,63 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventStatusTool(deps: ToolDeps) {
return {
name: "memory_event_status",
label: "Memory Event Status",
description:
"Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.",
parameters: Type.Object({
event_id: Type.String({ description: "The event ID to check" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { event_id: eventId } = params as { event_id: string };
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const ev = await deps.backend.getEvent(eventId);
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
const updated = String(ev.updated_at ?? "—").slice(0, 19).replace("T", " ");
let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`;
const results = ev.results as Record<string, unknown>[] | undefined;
if (results && Array.isArray(results) && results.length) {
const resultLines = results.map((r) => {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ?? "";
const evName = String(r.event ?? "");
return `- [${evName}] ${memory} (${memId})`;
});
text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`;
}
deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start });
return {
content: [{ type: "text", text }],
details: { event: ev },
};
} catch (err) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to get event: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
+9 -35
View File
@@ -1,22 +1,8 @@
/**
* memory_get tool — extracted from index.ts registerTools().
*
* Retrieves a specific memory by its ID from Mem0.
*/
import { Type } from "@sinclair/typebox";
import type { ToolContext } from "./index.ts";
import type { ToolDeps } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_get` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemoryGetTool(ctx: ToolContext) {
const { provider } = ctx;
export function createMemoryGetTool(deps: ToolDeps) {
const { provider } = deps;
return {
name: "memory_get",
@@ -28,29 +14,17 @@ export function createMemoryGetTool(ctx: ToolContext) {
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId } = params as { memoryId: string };
const start = Date.now();
try {
const memory = await provider!.get(memoryId);
const memory = await provider.get(memoryId);
deps.captureToolEvent("memory_get", { success: true, latency_ms: Date.now() - start });
return {
content: [
{
type: "text",
text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}`,
},
],
content: [{ type: "text", text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}` }],
details: { memory },
};
} catch (err) {
return {
content: [
{
type: "text",
text: `Memory get failed: ${String(err)}`,
},
],
details: { error: String(err) },
};
deps.captureToolEvent("memory_get", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory get failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
-77
View File
@@ -1,77 +0,0 @@
/**
* memory_history tool — extracted from index.ts registerTools().
*
* Views the edit history of a specific memory, showing all changes
* over time including previous values, new values, and timestamps.
*/
import { Type } from "@sinclair/typebox";
import type { ToolContext } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_history` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemoryHistoryTool(ctx: ToolContext) {
const { provider } = ctx;
return {
name: "memory_history",
label: "Memory History",
description:
"View the edit history of a specific memory. Shows all changes over time including previous values, new values, and timestamps. Useful for understanding how a memory evolved.",
parameters: Type.Object({
memoryId: Type.String({
description: "The memory ID to view history for",
}),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId } = params as { memoryId: string };
try {
const history = await provider!.history(memoryId);
if (!history || history.length === 0) {
return {
content: [
{
type: "text",
text: `No history found for memory ${memoryId}.`,
},
],
details: { count: 0 },
};
}
const text = history
.map(
(h, i) =>
`${i + 1}. [${h.event}] ${h.created_at}\n Old: ${h.old_memory || "(none)"}\n New: ${h.new_memory || "(none)"}`,
)
.join("\n\n");
return {
content: [
{
type: "text",
text: `History for memory ${memoryId} (${history.length} entries):\n\n${text}`,
},
],
details: { count: history.length, history },
};
} catch (err) {
return {
content: [
{ type: "text", text: `Memory history failed: ${String(err)}` },
],
details: { error: String(err) },
};
}
},
};
}
+28 -147
View File
@@ -1,183 +1,64 @@
/**
* memory_list tool — extracted from index.ts registerTools().
*
* Lists all stored memories for a user or agent. Supports scope filtering
* (session/long-term/all), deduplication, and CLI-parity parameters for
* pagination, category filtering, date ranges, and graph support.
*/
import { Type } from "@sinclair/typebox";
import type { Mem0Config, Mem0Provider, MemoryItem } from "../types.ts";
import type { MemoryItem } from "../types.ts";
import type { ToolDeps } from "./index.ts";
import type { ListOptions } from "../types.ts";
import type { ToolContext } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_list` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemoryListTool(ctx: ToolContext) {
const { provider, resolveUserId, getCurrentSessionId } = ctx;
export function createMemoryListTool(deps: ToolDeps) {
const { provider, resolveUserId, getCurrentSessionId } = deps;
return {
name: "memory_list",
label: "Memory List",
description:
"List all stored memories for a user or agent. Use this when you want to see everything that's been remembered, rather than searching for something specific.",
description: "List all stored memories for a user or agent.",
parameters: Type.Object({
userId: Type.Optional(
Type.String({
description:
"User ID to list memories for (default: configured userId)",
}),
),
agentId: Type.Optional(
Type.String({
description:
'Agent ID to list memories for a specific agent (e.g. "researcher"). Overrides userId.',
}),
),
userId: Type.Optional(Type.String({ description: "User ID (default: configured)" })),
agentId: Type.Optional(Type.String({ description: "Agent ID namespace" })),
scope: Type.Optional(
Type.Union(
[
Type.Literal("session"),
Type.Literal("long-term"),
Type.Literal("all"),
],
{
description:
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
},
),
),
// --- NEW CLI-parity parameters ---
page: Type.Optional(
Type.Number({ description: "Page number (default: 1)" }),
),
pageSize: Type.Optional(
Type.Number({ description: "Results per page (default: 100)" }),
),
category: Type.Optional(
Type.String({ description: "Filter by category" }),
),
after: Type.Optional(
Type.String({ description: "Created after date (YYYY-MM-DD)" }),
),
before: Type.Optional(
Type.String({ description: "Created before date (YYYY-MM-DD)" }),
),
enableGraph: Type.Optional(
Type.Boolean({ description: "Enable graph in listing" }),
Type.Union([Type.Literal("session"), Type.Literal("long-term"), Type.Literal("all")], {
description: 'Scope: "all" (default), "session", or "long-term"',
}),
),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const {
userId,
agentId,
scope = "all",
// New CLI-parity params
page,
pageSize,
category,
after,
before,
enableGraph,
} = params as {
userId?: string;
agentId?: string;
scope?: "session" | "long-term" | "all";
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
const { userId, agentId, scope = "all" } = params as {
userId?: string; agentId?: string; scope?: "session" | "long-term" | "all";
};
const start = Date.now();
try {
let memories: MemoryItem[] = [];
const uid = resolveUserId({ agentId, userId });
const currentSessionId = getCurrentSessionId();
// Build base options for getAll, incorporating new CLI-parity params
const buildGetAllOpts = (extra?: { run_id?: string }): ListOptions => {
const opts: Record<string, unknown> = {
user_id: uid,
source: "OPENCLAW",
};
if (extra?.run_id) opts.run_id = extra.run_id;
if (pageSize !== undefined) opts.page_size = pageSize;
if (page !== undefined) opts.page = page;
if (category !== undefined) opts.category = category;
if (after !== undefined) opts.after = after;
if (before !== undefined) opts.before = before;
if (enableGraph !== undefined) opts.enable_graph = enableGraph;
return opts as unknown as ListOptions;
};
if (scope === "session") {
if (currentSessionId) {
memories = await provider!.getAll(
buildGetAllOpts({ run_id: currentSessionId }),
);
}
if (currentSessionId) memories = await provider.getAll({ user_id: uid, run_id: currentSessionId, source: "OPENCLAW" });
} else if (scope === "long-term") {
memories = await provider!.getAll(buildGetAllOpts());
memories = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
} else {
// "all" — combine both scopes
const longTerm = await provider!.getAll(buildGetAllOpts());
const longTerm = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
let session: MemoryItem[] = [];
if (currentSessionId) {
session = await provider!.getAll(
buildGetAllOpts({ run_id: currentSessionId }),
);
}
if (currentSessionId) session = await provider.getAll({ user_id: uid, run_id: currentSessionId, source: "OPENCLAW" });
const seen = new Set(longTerm.map((r) => r.id));
memories = [...longTerm, ...session.filter((r) => !seen.has(r.id))];
}
deps.captureToolEvent("memory_list", { success: true, latency_ms: Date.now() - start, result_count: memories.length });
if (!memories || memories.length === 0) {
return {
content: [{ type: "text", text: "No memories stored yet." }],
details: { count: 0 },
};
return { content: [{ type: "text", text: "No memories stored yet." }], details: { count: 0 } };
}
const text = memories
.map((r, i) => `${i + 1}. ${r.memory} (id: ${r.id})`)
.join("\n");
const sanitized = memories.map((r) => ({
id: r.id,
memory: r.memory,
categories: r.categories,
created_at: r.created_at,
}));
const text = memories.map((r, i) => `${i + 1}. ${r.memory} (id: ${r.id})`).join("\n");
return {
content: [
{
type: "text",
text: `${memories.length} memories:\n\n${text}`,
},
],
details: { count: memories.length, memories: sanitized },
content: [{ type: "text", text: `${memories.length} memories:\n\n${text}` }],
details: {
count: memories.length,
memories: memories.map((r) => ({ id: r.id, memory: r.memory, categories: r.categories, created_at: r.created_at })),
},
};
} catch (err) {
return {
content: [
{
type: "text",
text: `Memory list failed: ${String(err)}`,
},
],
details: { error: String(err) },
};
deps.captureToolEvent("memory_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory list failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
+37 -194
View File
@@ -1,243 +1,86 @@
/**
* memory_search tool — extracted from index.ts registerTools().
*
* Searches long-term and session-scoped memories stored in Mem0.
* Supports scope filtering (session/long-term/all), categories,
* advanced filters, and deduplication.
*/
import { Type } from "@sinclair/typebox";
import type {
Mem0Config,
Mem0Provider,
MemoryItem,
SearchOptions,
} from "../types.ts";
import type { MemoryItem, SearchOptions } from "../types.ts";
import type { ToolDeps } from "./index.ts";
import type { ToolContext } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_search` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemorySearchTool(ctx: ToolContext) {
const {
cfg,
provider,
resolveUserId,
getCurrentSessionId,
buildSearchOptions,
} = ctx;
export function createMemorySearchTool(deps: ToolDeps) {
const { cfg, provider, resolveUserId, buildSearchOptions, getCurrentSessionId } = deps;
return {
name: "memory_search",
label: "Memory Search",
description:
"Search through long-term memories stored in Mem0. Use when you need context about user preferences, past decisions, or previously discussed topics.",
description: "Search through long-term memories stored in Mem0.",
parameters: Type.Object({
query: Type.String({ description: "Search query" }),
limit: Type.Optional(
Type.Number({
description: `Max results (default: ${cfg.topK})`,
}),
),
userId: Type.Optional(
Type.String({
description: "User ID to scope search (default: configured userId)",
}),
),
agentId: Type.Optional(
Type.String({
description:
'Agent ID to search memories for a specific agent (e.g. "researcher"). Overrides userId.',
}),
),
limit: Type.Optional(Type.Number({ description: `Max results (default: ${cfg.topK})` })),
userId: Type.Optional(Type.String({ description: "User ID to scope search" })),
agentId: Type.Optional(Type.String({ description: "Agent ID to search a specific agent's memories" })),
scope: Type.Optional(
Type.Union(
[
Type.Literal("session"),
Type.Literal("long-term"),
Type.Literal("all"),
],
{
description:
'Memory scope: "session" (current session only), "long-term" (user-scoped only), or "all" (both). Default: "all"',
},
),
),
categories: Type.Optional(
Type.Array(Type.String(), {
description:
'Filter results by category (e.g. ["identity", "preference"]). Only returns memories tagged with these categories.',
Type.Union([Type.Literal("session"), Type.Literal("long-term"), Type.Literal("all")], {
description: 'Scope: "long-term" (default), "session", or "all"',
}),
),
filters: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description:
'Advanced filters object. Supports date ranges and metadata filtering. Examples: {"created_at": {"gte": "2026-03-01"}} for recent memories, {"AND": [{"categories": {"contains": "decision"}}, {"created_at": {"gte": "2026-01-01"}}]} for decisions this year. Operators: eq, ne, gt, gte, lt, lte, in, contains, icontains. Logical: AND, OR, NOT.',
}),
),
// --- NEW CLI-parity parameters ---
rerank: Type.Optional(
Type.Boolean({ description: "Enable reranking (platform only)" }),
),
keyword: Type.Optional(
Type.Boolean({ description: "Use keyword search instead of semantic" }),
),
threshold: Type.Optional(
Type.Number({ description: "Minimum similarity score (0-1)" }),
),
topK: Type.Optional(
Type.Number({ description: "Maximum number of results" }),
),
fields: Type.Optional(
Type.Array(Type.String(), {
description: "Specific fields to return",
}),
),
enableGraph: Type.Optional(
Type.Boolean({ description: "Enable graph memory in search" }),
),
categories: Type.Optional(Type.Array(Type.String(), { description: "Filter by category" })),
filters: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Advanced filters" })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const {
query,
limit,
userId,
agentId,
scope = "all",
categories: filterCategories,
filters: agentFilters,
// New CLI-parity params
rerank,
keyword,
threshold,
topK,
fields,
enableGraph,
query, limit, userId, agentId, scope = "long-term",
categories: filterCategories, filters: agentFilters,
} = params as {
query: string;
limit?: number;
userId?: string;
agentId?: string;
scope?: "session" | "long-term" | "all";
categories?: string[];
query: string; limit?: number; userId?: string; agentId?: string;
scope?: "session" | "long-term" | "all"; categories?: string[];
filters?: Record<string, unknown>;
rerank?: boolean;
keyword?: boolean;
threshold?: number;
topK?: number;
fields?: string[];
enableGraph?: boolean;
};
const start = Date.now();
try {
let results: MemoryItem[] = [];
const uid = resolveUserId({ agentId, userId });
const currentSessionId = getCurrentSessionId();
// Apply agent-provided filters and new CLI-parity params to search options
const applyFilters = (opts: SearchOptions): SearchOptions => {
if (filterCategories?.length) opts.categories = filterCategories;
if (agentFilters) opts.filters = agentFilters;
// Pass through new CLI-parity parameters
if (rerank !== undefined) opts.reranking = rerank;
if (keyword !== undefined) opts.keyword_search = keyword;
if (threshold !== undefined) opts.threshold = threshold;
if (topK !== undefined) {
opts.top_k = topK;
opts.limit = topK;
}
// Note: 'fields' and 'enableGraph' are not in the legacy SearchOptions
// type from types.ts. They are passed through for forward compatibility
// when the backend supports them.
if (fields !== undefined)
(opts as unknown as Record<string, unknown>).fields = fields;
if (enableGraph !== undefined)
(opts as unknown as Record<string, unknown>).enable_graph =
enableGraph;
return opts;
};
if (scope === "session") {
if (currentSessionId) {
results = await provider!.search(
query,
applyFilters(buildSearchOptions(uid, limit, currentSessionId)),
);
results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, currentSessionId)));
}
} else if (scope === "long-term") {
results = await provider!.search(
query,
applyFilters(buildSearchOptions(uid, limit)),
);
results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
} else {
// "all" -- search both scopes and combine
const longTermResults = await provider!.search(
query,
applyFilters(buildSearchOptions(uid, limit)),
);
let sessionResults: MemoryItem[] = [];
const longTerm = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
let session: MemoryItem[] = [];
if (currentSessionId) {
sessionResults = await provider!.search(
query,
applyFilters(buildSearchOptions(uid, limit, currentSessionId)),
);
session = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, currentSessionId)));
}
// Deduplicate by ID, preferring long-term
const seen = new Set(longTermResults.map((r) => r.id));
results = [
...longTermResults,
...sessionResults.filter((r) => !seen.has(r.id)),
];
const seen = new Set(longTerm.map((r) => r.id));
results = [...longTerm, ...session.filter((r) => !seen.has(r.id))];
}
deps.captureToolEvent("memory_search", { success: true, latency_ms: Date.now() - start, result_count: results.length });
if (!results || results.length === 0) {
return {
content: [{ type: "text", text: "No relevant memories found." }],
details: { count: 0 },
};
return { content: [{ type: "text", text: "No relevant memories found." }], details: { count: 0 } };
}
const text = results
.map(
(r, i) =>
`${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`,
)
.join("\n");
const sanitized = results.map((r) => ({
id: r.id,
memory: r.memory,
score: r.score,
categories: r.categories,
created_at: r.created_at,
}));
const text = results.map((r, i) =>
`${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`
).join("\n");
return {
content: [
{
type: "text",
text: `Found ${results.length} memories:\n\n${text}`,
},
],
details: { count: results.length, memories: sanitized },
content: [{ type: "text", text: `Found ${results.length} memories:\n\n${text}` }],
details: {
count: results.length,
memories: results.map((r) => ({ id: r.id, memory: r.memory, score: r.score, categories: r.categories, created_at: r.created_at })),
},
};
} catch (err) {
return {
content: [
{
type: "text",
text: `Memory search failed: ${String(err)}`,
},
],
details: { error: String(err) },
};
deps.captureToolEvent("memory_search", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory search failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
-339
View File
@@ -1,339 +0,0 @@
/**
* memory_store tool — extracted from index.ts registerTools().
*
* Saves important information in long-term memory via Mem0.
* Supports skills mode (direct storage with infer=false) and
* legacy mode (mem0 extraction LLM handles deduplication).
*/
import { Type } from "@sinclair/typebox";
import type {
Mem0Config,
Mem0Provider,
AddOptions,
SearchOptions,
} from "../types.ts";
import { isSubagentSession } from "../isolation.ts";
import { resolveCategories, ttlToExpirationDate } from "../skill-loader.ts";
import type { ToolContext } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_store` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemoryStoreTool(ctx: ToolContext) {
const {
api,
cfg,
provider,
resolveUserId,
getCurrentSessionId,
buildAddOptions,
buildSearchOptions,
skillsActive,
} = ctx;
return {
name: "memory_store",
label: "Memory Store",
description:
"Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
parameters: Type.Object({
text: Type.Optional(
Type.String({
description:
"Single fact to remember. Use 'facts' array instead when storing multiple facts from one conversation turn.",
}),
),
facts: Type.Optional(
Type.Array(Type.String(), {
description:
"Array of facts to store in one call. ALL facts MUST share the same category. If a turn has facts in different categories, make one call per category. Category determines retention policy (TTL, immutability).",
}),
),
category: Type.Optional(
Type.String({
description:
'Memory category. Determines retention policy (TTL, immutability). All facts in this call inherit this category. Options: "identity", "preference", "decision", "rule", "project", "configuration", "technical", "relationship"',
}),
),
importance: Type.Optional(
Type.Number({
description:
"Importance override (0.0-1.0). Omit to use category default. Applies to all facts in this call. Defaults: identity/config 0.95, rules 0.90, preferences 0.85, decisions 0.80, projects 0.75, operational 0.60",
}),
),
userId: Type.Optional(
Type.String({
description: "User ID to scope this memory",
}),
),
agentId: Type.Optional(
Type.String({
description:
'Agent ID to store memory under a specific agent\'s namespace (e.g. "researcher"). Overrides userId.',
}),
),
metadata: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description: "Additional metadata to attach to this memory",
}),
),
longTerm: Type.Optional(
Type.Boolean({
description:
"Store as long-term (user-scoped) memory. Default: true. Set to false for session-scoped memory.",
}),
),
// --- NEW CLI-parity parameters ---
immutable: Type.Optional(
Type.Boolean({
description: "Prevent future updates to this memory",
}),
),
infer: Type.Optional(
Type.Boolean({
description: "Set to false to skip inference and store raw",
}),
),
expires: Type.Optional(
Type.String({
description: "Expiration date (YYYY-MM-DD)",
}),
),
enableGraph: Type.Optional(
Type.Boolean({
description: "Enable graph memory extraction",
}),
),
categories: Type.Optional(
Type.Array(Type.String(), {
description: "Categories for this memory",
}),
),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const p = params as {
text?: string;
facts?: string[];
category?: string;
importance?: number;
userId?: string;
agentId?: string;
metadata?: Record<string, unknown>;
longTerm?: boolean;
// New CLI-parity params
immutable?: boolean;
infer?: boolean;
expires?: string;
enableGraph?: boolean;
categories?: string[];
};
const { userId, agentId, longTerm = true } = p;
// Resolve facts: prefer 'facts' array, fall back to single 'text'
const allFacts: string[] = p.facts?.length
? p.facts
: p.text
? [p.text]
: [];
if (allFacts.length === 0) {
return {
content: [
{
type: "text",
text: "No facts provided. Pass 'text' or 'facts' array.",
},
],
details: { error: "missing_facts" },
};
}
try {
const currentSessionId = getCurrentSessionId();
// Block subagent writes at the tool level. The system prompt
// instructs subagents not to store, but a disobedient tool call
// would write to a transient namespace that is never read again.
if (isSubagentSession(currentSessionId)) {
api.logger.warn(
"openclaw-mem0: blocked memory_store from subagent session",
);
return {
content: [
{
type: "text",
text: "Memory storage is not available in subagent sessions. The main agent handles memory.",
},
],
details: { error: "subagent_blocked" },
};
}
const uid = resolveUserId({ agentId, userId });
const runId =
!longTerm && currentSessionId ? currentSessionId : undefined;
// Skills mode: bypass extraction LLM, store directly via infer=false
if (skillsActive) {
// Enforce batch homogeneity: if no category provided for a multi-fact
// batch, warn. The prompt teaches batch-by-category but this is the
// runtime safety net.
if (allFacts.length > 1 && !p.category) {
api.logger.warn(
`openclaw-mem0: multi-fact batch (${allFacts.length} facts) without category. Retention policy defaults to uncategorized. Prompt instructs batch-by-category.`,
);
}
// Resolve metadata: prefer explicit params, fall back to metadata record
const rawMetadata = p.metadata;
const category =
p.category ?? (rawMetadata?.category as string | undefined);
const importance =
p.importance ?? (rawMetadata?.importance as number | undefined);
const parsedMetadata: Record<string, unknown> = {
...(rawMetadata ?? {}),
...(category && { category }),
...(importance !== undefined && { importance }),
};
const categories = resolveCategories(cfg.skills);
const catConfig = category ? categories[category] : undefined;
const expirationDate = catConfig
? ttlToExpirationDate(catConfig.ttl)
: undefined;
const isImmutable = catConfig?.immutable ?? false;
// Single API call: all facts go as deduced_memories array
const addOpts: AddOptions = {
user_id: uid,
source: "OPENCLAW",
infer: false,
deduced_memories: allFacts,
metadata: parsedMetadata ?? {},
...(expirationDate && { expiration_date: expirationDate }),
...(isImmutable && { immutable: true }),
};
if (runId) addOpts.run_id = runId;
if (cfg.mode === "platform") {
addOpts.output_format = "v1.1";
if (cfg.enableGraph || cfg.skills?.triage?.enableGraph) {
addOpts.enable_graph = true;
}
}
// Apply new CLI-parity params (override category-derived values)
if (p.immutable !== undefined) addOpts.immutable = p.immutable;
if (p.infer !== undefined) addOpts.infer = p.infer;
if (p.expires !== undefined) addOpts.expiration_date = p.expires;
if (p.enableGraph !== undefined) addOpts.enable_graph = p.enableGraph;
if (p.categories !== undefined)
(addOpts as unknown as Record<string, unknown>).categories =
p.categories;
const result = await provider!.add(
[{ role: "user", content: allFacts.join("\n") }],
addOpts,
);
const count = result.results?.length ?? 0;
api.logger.info(
`openclaw-mem0: skills-mode stored ${count} memor${count === 1 ? "y" : "ies"} from ${allFacts.length} fact(s) in 1 API call (infer=false, category=${category ?? "none"})`,
);
return {
content: [
{
type: "text",
text: `Stored ${allFacts.length} fact(s) [${category ?? "uncategorized"}]: ${allFacts.map((f) => `"${f.slice(0, 60)}${f.length > 60 ? "..." : ""}"`).join(", ")}`,
},
],
details: {
action: "stored",
mode: "skills",
infer: false,
category,
factCount: allFacts.length,
results: result.results,
},
};
}
// Legacy mode: let mem0 extraction LLM handle it
const combinedText = allFacts.join("\n");
// Pre-check for near-duplicates so the extraction model has
// context about existing memories and can UPDATE rather than ADD
const preview = combinedText.slice(0, 200);
const dedupOpts = buildSearchOptions(uid, 3);
dedupOpts.threshold = 0.85;
const existing = await provider!.search(preview, dedupOpts);
if (existing.length > 0) {
api.logger.info(
`openclaw-mem0: found ${existing.length} similar existing memories — mem0 may update instead of add`,
);
}
const legacyAddOpts = buildAddOptions(uid, runId, currentSessionId);
// Apply new CLI-parity params to legacy mode as well
if (p.immutable !== undefined) legacyAddOpts.immutable = p.immutable;
if (p.infer !== undefined) legacyAddOpts.infer = p.infer;
if (p.expires !== undefined) legacyAddOpts.expiration_date = p.expires;
if (p.enableGraph !== undefined)
legacyAddOpts.enable_graph = p.enableGraph;
if (p.categories !== undefined)
(legacyAddOpts as unknown as Record<string, unknown>).categories =
p.categories;
const result = await provider!.add(
[{ role: "user", content: combinedText }],
legacyAddOpts,
);
const added = result.results?.filter((r) => r.event === "ADD") ?? [];
const updated =
result.results?.filter((r) => r.event === "UPDATE") ?? [];
const summary = [];
if (added.length > 0)
summary.push(
`${added.length} new memor${added.length === 1 ? "y" : "ies"} added`,
);
if (updated.length > 0)
summary.push(
`${updated.length} memor${updated.length === 1 ? "y" : "ies"} updated`,
);
if (summary.length === 0) summary.push("No new memories extracted");
return {
content: [
{
type: "text",
text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}`,
},
],
details: {
action: "stored",
results: result.results,
},
};
} catch (err) {
return {
content: [
{
type: "text",
text: `Memory store failed: ${String(err)}`,
},
],
details: { error: String(err) },
};
}
},
};
}
+15 -74
View File
@@ -1,94 +1,35 @@
/**
* memory_update tool — extracted from index.ts registerTools().
*
* Updates an existing memory's text in place. Preserves the memory's
* history and supports subagent blocking. Supports optional metadata
* updates for CLI parity.
*/
import { Type } from "@sinclair/typebox";
import { isSubagentSession } from "../isolation.ts";
import type { ToolContext } from "./index.ts";
import type { ToolDeps } from "./index.ts";
// ---------------------------------------------------------------------------
// Tool factory
// ---------------------------------------------------------------------------
/**
* Creates the `memory_update` tool config object suitable for
* `api.registerTool(config, { name })`.
*/
export function createMemoryUpdateTool(ctx: ToolContext) {
const { api, provider, getCurrentSessionId } = ctx;
export function createMemoryUpdateTool(deps: ToolDeps) {
const { api, provider, getCurrentSessionId } = deps;
return {
name: "memory_update",
label: "Memory Update",
description:
"Update an existing memory's text in place. Use when a fact has changed and you have the memory ID. This is atomic and preserves the memory's history. Preferred over delete-then-store for corrections.",
description: "Update an existing memory's text in place. Atomic and preserves history.",
parameters: Type.Object({
memoryId: Type.String({ description: "The memory ID to update" }),
text: Type.String({
description: "The new text for this memory (replaces the old text)",
}),
// --- NEW CLI-parity parameter ---
metadata: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description: "Metadata to update (JSON object)",
}),
),
text: Type.String({ description: "The new text (replaces old)" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId, text, metadata } = params as {
memoryId: string;
text: string;
metadata?: Record<string, unknown>;
};
const { memoryId, text } = params as { memoryId: string; text: string };
const start = Date.now();
try {
const currentSessionId = getCurrentSessionId();
if (isSubagentSession(currentSessionId)) {
api.logger.warn(
"openclaw-mem0: blocked memory_update from subagent session",
);
return {
content: [
{
type: "text",
text: "Memory update is not available in subagent sessions.",
},
],
details: { error: "subagent_blocked" },
};
if (isSubagentSession(getCurrentSessionId())) {
return { content: [{ type: "text", text: "Memory update is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
await provider!.update(memoryId, text);
// If metadata was provided, note it in the response. The provider's
// update() currently only accepts (id, text). Metadata-only updates
// may need to go through the backend for full support.
let metadataNote = "";
if (metadata && Object.keys(metadata).length > 0) {
metadataNote = `\nNote: metadata was provided but the current provider only supports text updates. Metadata-only updates may require backend support.`;
}
await provider.update(memoryId, text);
deps.captureToolEvent("memory_update", { success: true, latency_ms: Date.now() - start });
return {
content: [
{
type: "text",
text: `Updated memory ${memoryId}: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"${metadataNote}`,
},
],
details: { action: "updated", id: memoryId, metadata: metadata },
content: [{ type: "text", text: `Updated memory ${memoryId}: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"` }],
details: { action: "updated", id: memoryId },
};
} catch (err) {
return {
content: [
{ type: "text", text: `Memory update failed: ${String(err)}` },
],
details: { error: String(err) },
};
deps.captureToolEvent("memory_update", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory update failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
+1 -1
View File
@@ -19,6 +19,6 @@
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"],
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+3 -1
View File
@@ -1,9 +1,11 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
entry: ["index.ts", "fs-safe.ts"],
format: ["esm"],
splitting: true,
dts: true,
sourcemap: true,
clean: true,
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
});
-5
View File
@@ -9,11 +9,8 @@ export type Mem0Config = {
// Platform-specific
apiKey?: string;
baseUrl?: string;
orgId?: string;
projectId?: string;
customInstructions: string;
customCategories: Record<string, string>;
enableGraph: boolean;
// OSS-specific
customPrompt?: string;
oss?: {
@@ -40,7 +37,6 @@ export interface AddOptions {
run_id?: string;
custom_instructions?: string;
custom_categories?: Array<Record<string, string>>;
enable_graph?: boolean;
output_format?: string;
source?: string;
// Agentic harness additions
@@ -79,7 +75,6 @@ export interface SkillsConfig {
triage?: {
enabled?: boolean;
importanceThreshold?: number;
enableGraph?: boolean;
credentialPatterns?: string[];
};
recall?: {
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
alias: {
// OpenClaw SDK modules are resolved from the gateway at runtime.
// During unit tests we provide lightweight stubs.
"openclaw/plugin-sdk/plugin-entry": new URL(
"./test-shims/plugin-entry.ts",
import.meta.url,
).pathname,
"openclaw/plugin-sdk": new URL(
"./test-shims/plugin-sdk.ts",
import.meta.url,
).pathname,
},
},
});
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "1.0.10"
version = "1.0.11"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "support@mem0.ai" }
+189
View File
@@ -0,0 +1,189 @@
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.
"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 the 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 the 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 any 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
Copyright 2024 Mem0.ai
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.
+102
View File
@@ -0,0 +1,102 @@
# Mem0 CLI Skill for Claude
Manage memories from the terminal using the [Mem0 CLI](https://docs.mem0.ai/cli). This skill teaches Claude how to use every `mem0` command, flag, and output mode -- for both the Node.js and Python implementations.
## What This Skill Does
When installed, Claude can:
- **Run mem0 commands** correctly in your terminal (add, search, list, get, update, delete, import, config, init, status, entity, event)
- **Construct complex invocations** with the right flags, scoping, filters, and output formats
- **Pipe and script** mem0 commands in shell workflows, CI/CD pipelines, and agent loops
- **Debug issues** like missing API keys, entity scoping conflicts, and async processing delays
## Installation
### CLI (Claude Code, OpenCode, OpenClaw, or any tool that supports skills)
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
```
### Claude.ai
1. Download this `skills/mem0-cli` folder as a ZIP
2. Go to **Settings > Capabilities > Skills**
3. Click **Upload skill** and select the ZIP
### Claude API (Skills API)
```bash
curl -X POST https://api.anthropic.com/v1/skills \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mem0-cli", "source": "https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli"}'
```
## Prerequisites
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
- **Node.js 18+** or **Python 3.10+**
- Install the CLI:
```bash
# Node.js
npm install -g @mem0/cli
# Python
pip install mem0-cli
```
- Set the environment variable:
```bash
export MEM0_API_KEY="m0-your-api-key"
```
Or run `mem0 init` for the interactive setup wizard.
## Quick Start
After installing, just ask Claude:
- "Add a memory for user alice that she prefers dark mode"
- "Search alice's memories for dietary preferences"
- "List all memories and output as JSON"
- "Delete all memories for user bob"
- "Set up mem0 CLI in my CI pipeline"
- "Pipe the output of my script into mem0 add"
## What's Inside
```text
skills/mem0-cli/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
└── references/ # Documentation (loaded on demand)
├── command-reference.md # Every command, flag, option, and example
├── configuration.md # Config file, env vars, precedence, init wizard
└── workflows.md # Piping, scripting, CI/CD, agent mode recipes
```
## Links
- [Mem0 Platform Dashboard](https://app.mem0.ai)
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 CLI Docs](https://docs.mem0.ai/cli)
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
## Skill Graph
This skill is part of the **Mem0 skill graph** -- three interconnected skills for different interfaces to the Mem0 platform:
| Skill | Purpose | Link |
|-------|---------|------|
| **mem0** | Python/TypeScript SDK, REST API, framework integrations | [local](../mem0/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0) |
| **mem0-cli** (this skill) | Terminal commands for memory operations | [local](./SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli) |
| **mem0-vercel-ai-sdk** | Vercel AI SDK provider with automatic memory | [local](../mem0-vercel-ai-sdk/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
## License
Apache-2.0
+154
View File
@@ -0,0 +1,154 @@
---
name: mem0-cli
description: >
Mem0 CLI -- the command-line interface for mem0 memory operations.
TRIGGER when: user mentions "mem0 cli", "mem0 command line", "@mem0/cli",
"mem0-cli", "pip install mem0-cli", "npm install -g @mem0/cli", or is running
mem0 commands in a terminal/shell (mem0 add, mem0 search, mem0 list, mem0 get,
mem0 init, mem0 config, mem0 import). Also triggers when query includes CLI flags
like --user-id, --output, --json, --agent, or describes bash/zsh/terminal/shell usage.
DO NOT TRIGGER when: user asks about programmatic SDK integration in Python/TS
code (use mem0 skill), or Vercel AI SDK provider (use mem0-vercel-ai-sdk skill).
license: Apache-2.0
metadata:
author: mem0ai
version: "1.0.0"
category: ai-memory
tags: "cli, terminal, memory, ai, command-line"
compatibility: Node.js 18+ (npm install -g @mem0/cli) or Python 3.10+ (pip install mem0-cli), MEM0_API_KEY env var
---
# Mem0 CLI
The official command-line interface for the Mem0 memory platform. Add, search, list, update, and delete memories from the terminal -- for developers, AI agents, and CI/CD pipelines.
## Install
**Node.js (npm):**
```bash
npm install -g @mem0/cli
```
**Python (pip):**
```bash
pip install mem0-cli
```
Both packages install a `mem0` binary with identical commands, options, and output formats.
## Setup
**Interactive wizard:**
```bash
mem0 init
```
**Or set the environment variable directly:**
```bash
export MEM0_API_KEY="m0-xxx"
```
Get an API key at: https://app.mem0.ai/dashboard/api-keys
## Quick Reference
### Add a memory
```bash
mem0 add "I prefer dark mode" --user-id alice
```
### Search memories
```bash
mem0 search "preferences" --user-id alice
```
### List all memories for a user
```bash
mem0 list --user-id alice
```
### Get a specific memory
```bash
mem0 get <memory-id>
```
### Update a memory
```bash
mem0 update <memory-id> "new text"
```
### Delete a single memory
```bash
mem0 delete <memory-id>
```
### Delete all memories for a user
```bash
mem0 delete --all --user-id alice --force
```
## Agent / JSON Mode
Use `--json` or `--agent` to get structured output suitable for LLM consumption. Every command wraps its response in a standard envelope:
```json
{
"status": "success",
"command": "search",
"duration_ms": 245,
"scope": { "user_id": "alice" },
"count": 3,
"error": null,
"data": [
{ "id": "mem-abc", "memory": "User prefers dark mode", "score": 0.92 }
]
}
```
On error:
```json
{
"status": "error",
"command": "search",
"error": "Authentication failed. Your API key may be invalid or expired.",
"data": null
}
```
The `--agent` flag is an alias for `--json`. Both write spinners and progress to stderr so stdout is always clean, parseable JSON.
## Node and Python Parity
Both the Node.js (`@mem0/cli`) and Python (`mem0-cli`) CLIs are implemented from the same specification (`cli-spec.json`). They share:
- Identical command names, arguments, and flags
- Identical output formats (text, json, table, quiet)
- Identical entity ID resolution, graph tri-state, filter building
- Identical error messages and exit codes
Choose whichever runtime you already have installed. The behavior is the same.
## Common Edge Cases
- **Async processing delay:** After `mem0 add`, memories process asynchronously. Wait 2-3 seconds before searching for newly added content. Use `mem0 event list` to check processing status.
- **`--all` vs `--entity` delete modes:** `mem0 delete --all -u alice` deletes all memories for user alice. `mem0 delete --entity -u alice` deletes the entity itself AND all its memories (cascade). These are mutually exclusive modes.
- **Entity ID resolution:** If you pass any explicit scope flag (e.g. `--user-id`), the CLI uses ONLY the explicit IDs and ignores config defaults. If no scope flags are given, all configured defaults apply.
- **Stdin detection:** When no text argument is provided and input is piped (not a TTY), the CLI reads from stdin. Works with `add`, `search`, and `update`.
- **Graph tri-state:** `--no-graph` takes precedence over `--graph`, which takes precedence over the config default (`defaults.enable_graph`).
## References
Load these on demand for deeper detail:
| Topic | File |
|-------|------|
| Command reference (all commands, flags, options, examples) | [references/command-reference.md](references/command-reference.md) |
| Configuration (config file, env vars, precedence, init wizard) | [references/configuration.md](references/configuration.md) |
| Workflows (piping, scripting, CI/CD, agent mode recipes) | [references/workflows.md](references/workflows.md) |
## Related Mem0 Skills
| Skill | When to use | Link |
|-------|-------------|------|
| mem0 | Python/TypeScript SDK, REST API, framework integrations | [local](../mem0/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0) |
| mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [local](../mem0-vercel-ai-sdk/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
@@ -0,0 +1,690 @@
# Mem0 CLI Command Reference
Complete reference for every command, argument, flag, and output mode in the mem0 CLI. Both the Node.js (`@mem0/cli`) and Python (`mem0-cli`) implementations are identical in behavior.
---
## Global Options
These options are available on every command:
| Flag | Type | Description |
|------|------|-------------|
| `--json` / `--agent` | boolean | Agent mode: wrap all output in a structured JSON envelope on stdout. Spinners and progress go to stderr. |
| `-o, --output <format>` | string | Output format. Supported values vary per command (see matrix below). |
| `--api-key <key>` | string | Override the API key for this invocation. Takes precedence over env var and config file. |
| `--base-url <url>` | string | Override the API base URL (default: `https://api.mem0.ai`). |
| `--version` | boolean | Print version and exit. |
---
## Commands
### `mem0 init`
Interactive setup wizard. Configures API key and default user ID.
**Usage:** `mem0 init [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--api-key <key>` | string | - | API key (skip interactive prompt). |
| `-u, --user-id <id>` | string | - | Default user ID (skip interactive prompt). |
| `--email <addr>` | string | - | Login via email verification code instead of API key. |
| `--code <code>` | string | - | Verification code (use with `--email` for fully non-interactive login). |
| `--force` | boolean | false | Overwrite existing config without confirmation. |
**Behavior:**
- If `~/.mem0/config.json` already exists with an API key, warns and asks for confirmation (or errors in non-TTY unless `--force` is set).
- **Email login flow** (`--email`): sends a 6-digit code to the email via `POST /api/v1/auth/email_code/`. If `--code` is also given, verifies immediately. On success, saves API key, org_id, and project_id. Cannot be combined with `--api-key`.
- **API key flow**: if both `--api-key` and `--user-id` are given, runs fully non-interactively. Otherwise prompts for missing values.
- In non-TTY without sufficient flags, prints a usage hint and exits with error.
**Examples:**
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --api-key m0-xxx --user-id alice --force
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
```
---
### `mem0 add`
Add a memory from text, messages, file, or stdin.
**Usage:** `mem0 add [text] [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `text` | string | No | Text content to add as a memory. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-u, --user-id <id>` | string | - | Scope to user. |
| `--agent-id <id>` | string | - | Scope to agent. |
| `--app-id <id>` | string | - | Scope to app. |
| `--run-id <id>` | string | - | Scope to run. |
| `--messages <json>` | string | - | Conversation messages as JSON array (e.g. `'[{"role":"user","content":"..."}]'`). |
| `-f, --file <path>` | path | - | Read messages from a JSON file. |
| `-m, --metadata <json>` | string | - | Custom metadata as JSON object (e.g. `'{"source":"cli"}'`). |
| `--immutable` | boolean | false | Prevent future updates to this memory. |
| `--no-infer` | boolean | false | Skip inference; store the text verbatim. |
| `--expires <date>` | string | - | Expiration date in `YYYY-MM-DD` format. |
| `--categories <cats>` | string | - | Categories as JSON array or comma-separated string. |
| `--graph` | boolean | false | Enable graph memory extraction for this call. |
| `--no-graph` | boolean | false | Disable graph memory extraction for this call. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`, `quiet`. |
**Input priority:** `--file` > `--messages` > text argument > stdin (if piped and no text).
Text content is wrapped as `[{"role": "user", "content": "<text>"}]` before sending to the API. Messages from `--messages` or `--file` are sent as-is.
**Output events:** The API returns results with an `event` field per memory:
| Event | Meaning |
|-------|---------|
| `ADD` | New memory created |
| `UPDATE` | Existing memory updated (deduplication) |
| `DELETE` | Existing memory removed (contradiction) |
| `NOOP` | No change needed |
| `PENDING` | Processing asynchronously in background |
**Examples:**
```bash
mem0 add "I prefer dark mode" --user-id alice
mem0 add "allergic to nuts" -u alice -m '{"source":"onboarding"}'
mem0 add --messages '[{"role":"user","content":"I like Python"}]' -u alice
mem0 add --file conversation.json -u alice -o json
echo "I prefer dark mode" | mem0 add -u alice
mem0 add "temporary note" -u alice --expires 2025-12-31
mem0 add "important fact" -u alice --immutable
mem0 add "uses vim" -u alice --categories "tools,preferences"
```
---
### `mem0 search`
Search memories by semantic query.
**Usage:** `mem0 search <query> [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `query` | string | Yes | The search query. Falls back to stdin if piped. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-u, --user-id <id>` | string | - | Filter by user. |
| `--agent-id <id>` | string | - | Filter by agent. |
| `--app-id <id>` | string | - | Filter by app. |
| `--run-id <id>` | string | - | Filter by run. |
| `-k, --top-k, --limit <n>` | integer | 10 | Maximum number of results to return. |
| `--threshold <score>` | float | 0.3 | Minimum similarity score (0.0 to 1.0). |
| `--rerank` | boolean | false | Enable reranking for improved relevance (Platform only). |
| `--keyword` | boolean | false | Use keyword search instead of semantic. |
| `--filter <json>` | string | - | Advanced filter expression as JSON (AND/OR operators). |
| `--fields <list>` | string | - | Comma-separated list of fields to return. |
| `--graph` | boolean | false | Enable graph in search. |
| `--no-graph` | boolean | false | Disable graph in search. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`, `table`. |
**Examples:**
```bash
mem0 search "preferences" --user-id alice
mem0 search "tools" -u alice -o json -k 5
mem0 search "dietary restrictions" -u alice --threshold 0.5
mem0 search "project setup" -u alice --rerank
mem0 search "preferences" -u alice --filter '{"categories":{"contains":"food"}}'
echo "preferences" | mem0 search -u alice
```
---
### `mem0 get`
Get a specific memory by ID.
**Usage:** `mem0 get <memory_id> [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `memory_id` | string | Yes | The UUID of the memory to retrieve. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`. |
**Examples:**
```bash
mem0 get abc-123-def-456
mem0 get abc-123-def-456 -o json
```
---
### `mem0 list`
List memories with optional filters and pagination.
**Usage:** `mem0 list [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-u, --user-id <id>` | string | - | Filter by user. |
| `--agent-id <id>` | string | - | Filter by agent. |
| `--app-id <id>` | string | - | Filter by app. |
| `--run-id <id>` | string | - | Filter by run. |
| `--page <n>` | integer | 1 | Page number. |
| `--page-size <n>` | integer | 100 | Results per page. |
| `--category <name>` | string | - | Filter by category. |
| `--after <date>` | string | - | Created after (YYYY-MM-DD). |
| `--before <date>` | string | - | Created before (YYYY-MM-DD). |
| `--graph` | boolean | false | Enable graph in listing. |
| `--no-graph` | boolean | false | Disable graph in listing. |
| `-o, --output <fmt>` | string | `table` | Output format: `text`, `json`, `table`. |
**Examples:**
```bash
mem0 list -u alice
mem0 list --category prefs --after 2024-01-01 -o json
mem0 list -u alice --page 2 --page-size 50
mem0 list --before 2024-06-01 -o table
```
---
### `mem0 update`
Update a memory's text or metadata.
**Usage:** `mem0 update <memory_id> [text] [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `memory_id` | string | Yes | The UUID of the memory to update. |
| `text` | string | No | New memory text. Falls back to stdin if piped and no `--metadata`. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-m, --metadata <json>` | string | - | Update metadata as JSON object. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`, `quiet`. |
**Examples:**
```bash
mem0 update abc-123 "new text"
mem0 update abc-123 --metadata '{"priority":"high"}'
mem0 update abc-123 "new text" -m '{"priority":"high"}'
echo "new text" | mem0 update abc-123
```
---
### `mem0 delete`
Delete a memory, all memories matching a scope, or an entity. This command has three mutually exclusive modes.
**Usage:** `mem0 delete [memory_id] [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `memory_id` | string | No | Memory ID to delete (omit when using `--all` or `--entity`). |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `--all` | boolean | false | Delete all memories matching scope filters. |
| `--entity` | boolean | false | Delete the entity itself and all its memories (cascade). |
| `--project` | boolean | false | With `--all`: delete ALL memories project-wide (sends wildcard IDs). |
| `--dry-run` | boolean | false | Show what would be deleted without actually deleting. |
| `--force` | boolean | false | Skip confirmation prompt. |
| `-u, --user-id <id>` | string | - | Scope to user. |
| `--agent-id <id>` | string | - | Scope to agent. |
| `--app-id <id>` | string | - | Scope to app. |
| `--run-id <id>` | string | - | Scope to run. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`, `quiet`. |
**Three modes (mutually exclusive):**
1. **Single memory:** `mem0 delete <memory_id>` -- deletes one memory by its UUID.
2. **Bulk delete:** `mem0 delete --all [scope flags]` -- deletes all memories matching the scope. Add `--project` to wipe all memories project-wide (sends wildcard `*` entity IDs).
3. **Entity cascade:** `mem0 delete --entity [scope flags]` -- deletes the entity itself AND all its memories.
You cannot combine `<memory_id>` with `--all` or `--entity`, and you cannot combine `--all` with `--entity`. If none of these are provided, the command prints a usage hint and exits with an error.
**Dry-run behavior:**
- Single: fetches the memory, displays it, prints "No changes made."
- `--all`: lists matching memories with count, prints "No changes made."
- `--entity`: shows the affected scope without deleting.
**Confirmation:** Without `--force`, all destructive modes prompt `[y/N]`. With `--all --project`, the prompt explicitly warns about project-wide deletion.
**`--all --project` behavior:** Sends `DELETE /v1/memories/` with `user_id=*&agent_id=*&app_id=*&run_id=*`. The API returns an async response. The CLI prints "Deletion started. Memories will be removed in the background."
**Examples:**
```bash
mem0 delete abc-123-def-456
mem0 delete --all -u alice --force
mem0 delete --all --project --force
mem0 delete --entity -u alice --force
mem0 delete abc-123 --dry-run
mem0 delete --all -u alice --dry-run
```
---
### `mem0 import`
Import memories from a JSON file.
**Usage:** `mem0 import <file_path> [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `file_path` | string | Yes | Path to a JSON file containing memories. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-u, --user-id <id>` | string | - | Override user ID for all imported items. |
| `--agent-id <id>` | string | - | Override agent ID for all imported items. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`. |
**File format:** A JSON array (or single object) where each item has a `memory`, `text`, or `content` field for the text, plus optional `user_id`, `agent_id`, and `metadata` fields. CLI-provided `--user-id` and `--agent-id` override per-item values.
**Import format example:**
```json
[
{ "memory": "Prefers dark mode", "user_id": "alice" },
{ "text": "Allergic to nuts", "metadata": { "source": "intake" } },
{ "content": "Uses VS Code" }
]
```
**Behavior:** Iterates through items, calling the add API for each. Displays progress and reports `added` and `failed` counts on completion.
**Examples:**
```bash
mem0 import memories.json --user-id alice
mem0 import data.json -u alice -o json
```
---
### `mem0 config show`
Display current configuration with secrets redacted.
**Usage:** `mem0 config show [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`. |
**Examples:**
```bash
mem0 config show
mem0 config show -o json
```
---
### `mem0 config get`
Get a single configuration value.
**Usage:** `mem0 config get <key>`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `key` | string | Yes | Dotted config key (e.g. `platform.api_key`, `defaults.user_id`). |
**Valid keys:** `platform.api_key`, `platform.base_url`, `defaults.user_id`, `defaults.agent_id`, `defaults.app_id`, `defaults.run_id`, `defaults.enable_graph`.
API key values are always redacted in output.
**Examples:**
```bash
mem0 config get platform.api_key
mem0 config get defaults.user_id
```
---
### `mem0 config set`
Set a configuration value.
**Usage:** `mem0 config set <key> <value>`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `key` | string | Yes | Dotted config key (e.g. `defaults.user_id`). |
| `value` | string | Yes | Value to set. |
**Type coercion:** Boolean fields accept `true`/`1`/`yes` (case-insensitive) as true, anything else as false.
**Examples:**
```bash
mem0 config set defaults.user_id alice
mem0 config set platform.base_url https://api.mem0.ai
mem0 config set defaults.enable_graph true
```
---
### `mem0 config clear`
Clear the configuration file. Removes `~/.mem0/config.json`.
**Usage:** `mem0 config clear`
**Examples:**
```bash
mem0 config clear
```
---
### `mem0 entity list`
List all entities of a given type.
**Usage:** `mem0 entity list <entity_type> [OPTIONS]`
**Arguments:**
| Name | Type | Required | Choices | Description |
|------|------|----------|---------|-------------|
| `entity_type` | string | Yes | `users`, `agents`, `apps`, `runs` | Entity type to list. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `table` | Output format: `table`, `json`. |
**Behavior:** Calls `GET /v1/entities/` (returns all types), then filters client-side using the type map (`users` -> `user`, `agents` -> `agent`, etc.). Displays a table with "Name / ID" and "Created" columns.
**Examples:**
```bash
mem0 entity list users
mem0 entity list agents -o json
```
---
### `mem0 entity delete`
Delete an entity and ALL its memories (cascade).
**Usage:** `mem0 entity delete [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-u, --user-id <id>` | string | - | User ID of the entity to delete. |
| `--agent-id <id>` | string | - | Agent ID of the entity to delete. |
| `--app-id <id>` | string | - | App ID of the entity to delete. |
| `--run-id <id>` | string | - | Run ID of the entity to delete. |
| `--dry-run` | boolean | false | Show what would be deleted without deleting. |
| `--force` | boolean | false | Skip confirmation prompt. |
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`, `quiet`. |
At least one entity ID is required. Errors if none provided.
**Examples:**
```bash
mem0 entity delete --user-id alice --force
mem0 entity delete --user-id alice --dry-run
mem0 entity delete --agent-id bot1 --force
```
---
### `mem0 event list`
List recent background processing events.
**Usage:** `mem0 event list [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `table` | Output format: `text` (table), `json`. |
**Behavior:** Fetches all events for the project. Displays a table with columns: Event ID (first 8 chars), Type, Status (color-coded), Latency, Created. Status values: `PENDING`, `SUCCEEDED`, `FAILED`, `PROCESSING`.
**Examples:**
```bash
mem0 event list
mem0 event list --output json
```
---
### `mem0 event status`
Get the status and results of a specific background event.
**Usage:** `mem0 event status <event_id> [OPTIONS]`
**Arguments:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `event_id` | string | Yes | Event ID to inspect. |
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`. |
**Behavior:** Fetches the event by ID. Displays: Event ID, Type, Status, Latency, Created, Updated, and a list of result memories.
**Examples:**
```bash
mem0 event status evt-abc-123
mem0 event status evt-abc-123 --output json
```
---
### `mem0 status`
Check connectivity and authentication.
**Usage:** `mem0 status [OPTIONS]`
**Options:**
| Flag | Type | Default | Description |
|------|------|---------|-------------|
| `-o, --output <fmt>` | string | `text` | Output format: `text`, `json`. |
**Behavior:** Calls `GET /v1/ping/` to validate connectivity and authentication. Displays connection status, backend type, and base URL.
**JSON output:**
```json
{
"status": "success",
"command": "status",
"duration_ms": 112,
"data": {
"connected": true,
"backend": "platform",
"base_url": "https://api.mem0.ai"
}
}
```
**Examples:**
```bash
mem0 status
mem0 status -o json
```
---
## Agent Mode Envelope Format
When `--json` or `--agent` is passed, every command wraps its output in a consistent JSON envelope on stdout:
```json
{
"status": "success",
"command": "<command_name>",
"duration_ms": 245,
"scope": { "user_id": "alice", "agent_id": null },
"count": 10,
"error": null,
"data": { ... }
}
```
**Fields:**
- `status`: `"success"` or `"error"`.
- `command`: The command name (e.g. `"search"`, `"add"`, `"list"`).
- `duration_ms`: Elapsed time in milliseconds (optional).
- `scope`: Active entity scope, omitted if empty (optional).
- `count`: Number of results, where applicable (optional).
- `error`: Error message string, or `null` on success.
- `data`: Command-specific response data, or `null` on error.
**Sanitized data fields per command in agent mode:**
| Command | `data` shape |
|---------|-------------|
| `add` | `[{id, memory, event}]` or `[{status, event_id}]` for PENDING |
| `search` | `[{id, memory, score, created_at, categories}]` |
| `list` | `[{id, memory, created_at, categories}]` |
| `get` | `{id, memory, created_at, updated_at, categories, metadata}` |
| `update` | `{id, memory}` |
| `delete` | Raw API response |
| `entity list` | `[{name, type, count}]` |
| `event list` | `[{id, event_type, status, latency, created_at}]` |
| `event status` | `{id, event_type, status, latency, created_at, updated_at, results}` |
| `status` | `{connected, backend, base_url}` |
| `config show` | Config object (keys redacted) |
| `import` | `{added, failed, duration_s}` |
**Error envelope:**
```json
{
"status": "error",
"command": "search",
"error": "Authentication failed. Your API key may be invalid or expired.",
"data": null
}
```
---
## Entity ID Resolution
**Rule:** If **any** explicit entity ID is provided via CLI flags (`--user-id`, `--agent-id`, `--app-id`, `--run-id`), the CLI uses only the explicitly provided IDs. It does NOT mix in defaults from config for the other entity types.
If **no** explicit IDs are given, all configured defaults from config file and env vars apply.
**Rationale:** If a user passes `--user-id alice` and the config also has `agent_id=bot1`, they want only Alice's memories -- not the intersection of Alice AND bot1.
```
if any(user_id, agent_id, app_id, run_id) were passed as flags:
use only the explicitly provided IDs (others = null)
else:
use all configured defaults
```
This applies to commands with `resolveIds: true`: `add`, `search`, `list`, `delete`, `import`.
---
## Graph Tri-State
The `enable_graph` parameter follows a three-level precedence:
```
--no-graph (explicit disable) > --graph (explicit enable) > config default
```
If `--no-graph` is passed, graph is disabled regardless of other settings. If `--graph` is passed (without `--no-graph`), graph is enabled. If neither is passed, the config value `defaults.enable_graph` is used.
This applies to commands with `resolveGraph: true`: `add`, `search`, `list`.
---
## Filter Building
For `search` and `list`, entity IDs and additional filters are composed into the API filter structure:
1. If the user provides a pre-built filter via `--filter` containing `AND` or `OR` keys, it is passed through to the API as-is.
2. Otherwise, the CLI builds an array of AND conditions:
- Each entity ID becomes a condition: `{"user_id": "alice"}`, etc.
- Category filters: `{"categories": {"contains": "<category>"}}`.
- Date filters: `{"created_at": {"gte": "YYYY-MM-DD"}}` and/or `{"created_at": {"lte": "YYYY-MM-DD"}}`.
3. If exactly 1 condition: sent as a single object (no wrapping).
4. If 2+ conditions: wrapped as `{"AND": [condition1, condition2, ...]}`.
5. If 0 conditions: no filter sent.
---
## Output Mode Support Matrix
| Command | `text` | `json` | `table` | `quiet` | Default |
|---------|--------|--------|---------|---------|---------|
| `add` | Y | Y | - | Y | `text` |
| `search` | Y | Y | Y | - | `text` |
| `get` | Y | Y | - | - | `text` |
| `list` | Y | Y | Y | - | `table` |
| `update` | Y | Y | - | Y | `text` |
| `delete` | Y | Y | - | Y | `text` |
| `import` | Y | Y | - | - | `text` |
| `config show` | Y | Y | - | - | `text` |
| `config get` | raw | - | - | - | raw |
| `config set` | msg | - | - | - | msg |
| `entity list` | - | Y | Y | - | `table` |
| `entity delete` | Y | Y | - | Y | `text` |
| `event list` | Y (table) | Y | - | - | `table` |
| `event status` | Y | Y | - | - | `text` |
| `status` | Y | Y | - | - | `text` |
All commands additionally support agent mode (`--json`/`--agent`) which overrides the output format with the JSON envelope.
+244
View File
@@ -0,0 +1,244 @@
# Mem0 CLI Configuration
Everything about configuring the mem0 CLI: config file format, environment variables, the init wizard, and precedence rules.
---
## Config File Location
| Path | Permissions | Description |
|------|-------------|-------------|
| `~/.mem0/` | `0700` (owner rwx) | Config directory. Created automatically by `mem0 init`. |
| `~/.mem0/config.json` | `0600` (owner rw) | Config file. Contains API key, defaults, and platform settings. |
The restricted permissions ensure API keys are not world-readable.
---
## Config File Schema
```json
{
"version": 1,
"defaults": {
"user_id": "",
"agent_id": "",
"app_id": "",
"run_id": "",
"enable_graph": false
},
"platform": {
"api_key": "",
"base_url": "https://api.mem0.ai"
}
}
```
### Field Reference
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `version` | integer | `1` | Config schema version. |
| `defaults.user_id` | string | `""` | Default user ID for scoping commands. |
| `defaults.agent_id` | string | `""` | Default agent ID for scoping commands. |
| `defaults.app_id` | string | `""` | Default app ID for scoping commands. |
| `defaults.run_id` | string | `""` | Default run ID for scoping commands. |
| `defaults.enable_graph` | boolean | `false` | Default graph memory extraction toggle. |
| `platform.api_key` | string | `""` | API key for the Mem0 Platform. |
| `platform.base_url` | string | `"https://api.mem0.ai"` | Base URL for API requests. |
---
## `mem0 init` Wizard
The `init` command provides two authentication flows:
### API Key Flow (default)
```bash
# Fully interactive:
mem0 init
# Fully non-interactive:
mem0 init --api-key m0-xxx --user-id alice
```
**Interactive mode steps:**
1. Displays the mem0 banner.
2. Checks for existing config. If found with an API key, asks for confirmation to overwrite.
3. Prompts for API key (input masked with `*` characters; supports backspace and Ctrl+U to clear).
4. Prompts for default user ID (default value: `mem0-cli`).
5. Validates the connection by calling the status endpoint.
6. Saves config to `~/.mem0/config.json` with `0600` permissions.
7. Prints success message.
**Non-interactive mode:** When both `--api-key` and `--user-id` are provided, skips all prompts and saves directly. When running in a non-TTY without both flags, prints an error:
```
Non-interactive terminal detected and missing required flags.
Usage: mem0 init --api-key <key> --user-id <id>
```
### Email Login Flow
```bash
# Interactive (prompts for code):
mem0 init --email alice@company.com
# Fully non-interactive:
mem0 init --email alice@company.com --code 482901
```
**Steps:**
1. Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
2. If `--code` is provided, verifies immediately. Otherwise prompts for the code.
3. On success: receives API key, org_id, and project_id from the server.
4. Saves to config. Creates a new account if the email is not registered.
Cannot be combined with `--api-key`.
### Force Overwrite
If `~/.mem0/config.json` already exists with an API key, `mem0 init` warns and asks for confirmation. Use `--force` to skip:
```bash
mem0 init --api-key m0-new-key --user-id alice --force
```
---
## `mem0 config` Subcommands
### `mem0 config show`
Displays the current configuration as a formatted table (text mode) or JSON envelope (json mode). API keys are always redacted.
```bash
mem0 config show
mem0 config show -o json
```
### `mem0 config get <key>`
Reads a single configuration value. The key uses dotted notation.
```bash
mem0 config get platform.api_key # prints: m0-x...xxxx (redacted)
mem0 config get defaults.user_id # prints: alice
mem0 config get defaults.enable_graph # prints: false
```
**Valid keys:**
- `platform.api_key`
- `platform.base_url`
- `defaults.user_id`
- `defaults.agent_id`
- `defaults.app_id`
- `defaults.run_id`
- `defaults.enable_graph`
Unknown keys print an error message.
### `mem0 config set <key> <value>`
Sets a configuration value and saves the config file.
```bash
mem0 config set defaults.user_id alice
mem0 config set platform.base_url https://api.mem0.ai
mem0 config set defaults.enable_graph true
```
**Type coercion:**
- Boolean fields accept `true`, `1`, `yes` (case-insensitive) as true. Anything else is false.
- Integer fields are parsed with `parseInt`.
- String fields are stored as-is.
### `mem0 config clear`
Removes the config file (`~/.mem0/config.json`).
```bash
mem0 config clear
```
---
## Environment Variables
Environment variables override config file values but are overridden by CLI flags.
| Variable | Config Path | Type | Default |
|----------|-------------|------|---------|
| `MEM0_API_KEY` | `platform.api_key` | string | `""` |
| `MEM0_BASE_URL` | `platform.base_url` | string | `"https://api.mem0.ai"` |
| `MEM0_USER_ID` | `defaults.user_id` | string | `""` |
| `MEM0_AGENT_ID` | `defaults.agent_id` | string | `""` |
| `MEM0_APP_ID` | `defaults.app_id` | string | `""` |
| `MEM0_RUN_ID` | `defaults.run_id` | string | `""` |
| `MEM0_ENABLE_GRAPH` | `defaults.enable_graph` | boolean | `false` |
### Boolean Parsing for `MEM0_ENABLE_GRAPH`
Accepted truthy values (case-insensitive): `"true"`, `"1"`, `"yes"`. Everything else is treated as `false`.
```bash
export MEM0_ENABLE_GRAPH=true # enabled
export MEM0_ENABLE_GRAPH=1 # enabled
export MEM0_ENABLE_GRAPH=yes # enabled
export MEM0_ENABLE_GRAPH=false # disabled
export MEM0_ENABLE_GRAPH=0 # disabled
export MEM0_ENABLE_GRAPH="" # disabled
```
---
## Precedence
Configuration values are resolved in this order (highest priority first):
```
1. CLI flags --api-key, --user-id, --base-url, --graph, --no-graph, etc.
2. Environment vars MEM0_API_KEY, MEM0_USER_ID, MEM0_ENABLE_GRAPH, etc.
3. Config file ~/.mem0/config.json
4. Defaults Hardcoded defaults (empty strings, false, https://api.mem0.ai)
```
**Example:** If your config file has `user_id: "bob"`, the env var `MEM0_USER_ID=charlie` is set, and you pass `--user-id alice` on the command line, the effective user_id is `alice`.
---
## API Key Redaction Rules
Whenever an API key is displayed (in `config show`, `config get`, status output, etc.), it is redacted:
| Condition | Output |
|-----------|--------|
| Empty string | `(not set)` |
| Length <= 8 | First 2 characters + `***` |
| Length > 8 | First 4 characters + `...` + last 4 characters |
**Examples:**
- `""` -> `(not set)`
- `"m0-abc"` -> `m0***`
- `"m0-abcdefghijklmnop"` -> `m0-a...mnop`
The redaction function is named `redact_key` (Python) / `redactKey` (Node).
---
## Dotted Key Map
The `config get` and `config set` commands use dotted key paths. Here is the full mapping:
| Dotted Key | Section | Field |
|------------|---------|-------|
| `platform.api_key` | platform | api_key |
| `platform.base_url` | platform | base_url |
| `defaults.user_id` | defaults | user_id |
| `defaults.agent_id` | defaults | agent_id |
| `defaults.app_id` | defaults | app_id |
| `defaults.run_id` | defaults | run_id |
| `defaults.enable_graph` | defaults | enable_graph |
+439
View File
@@ -0,0 +1,439 @@
# Mem0 CLI Workflows
Practical recipes for using the mem0 CLI in scripts, pipelines, and agent loops.
---
## Piping Content via Stdin
The CLI reads from stdin when no text argument is provided and input is piped (not a TTY). This works with `add`, `search`, and `update`.
**Stdin detection method:**
- Python: `not sys.stdin.isatty()`
- Node: `!process.stdin.isTTY`
### Add from pipe
```bash
echo "I prefer dark mode" | mem0 add --user-id alice
```
### Pipe multi-line content
```bash
cat <<EOF | mem0 add --user-id alice
The user prefers dark mode in all applications.
They also like monospace fonts for code editing.
EOF
```
### Pipe from another command
```bash
git log --oneline -5 | mem0 add --user-id ci-bot --metadata '{"source":"git"}'
```
### Search from pipe
```bash
echo "preferences" | mem0 search --user-id alice
```
### Update from pipe
```bash
echo "Updated: prefers dark mode AND high contrast" | mem0 update abc-123-def-456
```
---
## File Import
Use `mem0 import` to bulk-load memories from a JSON file.
### Basic import
```bash
mem0 import memories.json --user-id alice
```
### File format
The file should be a JSON array where each item has a `memory`, `text`, or `content` field:
```json
[
{ "memory": "Prefers dark mode" },
{ "text": "Allergic to nuts", "metadata": { "source": "intake-form" } },
{ "content": "Uses VS Code", "user_id": "bob" }
]
```
CLI-provided `--user-id` overrides per-item `user_id` values.
### Import with JSON output
```bash
mem0 import data.json --user-id alice -o json
```
Output:
```json
{
"status": "success",
"command": "import",
"data": { "added": 42, "failed": 0, "duration_s": 3.14 },
"duration_ms": 3140
}
```
---
## Agent Mode for LLM Consumption
Use `--json` or `--agent` to get structured JSON output suitable for LLM tool calling or agent frameworks. Spinners and progress always go to stderr, keeping stdout clean.
### Search with agent mode
```bash
mem0 search "preferences" --user-id alice --agent
```
Output (stdout):
```json
{
"status": "success",
"command": "search",
"duration_ms": 187,
"scope": { "user_id": "alice" },
"count": 2,
"error": null,
"data": [
{ "id": "mem-abc", "memory": "User prefers dark mode", "score": 0.95, "created_at": "2025-01-15T10:00:00Z", "categories": ["preferences"] },
{ "id": "mem-def", "memory": "User likes monospace fonts", "score": 0.82, "created_at": "2025-01-15T10:01:00Z", "categories": ["preferences"] }
]
}
```
### Add with agent mode
```bash
mem0 add "Uses Python 3.12" --user-id alice --json
```
### Error handling in agent mode
Errors also return valid JSON with `"status": "error"`:
```bash
mem0 search "test" --user-id alice --api-key invalid --agent
```
Output:
```json
{
"status": "error",
"command": "search",
"error": "Authentication failed. Your API key may be invalid or expired.",
"data": null
}
```
---
## JSON Output + jq
Use `--output json` (or `-o json`) for raw JSON output, then pipe to `jq` for processing.
### Extract just memory text
```bash
mem0 list --user-id alice --output json | jq '.[] | .memory'
```
### Get memory IDs
```bash
mem0 list --user-id alice -o json | jq '.[].id'
```
### Count memories
```bash
mem0 list --user-id alice -o json | jq 'length'
```
### Filter by category in jq
```bash
mem0 list --user-id alice -o json | jq '[.[] | select(.categories[]? == "preferences")]'
```
### Extract search scores
```bash
mem0 search "tools" --user-id alice -o json | jq '.[] | {memory, score}'
```
---
## Bulk Operations
### Delete multiple memories by ID
```bash
# Get IDs, then delete each one
mem0 list --user-id alice -o json | jq -r '.[].id' | while read id; do
mem0 delete "$id" --force
done
```
### Bulk add from a text file (one memory per line)
```bash
while IFS= read -r line; do
mem0 add "$line" --user-id alice
done < memories.txt
```
### Copy memories between users
```bash
mem0 list --user-id alice -o json | jq -r '.[].memory' | while IFS= read -r mem; do
mem0 add "$mem" --user-id bob
done
```
### Export all memories to a file
```bash
mem0 list --user-id alice -o json > alice_memories.json
```
### Paginate through all results
```bash
page=1
while true; do
result=$(mem0 list --user-id alice -o json --page "$page" --page-size 100)
count=$(echo "$result" | jq 'length')
if [ "$count" -eq 0 ]; then
break
fi
echo "$result"
page=$((page + 1))
done
```
---
## CI/CD Patterns
### Store build context as a memory
```bash
mem0 add "Build #${BUILD_NUMBER} deployed ${APP_VERSION} to ${ENVIRONMENT} at $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--agent-id "ci-bot" \
--metadata "{\"build_number\":\"${BUILD_NUMBER}\",\"version\":\"${APP_VERSION}\",\"env\":\"${ENVIRONMENT}\"}"
```
### Retrieve deployment history
```bash
mem0 search "deployment to production" --agent-id ci-bot -o json -k 10
```
### Check CLI connectivity in CI
```bash
if mem0 status -o json | jq -e '.data.connected' > /dev/null 2>&1; then
echo "mem0 is connected"
else
echo "mem0 connection failed" >&2
exit 1
fi
```
### Non-interactive init in CI
```bash
mem0 init --api-key "$MEM0_API_KEY" --user-id ci-bot --force
```
Or simply use the environment variable (no init needed):
```bash
export MEM0_API_KEY="$MEM0_API_KEY"
mem0 add "CI run started" --user-id ci-bot
```
### Store test results
```bash
test_summary=$(cat test-results.txt | head -20)
mem0 add "$test_summary" --agent-id ci-bot --metadata '{"type":"test-results"}' --categories "ci,testing"
```
---
## Stdin Detection Details
The CLI reads from stdin only when ALL of these conditions are met:
1. No text argument was provided on the command line.
2. For `add`: no `--messages` and no `--file` flag.
3. For `update`: no `--metadata` flag.
4. stdin is piped (not a TTY).
**This means:**
- `mem0 add --user-id alice` in an interactive terminal will NOT hang waiting for input. It will print a usage error.
- `echo "text" | mem0 add --user-id alice` will read "text" from stdin.
- `mem0 add "explicit text" --user-id alice` will use the explicit text, even if stdin is piped.
**Reading method:**
- Python: `sys.stdin.read().strip()`
- Node: `fs.readFileSync(0, "utf-8").trim()`
---
## Common Shell Patterns
### Error handling with exit codes
```bash
set -e # Exit on error
# This will exit the script if the API key is invalid
mem0 status > /dev/null 2>&1
# Add with error check
if mem0 add "test memory" --user-id alice 2>/dev/null; then
echo "Memory added successfully"
else
echo "Failed to add memory" >&2
exit 1
fi
```
### Capture memory ID from add
```bash
# Use agent mode to get structured output
result=$(mem0 add "new fact" --user-id alice --agent 2>/dev/null)
memory_id=$(echo "$result" | jq -r '.data[0].id // empty')
if [ -n "$memory_id" ]; then
echo "Created memory: $memory_id"
fi
```
### Conditional memory addition
```bash
# Only add if search returns no results
count=$(mem0 search "dark mode" --user-id alice --agent 2>/dev/null | jq '.count // 0')
if [ "$count" -eq 0 ]; then
mem0 add "User prefers dark mode" --user-id alice
fi
```
### Quiet mode for scripts
```bash
# Suppress all output except errors
mem0 add "background note" --user-id alice --output quiet 2>/dev/null
mem0 delete --all --user-id temp-user --force --output quiet 2>/dev/null
```
### Using environment variables for scope
```bash
export MEM0_USER_ID="alice"
export MEM0_API_KEY="m0-xxx"
# All commands now default to user alice, no --user-id needed
mem0 add "prefers dark mode"
mem0 search "preferences"
mem0 list
```
### Timeout handling
The CLI uses a 30-second timeout for all API requests. For long-running scripts, handle timeouts:
```bash
if ! mem0 search "query" --user-id alice -o json 2>/dev/null; then
echo "Request failed or timed out" >&2
fi
```
---
## Processing Delay Workaround
Memories are processed asynchronously after `mem0 add`. If you need to search for a newly added memory immediately, add a short delay:
```bash
mem0 add "new preference" --user-id alice
sleep 3
mem0 search "new preference" --user-id alice
```
Or use the event system to poll for completion:
```bash
# Add and capture event ID from agent output
result=$(mem0 add "new preference" --user-id alice --agent 2>/dev/null)
event_id=$(echo "$result" | jq -r '.data[0].event_id // empty')
if [ -n "$event_id" ]; then
# Poll until processing completes
while true; do
status=$(mem0 event status "$event_id" --agent 2>/dev/null | jq -r '.data.status')
if [ "$status" = "SUCCEEDED" ] || [ "$status" = "FAILED" ]; then
break
fi
sleep 1
done
fi
```
---
## Multi-User Agent Pattern
For AI agents managing memories across multiple users:
```bash
#!/bin/bash
# agent_memory.sh -- manage memories for the current conversation
USER_ID="$1"
ACTION="$2"
shift 2
case "$ACTION" in
recall)
mem0 search "$*" --user-id "$USER_ID" --agent 2>/dev/null
;;
remember)
mem0 add "$*" --user-id "$USER_ID" --agent 2>/dev/null
;;
forget)
mem0 delete --all --user-id "$USER_ID" --force --agent 2>/dev/null
;;
history)
mem0 list --user-id "$USER_ID" --agent 2>/dev/null
;;
*)
echo '{"status":"error","error":"Unknown action: '"$ACTION"'"}' >&2
exit 1
;;
esac
```
Usage:
```bash
./agent_memory.sh alice recall "dietary preferences"
./agent_memory.sh alice remember "allergic to shellfish"
./agent_memory.sh alice history
```
+189
View File
@@ -0,0 +1,189 @@
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.
"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 the 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 the 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 any 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
Copyright 2024 Mem0.ai
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.
+87
View File
@@ -0,0 +1,87 @@
# Mem0 Vercel AI SDK Skill for Claude
Add persistent memory to any Vercel AI SDK application using [@mem0/vercel-ai-provider](https://www.npmjs.com/package/@mem0/vercel-ai-provider).
## What This Skill Does
When installed, Claude can:
- **Set up `@mem0/vercel-ai-provider`** in your TypeScript or Next.js project
- **Generate working code** using the wrapped model (`createMem0`) or standalone utilities (`retrieveMemories`, `addMemories`, etc.)
- **Configure multi-provider setups** (OpenAI, Anthropic, Google, Groq, Cohere)
- **Integrate memory** into streaming responses, structured output, and API routes
## Installation
### CLI (Claude Code, OpenCode, OpenClaw, or any tool that supports skills)
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
```
### Claude.ai
1. Download this `skills/mem0-vercel-ai-sdk` folder as a ZIP
2. Go to **Settings > Capabilities > Skills**
3. Click **Upload skill** and select the ZIP
### Claude API (Skills API)
```bash
curl -X POST https://api.anthropic.com/v1/skills \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mem0-vercel-ai-sdk", "source": "https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk"}'
```
### Prerequisites
- **Node.js 18+**
- **Vercel AI SDK v5** (`ai` package version 5.x)
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
- An LLM provider API key (OpenAI, Anthropic, Google, Groq, or Cohere)
- Set environment variables:
```bash
export MEM0_API_KEY="m0-xxx"
export OPENAI_API_KEY="sk-xxx" # or your chosen provider's key
```
## Quick Start
After installing, just ask Claude:
- "Add memory to my Vercel AI SDK app"
- "Set up mem0 with streamText in my Next.js API route"
- "Use retrieveMemories with Anthropic instead of the wrapped model"
- "Show me how to use graph memories with the Vercel AI provider"
- "Help me store conversation history with addMemories"
## What's Inside
```text
skills/mem0-vercel-ai-sdk/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
└── references/ # Documentation (loaded on demand)
├── provider-api.md # createMem0, Mem0Provider, types, config
├── memory-utilities.md # addMemories, retrieveMemories, getMemories, searchMemories
└── usage-patterns.md # Working examples: streaming, Next.js, multi-provider, graph
```
## Links
- [Mem0 Platform Dashboard](https://app.mem0.ai)
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
- [@mem0/vercel-ai-provider on npm](https://www.npmjs.com/package/@mem0/vercel-ai-provider)
- [Vercel AI SDK Documentation](https://ai-sdk.dev/docs)
## Skill Graph
This skill is part of the Mem0 skill graph. The three Mem0 skills (mem0, mem0-cli, mem0-vercel-ai-sdk) each cover a different interface to the same Mem0 Platform API.
## License
Apache-2.0
+193
View File
@@ -0,0 +1,193 @@
---
name: mem0-vercel-ai-sdk
description: >
Mem0 provider for Vercel AI SDK (@mem0/vercel-ai-provider).
TRIGGER when: user mentions "vercel ai sdk", "@mem0/vercel-ai-provider",
"createMem0", "retrieveMemories", "addMemories", "getMemories",
"searchMemories", "mem0 vercel", "AI SDK provider", "AI SDK memory",
or is using generateText/streamText with mem0. Also triggers for Next.js
apps needing memory-augmented AI.
DO NOT TRIGGER when: user asks about direct Python/TS SDK calls without Vercel
(use mem0 skill), or CLI terminal commands (use mem0-cli skill).
license: Apache-2.0
metadata:
author: mem0ai
version: "1.0.0"
category: ai-memory
tags: "vercel, ai-sdk, memory, nextjs, typescript, provider"
compatibility: Node.js 18+, npm install @mem0/vercel-ai-provider, Vercel AI SDK v5 (ai package), MEM0_API_KEY + LLM provider API key
---
# Mem0 Vercel AI SDK Provider
Memory-enhanced AI provider for Vercel AI SDK. Automatically retrieves and stores memories during LLM calls.
## Step 1: Install
```bash
npm install @mem0/vercel-ai-provider ai
```
## Step 2: Set up environment variables
```bash
export MEM0_API_KEY="m0-xxx"
export OPENAI_API_KEY="sk-xxx" # or ANTHROPIC_API_KEY, GOOGLE_API_KEY, etc.
```
Get a Mem0 API key at: https://app.mem0.ai/dashboard/api-keys
## Pattern 1: Wrapped Model
The wrapped model approach is the simplest. `createMem0` returns a provider that wraps any supported LLM with automatic memory retrieval and storage.
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "Recommend a restaurant",
});
```
What happens under the hood:
1. The prompt is sent to Mem0 search (`POST /v2/memories/search/`) to retrieve relevant memories
2. Retrieved memories are injected as a system message at the start of the prompt
3. The underlying LLM (e.g., OpenAI gpt-4-turbo) generates a response using the enriched prompt
4. The conversation is stored back to Mem0 (`POST /v1/memories/`) as a fire-and-forget async call (no await)
## Pattern 2: Standalone Utilities
Use standalone utilities when you want full control over the memory retrieve/store cycle, or you want to use a provider that is already configured separately.
```typescript
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
const prompt = "Recommend a restaurant";
// Retrieve memories -- returns a formatted system prompt string
const memories = await retrieveMemories(prompt, {
user_id: "alice",
mem0ApiKey: "m0-xxx",
});
// Generate using any provider with injected memories
const { text } = await generateText({
model: openai("gpt-4-turbo"),
prompt,
system: memories,
});
// Optionally store the conversation back
await addMemories(
[
{ role: "user", content: [{ type: "text", text: prompt }] },
{ role: "assistant", content: [{ type: "text", text }] },
],
{ user_id: "alice", mem0ApiKey: "m0-xxx" }
);
```
## Pattern 3: Streaming
Use `streamText` for streaming responses with memory augmentation:
```typescript
import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const result = streamText({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "What should I cook for dinner?",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
The wrapped model handles memory retrieval before streaming begins and stores the conversation after.
## Supported Providers
| Provider | Config value | Required env var |
|----------|-------------|------------------|
| OpenAI (default) | `"openai"` | `OPENAI_API_KEY` |
| Anthropic | `"anthropic"` | `ANTHROPIC_API_KEY` |
| Google | `"google"` | `GOOGLE_GENERATIVE_AI_API_KEY` |
| Groq | `"groq"` | `GROQ_API_KEY` |
| Cohere | `"cohere"` | `COHERE_API_KEY` |
Select a provider when creating the Mem0 instance:
```typescript
const mem0 = createMem0({ provider: "anthropic" });
const { text } = await generateText({
model: mem0("claude-sonnet-4-20250514", { user_id: "alice" }),
prompt: "Hello!",
});
```
## How It Works Internally
### Wrapped model flow
```
User prompt
--> searchInternalMemories (POST /v2/memories/search/)
--> memories injected as system message at start of prompt
--> underlying LLM generates response (doGenerate or doStream)
--> processMemories fires addMemories as fire-and-forget (no await)
--> response returned to caller
```
### Standalone flow
```
User controls each step:
1. retrieveMemories / getMemories / searchMemories -> fetch memories
2. inject into system prompt manually
3. call generateText / streamText with any provider
4. addMemories -> store new conversation to Mem0
```
## Key Differences Between the 4 Utility Functions
| Function | Returns | Use when |
|----------|---------|----------|
| `retrieveMemories` | Formatted system prompt **string** | Injecting directly into `system` parameter |
| `getMemories` | Raw memory **array** (or full response if `enable_graph`) | Processing memories programmatically |
| `searchMemories` | Full search **response** (results + relations) | Need relations, scores, metadata |
| `addMemories` | API response | Storing new messages to Mem0 |
All four accept `LanguageModelV2Prompt | string` as the first argument and optional `Mem0ConfigSettings` as the second.
## Common Edge Cases and Tips
- **Always provide `user_id`** (or `agent_id`/`app_id`/`run_id`) for consistent memory retrieval. Without an entity identifier, memories cannot be scoped.
- **Standalone utilities require explicit API key**: pass `mem0ApiKey` in the config object, or set the `MEM0_API_KEY` environment variable.
- **Graph memories**: set `enable_graph: true` in the config to retrieve graph relations alongside text memories. When enabled, `getMemories` returns the full response (with `results` and `relations`), not just the array.
- **This uses Vercel AI SDK v5** (LanguageModelV2 / ProviderV2 interfaces). It is not compatible with AI SDK v3 or v4.
- **`processMemories` fires `addMemories` as fire-and-forget** (`.then()` without `await`). Memory storage happens asynchronously and does not block the LLM response.
- **The `"gemini"` alias** exists in the provider switch but is NOT in the `supportedProviders` list. Use `"google"` instead.
- **Custom host**: set `host` in the config to point to a different Mem0 API endpoint (default: `https://api.mem0.ai`).
## References
| Topic | File |
|-------|------|
| Provider API (`createMem0`, `Mem0Provider`, types) | [local](references/provider-api.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk/references/provider-api.md) |
| Memory utilities (`addMemories`, `retrieveMemories`, etc.) | [local](references/memory-utilities.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk/references/memory-utilities.md) |
| Usage patterns and examples | [local](references/usage-patterns.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk/references/usage-patterns.md) |
## Related Mem0 Skills
| Skill | When to use | Link |
|-------|-------------|------|
| mem0 | Python/TypeScript SDK, REST API, framework integrations | [local](../mem0/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0) |
| mem0-cli | Terminal commands, scripting, CI/CD, agent tool loops | [local](../mem0-cli/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli) |
@@ -0,0 +1,284 @@
# Memory Utilities Reference
Complete reference for standalone utility functions exported from `@mem0/vercel-ai-provider`. These functions give you manual control over memory retrieval and storage, independent of the wrapped model pattern.
Source: `vercel-ai-sdk/src/mem0-utils.ts`
## `addMemories(messages, config?)`
Stores messages to Mem0 as new memories.
```typescript
import { addMemories } from "@mem0/vercel-ai-provider";
await addMemories(
[
{ role: "user", content: [{ type: "text", text: "I love Italian food" }] },
{ role: "assistant", content: [{ type: "text", text: "Noted! I'll remember that." }] },
],
{ user_id: "alice", mem0ApiKey: "m0-xxx" }
);
```
**Signature:**
```typescript
async function addMemories(
messages: LanguageModelV2Prompt | string,
config?: Mem0ConfigSettings
): Promise<any>;
```
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `LanguageModelV2Prompt \| string` | Messages to store. If a string, wrapped as `[{ role: "user", content: string }]` |
| `config` | `Mem0ConfigSettings` | Optional. Must include entity scope (`user_id`, etc.) and API key |
**Behavior:**
1. If `messages` is a string, wraps it as a single user message
2. Otherwise, converts `LanguageModelV2Prompt` to Mem0 format via `convertToMem0Format` (handles multimodal content)
3. Calls `POST /v1/memories/` with the converted messages and config
**Returns:** The API response from Mem0 (memory operation result).
---
## `retrieveMemories(prompt, config?)`
Retrieves memories and returns a **formatted system prompt string** ready to inject into a `system` parameter.
```typescript
import { retrieveMemories } from "@mem0/vercel-ai-provider";
const systemPrompt = await retrieveMemories("What restaurants do I like?", {
user_id: "alice",
mem0ApiKey: "m0-xxx",
});
// Returns: "System Message: These are the memories I have stored... Memory: User loves Italian food\n\n ..."
```
**Signature:**
```typescript
async function retrieveMemories(
prompt: LanguageModelV2Prompt | string,
config?: Mem0ConfigSettings
): Promise<string>;
```
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `prompt` | `LanguageModelV2Prompt \| string` | The query to search memories for |
| `config` | `Mem0ConfigSettings` | Optional. Entity scope and API key |
**Behavior:**
1. Flattens the prompt to a plain string (extracts text from `LanguageModelV2Prompt` parts)
2. Calls `searchInternalMemories` (`POST /v2/memories/search/`)
3. Formats each memory as `"Memory: {memory.memory}\n\n"`
4. If `enable_graph: true`, also appends graph relations as `"Relation: {source} -> {relationship} -> {target}\n\n"`
5. Wraps everything in a system prompt preamble
**Returns:** A **string** containing the formatted system prompt with embedded memories. Returns `""` (empty string) if no memories found.
**Output format:**
```
System Message: These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The System prompt starts after text System Message:
Memory: User loves Italian food
Memory: User is vegetarian
HERE ARE THE GRAPHS RELATIONS FOR THE PREFERENCES OF THE USER:
Relation: Alice -> likes -> Italian cuisine
```
---
## `getMemories(prompt, config?)`
Retrieves memories and returns the **raw memory array** (or full response if graph is enabled).
```typescript
import { getMemories } from "@mem0/vercel-ai-provider";
const memories = await getMemories("What are my preferences?", {
user_id: "alice",
mem0ApiKey: "m0-xxx",
});
// Returns: [{ memory: "User loves Italian food", id: "...", ... }, ...]
// With graph enabled:
const graphMemories = await getMemories("What are my preferences?", {
user_id: "alice",
mem0ApiKey: "m0-xxx",
enable_graph: true,
});
// Returns: { results: [...], relations: [...] }
```
**Signature:**
```typescript
async function getMemories(
prompt: LanguageModelV2Prompt | string,
config?: Mem0ConfigSettings
): Promise<any>;
```
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `prompt` | `LanguageModelV2Prompt \| string` | The query to search memories for |
| `config` | `Mem0ConfigSettings` | Optional. Entity scope and API key |
**Behavior:**
1. Flattens the prompt to a plain string
2. Calls `searchInternalMemories` (`POST /v2/memories/search/`)
3. If `enable_graph` is **not** set: returns `memories.results` (the array of memory objects)
4. If `enable_graph` is set: returns the full response object (with both `results` and `relations`)
**Returns:** Memory object array, or full response object when graph is enabled.
---
## `searchMemories(prompt, config?)`
Retrieves the **full search API response** including results, relations, scores, and metadata.
```typescript
import { searchMemories } from "@mem0/vercel-ai-provider";
const response = await searchMemories("cooking preferences", {
user_id: "alice",
mem0ApiKey: "m0-xxx",
});
// Returns: { results: [{ memory: "...", score: 0.95, ... }], relations: [...] }
```
**Signature:**
```typescript
async function searchMemories(
prompt: LanguageModelV2Prompt | string,
config?: Mem0ConfigSettings
): Promise<any>;
```
**Parameters:**
| Parameter | Type | Description |
|-----------|------|-------------|
| `prompt` | `LanguageModelV2Prompt \| string` | The query to search memories for |
| `config` | `Mem0ConfigSettings` | Optional. Entity scope and API key |
**Behavior:**
1. Flattens the prompt to a plain string
2. Calls `searchInternalMemories` (`POST /v2/memories/search/`)
3. Returns the full response without any filtering
**Returns:** The complete API response object. On error, returns `[]`.
**Note:** Unlike `getMemories`, this always returns the full response regardless of `enable_graph` setting.
---
## When to Use Which Function
| Function | Returns | Use when |
|----------|---------|----------|
| `retrieveMemories` | Formatted system prompt **string** | Injecting directly into a `system` parameter for `generateText`/`streamText` |
| `getMemories` | Memory **array** (or full response if `enable_graph`) | Processing memories programmatically (filtering, transforming, counting) |
| `searchMemories` | Full API **response** (results + relations) | Need relations, similarity scores, or complete metadata regardless of graph setting |
| `addMemories` | API response | Storing new conversation messages as memories |
## Internal: `searchInternalMemories(query, config?, top_k?)`
Not exported. Used by all retrieval functions.
```typescript
async function searchInternalMemories(
query: string,
config?: Mem0ConfigSettings,
top_k: number = 5
): Promise<any>;
```
**Behavior:**
1. Builds an `OR` filter from entity identifiers (`user_id`, `app_id`, `agent_id`, `run_id`)
2. Resolves org/project identifiers (`org_id` takes precedence over `org_name`)
3. Loads the API key from `config.mem0ApiKey` or `MEM0_API_KEY` env var
4. Calls `POST {host}/v2/memories/search/` with:
- `query`: the search string
- `filters`: the OR filter object
- `top_k`: from config or default 5
- `version`: `"v2"`
- `output_format`: `"v1.1"`
- All other config fields spread into the request body
**Default host:** `https://api.mem0.ai`
## Internal: `convertToMem0Format(messages)`
Not exported. Used by `addMemories` to convert `LanguageModelV2Prompt` messages to Mem0's format.
**Multimodal content mapping:**
| Input type | Input format | Output type | Output format |
|-----------|-------------|-------------|---------------|
| Text | `{ type: "text", text: "..." }` | Plain string | `{ role, content: "..." }` |
| Image | `{ type: "image_url", image_url: { url } }` or `{ type: "image", ... }` | Image URL | `{ role, content: { type: "image_url", image_url: { url } } }` |
| PDF file | `{ type: "file", data: url, mediaType: "application/pdf" }` | PDF URL | `{ role, content: { type: "pdf_url", pdf_url: { url } } }` |
| Markdown file | `{ type: "file", data: url, mediaType: "text/markdown" }` or `"application/mdx"` | MDX URL | `{ role, content: { type: "mdx_url", mdx_url: { url } } }` |
| Image file | `{ type: "file", data: url, mediaType: "image/*" }` | Image URL | `{ role, content: { type: "image_url", image_url: { url } } }` |
| MDX content | `{ type: "mdx_url", mdx_url: { url } }` or `{ type: "mdx", ... }` | MDX URL | `{ role, content: { type: "mdx_url", mdx_url: { url } } }` |
| PDF content | `{ type: "pdf_url", pdf_url: { url } }` or `{ type: "pdf", ... }` | PDF URL | `{ role, content: { type: "pdf_url", pdf_url: { url } } }` |
The function handles three message content shapes:
1. **String content**: passed through directly
2. **Array content**: each element mapped individually, nulls filtered out
3. **Single object content**: mapped as a single element
## Internal: `flattenPrompt(prompt)`
Not exported. Extracts plain text from `LanguageModelV2Prompt` for use as a search query.
- Iterates over prompt parts, extracting text from `user` role messages
- For `text` type content: extracts `.text`
- For `file` type content: returns descriptive placeholders (`[PDF document]`, `[Markdown document]`, `[Image]`, `[File attachment]`)
- For other content types: returns `[multimodal content]`
- Joins all parts with spaces
## `Mem0ConfigSettings` Fields Reference
All fields are optional. Used across all utility functions.
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `user_id` | `string` | -- | Scope memories to a user |
| `app_id` | `string` | -- | Scope memories to an application |
| `agent_id` | `string` | -- | Scope memories to an agent |
| `run_id` | `string` | -- | Scope memories to a session/run |
| `org_name` | `string` | -- | Organization name (fallback if `org_id` not set) |
| `project_name` | `string` | -- | Project name (fallback if `org_id` not set) |
| `org_id` | `string` | -- | Organization ID (takes precedence) |
| `project_id` | `string` | -- | Project ID |
| `metadata` | `Record<string, any>` | -- | Custom metadata |
| `filters` | `Record<string, any>` | -- | Custom search filters |
| `infer` | `boolean` | -- | Enable inference |
| `page` | `number` | -- | Pagination page number |
| `page_size` | `number` | -- | Results per page |
| `mem0ApiKey` | `string` | `MEM0_API_KEY` env | Mem0 API key |
| `top_k` | `number` | `5` | Number of memories to retrieve |
| `threshold` | `number` | -- | Minimum similarity score |
| `rerank` | `boolean` | -- | Enable re-ranking |
| `enable_graph` | `boolean` | -- | Enable graph memory (relations) |
| `host` | `string` | `https://api.mem0.ai` | Custom API host |
| `output_format` | `string` | -- | Output format version |
| `filter_memories` | `boolean` | -- | Enable memory filtering |
| `async_mode` | `boolean` | -- | Enable async processing |
@@ -0,0 +1,239 @@
# Provider API Reference
Complete reference for the `@mem0/vercel-ai-provider` provider layer. Source: `vercel-ai-sdk/src/`.
## `createMem0(options?)`
Factory function that creates a `Mem0Provider` instance. This is the primary entry point for the wrapped model approach.
```typescript
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0(); // defaults: provider "openai"
const mem0 = createMem0({ provider: "anthropic" }); // use Anthropic as LLM backend
```
**Signature:**
```typescript
function createMem0(options?: Mem0ProviderSettings): Mem0Provider;
```
When called with no arguments, defaults to `{ provider: "openai" }`.
**Returns:** `Mem0Provider` -- a callable function that also exposes `.chat()`, `.completion()`, and `.languageModel()` methods.
## `Mem0Provider` Interface
Implements `ProviderV2` from `@ai-sdk/provider`.
```typescript
interface Mem0Provider extends ProviderV2 {
// Call directly as a function
(modelId: Mem0ChatModelId, settings?: Mem0ChatSettings): LanguageModelV2;
// Or use named methods
chat(modelId: Mem0ChatModelId, settings?: Mem0ChatSettings): LanguageModelV2;
completion(modelId: Mem0ChatModelId, settings?: Mem0ChatSettings): LanguageModelV2;
languageModel(modelId: Mem0ChatModelId, settings?: Mem0ChatSettings): LanguageModelV2;
}
```
- **Direct call** (`mem0("gpt-4-turbo", {...})`): creates a generic language model (neither chat nor completion mode forced).
- **`chat()`**: creates a model with `modelType: "chat"` (note: in the current source, the chat constructor sets `modelType: "completion"` -- this appears to be a bug; functionally equivalent to `completion()` at present).
- **`completion()`**: creates a model with `modelType: "completion"`.
- **`languageModel()`**: alias for the generic model (same as direct call).
All three return a `Mem0GenericLanguageModel` instance implementing `LanguageModelV2`.
## `Mem0ProviderSettings` Interface
Configuration passed to `createMem0()`.
```typescript
interface Mem0ProviderSettings {
baseURL?: string; // Base URL for the LLM provider (default: "http://api.openai.com")
headers?: Record<string, string>; // Custom headers for LLM requests
provider?: string; // LLM provider name (default: "openai")
mem0ApiKey?: string; // Mem0 Platform API key (or use MEM0_API_KEY env var)
apiKey?: string; // LLM provider API key (e.g., OpenAI key)
mem0Config?: Mem0Config; // Default Mem0 config (user_id, etc.) applied to all calls
config?: LLMProviderSettings; // Provider-specific settings (OpenAI, Anthropic, etc.)
fetch?: typeof fetch; // Custom fetch implementation (for testing/middleware)
generateId?: () => string; // Custom ID generator (internal use)
name?: string; // Provider instance name
modelType?: "completion" | "chat"; // Force model type
}
```
### Key fields explained
| Field | Purpose | Example |
|-------|---------|---------|
| `provider` | Which LLM backend to use | `"openai"`, `"anthropic"`, `"google"`, `"groq"`, `"cohere"` |
| `mem0ApiKey` | Mem0 Platform API key | `"m0-xxx"` |
| `apiKey` | LLM provider API key | `"sk-xxx"` (OpenAI), `"sk-ant-xxx"` (Anthropic) |
| `mem0Config` | Default Mem0 settings for all calls | `{ user_id: "alice", enable_graph: true }` |
| `config` | Provider-specific SDK settings | `{ organization: "org-xxx" }` for OpenAI |
| `baseURL` | Override LLM provider base URL | `"https://my-proxy.example.com"` |
## `mem0` Singleton
A pre-configured instance using default settings (OpenAI provider, no API keys set -- relies on env vars).
```typescript
import { mem0 } from "@mem0/vercel-ai-provider";
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "Hello",
});
```
Equivalent to `createMem0()` with no arguments.
## `Mem0ConfigSettings` Interface
Configuration for memory operations. Used as `Mem0ChatSettings` (per-call) or `Mem0Config` (provider-level default). All fields are optional.
```typescript
interface Mem0ConfigSettings {
user_id?: string; // Scope memories to a specific user
app_id?: string; // Scope memories to an application
agent_id?: string; // Scope memories to an agent
run_id?: string; // Scope memories to a specific run/session
org_name?: string; // Organization name (used if org_id not set)
project_name?: string; // Project name (used if org_id not set)
org_id?: string; // Organization ID (takes precedence over org_name)
project_id?: string; // Project ID (takes precedence over project_name)
metadata?: Record<string, any>; // Custom metadata attached to memories
filters?: Record<string, any>; // Custom filters for memory search
infer?: boolean; // Enable inference during memory operations
page?: number; // Pagination: page number
page_size?: number; // Pagination: results per page
mem0ApiKey?: string; // Mem0 API key (overrides provider-level key)
top_k?: number; // Number of memories to retrieve (default: 5)
threshold?: number; // Minimum similarity score for retrieval
rerank?: boolean; // Enable re-ranking of search results
enable_graph?: boolean; // Enable graph memory (returns relations)
host?: string; // Custom Mem0 API host (default: "https://api.mem0.ai")
output_format?: string; // Output format version
filter_memories?: boolean; // Enable memory filtering
async_mode?: boolean; // Enable async processing of memory operations
}
```
## `Mem0ChatConfig` Type
Combined type used internally by the language model. Merges memory config with provider config.
```typescript
interface Mem0ChatConfig extends Mem0ConfigSettings, Mem0ProviderSettings {}
```
This means a `Mem0ChatConfig` has all fields from both `Mem0ConfigSettings` and `Mem0ProviderSettings`.
## `Mem0ChatSettings` Type
Alias for `Mem0ConfigSettings`. Passed as the second argument when creating a model:
```typescript
mem0("gpt-4-turbo", { user_id: "alice", enable_graph: true })
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// This object is Mem0ChatSettings
```
## `LLMProviderSettings` Type
Union of provider-specific settings. Extends all supported provider setting interfaces:
```typescript
interface LLMProviderSettings extends
OpenAIProviderSettings,
AnthropicProviderSettings,
CohereProviderSettings,
GroqProviderSettings {}
```
Pass via the `config` field of `Mem0ProviderSettings` to forward settings to the underlying LLM provider SDK.
## Provider Selection: `Mem0ClassSelector`
Internal class that maps the `provider` string to the correct AI SDK provider.
```typescript
class Mem0ClassSelector {
static supportedProviders = ["openai", "anthropic", "cohere", "groq", "google"];
// ...
}
```
**Important:** The `"gemini"` alias exists in the provider switch statement (maps to `createGoogleGenerativeAI`) but is **NOT** in the `supportedProviders` list. The constructor validates against `supportedProviders`, so using `"gemini"` will throw `"Model not supported: gemini"`. Use `"google"` instead.
### Provider mapping
| Config value | SDK used | Factory function |
|-------------|----------|------------------|
| `"openai"` | `@ai-sdk/openai` | `createOpenAI` |
| `"anthropic"` | `@ai-sdk/anthropic` | `createAnthropic` |
| `"cohere"` | `@ai-sdk/cohere` | `createCohere` |
| `"groq"` | `@ai-sdk/groq` | `createGroq` |
| `"google"` | `@ai-sdk/google` | `createGoogleGenerativeAI` |
## `Mem0` Facade Class
An alternative exported class that creates models directly without the callable-function pattern.
```typescript
import { Mem0 } from "@mem0/vercel-ai-provider";
const mem0 = new Mem0({ provider: "openai" });
const chatModel = mem0.chat("gpt-4-turbo", { user_id: "alice" });
const completionModel = mem0.completion("gpt-3.5-turbo-instruct");
```
The facade defaults its base URL to `"http://127.0.0.1:11434/api"` (Ollama-style) rather than `"http://api.openai.com"`. It always uses `"openai"` as the provider for created models.
**Methods:**
- `chat(modelId, settings?)` -- creates a model with `modelType: "chat"`
- `completion(modelId, settings?)` -- creates a model with `modelType: "completion"`
## `Mem0GenericLanguageModel` Class
The core class implementing `LanguageModelV2`. Created by `createMem0` or the `Mem0` facade.
```typescript
class Mem0GenericLanguageModel implements LanguageModelV2 {
readonly specificationVersion = "v2";
readonly defaultObjectGenerationMode = "json";
readonly supportsImageUrls = false;
readonly supportedUrls: Record<string, RegExp[]> = { '*': [/.*/] };
provider: string; // e.g., "openai"
modelId: string; // e.g., "gpt-4-turbo"
settings: Mem0ChatSettings;
config: Mem0ChatConfig;
async doGenerate(options: LanguageModelV2CallOptions): Promise<...>;
async doStream(options: LanguageModelV2CallOptions): Promise<...>;
}
```
Both `doGenerate` and `doStream` follow the same internal flow:
1. Build `Mem0ConfigSettings` from `config.mem0Config` merged with `settings`
2. Call `processMemories`:
- Fire `addMemories` as fire-and-forget (no await, `.then().catch()`)
- Await `getMemories` to retrieve relevant memories
- Format memories as a system message and prepend to the prompt
3. Create the underlying LLM model via `Mem0ClassSelector`
4. Delegate to the underlying model's `doGenerate` or `doStream`
5. Return the result
## Type: `Mem0ChatModelId`
```typescript
type Mem0ChatModelId = string & NonNullable<unknown>;
```
Any non-null string. The model ID is passed through to the underlying provider (e.g., `"gpt-4-turbo"`, `"claude-sonnet-4-20250514"`, `"gemini-pro"`).
@@ -0,0 +1,431 @@
# Usage Patterns and Examples
Working examples for `@mem0/vercel-ai-provider`. All examples assume environment variables `MEM0_API_KEY` and the relevant LLM provider API key are set.
## 1. Wrapped Model with generateText (Basic)
The simplest way to add memory to any LLM call.
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "Recommend a restaurant based on my preferences",
});
console.log(text);
```
Memories are automatically retrieved before the call and stored after.
## 2. Wrapped Model with streamText (Streaming)
Stream responses with automatic memory augmentation.
```typescript
import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const result = streamText({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "What should I cook for dinner tonight?",
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
Memory retrieval happens before streaming begins. The conversation is stored to Mem0 as a fire-and-forget call (non-blocking).
## 3. Standalone Utilities with OpenAI
Full control over the memory lifecycle using standalone functions with OpenAI.
```typescript
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
const userId = "alice";
const prompt = "Suggest a weekend trip";
// Step 1: Retrieve memories as a formatted system prompt
const memories = await retrieveMemories(prompt, {
user_id: userId,
});
// Step 2: Generate with the memories injected as system context
const { text } = await generateText({
model: openai("gpt-4-turbo"),
prompt,
system: memories,
});
console.log(text);
// Step 3: Store the conversation as new memories
await addMemories(
[
{ role: "user", content: [{ type: "text", text: prompt }] },
{ role: "assistant", content: [{ type: "text", text }] },
],
{ user_id: userId }
);
```
## 4. Standalone Utilities with Anthropic
Same pattern, different LLM provider.
```typescript
import { anthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
const prompt = "Help me plan my exercise routine";
const config = { user_id: "bob" };
const memories = await retrieveMemories(prompt, config);
const { text } = await generateText({
model: anthropic("claude-sonnet-4-20250514"),
prompt,
system: memories,
});
await addMemories(
[
{ role: "user", content: [{ type: "text", text: prompt }] },
{ role: "assistant", content: [{ type: "text", text }] },
],
config
);
```
## 5. Structured Output with generateObject
Use with `generateObject` for typed, structured responses enriched with memory.
```typescript
import { generateObject } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
import { z } from "zod";
const mem0 = createMem0();
const { object } = await generateObject({
model: mem0("gpt-4-turbo", { user_id: "alice" }),
prompt: "Suggest a meal plan for today",
schema: z.object({
breakfast: z.string(),
lunch: z.string(),
dinner: z.string(),
snacks: z.array(z.string()),
notes: z.string().describe("Personalization notes based on known preferences"),
}),
});
console.log(object);
// { breakfast: "Avocado toast (you mentioned loving it)", lunch: "...", ... }
```
The `defaultObjectGenerationMode` is `"json"`, so structured output works out of the box.
## 6. Graph Memory Enabled
Retrieve both text memories and entity relationship graphs.
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-4-turbo", {
user_id: "alice",
enable_graph: true,
}),
prompt: "What connections do you know about between my friends?",
});
console.log(text);
```
With `enable_graph: true`, the system prompt includes both:
- **Text memories**: `"Memory: Alice is friends with Bob"`
- **Graph relations**: `"Relation: Alice -> friends_with -> Bob"`
### Using graph with standalone utilities
```typescript
import { getMemories, searchMemories } from "@mem0/vercel-ai-provider";
// getMemories with graph returns the full response
const graphResult = await getMemories("my social connections", {
user_id: "alice",
enable_graph: true,
});
console.log(graphResult.results); // memory objects
console.log(graphResult.relations); // graph relations
// searchMemories always returns the full response
const fullResponse = await searchMemories("my social connections", {
user_id: "alice",
});
console.log(fullResponse.results);
console.log(fullResponse.relations);
```
## 7. Multi-Provider Setup
Configure different LLM providers with the wrapped model.
### OpenAI (default)
```typescript
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0(); // defaults to "openai"
const model = mem0("gpt-4-turbo", { user_id: "alice" });
```
### Anthropic
```typescript
const mem0 = createMem0({ provider: "anthropic" });
const model = mem0("claude-sonnet-4-20250514", { user_id: "alice" });
```
### Google
```typescript
const mem0 = createMem0({ provider: "google" });
const model = mem0("gemini-2.0-flash", { user_id: "alice" });
```
### Groq
```typescript
const mem0 = createMem0({ provider: "groq" });
const model = mem0("llama-3.3-70b-versatile", { user_id: "alice" });
```
### Cohere
```typescript
const mem0 = createMem0({ provider: "cohere" });
const model = mem0("command-r-plus", { user_id: "alice" });
```
### With explicit API keys (no env vars)
```typescript
const mem0 = createMem0({
provider: "openai",
apiKey: "sk-xxx", // OpenAI API key
mem0ApiKey: "m0-xxx", // Mem0 API key
});
```
## 8. Next.js API Route Integration
A POST handler that uses the wrapped model in a Next.js App Router API route.
```typescript
// app/api/chat/route.ts
import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
export async function POST(req: Request) {
const { messages, userId } = await req.json();
const lastMessage = messages[messages.length - 1];
const result = streamText({
model: mem0("gpt-4-turbo", { user_id: userId }),
prompt: lastMessage.content,
});
return result.toDataStreamResponse();
}
```
### With standalone utilities for more control
```typescript
// app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
export async function POST(req: Request) {
const { messages, userId } = await req.json();
const lastMessage = messages[messages.length - 1];
// Retrieve relevant memories
const memories = await retrieveMemories(lastMessage.content, {
user_id: userId,
});
// Stream the response
const result = streamText({
model: openai("gpt-4-turbo"),
prompt: lastMessage.content,
system: memories,
});
// Store conversation in the background (fire-and-forget)
result.text.then(async (text) => {
await addMemories(
[
{ role: "user", content: [{ type: "text", text: lastMessage.content }] },
{ role: "assistant", content: [{ type: "text", text }] },
],
{ user_id: userId }
);
});
return result.toDataStreamResponse();
}
```
## 9. How Memory Processing Works Internally
### Wrapped model flow (doGenerate / doStream)
```
1. doGenerate(options) or doStream(options) is called
2. processMemories(messagesPrompts, mem0Config):
a. addMemories(messagesPrompts, mem0Config)
--> fire-and-forget: .then().catch(), NO await
--> POST /v1/memories/ with converted messages
b. await getMemories(messagesPrompts, mem0Config)
--> POST /v2/memories/search/ with flattened prompt
--> returns memory array (or {results, relations} if enable_graph)
c. Format memories into system message string
d. Prepend system message to messagesPrompts array
e. Return { memories, messagesPrompts }
3. Create underlying LLM via Mem0ClassSelector.createProvider()
4. Call model.doGenerate(updatedOptions) or model.doStream(updatedOptions)
5. Return result
```
**Critical detail:** The `addMemories` call in step 2a is **NON-BLOCKING**. It uses `.then().catch()` without `await`, meaning:
- Memory storage happens asynchronously in the background
- The LLM response is not delayed by the memory write
- If the memory write fails, it logs an error but does not affect the response
- There is a brief window where the latest conversation is not yet stored
### Memory injection format
The memories are injected as a system message at position 0 of the prompt array:
```typescript
{
role: "system",
content: "System Message: These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The System prompt starts after text System Message: \n\n Memory: ... \n\n Memory: ... \n\n"
}
```
## 10. Custom Configuration
### Custom Mem0 API host
```typescript
const mem0 = createMem0({
mem0Config: {
host: "https://my-mem0-instance.example.com",
},
});
```
Or with standalone utilities:
```typescript
const memories = await retrieveMemories(prompt, {
user_id: "alice",
host: "https://my-mem0-instance.example.com",
});
```
### Organization and project scoping
```typescript
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-4-turbo", {
user_id: "alice",
org_id: "org-123",
project_id: "proj-456",
}),
prompt: "Hello",
});
```
Note: `org_id` takes precedence over `org_name`. If `org_id` is set, `org_name` and `project_name` are not sent in the request.
### Memory filtering and ranking
```typescript
const mem0 = createMem0();
const model = mem0("gpt-4-turbo", {
user_id: "alice",
top_k: 10, // retrieve up to 10 memories (default: 5)
threshold: 0.8, // only memories with score >= 0.8
rerank: true, // enable re-ranking of results
filter_memories: true,
});
```
### Provider-specific configuration
Pass SDK-specific settings via the `config` field:
```typescript
const mem0 = createMem0({
provider: "openai",
config: {
organization: "org-xxx",
project: "proj-xxx",
},
});
```
### Default Mem0 config for all calls
Set defaults at the provider level that apply to every model created:
```typescript
const mem0 = createMem0({
mem0Config: {
user_id: "alice",
enable_graph: true,
top_k: 10,
},
});
// These calls inherit user_id, enable_graph, and top_k from mem0Config
const { text } = await generateText({
model: mem0("gpt-4-turbo"),
prompt: "Hello",
});
```
Per-call settings (passed as the second argument to `mem0()`) are merged on top of `mem0Config`, so you can override specific fields:
```typescript
// Override user_id for this specific call
const model = mem0("gpt-4-turbo", { user_id: "bob" });
```
The merge order is: `config.mem0Config` (provider defaults) < `settings` (per-call overrides).
+10 -4
View File
@@ -1,13 +1,15 @@
# Mem0 Skill for Claude
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai).
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai) or the open-source self-hosted SDK.
> **Part of the Mem0 Skill Graph:** See also [mem0-cli](../mem0-cli/SKILL.md) (terminal) and [mem0-vercel-ai-sdk](../mem0-vercel-ai-sdk/SKILL.md) (Vercel AI SDK).
## What This Skill Does
When installed, Claude can:
- **Set up Mem0** in your Python or TypeScript project
- **Integrate memory** into your existing AI app (LangChain, CrewAI, Vercel AI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
- **Set up Mem0** in your Python or TypeScript project (Platform or OSS)
- **Integrate memory** into your existing AI app (LangChain, CrewAI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
- **Generate working code** using real API references and tested patterns
- **Search live docs** on demand for the latest Mem0 documentation
@@ -61,6 +63,10 @@ skills/mem0/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
├── client/ # Language-specific SDK references (Platform + OSS)
│ ├── python.md # Python SDK (MemoryClient + Memory OSS)
│ ├── node.md # TypeScript SDK (MemoryClient + Memory OSS)
│ └── differences.md # Python vs TypeScript comparison
├── scripts/
│ └── mem0_doc_search.py # Search live Mem0 docs on demand
└── references/ # Documentation (loaded on demand)
@@ -69,7 +75,7 @@ skills/mem0/
├── api-reference.md # REST endpoints, filters, memory object
├── architecture.md # Processing pipeline, lifecycle, scoping, performance
├── features.md # Retrieval, graph, categories, MCP, webhooks, multimodal
├── integration-patterns.md # LangChain, CrewAI, Vercel AI, LangGraph, LlamaIndex, etc.
├── integration-patterns.md # LangChain, CrewAI, OpenAI Agents, LangGraph, LlamaIndex, etc.
└── use-cases.md # 7 real-world patterns with Python + TypeScript code
```
+41 -15
View File
@@ -1,25 +1,34 @@
---
name: mem0
description: >
Integrate Mem0 Platform into AI applications for persistent memory, personalization, and semantic search.
Use this skill when the user mentions "mem0", "memory layer", "remember user preferences",
"persistent context", "personalization", or needs to add long-term memory to chatbots, agents,
or AI apps. Covers Python and TypeScript SDKs, framework integrations (LangChain, CrewAI,
Vercel AI SDK, OpenAI Agents SDK, Pipecat), and the full Platform API. Use even when the user
doesn't explicitly say "mem0" but describes needing conversation memory, user context retention,
or knowledge retrieval across sessions.
Mem0 Platform SDK for adding persistent memory to AI applications.
TRIGGER when: user mentions "mem0", "MemoryClient", "memory layer",
"remember user preferences", "persistent context", "personalization",
or needs to add long-term memory to chatbots, agents, or AI apps.
Covers Python SDK (mem0ai), TypeScript SDK (mem0ai), and framework integrations
(LangChain, CrewAI, OpenAI Agents SDK, Pipecat, LlamaIndex, AutoGen, LangGraph).
Also covers the open-source self-hosted Memory class.
This is the DEFAULT mem0 skill for ambiguous queries.
DO NOT TRIGGER when: user asks about CLI commands, terminal usage, or shell
scripts (use mem0-cli), or Vercel AI SDK / @mem0/vercel-ai-provider / createMem0
(use mem0-vercel-ai-sdk).
license: Apache-2.0
metadata:
author: mem0ai
version: "1.0.0"
version: "2.0.0"
category: ai-memory
tags: "memory, personalization, ai, python, typescript, vector-search"
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var, and internet access to api.mem0.ai
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var (Platform), and internet access to api.mem0.ai
---
# Mem0 Platform Integration
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy.
> **Skill Graph:** This skill is part of the Mem0 skill graph:
> - **mem0** (this skill) -- Platform Client SDK + OSS (Python + TypeScript)
> - **[mem0-cli](../mem0-cli/SKILL.md)** ([GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli)) -- Command-line interface
> - **[mem0-vercel-ai-sdk](../mem0-vercel-ai-sdk/SKILL.md)** ([GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk)) -- Vercel AI SDK provider
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy. For self-hosted usage, see the OSS section in the client references below.
## Step 1: Install and authenticate
@@ -134,14 +143,24 @@ def chat(user_input: str, user_id: str) -> str:
For the latest docs beyond what's in the references, use the doc search tool:
```bash
python scripts/mem0_doc_search.py --query "topic"
python scripts/mem0_doc_search.py --page "/platform/features/graph-memory"
python scripts/mem0_doc_search.py --index
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --query "topic"
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --page "/platform/features/graph-memory"
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --index
```
No API key needed — searches docs.mem0.ai directly.
## References
## Client SDK References
Language-specific deep references (Platform + OSS):
| Language | File |
|----------|------|
| Python (MemoryClient + AsyncMemoryClient + Memory OSS) | [client/python.md](client/python.md) |
| TypeScript/Node.js (MemoryClient + Memory OSS) | [client/node.md](client/node.md) |
| Python vs TypeScript differences | [client/differences.md](client/differences.md) |
## Platform References
Load these on demand for deeper detail:
@@ -152,5 +171,12 @@ Load these on demand for deeper detail:
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
| Framework integrations (LangChain, CrewAI, Vercel AI, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
| Framework integrations (LangChain, CrewAI, OpenAI Agents, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
## Related Mem0 Skills
| Skill | When to use | Link |
|-------|-------------|------|
| mem0-cli | Terminal commands, scripting, CI/CD, agent tool loops | [local](../mem0-cli/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli) |
| mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [local](../mem0-vercel-ai-sdk/SKILL.md) / [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
+125
View File
@@ -0,0 +1,125 @@
# Python vs TypeScript SDK Differences
Quick-reference cheatsheet for developers working across both Mem0 SDKs.
## Constructor
| Aspect | Python | TypeScript |
|--------|--------|------------|
| Import (Platform) | `from mem0 import MemoryClient` | `import MemoryClient from 'mem0ai'` |
| Import (OSS) | `from mem0 import Memory` | `import { Memory } from 'mem0ai/oss'` |
| Constructor | `MemoryClient(api_key="m0-xxx")` | `new MemoryClient({ apiKey: 'm0-xxx' })` |
| Required param | `api_key` (positional or kwarg) | `apiKey` (in options object) |
Both read from `MEM0_API_KEY` env var if no key provided.
## Method Naming
| Operation | Python | TypeScript |
|-----------|--------|------------|
| Add | `add()` | `add()` |
| Search | `search()` | `search()` |
| Get | `get()` | `get()` |
| Get all | `get_all()` | `getAll()` |
| Update | `update()` | `update()` |
| Delete | `delete()` | `delete()` |
| Delete all | `delete_all()` | `deleteAll()` |
| History | `history()` | `history()` |
| Batch update | `batch_update()` | `batchUpdate()` |
| Batch delete | `batch_delete()` | `batchDelete()` |
| List users | `users()` | `users()` |
| Delete users | `delete_users()` | `deleteUsers()` |
| Get project | `project.get()` | `getProject()` |
| Update project | `project.update()` | `updateProject()` |
| Create webhook | `create_webhook()` | `createWebhook()` |
| Get webhooks | `get_webhooks()` | `getWebhooks()` |
| Update webhook | `update_webhook()` | `updateWebhook()` |
| Delete webhook | `delete_webhook()` | `deleteWebhook()` |
| Create export | `create_memory_export()` | `createMemoryExport()` |
| Get export | `get_memory_export()` | `getMemoryExport()` |
| Feedback | `feedback()` | `feedback()` |
**Rule:** Python uses `snake_case`, TypeScript uses `camelCase` for method names.
## Parameter Passing
```python
# Python: kwargs
client.add(messages, user_id="alice", metadata={"source": "chat"})
client.search("query", user_id="alice", top_k=5, rerank=True)
```
```typescript
// TypeScript: options object
await client.add(messages, { user_id: 'alice', metadata: { source: 'chat' } });
await client.search('query', { user_id: 'alice', top_k: 5, rerank: true });
```
**Important:** Both use `snake_case` for API parameter names (`user_id`, `agent_id`, `top_k`, etc.). Only method names differ.
Exception: OSS TypeScript uses `camelCase` for config params (`userId`, `agentId`, `runId`).
## Architectural Differences
| Aspect | Python | TypeScript |
|--------|--------|------------|
| HTTP library | httpx | axios |
| Default timeout | 300s | 60s |
| Sync support | Yes (`MemoryClient`) | No (all async) |
| Async support | Yes (`AsyncMemoryClient`) | All methods are async |
| Project management | `client.project.*` (separate class) | `client.getProject()` / `client.updateProject()` |
| Context manager | `async with AsyncMemoryClient()` | Not supported |
## Platform Features: Python-only
These methods exist in Python but not TypeScript:
| Method | Description |
|--------|-------------|
| `get_summary(filters)` | Get summary of memories |
| `reset()` | Delete ALL data (users + memories) |
| `project.create(name)` | Create a new project |
| `project.delete()` | Delete current project |
| `project.get_members()` | List project members |
| `project.add_member(email, role)` | Add member to project |
| `project.update_member(email, role)` | Change member role |
| `project.remove_member(email)` | Remove member |
## Platform Features: TypeScript-only
| Method | Description |
|--------|-------------|
| `deleteUser(data)` | Convenience method for single entity deletion |
| `ping()` | Health check endpoint |
## OSS Config Naming
| Python config key | TypeScript config key |
|-------------------|----------------------|
| `vector_store` | `vectorStore` |
| `graph_store` | `graphStore` |
| `history_db_path` | `historyDbPath` |
| `custom_fact_extraction_prompt` | `customPrompt` |
| `enable_graph` | `enableGraph` |
## OSS Scope Parameter Naming
| Python | TypeScript |
|--------|------------|
| `user_id="alice"` | `userId: 'alice'` |
| `agent_id="bot"` | `agentId: 'bot'` |
| `run_id="session"` | `runId: 'session'` |
## Common Gotcha
When searching/filtering, **both SDKs use `snake_case`** for filter keys:
```python
# Python
filters = {"AND": [{"user_id": "alice"}, {"categories": {"contains": "health"}}]}
```
```typescript
// TypeScript -- same snake_case in filter objects!
const filters = { AND: [{ user_id: 'alice' }, { categories: { contains: 'health' } }] };
```
+412
View File
@@ -0,0 +1,412 @@
# Mem0 Node.js / TypeScript SDK Reference
Complete reference for the `mem0ai` npm package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
**Constructor:** `new MemoryClient({ apiKey })`. If `apiKey` is not provided, reads from `MEM0_API_KEY` environment variable.
- HTTP library: `axios`
- Timeout: 60 seconds
- Base URL: `https://api.mem0.ai`
- All methods are async (return `Promise`)
---
### Memory Methods
#### add(messages, options?)
Store new memories from messages.
```typescript
const messages = [
{ role: 'user', content: "I'm a vegetarian and allergic to nuts." },
{ role: 'assistant', content: "Got it! I'll remember that." },
];
await client.add(messages, { user_id: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `Message[]` | Array of `{role, content}` objects |
| `options.user_id` | string | User identifier |
| `options.agent_id` | string | Agent identifier |
| `options.app_id` | string | Application identifier |
| `options.run_id` | string | Session identifier |
| `options.metadata` | object | Custom key-value pairs |
| `options.enable_graph` | boolean | Activate knowledge graph |
| `options.infer` | boolean | If false, store raw text (default: true) |
| `options.immutable` | boolean | Prevent future modification |
| `options.expiration_date` | string | Auto-expiry (`YYYY-MM-DD`) |
| `options.includes` | string | Preference filter for inclusion |
| `options.excludes` | string | Preference filter for exclusion |
**Returns:** `Promise<any>` -- list of events
#### search(query, options?)
Search memories by semantic similarity.
```typescript
const results = await client.search('dietary preferences', { user_id: 'alice' });
for (const mem of results.results) {
console.log(mem.memory, mem.score);
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Natural language search query |
| `options.user_id` | string | Filter by user |
| `options.agent_id` | string | Filter by agent |
| `options.filters` | object | V2 filter object (`AND`/`OR`/`NOT`) |
| `options.top_k` | number | Number of results (default: 10) |
| `options.rerank` | boolean | Enable semantic reranking |
| `options.threshold` | number | Minimum similarity (default: 0.3) |
| `options.keyword_search` | boolean | Enable keyword search |
| `options.enable_graph` | boolean | Include graph relations |
| `options.filter_memories` | boolean | Precision filtering |
**Returns:** `Promise<SearchResult>` -- `{results: [{id, memory, score, ...}], relations: [...]}`
#### get(memoryId)
```typescript
const memory = await client.get('ea925981-...');
```
#### getAll(options?)
Retrieve all memories. Requires at least one entity identifier in filters.
```typescript
const memories = await client.getAll({ user_id: 'alice' });
// With filters
const filtered = await client.getAll({
filters: { AND: [{ user_id: 'alice' }, { categories: { contains: 'health' } }] },
});
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.user_id` | string | Filter by user |
| `options.filters` | object | V2 filter object |
| `options.page` | number | Page number |
| `options.page_size` | number | Results per page |
| `options.enable_graph` | boolean | Include graph relations |
#### update(memoryId, data)
```typescript
await client.update('ea925981-...', { text: 'Updated: vegan since 2024' });
await client.update('ea925981-...', { text: 'Updated', metadata: { verified: true } });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `memoryId` | string | Memory ID |
| `data.text` | string | New content |
| `data.metadata` | object | New metadata |
| `data.timestamp` | string | New timestamp |
#### delete(memoryId)
```typescript
await client.delete('ea925981-...');
```
#### deleteAll(options?)
```typescript
await client.deleteAll({ user_id: 'alice' });
```
#### history(memoryId)
```typescript
const history = await client.history('ea925981-...');
// Returns: [{previous_value, new_value, action, timestamps}]
```
---
### Batch Methods
#### batchUpdate(memories)
```typescript
await client.batchUpdate([
{ memoryId: 'uuid-1', text: 'Updated text' },
{ memoryId: 'uuid-2', text: 'Another update' },
]);
```
#### batchDelete(memories)
```typescript
await client.batchDelete(['uuid-1', 'uuid-2', 'uuid-3']);
```
---
### User/Entity Management
#### users()
```typescript
const users = await client.users();
// Returns: {results: [{type: "user", name: "alice"}, ...]}
```
#### deleteUser(data) / deleteUsers(data)
```typescript
await client.deleteUser({ user_id: 'alice' }); // Single entity
await client.deleteUsers({ agent_id: 'bot-1' }); // Flexible
```
---
### Project Management
```typescript
// Get project config
const config = await client.getProject({ fields: ['custom_categories'] });
// Update project settings
await client.updateProject({
custom_instructions: 'Extract dietary preferences and health info',
custom_categories: [{ health: 'Medical and dietary info' }],
enable_graph: true,
});
```
---
### Webhooks
```typescript
// List
const webhooks = await client.getWebhooks({ project_id: 'proj_123' });
// Create
const webhook = await client.createWebhook({
url: 'https://your-app.com/webhook',
name: 'Memory Logger',
project_id: 'proj_123',
event_types: ['memory_add', 'memory_update'],
});
// Update
await client.updateWebhook({
webhook_id: 'wh_123',
name: 'Updated Logger',
url: 'https://new-url.com',
});
// Delete
await client.deleteWebhook({ webhook_id: 'wh_123' });
```
---
### Feedback
```typescript
await client.feedback({
memory_id: 'mem-123',
feedback: 'POSITIVE',
feedback_reason: 'Accurately captured preference',
});
```
---
### Export
```typescript
const exportReq = await client.createMemoryExport({
schema: JSON.stringify({ type: 'object', properties: { name: { type: 'string' } } }),
filters: { user_id: 'alice' },
});
const result = await client.getMemoryExport({ memory_export_id: exportReq.id });
```
---
### TypeScript Types
Key interfaces from `mem0.types.ts`:
```typescript
interface Message { role: string; content: string; }
interface Memory { id: string; memory: string; user_id: string; categories: string[]; score?: number; /* ... */ }
interface MemoryOptions { user_id?: string; agent_id?: string; app_id?: string; run_id?: string; metadata?: object; /* ... */ }
interface SearchOptions { user_id?: string; filters?: object; top_k?: number; rerank?: boolean; threshold?: number; /* ... */ }
interface MemoryHistory { id: string; memory_id: string; previous_value: string; new_value: string; action: string; /* ... */ }
interface FeedbackPayload { memory_id: string; feedback: string; feedback_reason?: string; }
interface WebhookCreatePayload { url: string; name: string; project_id: string; event_types: string[]; }
enum OutputFormat { v1_0 = 'v1.0', v1_1 = 'v1.1' }
enum API_VERSION { v1 = 'v1', v2 = 'v2' }
```
---
## Open Source / Self-Hosted
### Installation
```bash
npm install mem0ai
```
### Memory Class
```typescript
import { Memory } from 'mem0ai/oss';
const m = new Memory(); // Uses default config
```
**Import:** `from 'mem0ai/oss'` (NOT the default export -- that is `MemoryClient` for Platform)
### Configuration
```typescript
const config = {
llm: {
provider: 'openai', // openai, groq, anthropic, google, ollama, lmstudio, mistral, azure
config: {
model: 'gpt-4o-mini',
apiKey: 'sk-xxx',
},
},
embedder: {
provider: 'openai', // openai, ollama, lmstudio, google, azure, langchain, anthropic
config: {
model: 'text-embedding-3-small',
apiKey: 'sk-xxx',
},
},
vectorStore: {
provider: 'qdrant', // memory, qdrant, redis, supabase, langchain, azure_ai_search, pgvector
config: {
collectionName: 'my_memories',
host: 'localhost',
port: 6333,
},
},
graphStore: { // Optional
provider: 'neo4j',
config: {
url: 'neo4j://localhost:7687',
username: 'neo4j',
password: 'password',
},
},
historyDbPath: 'history.db',
customPrompt: '...',
enableGraph: false,
disableHistory: false,
};
const m = new Memory(config);
// Or from dict with validation:
const m2 = Memory.fromConfig(config);
```
### Methods
All methods are async (return `Promise`):
#### add(messages, config)
```typescript
await m.add('I prefer dark mode', { userId: 'alice' });
await m.add([
{ role: 'user', content: 'I like hiking' },
{ role: 'assistant', content: 'Great outdoor activity!' },
], { userId: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `string \| Message[]` | Content to store |
| `config.userId` | string | User identifier (at least one scope required) |
| `config.agentId` | string | Agent identifier |
| `config.runId` | string | Session identifier |
| `config.metadata` | object | Custom key-value pairs |
| `config.filters` | object | Additional filters |
| `config.infer` | boolean | LLM inference (default: true) |
**Returns:** `Promise<{results: [...], relations?: [...]}>`
#### search(query, config)
```typescript
const results = await m.search('dietary preferences', { userId: 'alice', limit: 5 });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Search query |
| `config.userId` | string | Filter by user |
| `config.agentId` | string | Filter by agent |
| `config.runId` | string | Filter by run |
| `config.limit` | number | Max results (default: 100) |
| `config.filters` | object | Advanced filters |
#### get(memoryId) / getAll(config) / update(memoryId, data) / delete(memoryId) / deleteAll(config) / history(memoryId)
Same interface patterns. Note: OSS `update` takes a string for data, not an object.
```typescript
await m.update('mem-id', 'new content');
```
#### reset()
Clear the entire vector store and history.
```typescript
await m.reset();
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `import MemoryClient from 'mem0ai'` | `import { Memory } from 'mem0ai/oss'` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Param style** | `snake_case` in options (`user_id`) | `camelCase` in config (`userId`) |
| **Batch ops** | `batchUpdate`, `batchDelete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `createMemoryExport` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `getProject`, `updateProject` | Not available |
| **User listing** | `users()`, `deleteUser()` | Not available |
| **Graph store** | Platform-managed | Self-managed (Neo4j) |
| **History** | Platform-managed | SQLite (configurable) |
+481
View File
@@ -0,0 +1,481 @@
# Mem0 Python SDK Reference
Complete reference for the `mem0ai` Python package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient (Synchronous)
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**Constructor:** `MemoryClient(api_key=None)`. If `api_key` is not provided, reads from `MEM0_API_KEY` environment variable. Raises `ValueError` if no key found.
- HTTP library: `httpx`
- Timeout: 300 seconds
- Base URL: `https://api.mem0.ai`
### AsyncMemoryClient (Asynchronous)
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-xxx")
# Or use as context manager
async with AsyncMemoryClient(api_key="m0-xxx") as client:
results = await client.search("query", user_id="alice")
```
Same methods as `MemoryClient`, all `async`/`await`. Supports async context manager.
---
### Memory Methods
#### add(messages, **kwargs)
Store new memories from messages.
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `messages` | str \| dict \| list[dict] | required | Message content. Strings auto-convert to user messages |
| `user_id` | str | None | User identifier |
| `agent_id` | str | None | Agent identifier |
| `app_id` | str | None | Application identifier |
| `run_id` | str | None | Session/run identifier |
| `metadata` | dict | None | Custom key-value pairs |
| `enable_graph` | bool | None | Activate knowledge graph extraction |
| `infer` | bool | True | If False, store raw text without LLM inference |
| `immutable` | bool | None | If True, prevents future modification |
| `expiration_date` | str | None | Auto-expiry date (`YYYY-MM-DD`) |
| `includes` | str | None | Preference filter for inclusion |
| `excludes` | str | None | Preference filter for exclusion |
| `async_mode` | bool | True | If False, wait for processing to complete |
| `custom_categories` | list | None | Override project categories |
| `custom_instructions` | str | None | Override extraction instructions |
| `timestamp` | int \| float \| str | None | Custom timestamp (Unix epoch or ISO 8601) |
**Returns:** `dict` -- list of events: `[{"id": "...", "event": "ADD|UPDATE|DELETE", "data": {"memory": "..."}}]`
#### search(query, **kwargs)
Search memories by semantic similarity.
```python
results = client.search("dietary preferences", user_id="alice")
for mem in results.get("results", []):
print(mem["memory"], mem["score"])
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | str | required | Natural language search query |
| `user_id` | str | None | Filter by user |
| `agent_id` | str | None | Filter by agent |
| `app_id` | str | None | Filter by app |
| `top_k` | int | 10 | Number of results |
| `filters` | dict | None | V2 filter object (`AND`/`OR`/`NOT`) |
| `rerank` | bool | None | Enable deep semantic reranking (+150-200ms) |
| `threshold` | float | 0.3 | Minimum similarity score |
| `keyword_search` | bool | None | Enable keyword-based search (+10ms) |
| `enable_graph` | bool | None | Include graph relations in results |
| `filter_memories` | bool | None | Precision filtering, removes low-relevance (+200-300ms) |
| `fields` | list | None | Specific fields to return |
| `categories` | list | None | Filter by category |
**Returns:** `dict` -- `{"results": [{id, memory, user_id, categories, score, created_at, ...}], "relations": [...]}`
#### get(memory_id)
Retrieve a single memory by ID.
```python
memory = client.get(memory_id="ea925981-...")
```
**Returns:** `dict` -- full memory object
#### get_all(**kwargs)
Retrieve all memories with optional filtering. Requires at least one entity identifier.
```python
memories = client.get_all(user_id="alice")
# With filters
memories = client.get_all(filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "health"}}]})
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `user_id` | str | None | Filter by user |
| `agent_id` | str | None | Filter by agent |
| `app_id` | str | None | Filter by app |
| `top_k` | int | None | Limit results |
| `page` | int | None | Page number |
| `page_size` | int | None | Results per page |
| `filters` | dict | None | V2 filter object |
| `enable_graph` | bool | None | Include graph relations |
**Returns:** `dict` -- `{"results": [...]}`
#### update(memory_id, text=None, metadata=None, timestamp=None)
Update a memory's content, metadata, or timestamp. At least one parameter required. Cannot update immutable memories.
```python
client.update("ea925981-...", text="Updated: vegan since 2024")
client.update("ea925981-...", metadata={"verified": True})
```
**Returns:** `dict` -- updated memory
#### delete(memory_id)
Permanently delete a single memory.
```python
client.delete("ea925981-...")
```
#### delete_all(**kwargs)
Delete all memories matching filters. Irreversible.
```python
client.delete_all(user_id="alice")
```
#### history(memory_id)
Get the change history of a memory.
```python
history = client.history("ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
---
### Batch Methods
#### batch_update(memories)
Update up to 1000 memories in a single request.
```python
client.batch_update([
{"memory_id": "uuid-1", "text": "Updated text"},
{"memory_id": "uuid-2", "text": "Another update", "metadata": {"verified": True}},
])
```
#### batch_delete(memories)
Delete up to 1000 memories in a single request.
```python
client.batch_delete([
{"memory_id": "uuid-1"},
{"memory_id": "uuid-2"},
])
```
---
### User/Entity Management
#### users()
List all users, agents, and sessions that have memories.
```python
users = client.users()
# Returns: {"results": [{"type": "user", "name": "alice"}, ...]}
```
#### delete_users(user_id=None, agent_id=None, app_id=None, run_id=None)
Delete a specific entity and all its memories.
```python
client.delete_users(user_id="alice")
```
#### reset()
Delete ALL users, agents, sessions, and memories. Complete data reset.
```python
client.reset()
```
---
### Export & Summary
#### create_memory_export(schema, **kwargs)
Create a structured export of memories.
```python
import json
schema = json.dumps({
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
}
})
export = client.create_memory_export(schema=schema, user_id="alice")
```
#### get_memory_export(**kwargs)
Retrieve a previously created export.
```python
result = client.get_memory_export(memory_export_id=export["id"])
```
#### get_summary(filters=None)
Get a summary of memories.
```python
summary = client.get_summary(filters={"user_id": "alice"})
```
---
### Feedback
#### feedback(memory_id, feedback=None, feedback_reason=None)
Provide quality feedback on a memory.
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE", # POSITIVE | NEGATIVE | VERY_NEGATIVE | None (clear)
feedback_reason="Accurately captured preference"
)
```
---
### Webhooks
```python
# List
webhooks = client.get_webhooks(project_id="proj_123")
# Create
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_update"]
)
# Update
client.update_webhook(webhook_id=123, name="Updated", url="https://new-url.com")
# Delete
client.delete_webhook(webhook_id=123)
```
---
### Project Management
Access via `client.project.*`:
```python
# Get project config
config = client.project.get(fields=["custom_categories", "custom_instructions"])
# Update project settings
client.project.update(
custom_instructions="Extract dietary preferences and health info",
custom_categories=[{"health": "Medical and dietary info"}],
enable_graph=True,
multilingual=True,
)
# Create/delete project (requires org_id)
client.project.create(name="My Project", description="...")
client.project.delete()
# Member management
members = client.project.get_members()
client.project.add_member(email="user@example.com", role="READER") # READER or OWNER
client.project.update_member(email="user@example.com", role="OWNER")
client.project.remove_member(email="user@example.com")
```
---
## Open Source / Self-Hosted
### Installation
```bash
pip install mem0ai
```
### Memory Class
```python
from mem0 import Memory
m = Memory() # Uses default config (OpenAI embedder + in-memory vector store)
```
**Import:** `from mem0 import Memory` (NOT `MemoryClient` -- that is the Platform client)
### Configuration
```python
config = {
"llm": {
"provider": "openai", # openai, groq, azure, ollama, lmstudio, google, anthropic, mistral
"config": {
"model": "gpt-4o-mini",
"api_key": "sk-xxx",
}
},
"embedder": {
"provider": "openai", # openai, ollama, azure, lmstudio, google, huggingface
"config": {
"model": "text-embedding-3-small",
"api_key": "sk-xxx",
}
},
"vector_store": {
"provider": "qdrant", # faiss, qdrant, pgvector, redis, supabase, azure_ai_search, memory
"config": {
"collection_name": "my_memories",
"host": "localhost",
"port": 6333,
}
},
"graph_store": { # Optional
"provider": "neo4j",
"config": {
"url": "neo4j://localhost:7687",
"username": "neo4j",
"password": "password",
}
},
"history_db_path": "history.db", # SQLite path for change history
"custom_fact_extraction_prompt": "...", # Custom LLM prompt for extraction
"custom_update_memory_prompt": "...", # Custom LLM prompt for updates
"enable_graph": False, # Enable graph memory
}
m = Memory.from_config(config)
```
### Context Manager
```python
with Memory(config) as m:
m.add("I prefer dark mode", user_id="alice")
results = m.search("preferences", user_id="alice")
# SQLite connections released automatically
```
### Methods
All methods mirror the Platform client but run locally:
#### add(messages, *, user_id, agent_id, run_id, metadata, infer=True)
```python
m.add("I'm a vegetarian", user_id="alice")
m.add([
{"role": "user", "content": "I like hiking"},
{"role": "assistant", "content": "Great outdoor activity!"}
], user_id="alice")
```
At least one of `user_id`, `agent_id`, `run_id` required.
**Returns:** `{"results": [...], "relations": [...]}`
#### search(query, *, user_id, agent_id, run_id, limit=100, filters=None, threshold=None, rerank=True)
```python
results = m.search("dietary preferences", user_id="alice", limit=5)
```
Supports filter operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`.
#### get(memory_id) / get_all(**kwargs) / update(memory_id, data, metadata=None) / delete(memory_id) / delete_all(**kwargs) / history(memory_id)
Same interface as Platform client.
#### reset()
Clear the entire vector store collection and history database. Recreates the vector store.
```python
m.reset()
```
#### close()
Release SQLite connections. Called automatically when using context manager.
### AsyncMemory
```python
from mem0 import AsyncMemory
m = AsyncMemory(config)
await m.add("text", user_id="alice")
results = await m.search("query", user_id="alice")
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `from mem0 import MemoryClient` | `from mem0 import Memory` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Batch ops** | `batch_update`, `batch_delete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `create_memory_export`, `get_memory_export` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `client.project.*` | Not available |
| **User listing** | `users()`, `delete_users()` | Not available |
| **Custom prompts** | Via project settings | Direct config |
| **Graph store** | Platform-managed | Self-managed (Neo4j) |
| **History** | Platform-managed | SQLite (configurable) |
| **Async** | `AsyncMemoryClient` | `AsyncMemory` |
+4 -53
View File
@@ -116,73 +116,24 @@ result = crew.kickoff()
## Vercel AI SDK
Source: [docs.mem0.ai/integrations/vercel-ai-sdk](https://docs.mem0.ai/integrations/vercel-ai-sdk)
> **Dedicated skill available.** For comprehensive Vercel AI SDK documentation, see the [mem0-vercel-ai-sdk skill](../mem0-vercel-ai-sdk/SKILL.md) ([GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk)).
Install: `npm install @mem0/vercel-ai-provider`
### Basic Text Generation with Memory
Quick example (wrapped model with automatic memory):
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0({
provider: "openai",
mem0ApiKey: "m0-xxx",
apiKey: "openai-api-key",
});
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
```
### Streaming with Memory
```typescript
import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { textStream } = streamText({
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
for await (const textPart of textStream) {
process.stdout.write(textPart);
}
```
### Using Memory Utilities Standalone
```typescript
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
// Retrieve memories and inject into any provider
const prompt = "Suggest me a good car to buy.";
const memories = await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
const { text } = await generateText({
model: openai("gpt-4-turbo"),
prompt: prompt,
system: memories,
});
// Store new memories
await addMemories(
[{ role: "user", content: [{ type: "text", text: "I love red cars." }] }],
{ user_id: "borat", mem0ApiKey: "m0-xxx" }
);
```
### Supported Providers
`openai`, `anthropic`, `google`, `groq`
Supported providers: `openai`, `anthropic`, `google`, `groq`, `cohere`
---
+2
View File
@@ -2,6 +2,8 @@
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
> **For language-specific deep references (including OSS):** See [client/python.md](../client/python.md) and [client/node.md](../client/node.md). For Python vs TypeScript differences: [client/differences.md](../client/differences.md).
## Initialization
**Python:**
+44
View File
@@ -91,6 +91,50 @@ def test_generate_response_with_tools(mock_openai_client):
assert response["tool_calls"][0]["arguments"] == {"data": "Today is a sunny day."}
def test_generate_response_with_response_format(mock_openai_client):
config = AzureOpenAIConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P)
llm = AzureOpenAILLM(config)
messages = [
{"role": "system", "content": "You are a memory extraction assistant."},
{"role": "user", "content": "I like hiking on weekends."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content='{"facts": ["User likes hiking on weekends"]}'))]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages, response_format={"type": "json_object"})
mock_openai_client.chat.completions.create.assert_called_once_with(
model=MODEL,
messages=messages,
temperature=TEMPERATURE,
max_tokens=MAX_TOKENS,
top_p=TOP_P,
response_format={"type": "json_object"},
)
assert response == '{"facts": ["User likes hiking on weekends"]}'
def test_generate_response_without_response_format(mock_openai_client):
config = AzureOpenAIConfig(model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS, top_p=TOP_P)
llm = AzureOpenAILLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Tell me a joke."},
]
mock_response = Mock()
mock_response.choices = [Mock(message=Mock(content="Why did the chicken cross the road?"))]
mock_openai_client.chat.completions.create.return_value = mock_response
response = llm.generate_response(messages)
call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
assert "response_format" not in call_kwargs
assert response == "Why did the chicken cross the road?"
def test_reasoning_model_with_reasoning_effort(mock_openai_client):
"""Test that reasoning_effort is passed to the API for Azure reasoning models."""
config = AzureOpenAIConfig(model="o3-mini", reasoning_effort="low")
+222
View File
@@ -627,6 +627,228 @@ async def test_async_update_preserves_actor_id_when_different_actor_updates(mock
assert stored["actor_id"] == "Alice"
class TestHallucinatedIdGuard:
"""Tests for temp_uuid_mapping guard against LLM-hallucinated IDs (issue #3931).
When the LLM returns an UPDATE or DELETE with an ID that doesn't exist in
temp_uuid_mapping, the code should skip gracefully instead of raising KeyError.
"""
def test_sync_update_with_hallucinated_id_skips_gracefully(self, mocker, caplog):
"""Sync UPDATE with an out-of-range ID should be skipped with a warning."""
memory = _build_memory_instance(mocker, Memory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
# Simulate: 2 existing memories (IDs "0" and "1"), but LLM returns UPDATE for ID "12"
existing_mem = MagicMock()
existing_mem.id = "uuid-aaa"
existing_mem.payload = {"data": "User likes coffee"}
memory.vector_store.search.return_value = [existing_mem]
# First LLM call: fact extraction → returns one fact
# Second LLM call: memory update actions → returns UPDATE with hallucinated ID "12"
memory.llm.generate_response.side_effect = [
'{"facts": ["User likes tea"]}',
'{"memory": [{"id": "12", "text": "User likes tea", "event": "UPDATE", "old_memory": "User likes coffee"}]}',
]
with caplog.at_level(logging.WARNING):
result = memory._add_to_vector_store(
messages=[{"role": "user", "content": "I like tea"}],
metadata={},
filters={},
infer=True,
)
# Should not crash, should return empty (the hallucinated UPDATE was skipped)
assert result == []
assert "UPDATE skipped: LLM returned unknown id" in caplog.text
# _update_memory should NOT have been called
memory.vector_store.update.assert_not_called()
def test_sync_delete_with_hallucinated_id_skips_gracefully(self, mocker, caplog):
"""Sync DELETE with an out-of-range ID should be skipped with a warning."""
memory = _build_memory_instance(mocker, Memory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-aaa"
existing_mem.payload = {"data": "User likes coffee"}
memory.vector_store.search.return_value = [existing_mem]
memory.llm.generate_response.side_effect = [
'{"facts": ["Remove coffee preference"]}',
'{"memory": [{"id": "9", "text": "User likes coffee", "event": "DELETE"}]}',
]
with caplog.at_level(logging.WARNING):
result = memory._add_to_vector_store(
messages=[{"role": "user", "content": "I no longer like coffee"}],
metadata={},
filters={},
infer=True,
)
assert result == []
assert "DELETE skipped: LLM returned unknown id" in caplog.text
memory.vector_store.delete.assert_not_called()
def test_sync_valid_id_still_processes_normally(self, mocker, caplog):
"""A valid ID should still be processed — the guard must not block legitimate operations."""
memory = _build_memory_instance(mocker, Memory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-aaa"
existing_mem.payload = {"data": "User likes coffee"}
memory.vector_store.search.return_value = [existing_mem]
memory.vector_store.get.return_value = MagicMock(
payload={"data": "User likes coffee", "created_at": "2026-01-01T00:00:00+00:00"}
)
# ID "0" is valid since there's exactly 1 existing memory
memory.llm.generate_response.side_effect = [
'{"facts": ["User likes tea now"]}',
'{"memory": [{"id": "0", "text": "User likes tea now", "event": "UPDATE", "old_memory": "User likes coffee"}]}',
]
with caplog.at_level(logging.WARNING):
result = memory._add_to_vector_store(
messages=[{"role": "user", "content": "I like tea now"}],
metadata={},
filters={},
infer=True,
)
assert len(result) == 1
assert result[0]["event"] == "UPDATE"
assert result[0]["memory"] == "User likes tea now"
assert result[0]["id"] == "uuid-aaa"
assert "skipped" not in caplog.text
@pytest.mark.asyncio
async def test_async_update_with_hallucinated_id_skips_gracefully(self, mocker, caplog):
"""Async UPDATE with an out-of-range ID should be skipped with a warning."""
memory = _build_memory_instance(mocker, AsyncMemory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-bbb"
existing_mem.payload = {"data": "User works at Acme"}
memory.vector_store.search.return_value = [existing_mem]
memory.llm.generate_response.side_effect = [
'{"facts": ["User works at Globex"]}',
'{"memory": [{"id": "7", "text": "User works at Globex", "event": "UPDATE", "old_memory": "User works at Acme"}]}',
]
with caplog.at_level(logging.WARNING):
result = await memory._add_to_vector_store(
messages=[{"role": "user", "content": "I now work at Globex"}],
metadata={},
effective_filters={},
infer=True,
)
assert result == []
assert "UPDATE skipped: LLM returned unknown id" in caplog.text
@pytest.mark.asyncio
async def test_async_delete_with_hallucinated_id_skips_gracefully(self, mocker, caplog):
"""Async DELETE with an out-of-range ID should be skipped with a warning."""
memory = _build_memory_instance(mocker, AsyncMemory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-ccc"
existing_mem.payload = {"data": "User lives in SF"}
memory.vector_store.search.return_value = [existing_mem]
memory.llm.generate_response.side_effect = [
'{"facts": ["Remove SF reference"]}',
'{"memory": [{"id": "16", "text": "User lives in SF", "event": "DELETE"}]}',
]
with caplog.at_level(logging.WARNING):
result = await memory._add_to_vector_store(
messages=[{"role": "user", "content": "I moved away from SF"}],
metadata={},
effective_filters={},
infer=True,
)
assert result == []
assert "DELETE skipped: LLM returned unknown id" in caplog.text
def test_sync_update_with_missing_id_key_skips_gracefully(self, mocker, caplog):
"""UPDATE where the LLM omits the 'id' field entirely should be skipped."""
memory = _build_memory_instance(mocker, Memory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-aaa"
existing_mem.payload = {"data": "User likes coffee"}
memory.vector_store.search.return_value = [existing_mem]
# LLM response has no "id" key at all
memory.llm.generate_response.side_effect = [
'{"facts": ["User likes tea"]}',
'{"memory": [{"text": "User likes tea", "event": "UPDATE", "old_memory": "User likes coffee"}]}',
]
with caplog.at_level(logging.WARNING):
result = memory._add_to_vector_store(
messages=[{"role": "user", "content": "I like tea"}],
metadata={},
filters={},
infer=True,
)
assert result == []
assert "UPDATE skipped: LLM returned unknown id" in caplog.text
@pytest.mark.asyncio
async def test_async_valid_id_still_processes_normally(self, mocker, caplog):
"""Async path: a valid ID should process normally — no false positives from the guard."""
memory = _build_memory_instance(mocker, AsyncMemory)
memory.embedding_model.embed.return_value = [0.1, 0.2, 0.3]
mocker.patch("mem0.memory.main.capture_event")
existing_mem = MagicMock()
existing_mem.id = "uuid-bbb"
existing_mem.payload = {"data": "User works at Acme"}
memory.vector_store.search.return_value = [existing_mem]
memory.vector_store.get.return_value = MagicMock(
payload={"data": "User works at Acme", "created_at": "2026-01-01T00:00:00+00:00"}
)
# ID "0" is valid since there's exactly 1 existing memory
memory.llm.generate_response.side_effect = [
'{"facts": ["User works at Globex now"]}',
'{"memory": [{"id": "0", "text": "User works at Globex now", "event": "UPDATE", "old_memory": "User works at Acme"}]}',
]
with caplog.at_level(logging.WARNING):
result = await memory._add_to_vector_store(
messages=[{"role": "user", "content": "I now work at Globex"}],
metadata={},
effective_filters={},
infer=True,
)
assert len(result) == 1
assert result[0]["event"] == "UPDATE"
assert result[0]["memory"] == "User works at Globex now"
assert result[0]["id"] == "uuid-bbb"
assert "skipped" not in caplog.text
def test_normalize_iso_timestamp_to_utc_preserves_naive_values():
assert _normalize_iso_timestamp_to_utc("2026-03-18T00:00:00") == "2026-03-18T00:00:00"