Compare commits

..

1 Commits

Author SHA1 Message Date
Harsh Vardhan Gupta b91b2b8fc4 fix: apply pnpm overrides for HIGH severity vulnerabilities
- Add immutable >=5.1.5 override to openmemory/ui (CVE-2026-29063)
- Add langsmith >=0.6.0 override to openclaw (CVE-2026-32460)
- Add langsmith >=0.6.0 override to mem0-ts (CVE-2026-32460)

Fixes #5318, #5320
2026-05-31 00:34:27 +05:30
1279 changed files with 45526 additions and 100804 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "mem0",
"source": {
"source": "local",
"path": "./integrations/mem0-plugin"
"path": "./mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
+2 -2
View File
@@ -10,9 +10,9 @@
"plugins": [
{
"name": "mem0",
"source": "./integrations/mem0-plugin",
"source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.14"
"version": "0.2.8"
}
]
}
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "mem0",
"source": {
"source": "local",
"path": "./integrations/mem0-plugin"
"path": "./mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
+2 -2
View File
@@ -10,9 +10,9 @@
"plugins": [
{
"name": "mem0",
"source": "./integrations/mem0-plugin",
"source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.14"
"version": "0.2.8"
}
]
}
+5 -3
View File
@@ -9,10 +9,12 @@ body:
label: Component
description: Which part of mem0 is affected?
options:
- Python SDK
- Core / Python SDK
- TypeScript SDK
- Vector Store
- Plugin
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenClaw
- REST API
- Other
validations:
+6 -3
View File
@@ -9,11 +9,14 @@ body:
label: Component
description: Which part of mem0 does this relate to?
options:
- Python SDK
- Core / Python SDK
- TypeScript SDK
- Vector Store
- Plugin
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenClaw
- REST API
- Benchmarks / Evals
- Other
validations:
required: true
+18 -15
View File
@@ -1,15 +1,18 @@
policy:
- section:
- id: ['component']
block-list: ['Other']
label:
- name: 'sdk-python'
keys: ['Python SDK']
- name: 'sdk-typescript'
keys: ['TypeScript SDK']
- name: 'vector-store'
keys: ['Vector Store']
- name: 'plugin'
keys: ['Plugin']
- name: 'rest-api'
keys: ['REST API']
# Maps dropdown selections to GitHub labels
# Used by the advanced-issue-labeler GitHub Action
component:
- label: "sdk-python"
matcher: "Core / Python SDK"
- label: "sdk-typescript"
matcher: "TypeScript SDK"
- label: "vector-store"
matcher: "Vector Store"
- label: "graph-memory"
matcher: "Graph Memory"
- label: "ollama"
matcher: "Ollama"
- label: "openclaw"
matcher: "OpenClaw"
- label: "rest-api"
matcher: "REST API"
-40
View File
@@ -1,40 +0,0 @@
{
"language": {
"sdk-python": [
"python", "pip install", "pypi", "pyproject", "requirements.txt",
"from mem0", "import mem0", "traceback", "pydantic", "asyncmemory",
"poetry", "virtualenv", "venv", "conda", "pytest", "async def"
],
"sdk-typescript": [
"typescript", "javascript", "pnpm", "yarn", "node.js", "nodejs",
"mem0-ts", "mem0ai/oss", "tsconfig", "await import",
"=> {", "undefined is not"
]
},
"area": {
"plugin": [
"openclaw", "openclaw-mem0", "openclaw.json", "openclaw plugin",
"claude code", "opencode", "pi agent", "mem0-plugin",
"cursor plugin", "codex plugin", "editor plugin"
],
"cli": ["mem0-cli", "@mem0/cli", "npx mem0", "command line"],
"vector-store": [
"pgvector", "pinecone", "chroma", "chromadb", "weaviate",
"milvus", "faiss", "vector store", "vectorstore",
"elasticsearch", "supabase", "azure ai search",
"s3 vectors", "mongodb"
],
"integrations": [
"vercel ai", "vercel-ai-sdk", "@mem0/vercel-ai-provider",
"llamaindex", "crewai", "autogen", "langgraph"
],
"rest-api": [
"rest api", "fastapi", "docker-compose", "/v1/memories",
"localhost:8000", "localhost:8888", "curl -x", "http endpoint"
],
"documentation": [
"docs.mem0.ai", "documentation", "typo", "readme", "docstring", "broken link",
"issue on docs", "docs:", "link to the docs page", "issue with current documentation"
]
}
}
-59
View File
@@ -1,59 +0,0 @@
sdk-python:
- changed-files:
- any-glob-to-any-file:
- 'mem0/**'
- 'tests/**'
- 'cli/python/**'
- 'pyproject.toml'
- 'poetry.lock'
sdk-typescript:
- changed-files:
- any-glob-to-any-file:
- 'mem0-ts/**'
- 'cli/node/**'
vector-store:
- changed-files:
- any-glob-to-any-file:
- 'mem0/vector_stores/**'
- 'mem0-ts/src/oss/src/vector_stores/**'
rest-api:
- changed-files:
- any-glob-to-any-file: 'server/**'
integrations:
- changed-files:
- any-glob-to-any-file: 'integrations/**'
plugin:
- changed-files:
- all-globs-to-any-file:
- 'integrations/**'
- '!integrations/vercel-ai-sdk/**'
- any-glob-to-any-file:
- 'skills/**'
- '.agents/**'
- '.claude-plugin/**'
- '.codex-plugin/**'
- '.cursor-plugin/**'
- 'marketplace.json'
cli:
- changed-files:
- any-glob-to-any-file: 'cli/**'
documentation:
- changed-files:
- any-glob-to-any-file:
- 'docs/**'
- 'examples/**'
- '*.md'
ci:
- changed-files:
- any-glob-to-any-file:
- '.github/**'
- 'scripts/**'
- '.pre-commit-config.yaml'
-44
View File
@@ -1,44 +0,0 @@
const fs = require('fs');
function componentLabels(keywords) {
return Object.values(keywords).flatMap(Object.keys);
}
function toMatcher(term) {
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const prefix = /^[a-z0-9]/i.test(term) ? '\\b' : '';
return new RegExp(prefix + escaped, 'i');
}
function scoreGroup(text, group) {
let winner = null;
let best = 0;
for (const [label, terms] of Object.entries(group)) {
const score = terms.reduce((n, term) => n + (toMatcher(term).test(text) ? 1 : 0), 0);
if (score > best) {
winner = label;
best = score;
}
}
return winner;
}
const UMBRELLA = { plugin: 'integrations' };
function inferComponentLabels(text, keywords) {
if (!text) return [];
const labels = [scoreGroup(text, keywords.language), scoreGroup(text, keywords.area)].filter(
Boolean,
);
for (const label of labels.slice()) {
const parent = UMBRELLA[label];
if (parent && !labels.includes(parent)) labels.push(parent);
}
return labels;
}
function loadKeywords(file) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
module.exports = { componentLabels, inferComponentLabels, loadKeywords };
@@ -1,102 +0,0 @@
const assert = require('assert');
const path = require('path');
const { inferComponentLabels, loadKeywords } = require('./infer-component-labels.js');
const keywords = loadKeywords(path.join(__dirname, '..', 'component-keywords.json'));
const cases = [
{
number: 6210,
title: "but(anthropic): sampling parameters returns 400 error for new model",
body: "### Component\n\nCore / Python SDK\n\n### Description\n\n### Summary\n\nWhen using Anthropic latest models such as `claude-opus-4-7`, `claude-opus-4-8`, or `claude-sonnet-5`, Mem0 still sends sampling parameters like `temperature` / `top_p`. These models do not support those parameters, causing Anthropic API requests to fail.\n\nSee https://platform.claude.com/docs/en/about-claude/models/migration-guide\n\n### Steps to Reproduce\n\n```python\n from mem0 import Memory\n\n m = Memory.from_config({\n \"llm\": {\n \"provider\": \"anthropic\",\n \"config\": {\n \"model\": \"claude-opus-4-8\",\n \"api_key\": \"your-anthropic-api-key\"\n },\n },\n ...\n })\n```\n\n### Expected Behavior\n\nMem0 should detect Anthropic models that do not support sampling parameters and omit temperature and top_p from the request.\n\nFor models that still support sampling parameters, such as claude-opus-4-6, claude-sonnet-4-6, and claude-haiku-4-5, Mem0 should continue sending supported sampling parameters till they're deprecated.\n\n### Actual Behavior\n\nMem0 includes temperature by default for Anthropic requests. With newer Anthropic models that do not support sampling parameters, the API request fails because unsupported parameters are sent.\n\n### Environment\n\n - mem0 version: 2.0.11\n - Python/Node version: Python 3.11\n - OS: macOS\n",
expected: ["sdk-python"],
},
{
number: 5770,
title: "feat(ts-sdk): add FastEmbed embedding provider",
body: "## Summary\n\nThe Python SDK supports **FastEmbed** as an embedding provider, but the TypeScript OSS SDK (`mem0ai/oss`) does not. Add it to bring the TS SDK to parity.\n\n| | |\n|---|---|\n| Python reference | `mem0/embeddings/fastembed.py` |\n| Registered in (Python) | `mem0/utils/factory.py` (EmbedderFactory) |\n| Target file (TypeScript) | `mem0-ts/src/oss/src/embeddings/fastembed.ts` |\n| Suggested implementation | Use the `fastembed` npm package (ONNX local embeddings). |\n\n## Requirements\n\n- [ ] Implement `FastEmbedEmbedder` in `mem0-ts/src/oss/src/embeddings/fastembed.ts`, extending `Embedder` (`mem0-ts/src/oss/src/embeddings/base.ts`) and mirroring the Python provider's behavior (embed / embedBatch).\n- [ ] Register the `\"fastembed\"` provider in `mem0-ts/src/oss/src/utils/factory.ts` (EmbedderFactory).\n- [ ] Add config typing in `mem0-ts/src/oss/src/types/`.\n- [ ] Add a unit test under `mem0-ts/src/oss/src/tests/`.\n- [ ] Add `fastembed` to `mem0-ts/package.json` (optional/peer dependency, lazy-imported like other providers).\n- [ ] Update docs under `docs/` if this provider is user-facing.\n\n## Reference pattern\n\nMirror an existing TS provider: `embeddings/openai.ts`.\n\n## Notes\n\n`fastembed` (v2.x) is the JS port of Qdrant's FastEmbed — local/offline embeddings. Mirror the default model in `mem0/embeddings/fastembed.py`.\n\n---\n_Part of the TypeScript ↔ Python SDK provider-parity effort. One provider per issue (atomic)._\n",
expected: ["sdk-typescript"],
},
{
number: 3940,
title: "Milvus database will return distance not similarity score",
body: "### 🐛 Describe the bug\n\nMilvus database will return distance not similarity score\n\n## in milvus.py\n\ndef _parse_output(self, data: list):\n \"\"\"\n Parse the output data.\n\n Args:\n data (Dict): Output data.\n\n Returns:\n List[OutputData]: Parsed output data.\n \"\"\"\n memory = []\n\n for value in data:\n uid, score, metadata = (\n value.get(\"id\"),\n value.get(\"distance\"), # here\n value.get(\"entity\", {}).get(\"metadata\"),\n )\n\n memory_obj = OutputData(id=uid, score=score, payload=metadata)\n memory.append(memory_obj)\n\n return memory\n",
expected: ["vector-store"],
},
{
number: 5290,
title: "Recall search failed: Bad Request Using OpenAI Embedding Model",
body: "### Component\n\nOpenClaw\n\n### Description\n\n### Summary\nuse openclaw.json config:\n\n```json\n...\n\"embedder\": {\n \"provider\": \"openai\",\n \"config\": {\n \"model\": \"bge-base-zh-v1.5\",\n \"embedding_dims\": 1024,\n \"embeddingDims\": 1024,\n \"url\": \"https://xxxxxxxxx/v1\",\n \"apiKey\": \"xxxxxxxxxxxx\"\n }\n },\n\"vectorStore\": {\n \"provider\": \"qdrant\",\n \"config\": {\n \"url\": \"http://qdrant:6333\",\n \"apiKey\": \"${QDRANT_API_KEY}\",\n \"collectionName\": \"mem0\",\n \"embeddingModelDims\": 1024\n }\n }\n```\n```\n\nopenclaw log info is:\n\n```\n23:14:20 Api key is used with unsecure connection.\n23:14:21 [mem0] Recall search failed: Bad Request\n23:14:21 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:14:22 [ws] ⇄ res ✓ sessions.list 256ms conn=d1eb9bc4…17da id=201b8113…c9dc\n23:14:22 [ws] ⇄ res ✓ sessions.list 264ms conn=d1eb9bc4…17da id=4939f962…2f16\n23:14:34 [ws] ⇄ res ✓ sessions.list 250ms conn=d1eb9bc4…17da id=f7ad503f…baa6\n23:15:12 [mem0] **Recall search failed: Bad Request**\n23:15:12 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:15:12 [ws] ⇄ res ✓ sessions.list 288ms conn=d1eb9bc4…17da id=9e20bb86…371e\n23:15:13 [ws] ⇄ res ✓ sessions.list 268ms conn=d1eb9bc4…17da id=3b49a2ad…7ada\n23:15:20 [ws] ⇄ res ✓ sessions.list 235ms conn=d1eb9bc4…17da id=a192da30…069f\n```\n\n### Actual Behavior\n\nembedding model response ok,response message has 1024 vectors,but the vectors are submitted to vector-db:qdrant with all zero vectors,and vectors has only 256 size.\n\n```http\nPOST /collections/mem0/points/search HTTP/1.1\nhost: qdrant:6333\nconnection: keep-alive\nuser-agent: qdrant-js/1.13.0\napi-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx\nContent-Type: application/json\nAccept: application/json\naccept-language: *\nsec-fetch-mode: cors\naccept-encoding: gzip, deflate\ncontent-length: 651\n\n{\"vector\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"limit\":120,\"offset\":0,\"filter\":{\"must\":[{\"key\":\"user_id\",\"match\":{\"value\":\"agent\"}}]},\"with_payload\":true,\"with_vector\":false}\n\n**HTTP/1.1 400 Bad Request**\ntransfer-encoding: chunked\ncontent-type: application/json\nvary: accept-encoding, Origin, Access-Control-Request-Method, Access-Control-Request-Headers\ncontent-encoding: gzip\n\n```\n\n### Expected Behavior\n\nembedding model response ok by tcpdump, response message has 1024 vectors,and this vectors are submitted to vector-db:qdrant with the same vectors,and vectors has also 1024 size.\n\n\n### Environment\n\n- openclaw-mem0 version: 1.0.11\n- qdrant: 1.13.6\n",
expected: ["plugin", "integrations"],
},
{
number: 3696,
title: "Cannot set expiration_date for memory in REST API server (Docker Compose)",
body: "### 🐛 Describe the bug\n\nI'm using docker compose to deploy a REST API server. When adding memory, I'm unable to set the expiration_date. Is this feature not supported?",
expected: ["rest-api"],
},
{
number: 6252,
title: "cursor: on_file_read_cursor.sh ignores auto_search / MEM0_AUTO_SEARCH",
body: "### Component\n\nCursor / mem0-plugin\n\n### Description\n\n`on_file_read_cursor.sh` never checks `MEM0_AUTO_SEARCH`. In Claude Code, #6065/#6071 added a guard on `on_file_read.sh`, but the Cursor PreToolUse variant still always calls `file_context.py` (and thus Platform search) once `MEM0_API_KEY` is set.\n\n### Expected\n\nWhen `auto_search: false` / `MEM0_AUTO_SEARCH=false`, `on_file_read_cursor.sh` should exit 0 without searching.\n\n### Actual\n\nTimeline search still runs.\n\n### Related\n\n#6065, #6071, #6250\n",
expected: ["plugin", "integrations"],
},
{
number: 6032,
title: "docs: fix typos and punctuation errors across docs",
body: "### Description\n\n### Page\nMultiple pages — see list below.\n\n### What's Wrong or Missing\n1. https://docs.mem0.ai/components/llms/overview — \"a llm\" should be \"an LLM\"\n2. https://docs.mem0.ai/components/vectordbs/dbs/azure — 2 comma splices + \"setup\" used as a verb (should be \"set up\")\n3. https://docs.mem0.ai/components/embedders/models/azure_openai — \"from the Azure.\" is an incomplete sentence\n4. https://docs.mem0.ai/components/llms/models/azure_openai — same incomplete \"from the Azure\" phrasing\n5. https://docs.mem0.ai/cookbooks/companions/voice-companion-openai — \"an important information\" (uncountable noun)\n6. https://docs.mem0.ai/cookbooks/essentials/exporting-memories — comma splice\n7. https://docs.mem0.ai/cookbooks/integrations/tavily-search — \"usecase\" should be \"use case\"\n8. https://docs.mem0.ai/cookbooks/overview — broken parallelism in bullet list\n9. README.md — \"Github App\" should be \"GitHub App\"\n10. https://docs.mem0.ai/platform/overview — table cell not capitalized like other rows\n\n### Suggested Fix\nApply the corrections listed above for each page. I will submit a PR soon addressing all of the issues mentioned.",
expected: ["documentation"],
},
];
const cliRegressionCase = {
number: 3144,
title: "Bug Report: Memory Score Does Not Match Expected Relevance in Local Search",
body: "### 🐛 Describe the bug\n\n#### Description\n\nWhen using the locally deployed `mem0` server, the returned memory `score` from the `search` interface does not align with the expected semantic relevance. In particular, irrelevant or less relevant memories sometimes receive higher scores than directly related ones.\n\n#### Reproduction Steps\n\n```python\nmem0 = mem0_client(mode=\"local\")\nprint(\"Mem0 client initialized successfully.\")\n\nprint(\"Adding memories...\")\nresult = mem0.add(messages=[\n {\"role\": \"user\", \"content\": \"I like drinking coffee in the morning\"},\n {\"role\": \"user\", \"content\": \"I enjoy reading books at night\"}\n], user_id=\"alice\")\nprint(\"Memory added:\", result)\n\nprint(\"Searching memories...\")\nsearch_result = mem0.search(query=\"coffee\", user_id=\"alice\", top_k=2)\nprint(\"Search results:\", search_result)\n```\n\n#### Actual Output\n\n```json\n{\n \"results\": [\n {\n \"id\": \"5099b5be-c673-4f09-99de-a196f43b6476\",\n \"memory\": \"Likes drinking coffee in the morning\",\n \"score\": 0.5115111920687857\n },\n {\n \"id\": \"08df5c51-c52b-4c45-a5b6-b3f864ea149a\",\n \"memory\": \"Enjoys reading books at night\",\n \"score\": 0.7755568273863331\n }\n ],\n \"relations\": [\n {\"source\": \"coffee\", \"relationship\": \"consumed_in\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes_drinking\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"in_time\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"drinks_in\", \"destination\": \"morning\"}\n ]\n}\n```\n\n#### Expected Behavior\n\nThe memory `\"Likes drinking coffee in the morning\"` should have a **higher score** than `\"Enjoys reading books at night\"` when querying for `\"coffee\"`, since it is directly semantically related.",
};
let failures = 0;
function run(name, fn) {
try {
fn();
console.log(`PASS ${name}`);
} catch (err) {
failures++;
console.error(`FAIL ${name}: ${err.message}`);
}
}
for (const { number, title, body, expected } of cases) {
const text = `${title}
${body}`;
run(`#${number}`, () => {
assert.deepStrictEqual(inferComponentLabels(text, keywords), expected);
});
}
run('#3144 cliKeywordPrefixSubstringRegression', () => {
const text = `${cliRegressionCase.title}
${cliRegressionCase.body}`;
const inferred = inferComponentLabels(text, keywords);
assert.ok(!inferred.includes('cli'), `expected 'cli' absent (body contains 'Mem0 client', a substring of the removed 'mem0 cli' term), got ${JSON.stringify(inferred)}`);
});
run('noKeywordMatchReturnsEmptyArray', () => {
const text = 'The weather today is sunny and I went for a walk in the park with my dog.';
assert.deepStrictEqual(inferComponentLabels(text, keywords), []);
});
run('emptyStringReturnsEmptyArray', () => {
assert.deepStrictEqual(inferComponentLabels('', keywords), []);
});
if (failures > 0) {
console.error(`
${failures} test(s) failed.`);
process.exit(1);
}
console.log(`
All ${cases.length + 3} tests passed.`);
+4 -18
View File
@@ -1,33 +1,18 @@
name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
# Dispatched by release.yml (Release Router) when a release tagged v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. v1.2.3)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
# like vercel-ai-v* that also start with 'v'
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
if: startsWith(github.event.release.tag_name, 'v')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v2
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v2
@@ -54,6 +39,7 @@ jobs:
# packages_dir: dist/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: dist/
-208
View File
@@ -1,208 +0,0 @@
name: CI Gate
# Single required status check for all PRs.
#
# Path-filtered CI workflows can't be marked as required in branch
# protection: on a PR that doesn't touch their paths they never report, and
# the required check hangs at "Expected" forever. This gate solves that. It
# runs on every PR, detects which packages changed, calls only the relevant
# package CI workflows (as reusable workflows), and the final "CI Gate" job
# reports the aggregate result — success when every invoked pipeline passed
# (skipped pipelines are fine), failure when any failed.
#
# Branch protection should require exactly one status check: "CI Gate".
#
# Package CI workflows keep their own push-to-main and workflow_dispatch
# triggers; only their pull_request triggers moved here. To wire in a new
# package: add a filter under the `changes` job, a call job that `uses:` the
# package workflow, and list the call job in the gate's `needs`.
on:
pull_request:
concurrency:
group: ci-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
changes:
name: Detect changed packages
runs-on: ubuntu-latest
outputs:
python_sdk: ${{ steps.filter.outputs.python_sdk }}
ts_sdk: ${{ steps.filter.outputs.ts_sdk }}
cli_python: ${{ steps.filter.outputs.cli_python }}
cli_node: ${{ steps.filter.outputs.cli_node }}
openclaw: ${{ steps.filter.outputs.openclaw }}
mem0_plugin: ${{ steps.filter.outputs.mem0_plugin }}
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
# Each filter mirrors the package workflow's old pull_request
# paths, plus the package workflow file itself and this gate file
# (changing either must re-exercise the pipeline).
filters: |
python_sdk:
- 'mem0/**'
- 'tests/**'
- 'pyproject.toml'
- '.github/workflows/ci.yml'
- '.github/workflows/ci-gate.yml'
ts_sdk:
- 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml'
- '.github/workflows/ci-gate.yml'
cli_python:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
- '.github/workflows/ci-gate.yml'
cli_node:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
- '.github/workflows/ci-gate.yml'
openclaw:
- 'integrations/openclaw/**'
- '.github/workflows/openclaw-checks.yml'
- '.github/workflows/ci-gate.yml'
mem0_plugin:
- 'integrations/mem0-plugin/**'
- '!integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/mem0-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
opencode_plugin:
- 'integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
pi_agent_plugin:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
n8n_nodes_mem0:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
zapier_mem0:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
docs_llms_txt:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
- 'scripts/check-llms-txt-coverage.py'
- 'scripts/llms-txt-ignore.txt'
- '.github/workflows/docs-llms-txt-check.yml'
- '.github/workflows/ci-gate.yml'
python-sdk:
name: Python SDK
needs: changes
if: needs.changes.outputs.python_sdk == 'true'
uses: ./.github/workflows/ci.yml
secrets: inherit
ts-sdk:
name: TypeScript SDK
needs: changes
if: needs.changes.outputs.ts_sdk == 'true'
uses: ./.github/workflows/ts-sdk-ci.yml
secrets: inherit
cli-python:
name: Python CLI
needs: changes
if: needs.changes.outputs.cli_python == 'true'
uses: ./.github/workflows/cli-python-ci.yml
secrets: inherit
cli-node:
name: Node CLI
needs: changes
if: needs.changes.outputs.cli_node == 'true'
uses: ./.github/workflows/cli-node-ci.yml
secrets: inherit
openclaw:
name: OpenClaw
needs: changes
if: needs.changes.outputs.openclaw == 'true'
uses: ./.github/workflows/openclaw-checks.yml
secrets: inherit
mem0-plugin:
name: Mem0 Plugin
needs: changes
if: needs.changes.outputs.mem0_plugin == 'true'
uses: ./.github/workflows/mem0-plugin-checks.yml
secrets: inherit
opencode-plugin:
name: OpenCode Plugin
needs: changes
if: needs.changes.outputs.opencode_plugin == 'true'
uses: ./.github/workflows/opencode-plugin-checks.yml
secrets: inherit
pi-agent-plugin:
name: Pi Agent Plugin
needs: changes
if: needs.changes.outputs.pi_agent_plugin == 'true'
uses: ./.github/workflows/pi-agent-plugin-checks.yml
secrets: inherit
n8n-nodes-mem0:
name: n8n Node
needs: changes
if: needs.changes.outputs.n8n_nodes_mem0 == 'true'
uses: ./.github/workflows/n8n-nodes-mem0-checks.yml
zapier-mem0:
name: Zapier App
needs: changes
if: needs.changes.outputs.zapier_mem0 == 'true'
uses: ./.github/workflows/zapier-mem0-checks.yml
secrets: inherit
docs-llms-txt:
name: docs llms.txt
needs: changes
if: needs.changes.outputs.docs_llms_txt == 'true'
uses: ./.github/workflows/docs-llms-txt-check.yml
secrets: inherit
gate:
name: CI Gate
needs:
- changes
- python-sdk
- ts-sdk
- cli-python
- cli-node
- openclaw
- mem0-plugin
- opencode-plugin
- pi-agent-plugin
- n8n-nodes-mem0
- zapier-mem0
- docs-llms-txt
if: always()
runs-on: ubuntu-latest
steps:
- name: Evaluate pipeline results
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
failed=$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key] | join(", ")')
if [ -n "$failed" ]; then
echo "::error::Failing pipelines: $failed"
exit 1
fi
echo "All pipelines relevant to this change passed."
+2 -4
View File
@@ -1,11 +1,9 @@
name: ci
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main runs remain standalone.
on:
push:
branches: [main]
workflow_call:
pull_request:
jobs:
changelog_check:
@@ -106,7 +104,7 @@ jobs:
run: |
pip install --upgrade pip
pip install -e ".[test,graph,vector_stores,llms,extras]"
pip install ruff==0.16.0
pip install ruff
- name: Run Linting
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make lint
+4 -18
View File
@@ -1,25 +1,13 @@
name: Publish @mem0/cli 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# cli-node-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/cli 📦 to npm
if: startsWith(inputs.tag, 'cli-node-v')
if: startsWith(github.event.release.tag_name, 'cli-node-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -28,8 +16,6 @@ jobs:
working-directory: cli/node
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
+4 -3
View File
@@ -1,7 +1,5 @@
name: CLI Node CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
@@ -9,7 +7,10 @@ on:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
workflow_call:
pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs:
lint:
+3 -16
View File
@@ -1,24 +1,13 @@
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged cli-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish mem0-cli 📦 to PyPI
if: startsWith(inputs.tag, 'cli-v')
if: startsWith(github.event.release.tag_name, 'cli-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -27,8 +16,6 @@ jobs:
working-directory: cli/python
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v5
+4 -3
View File
@@ -1,7 +1,5 @@
name: CLI Python CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
@@ -9,7 +7,10 @@ on:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
workflow_call:
pull_request:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
jobs:
lint:
+6 -3
View File
@@ -6,10 +6,13 @@ name: docs - llms.txt check
# python scripts/check-llms-txt-coverage.py # read-only
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
# On PRs this is invoked by ci-gate.yml (the single required check);
# manual runs remain standalone.
on:
workflow_call:
pull_request:
paths:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
- 'scripts/check-llms-txt-coverage.py'
- 'scripts/llms-txt-ignore.txt'
workflow_dispatch: {}
permissions:
+12 -40
View File
@@ -12,56 +12,28 @@ jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: stefanbuck/github-issue-parser@v3
id: issue-parser
continue-on-error: true
with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
continue-on-error: true
with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: component
token: ${{ secrets.GITHUB_TOKEN }}
config-path: .github/advanced-issue-labeler.yml
- name: Infer component from text when the form was not used
uses: actions/github-script@v7
- uses: stefanbuck/github-issue-parser@v3
id: feature-parser
if: contains(github.event.issue.labels.*.name, 'enhancement')
with:
script: |
const {
componentLabels,
inferComponentLabels,
loadKeywords,
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`);
const known = componentLabels(keywords);
const existing = issue.labels.map((label) => label.name || label);
if (existing.some((name) => known.includes(name))) {
core.info(`Component label already present: ${existing.join(', ')}`);
return;
}
const labels = inferComponentLabels(`${issue.title}\n\n${issue.body || ''}`, keywords);
if (labels.length === 0) {
core.info('No component could be inferred from the issue text');
return;
}
core.info(`Inferred: ${labels.join(', ')}`);
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels,
});
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
if: contains(github.event.issue.labels.*.name, 'enhancement')
with:
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
section: component
token: ${{ secrets.GITHUB_TOKEN }}
config-path: .github/advanced-issue-labeler.yml
-58
View File
@@ -1,58 +0,0 @@
name: Mem0 Plugin Checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
#
# Covers the Python plugin (scripts/ + tests/). The nested .opencode-plugin/
# is a separate package with its own workflow (opencode-plugin-checks.yml).
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/mem0-plugin/**'
- '!integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/mem0-plugin-checks.yml'
workflow_call:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
working-directory: integrations/mem0-plugin
run: |
pip install -r requirements.txt
pip install pytest
- name: Verify hook entry points are executable
working-directory: integrations/mem0-plugin
run: |
missing=$(find scripts -name '*.sh' ! -name '_*' ! -perm -u+x -print)
if [ -n "$missing" ]; then
echo "Hook entry points must be executable:"
echo "$missing"
exit 1
fi
- name: Check hook manifests are valid JSON
working-directory: integrations/mem0-plugin
run: |
for f in plugin.json mcp_config.json hooks.json hooks/*.json; do
jq empty "$f" || (echo "Invalid JSON: $f" && exit 1)
done
- name: Run tests
working-directory: integrations/mem0-plugin
run: pytest -q
-60
View File
@@ -1,60 +0,0 @@
name: Publish n8n-nodes-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# n8n-nodes-mem0-v* is published. Can also be dispatched manually to
# re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. n8n-nodes-mem0-v0.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish n8n-nodes-mem0 📦 to npm
if: startsWith(inputs.tag, 'n8n-nodes-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/n8n-nodes-mem0
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -1,88 +0,0 @@
name: n8n-nodes-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
- name: Lint
run: cd integrations/n8n-nodes-mem0 && pnpm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
- name: Run tests
run: cd integrations/n8n-nodes-mem0 && pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
- name: Build
run: cd integrations/n8n-nodes-mem0 && pnpm run build
- name: Verify dist output exists
run: |
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/Mem0.node.js || (echo "Build output missing: dist/nodes/Mem0/Mem0.node.js" && exit 1)
test -f integrations/n8n-nodes-mem0/dist/credentials/Mem0Api.credentials.js || (echo "Build output missing: dist/credentials/Mem0Api.credentials.js" && exit 1)
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/mem0.svg || (echo "Build output missing: dist/nodes/Mem0/mem0.svg" && exit 1)
+6 -20
View File
@@ -1,35 +1,21 @@
name: Publish @mem0/openclaw-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# openclaw-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/openclaw-mem0 📦 to npm
if: startsWith(inputs.tag, 'openclaw-v')
if: startsWith(github.event.release.tag_name, 'openclaw-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/openclaw
working-directory: openclaw
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -42,7 +28,7 @@ jobs:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
+17 -16
View File
@@ -1,15 +1,16 @@
name: openclaw checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/openclaw/**'
- 'openclaw/**'
- '.github/workflows/openclaw-checks.yml'
pull_request:
paths:
- 'openclaw/**'
- '.github/workflows/openclaw-checks.yml'
workflow_call:
jobs:
lint:
@@ -27,13 +28,13 @@ jobs:
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile
run: cd openclaw && pnpm install --frozen-lockfile
- name: Type check
run: cd integrations/openclaw && pnpm exec tsc --noEmit
run: cd openclaw && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
@@ -53,20 +54,20 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile
run: cd openclaw && pnpm install --frozen-lockfile
- name: Run tests with coverage
run: cd integrations/openclaw && pnpm exec vitest run --coverage
run: cd openclaw && pnpm exec vitest run --coverage
- name: Upload coverage to Codecov
if: matrix.node-version == 20
uses: codecov/codecov-action@v4
with:
flags: openclaw
directory: integrations/openclaw/coverage
directory: openclaw/coverage
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -85,15 +86,15 @@ jobs:
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile
run: cd openclaw && pnpm install --frozen-lockfile
- name: Build
run: cd integrations/openclaw && pnpm build
run: cd openclaw && pnpm build
- name: Verify dist output exists
run: |
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
+5 -19
View File
@@ -1,35 +1,21 @@
name: Publish @mem0/opencode-plugin 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# opencode-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. opencode-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/opencode-plugin 📦 to npm
if: startsWith(inputs.tag, 'opencode-v')
if: startsWith(github.event.release.tag_name, 'opencode-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
working-directory: mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install Bun
uses: oven-sh/setup-bun@v2
@@ -50,7 +36,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
+6 -5
View File
@@ -1,22 +1,23 @@
name: opencode-plugin checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/mem0-plugin/.opencode-plugin/**'
- 'mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
pull_request:
paths:
- 'mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
working-directory: mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
-60
View File
@@ -1,60 +0,0 @@
name: Publish @mem0/pi-agent-plugin 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# pi-agent-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. pi-agent-v0.1.1)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/pi-agent-plugin 📦 to npm
if: startsWith(inputs.tag, 'pi-agent-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/pi-agent-plugin
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -1,92 +0,0 @@
name: pi-agent-plugin checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Type check
run: cd integrations/pi-agent-plugin && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Run tests
run: cd integrations/pi-agent-plugin && pnpm exec vitest run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Build
run: cd integrations/pi-agent-plugin && pnpm build
- name: Verify dist output exists
run: |
test -f integrations/pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
test -f integrations/pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
test -f integrations/pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
-64
View File
@@ -1,64 +0,0 @@
name: PR Labeler
on:
pull_request_target:
types: [opened, synchronize, reopened, edited]
concurrency:
group: pr-labeler-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
issues: read
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Propagate labels from linked issues
uses: actions/github-script@v7
with:
script: |
const allowed = new Set([
'sdk-python', 'sdk-typescript', 'vector-store', 'plugin',
'rest-api', 'documentation', 'ci', 'cli', 'integrations',
]);
const umbrella = { plugin: 'integrations' };
const { repository } = await github.graphql(
`query ($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 20) {
nodes { labels(first: 50) { nodes { name } } }
}
}
}
}`,
{ owner: context.repo.owner, repo: context.repo.repo, number: context.issue.number },
);
const labels = new Set();
for (const issue of repository.pullRequest.closingIssuesReferences.nodes) {
for (const label of issue.labels.nodes) {
if (allowed.has(label.name)) labels.add(label.name);
}
}
for (const label of [...labels]) {
if (umbrella[label]) labels.add(umbrella[label]);
}
if (labels.size > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [...labels],
});
}
-69
View File
@@ -1,69 +0,0 @@
name: Release Router 🚦
# Single entry point for all release publishing.
#
# Package CD workflows no longer listen to release events themselves — this
# router inspects the release tag and dispatches only the matching pipeline,
# so each release produces one routed run instead of one real run plus seven
# skipped ones.
#
# Re-publishing a release (e.g. after fixing registry settings) does NOT
# require deleting and recreating it anymore — manually dispatch the
# package's CD workflow from the tag instead:
#
# gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>
#
# Note: dispatching runs the workflow file as it exists at the given ref, so
# this router can only dispatch tags created after the workflow_dispatch
# conversion landed on main. For older tags, dispatch manually from main.
on:
release:
types: [published]
permissions:
actions: write
jobs:
route:
name: Route ${{ github.event.release.tag_name }} to its CD pipeline
runs-on: ubuntu-latest
steps:
- name: Match tag prefix to CD workflow
id: match
env:
TAG: ${{ github.event.release.tag_name }}
run: |
# Specific package prefixes first; the bare v* (Python SDK) arm
# must stay last so prefixed tags that also start with 'v'
# (vercel-ai-v*) can never be routed to the Python pipeline.
case "$TAG" in
ts-v*) workflow="ts-sdk-cd.yml" ;;
cli-node-v*) workflow="cli-node-cd.yml" ;;
cli-v*) workflow="cli-python-cd.yml" ;;
vercel-ai-v*) workflow="vercel-ai-cd.yml" ;;
openclaw-v*) workflow="openclaw-cd.yml" ;;
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
v*) workflow="cd.yml" ;;
*)
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
exit 1
;;
esac
echo "workflow=$workflow" >> "$GITHUB_OUTPUT"
echo ":outbox_tray: Routed \`$TAG\` → \`$workflow\`" >> "$GITHUB_STEP_SUMMARY"
- name: Dispatch ${{ steps.match.outputs.workflow }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
run: |
# --ref points at the tag so the dispatched run builds (and signs
# provenance for) the exact tagged commit.
gh workflow run "${{ steps.match.outputs.workflow }}" \
--repo "$GITHUB_REPOSITORY" \
--ref "refs/tags/$TAG" \
-f tag="$TAG" \
-f prerelease="${{ github.event.release.prerelease }}"
+4 -17
View File
@@ -1,24 +1,13 @@
name: Publish mem0ai 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged ts-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish mem0ai 📦 to npm
if: startsWith(inputs.tag, 'ts-v')
if: startsWith(github.event.release.tag_name, 'ts-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -27,8 +16,6 @@ jobs:
working-directory: mem0-ts
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -51,7 +38,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
+3 -3
View File
@@ -1,14 +1,14 @@
name: TypeScript SDK CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main runs remain standalone.
on:
push:
branches: [main]
paths:
- 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml'
workflow_call:
pull_request:
paths:
- 'mem0-ts/**'
jobs:
check_changes:
+6 -20
View File
@@ -1,35 +1,21 @@
name: Publish @mem0/vercel-ai-provider 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# vercel-ai-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
release:
types: [published]
jobs:
build-n-publish:
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
if: startsWith(inputs.tag, 'vercel-ai-v')
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/vercel-ai-sdk
working-directory: vercel-ai-sdk
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -42,7 +28,7 @@ jobs:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
-42
View File
@@ -1,42 +0,0 @@
name: Deploy zapier-mem0 to Zapier
# Zapier apps deploy to Zapier's own platform (not npm), so this is NOT wired
# into the npm release router (release.yml). It is manual workflow_dispatch
# only and requires the ZAPIER_DEPLOY_KEY repo secret.
#
# gh workflow run zapier-mem0-cd.yml --ref main
on:
workflow_dispatch:
jobs:
push:
name: Push zapier-mem0 to Zapier
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/zapier-mem0
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build TypeScript
run: pnpm build
- name: Push to Zapier
env:
ZAPIER_DEPLOY_KEY: ${{ secrets.ZAPIER_DEPLOY_KEY }}
run: npx zapier-platform-cli@19 push
-47
View File
@@ -1,47 +0,0 @@
name: zapier-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
#
# CI compiles the TypeScript app, runs `zapier validate` (offline schema + style
# checks) against the build, plus the offline jest unit suite (test/unit.test.ts —
# mocked z.request, no network). The end-to-end jest suite is skipped here because
# it hits the live Mem0 API — it runs locally with MEM0_API_KEY set (see README).
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
workflow_call:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/zapier-mem0 && pnpm install --frozen-lockfile
- name: Build TypeScript
run: cd integrations/zapier-mem0 && pnpm build
- name: Validate Zapier app definition
run: cd integrations/zapier-mem0 && npx zapier-platform-cli@19 validate
- name: Run offline unit tests
run: cd integrations/zapier-mem0 && pnpm test:unit
-2
View File
@@ -189,5 +189,3 @@ eval/
qdrant_storage/
.crossnote
testing.ipynb
.weave/
-4
View File
@@ -1,4 +0,0 @@
[submodule "evaluation"]
path = evaluation
url = https://github.com/mem0ai/memory-benchmarks
branch = main
+67 -76
View File
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
### Key Directories
@@ -22,19 +22,17 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `integrations/n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` — n8n community node; add / search / get / update / delete memories |
| `integrations/zapier-mem0/` | `@mem0/zapier` — Zapier Platform CLI app (deploys to Zapier, not npm); add / search / get / delete memories |
| `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) |
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/`, `mem0-oss-to-platform/` |
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
| `docs/` | Documentation site (Mintlify) |
| `tests/` | Python SDK tests (pytest) |
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
| `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 |
| `pr-reviews/` | Pull request review materials |
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
@@ -51,8 +49,8 @@ mem0 (Python SDK) mem0-ts (TypeScript SDK)
cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls)
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
integrations/openclaw/ ──▶ mem0ai (npm)
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
openclaw/ ──▶ mem0ai (npm)
```
## Development Setup
@@ -63,7 +61,7 @@ integrations/openclaw/ ──▶ mem0ai (npm)
- **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/` development
- **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup
@@ -75,8 +73,8 @@ pre-commit install # install git hooks
# TypeScript packages
cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
cd integrations/openclaw && pnpm install # OpenClaw plugin
cd vercel-ai-sdk && pnpm install # Vercel AI provider
cd openclaw && pnpm install # OpenClaw plugin
```
## Build, Lint, and Test Commands
@@ -164,10 +162,10 @@ pnpm run dev # tsx src/index.ts (development)
- **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
```bash
cd integrations/vercel-ai-sdk
cd vercel-ai-sdk
pnpm install
pnpm run build # tsup
pnpm run lint # eslint
@@ -182,10 +180,10 @@ pnpm run test:node # vitest (node runtime)
- **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`integrations/openclaw/`)
### OpenClaw Plugin (`openclaw/`)
```bash
cd integrations/openclaw
cd openclaw
pnpm install
pnpm run build # tsup
pnpm run test # vitest run
@@ -215,6 +213,28 @@ docker-compose up # starts all 3 services
- **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
@@ -225,19 +245,18 @@ make docs # or: cd docs && mintlify dev
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation / Benchmarking
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
### Evaluation (`evaluation/`)
```bash
git clone https://github.com/mem0ai/memory-benchmarks.git
cd memory-benchmarks
pip install -r requirements.txt
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
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
@@ -310,6 +329,7 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
- 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 `openmemory/` from root config.
### TypeScript Conventions
@@ -322,8 +342,8 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
|---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest |
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `integrations/openclaw/` | — | — | vitest |
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `openclaw/` | — | — | vitest |
### Type Checking
@@ -360,14 +380,15 @@ Optional layer on top of vector memory for relationship-aware retrieval. Configu
Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Plugin:** MCP tools in `integrations/mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
- **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
- `integrations/mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `skills/` contains structured skill definitions for AI agents, split into two categories:
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch (the two are loosely coupled via `.mem0-integration/` artifacts); `mem0-oss-to-platform` migrates an existing project from Mem0 OSS to the hosted Platform SDK (plan, then execute on approval).
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch. The two are loosely coupled via `.mem0-integration/` artifacts.
### Adding a New Provider
@@ -381,47 +402,23 @@ To add a new LLM, embedding, vector store, or reranker provider:
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
### Adding a New Integration
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
1. Create `integrations/<name>/` and build the integration there.
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
## CI/CD
### CI Workflows (automated testing)
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
| Workflow | File | Standalone Triggers | Tests |
|----------|------|---------------------|-------|
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| Mem0 Plugin | `mem0-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/`, excluding `.opencode-plugin/`), manual | pytest + hook entry-point exec bits + JSON manifest validation on Python 3.10, 3.11, 3.12 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (on `integrations/n8n-nodes-mem0/`), manual | ESLint (n8n-nodes-base) + tsc build (dist artifact check) on Node 20 |
| Zapier App | `zapier-mem0-checks.yml` | Push to main (on `integrations/zapier-mem0/`), manual | build (tsc) + `zapier validate` + offline unit tests on Node 22 |
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
| Workflow | File | Triggers | Tests |
|----------|------|----------|-------|
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to `mem0-plugin/.opencode-plugin/`, PRs, manual | Bun: tsc type-check + build + dist artifact check |
### CD Workflows (automated publishing)
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
| Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------|
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
@@ -429,22 +426,15 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
- The **Zapier app** (`integrations/zapier-mem0`) deploys to Zapier's own platform, not npm, so it is **not** in the release router. Deploy it manually: `gh workflow run zapier-mem0-cd.yml --ref main` (requires the `ZAPIER_DEPLOY_KEY` secret).
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
### Utility Workflows
| Workflow | File | Purpose |
|----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
| PR Labeler | `pr-labeler.yml` | Path-based PR labeling plus propagating labels from linked issues |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
@@ -567,7 +557,7 @@ N/A
- 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/` work, use Docker Compose for local development.
- 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
@@ -590,4 +580,5 @@ N/A
- 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/`.
+43 -130
View File
@@ -1,157 +1,72 @@
# Contributing to Mem0
# Contributing to mem0
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
community-driven project and we welcome contributions of all kinds — bug fixes,
new features, documentation, examples, and integrations.
Let us make contribution easy, collaborative and fun.
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
## Submit your Contribution through PR
## Before You Start
To make a contribution, follow these steps:
### 1. Open an Issue First
1. Fork and clone this repository
2. Do the changes on your fork with dedicated feature branch `feature/f1`
3. If you modified the code (new feature or bug-fix), please add tests for it
4. Include proper documentation / docstring and examples to run the feature
5. Ensure that all tests pass
6. Submit a pull request
**Always open an issue before opening a pull request.** This lets us discuss the
change, avoid duplicate effort, and agree on the approach before you invest time
in code.
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
your bug or idea already exists.
- If it doesn't, open a
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
before starting significant work.
Every pull request must link to an issue using `Closes #<issue-number>`.
### 📦 Development Environment
### 2. Sign the Contributor License Agreement (CLA)
**We cannot accept or merge any pull request until you have signed our Contributor
License Agreement (CLA).**
When you open your first PR, the CLA bot will automatically comment with a link to
sign. Signing takes less than a minute and only needs to be done once. Pull
requests from contributors who have not signed the CLA will be blocked from
merging.
## Repository Layout
The two most common contribution targets are the SDKs:
| Package | Path | Language | Package manager |
| --------------------- | ---------- | ------------ | --------------- |
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
(`integrations/`), the self-hosted `server/`, and the docs site
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
## Development Workflow
1. **Fork** the repository and **clone** your fork.
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
`fix/issue-1234`).
3. Make your changes — add **tests**, **documentation**, and **examples** as
appropriate.
4. Run **linting and tests** for every package you touched (see below).
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
6. Push and open a **pull request** against `main`, linking the issue with
`Closes #<number>` and filling out the
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
### Contributing to the Python SDK (`mem0/`)
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
**Do not use `pip` or `conda` for dependency management.**
We use `hatch` for managing development environments. To set up:
```bash
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
hatch shell dev_py_3_11
# Activate environment for specific Python version:
hatch shell dev_py_3_9 # Python 3.9
hatch shell dev_py_3_10 # Python 3.10
hatch shell dev_py_3_11 # Python 3.11
hatch shell dev_py_3_12 # Python 3.12
# Install pre-commit hooks (runs ruff + isort on commit)
pre-commit install
# Lint, format, and sort imports
make lint
make format
make sort
# Run the test suite (run `make install_all` first if deps are missing)
# The environment will automatically install all dev dependencies
# Run tests within the activated shell:
make test
```
- **Linter / formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Tests:** pytest (in `tests/`)
### 📌 Pre-commit
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
environment details.
### Contributing to the TypeScript SDK (`mem0-ts/`)
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
`npm` or `yarn`.**
To ensure our standards, make sure to install pre-commit before starting to contribute.
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup (CJS + ESM)
pnpm run test # jest (all tests)
pnpm run test:unit # unit tests with coverage
pre-commit install
```
- **Build:** tsup
- **Formatter:** Prettier
- **Tests:** jest
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
- Use ES module `import` syntax — never `require()`.
### 🧪 Testing
## Good Contribution Practices
We use `pytest` to test our code across multiple Python versions. You can run tests using:
- **Keep PRs small and focused.** One logical change per PR is easier to review and
merge.
- **Follow existing patterns.** Match the style, structure, and conventions of the
code around you. Don't introduce new frameworks or abstractions without
discussion.
- **Write tests** that would fail without your change — regression tests for bugs,
coverage for new features.
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
must be added to `docs/llms.txt` (run
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
- **Add examples** when introducing new user-facing behavior.
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
via the CI Gate.
- **Never commit secrets** — no `.env` files, API keys, or credentials.
- **Don't add core dependencies lightly.** New Python dependencies belong in an
optional group in `pyproject.toml`, not the core `dependencies` list.
- **Be responsive** to review feedback and keep your branch up to date with `main`.
```bash
# Run tests with default Python version
make test
## Pull Request Checklist
# Test specific Python versions:
make test-py-3.9 # Python 3.9 environment
make test-py-3.10 # Python 3.10 environment
make test-py-3.11 # Python 3.11 environment
make test-py-3.12 # Python 3.12 environment
Before requesting review, make sure:
# When using hatch shells, run tests with:
make test # After activating a shell with hatch shell test_XX
```
- [ ] An issue exists and is linked with `Closes #<number>`
- [ ] You have signed the CLA
- [ ] Your code follows the project's style guidelines (lint passes)
- [ ] You performed a self-review of your changes
- [ ] Tests are added/updated and pass locally
- [ ] Documentation is updated if needed
Make sure that all tests pass across all supported Python versions before submitting a pull request.
## Reporting Security Issues
We look forward to your pull requests and can't wait to see your contributions!
**Do not report security vulnerabilities through public issues or pull requests.**
Please follow our [Security Policy](./SECURITY.md) to report them privately.
### 🚀 Releasing
## Releasing
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
All packages are published automatically via GitHub Actions when a GitHub Release
is created with the correct tag prefix.
### Tag Prefixes
#### Tag Prefixes
| Package | Registry | Tag Prefix | Example |
|---------|----------|------------|---------|
@@ -162,17 +77,15 @@ is created with the correct tag prefix.
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
### How to Release
#### How to Release
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
### Publishing Details
#### Publishing Details
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
- All workflows require `permissions: id-token: write` for OIDC authentication
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
We look forward to your pull requests and can't wait to see your contributions!
+1 -2
View File
@@ -1026,8 +1026,7 @@ def get_user_preferences(user_id: str):
### AutoGen Integration
```python
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
from helper.mem0_teachability import Mem0Teachability
from cookbooks.helper.mem0_teachability import Mem0Teachability
from mem0 import Memory
# Add memory capability to AutoGen agents
+1 -1
View File
@@ -11,7 +11,7 @@ install:
hatch env create
install_all:
pip install ruff==0.16.0 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
+6 -7
View File
@@ -46,12 +46,12 @@
| Benchmark | Old | New | Tokens | Latency p50 |
| --- | --- | --- | --- | --- |
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops) at a top_200 retrieval budget. Scores reflect Mem0's managed platform, which includes proprietary optimizations not available in the open-source SDK; open-source users should expect directionally similar gains but not identical numbers.
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops).
**What changed:**
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
@@ -63,8 +63,8 @@ All benchmarks run on the same production-representative model stack. Single-pas
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
## Research Highlights
- **92.5 on LoCoMo** -- +21 points over the previous algorithm
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
- [Read the full paper](https://mem0.ai/research)
@@ -186,10 +186,9 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
```
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. Use `/mem0-oss-to-platform` to migrate an existing project from Mem0 OSS to the hosted Platform SDK. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
### Basic Usage
-48
View File
@@ -1,48 +0,0 @@
# Security Policy
We take the security of Mem0 and our community seriously. Thank you for helping
keep Mem0 and its users safe by disclosing vulnerabilities responsibly.
## Reporting a Vulnerability
Please **do not** report security vulnerabilities through public GitHub issues,
pull requests, or discussions.
If you believe you have found a security vulnerability in Mem0, please report it
privately through one of the following channels:
1. **GitHub Private Vulnerability Reporting** — open a
[private security advisory](https://github.com/mem0ai/mem0/security/advisories/new)
directly on this repository.
2. **Email** the maintainers at **support@mem0.ai** with the subject line:
`SECURITY: Mem0 vulnerability report`
To help us triage and resolve the issue quickly, please include as much of the
following as you can:
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, CLI)
- Affected version, tag, or commit
- Clear, step-by-step reproduction instructions
- The security impact and a proof of concept, if available
- Any suggested fix or mitigation
## Response Process
- We will acknowledge receipt of your report within **72 hours**.
- We will work with you privately to confirm the issue and assess its impact.
- Once a fix or mitigation is ready, we will coordinate a disclosure timeline
with you and credit you for the discovery, unless you prefer to remain anonymous.
## Public Disclosure
Please avoid sharing technical details of the vulnerability publicly until the
maintainers have reviewed the issue and a fix or mitigation has been released. We
are committed to resolving valid reports promptly and keeping you informed
throughout the process.
## Supported Versions
We release security fixes against the latest published version of each package.
Whenever possible, please reproduce the issue on the most recent release before
reporting.
+2 -1
View File
@@ -60,8 +60,9 @@ mem0 delete <memory-id>
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
| `mem0 status` | Verify API connection and display current project |
| `mem0 version` | Print the CLI version |
Run `mem0 <command> --help` for detailed usage on any command, or `mem0 --version` to print the CLI version.
Run `mem0 <command> --help` for detailed usage on any command.
## Agent mode
+1 -13
View File
@@ -207,11 +207,7 @@
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Not supported on add, use --custom-categories instead." },
{ "name": "custom_instructions", "flags": ["--custom-instructions"], "type": "string", "help": "Custom instructions for fact extraction." },
{ "name": "custom_categories", "flags": ["--custom-categories"], "type": "string", "help": "Custom categories as a JSON array of {name: description} objects." },
{ "name": "structured_data_schema", "flags": ["--structured-data-schema"], "type": "string", "help": "Schema for structured data extraction, as JSON." },
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
@@ -248,9 +244,6 @@
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Search" },
{ "name": "reference_date", "flags": ["--reference-date"], "type": "string", "help": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).", "panel": "Search" },
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Search" },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
@@ -303,8 +296,6 @@
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Filters" },
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Filters" },
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
@@ -338,8 +329,6 @@
],
"options": [
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
],
"apiEndpoint": "update"
@@ -367,7 +356,6 @@
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
{ "name": "delete_linked", "flags": ["--delete-linked"], "type": "boolean", "default": false, "help": "Also delete memories linked to this memory." },
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
+36
View File
@@ -0,0 +1,36 @@
# Changelog
All notable changes to `@mem0/cli` are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.7] — 2026-05-20
### Added
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
leaderboard identifier). Reads from local config, no network call.
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
platform error codes into actionable hints (e.g. `agentrush_search_first`
→ "Run 3 'mem0 agent-rush search' commands before adding.").
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
explicit `y` to acknowledge that AGENTRUSH memories are public; the
acknowledgement is persisted in `~/.mem0/config.json` under
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
Non-interactive (agent) invocations surface the warning to stderr without
blocking.
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
empty until first interactive acknowledgement).
### Changed
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
in addition to the existing source headers, so platform telemetry can split
game traffic from regular CLI usage.
## [0.2.6] and earlier
Unlogged historical releases. See git history under `cli/node/`.
+8 -2
View File
@@ -232,6 +232,14 @@ Verify your API connection and display the current project.
mem0 status
```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
@@ -290,8 +298,6 @@ These flags are available on all commands:
| `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format |
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
## Environment variables
| Variable | Description |
+1 -1
View File
@@ -51,7 +51,7 @@ pnpm link --global
# Now use it like a normal CLI
mem0 --help
mem0 --version
mem0 version
```
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
+2 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.12",
"version": "0.2.7",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
@@ -40,19 +40,8 @@
"typescript": "^5.4.0",
"tsup": "^8.0.0",
"tsx": "^4.7.0",
"vite": "^6.0.0",
"vitest": "^4.1.0",
"vitest": "^1.5.0",
"@biomejs/biome": "^1.7.0",
"@types/node": "^20.0.0"
},
"pnpm": {
"overrides": {
"jws@4.0.0": "4.0.1",
"langsmith@<0.6.0": "^0.6.0",
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"picomatch@<2.3.2": "^2.3.2",
"esbuild": ">=0.28.1",
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
}
}
}
+724 -328
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
packages:
- '.'
onlyBuiltDependencies:
- "@biomejs/biome"
- esbuild
overrides:
jws@4.0.0: 4.0.1
langsmith@<0.6.0: ^0.6.0
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
picomatch@<2.3.2: ^2.3.2
"esbuild": ">=0.28.1"
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
+1 -16
View File
@@ -14,10 +14,7 @@ export interface AddOptions {
immutable?: boolean;
infer?: boolean;
expires?: string;
customInstructions?: string;
customCategories?: Record<string, string>[];
structuredDataSchema?: Record<string, unknown>;
timestamp?: number;
categories?: string[];
}
export interface SearchOptions {
@@ -31,9 +28,6 @@ export interface SearchOptions {
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
showExpired?: boolean;
referenceDate?: string | number;
latestOnly?: boolean;
}
export interface ListOptions {
@@ -46,8 +40,6 @@ export interface ListOptions {
category?: string;
after?: string;
before?: string;
showExpired?: boolean;
latestOnly?: boolean;
}
export interface DeleteOptions {
@@ -56,12 +48,6 @@ export interface DeleteOptions {
agentId?: string;
appId?: string;
runId?: string;
deleteLinked?: boolean;
}
export interface UpdateOptions {
expirationDate?: string;
timestamp?: number;
}
export interface EntityIds {
@@ -91,7 +77,6 @@ export interface Backend {
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
opts?: UpdateOptions,
): Promise<Record<string, unknown>>;
delete(
+19 -53
View File
@@ -15,13 +15,8 @@ import {
type ListOptions,
NotFoundError,
type SearchOptions,
type UpdateOptions,
} from "./base.js";
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
@@ -151,13 +146,7 @@ export class PlatformBackend implements Backend {
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.customInstructions)
payload.custom_instructions = opts.customInstructions;
if (opts.customCategories)
payload.custom_categories = opts.customCategories;
if (opts.structuredDataSchema)
payload.structured_data_schema = opts.structuredDataSchema;
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
if (opts.categories) payload.categories = opts.categories;
payload.source = "CLI";
return (await this._request("POST", "/v3/memories/add/", {
@@ -218,10 +207,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.showExpired) payload.show_expired = true;
if (opts.referenceDate !== undefined)
payload.reference_date = opts.referenceDate;
if (opts.latestOnly) payload.latest_only = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/search/", {
@@ -233,13 +218,9 @@ export class PlatformBackend implements Backend {
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request(
"GET",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
}
async listMemories(
@@ -276,8 +257,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.showExpired) payload.show_expired = true;
if (opts.latestOnly) payload.latest_only = true;
payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/", {
@@ -293,21 +272,14 @@ export class PlatformBackend implements Backend {
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
opts: UpdateOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
if (opts.expirationDate) payload.expiration_date = opts.expirationDate;
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
payload.source = "CLI";
return (await this._request(
"PUT",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
json: payload,
},
)) as Record<string, unknown>;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async delete(
@@ -325,13 +297,9 @@ export class PlatformBackend implements Backend {
})) as Record<string, unknown>;
}
if (memoryId) {
const params: Record<string, string> = { source: "CLI" };
if (opts.deleteLinked) params.delete_linked = "true";
return (await this._request(
"DELETE",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{ params },
)) as Record<string, unknown>;
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
params: { source: "CLI" },
})) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
@@ -348,18 +316,16 @@ export class PlatformBackend implements Backend {
if (entities.length === 0) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
// Delete each provided entity via the v2 path-based endpoint. Key each
// response by entity type so a multi-entity delete (e.g. --user-id and
// --agent-id together) doesn't discard everything but the last result.
const results: Record<string, unknown> = {};
// Delete each provided entity via the v2 path-based endpoint
let result: Record<string, unknown> = {};
for (const [entityType, entityId] of entities) {
results[entityType] = (await this._request(
result = (await this._request(
"DELETE",
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`,
`/v2/entities/${entityType}/${entityId}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
return results;
return result;
}
async ping(): Promise<Record<string, unknown>> {
@@ -418,9 +384,9 @@ export class PlatformBackend implements Backend {
}
async getEvent(eventId: string): Promise<Record<string, unknown>> {
return (await this._request(
"GET",
`/v1/event/${encodePathSegment(eventId)}/`,
)) as Record<string, unknown>;
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
string,
unknown
>;
}
}
+33 -75
View File
@@ -21,19 +21,16 @@ import {
formatSingleMemory,
printResultSummary,
} from "../output.js";
import { isAgentMode, setCurrentCommand, stdinIsPiped } from "../state.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
/** Exit 1 if value is not a future YYYY-MM-DD date. */
function _validateExpires(value: string): void {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
printError(
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
);
process.exit(1);
}
if (new Date(value) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
function _stdinIsPiped(): boolean {
if (isAgentMode()) return false;
try {
const stat = fs.fstatSync(0);
return stat.isFIFO() || stat.isFile();
} catch {
return false;
}
}
@@ -52,22 +49,10 @@ export async function cmdAdd(
infer?: boolean;
expires?: string;
categories?: string;
customInstructions?: string;
customCategories?: string;
structuredDataSchema?: string;
timestamp?: number;
output: string;
},
): Promise<void> {
setCurrentCommand("add");
if (opts.categories) {
printError(
"--categories is not supported on add. Use --custom-categories instead.",
);
process.exit(1);
}
let msgs: Record<string, unknown>[] | undefined;
let content = text;
@@ -93,7 +78,7 @@ export async function cmdAdd(
}
}
// Read from stdin only if stdin is an actual pipe or file redirect
else if (!content && stdinIsPiped()) {
else if (!content && _stdinIsPiped()) {
content = fs.readFileSync(0, "utf-8").trim();
}
@@ -108,6 +93,20 @@ export async function cmdAdd(
process.exit(1);
}
// Validate --expires
if (opts.expires) {
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
printError(
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
);
process.exit(1);
}
if (new Date(opts.expires) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
}
}
let meta: Record<string, unknown> | undefined;
if (opts.metadata) {
try {
@@ -118,28 +117,15 @@ export async function cmdAdd(
}
}
let customCats: Record<string, string>[] | undefined;
if (opts.customCategories) {
let cats: string[] | undefined;
if (opts.categories) {
try {
customCats = JSON.parse(opts.customCategories);
cats = JSON.parse(opts.categories);
} catch {
printError("Invalid JSON in --custom-categories.");
process.exit(1);
cats = opts.categories.split(",").map((c) => c.trim());
}
}
let schema: Record<string, unknown> | undefined;
if (opts.structuredDataSchema) {
try {
schema = JSON.parse(opts.structuredDataSchema);
} catch {
printError("Invalid JSON in --structured-data-schema.");
process.exit(1);
}
}
if (opts.expires) _validateExpires(opts.expires);
let result: Record<string, unknown>;
try {
result = await timedStatus("Adding memory...", async () => {
@@ -152,10 +138,7 @@ export async function cmdAdd(
immutable: opts.immutable,
infer: opts.infer !== false,
expires: opts.expires,
customInstructions: opts.customInstructions,
customCategories: customCats,
structuredDataSchema: schema,
timestamp: opts.timestamp,
categories: cats,
});
});
} catch (e) {
@@ -240,9 +223,6 @@ export async function cmdSearch(
keyword: boolean;
filterJson?: string;
fields?: string;
showExpired?: boolean;
referenceDate?: string;
latestOnly?: boolean;
output: string;
},
): Promise<void> {
@@ -291,9 +271,6 @@ export async function cmdSearch(
keyword: opts.keyword,
filters,
fields: fieldList,
showExpired: opts.showExpired,
referenceDate: opts.referenceDate,
latestOnly: opts.latestOnly,
});
});
} catch (e) {
@@ -387,8 +364,6 @@ export async function cmdList(
category?: string;
after?: string;
before?: string;
showExpired?: boolean;
latestOnly?: boolean;
output: string;
},
): Promise<void> {
@@ -416,8 +391,6 @@ export async function cmdList(
category: opts.category,
after: opts.after,
before: opts.before,
showExpired: opts.showExpired,
latestOnly: opts.latestOnly,
});
});
} catch (e) {
@@ -477,12 +450,7 @@ export async function cmdUpdate(
backend: Backend,
memoryId: string,
text: string | undefined,
opts: {
metadata?: string;
expires?: string;
timestamp?: number;
output: string;
},
opts: { metadata?: string; output: string },
): Promise<void> {
setCurrentCommand("update");
let meta: Record<string, unknown> | undefined;
@@ -495,16 +463,11 @@ export async function cmdUpdate(
}
}
if (opts.expires) _validateExpires(opts.expires);
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Updating memory...", async () => {
return backend.update(memoryId, text, meta, {
expirationDate: opts.expires,
timestamp: opts.timestamp,
});
return backend.update(memoryId, text, meta);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
@@ -530,12 +493,7 @@ export async function cmdUpdate(
export async function cmdDelete(
backend: Backend,
memoryId: string,
opts: {
output: string;
dryRun?: boolean;
force?: boolean;
deleteLinked?: boolean;
},
opts: { output: string; dryRun?: boolean; force?: boolean },
): Promise<void> {
setCurrentCommand("delete");
if (opts.dryRun) {
@@ -556,7 +514,7 @@ export async function cmdDelete(
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting...", async () => {
return backend.delete(memoryId, { deleteLinked: opts.deleteLinked });
return backend.delete(memoryId);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
+6 -1
View File
@@ -1,5 +1,5 @@
/**
* Utility commands: status, import.
* Utility commands: status, version, import.
*/
import fs from "node:fs";
@@ -8,6 +8,7 @@ import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { setCurrentCommand } from "../state.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
@@ -81,6 +82,10 @@ export async function cmdStatus(
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
+5 -57
View File
@@ -17,7 +17,6 @@ import {
isAgentMode,
setAgentMode,
setCurrentCommand,
stdinIsPiped,
takeNotice,
} from "./state.js";
import { captureEvent } from "./telemetry.js";
@@ -320,25 +319,7 @@ program
.option("--immutable", "Prevent future updates.", false)
.option("--no-infer", "Skip inference, store raw.")
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
.option(
"--categories <value>",
"Not supported on add, use --custom-categories instead.",
)
.option(
"--custom-instructions <text>",
"Custom instructions for fact extraction.",
)
.option(
"--custom-categories <json>",
"Custom categories as a JSON array of {name: description} objects.",
)
.option(
"--structured-data-schema <json>",
"Schema for structured data extraction, as JSON.",
)
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
Number.parseInt(v),
)
.option("--categories <value>", "Categories (JSON array or comma-separated).")
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
@@ -385,16 +366,6 @@ program
.option("--keyword", "Use keyword search.", false)
.option("--filter <json>", "Advanced filter expression (JSON).")
.option("--fields <list>", "Specific fields to return (comma-separated).")
.option("--show-expired", "Include expired memories.", false)
.option(
"--reference-date <date>",
"Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
)
.option(
"--latest-only",
"Only return the latest version of each memory.",
false,
)
.option("-o, --output <format>", "Output: text, json, table.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
@@ -404,7 +375,7 @@ program
)
.action(async (query, opts) => {
let resolvedQuery = query;
if (!resolvedQuery && stdinIsPiped()) {
if (!resolvedQuery && !process.stdin.isTTY) {
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
}
if (!resolvedQuery) {
@@ -427,9 +398,6 @@ program
keyword: opts.keyword,
filterJson: opts.filter,
fields: opts.fields,
showExpired: opts.showExpired,
referenceDate: opts.referenceDate,
latestOnly: opts.latestOnly,
output,
});
});
@@ -473,12 +441,6 @@ program
.option("--category <name>", "Filter by category.")
.option("--after <date>", "Created after (YYYY-MM-DD).")
.option("--before <date>", "Created before (YYYY-MM-DD).")
.option("--show-expired", "Include expired memories.", false)
.option(
"--latest-only",
"Only return the latest version of each memory.",
false,
)
.option("-o, --output <format>", "Output: text, json, table.", "table")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
@@ -502,8 +464,6 @@ program
category: opts.category,
after: opts.after,
before: opts.before,
showExpired: opts.showExpired,
latestOnly: opts.latestOnly,
output,
});
});
@@ -514,10 +474,6 @@ program
.command("update <memoryId> [text]")
.description("Update a memory's text or metadata.")
.option("-m, --metadata <json>", "Update metadata (JSON).")
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
Number.parseInt(v),
)
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.")
@@ -527,7 +483,7 @@ program
)
.action(async (memoryId, text, opts) => {
let resolvedText = text;
if (!resolvedText && stdinIsPiped()) {
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
resolvedText = fs.readFileSync(0, "utf-8").trim();
}
const { cmdUpdate } = await import("./commands/memory.js");
@@ -536,8 +492,6 @@ program
const output = isAgent ? "agent" : opts.output;
await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata,
expires: opts.expires,
timestamp: opts.timestamp,
output,
});
});
@@ -556,11 +510,6 @@ program
.option("--project", "With --all: delete ALL memories project-wide.", false)
.option("--dry-run", "Show what would be deleted without deleting.", false)
.option("--force", "Skip confirmation.", false)
.option(
"--delete-linked",
"Also delete memories linked to this memory.",
false,
)
.option("-u, --user-id <id>", "Scope to user.")
.option("--agent-id <id>", "Scope to agent.")
.option("--app-id <id>", "Scope to app.")
@@ -614,7 +563,6 @@ program
output,
dryRun: opts.dryRun,
force: opts.force,
deleteLinked: opts.deleteLinked,
});
return;
}
@@ -858,8 +806,8 @@ program
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
.action((opts) => {
// opts.json is set when `mem0 help --json` is used (subcommand flag).
// program.opts().json/.agent is set when a root global flag was used first.
if (opts.json || program.opts().json || program.opts().agent) {
// program.opts().json is set when the root --json global flag was used first.
if (opts.json || program.opts().json) {
// Load spec from parent directory
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
+3 -17
View File
@@ -282,24 +282,11 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
}
case "search":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, [
"id",
"memory",
"score",
"created_at",
"categories",
"expiration_date",
]),
pick(r, ["id", "memory", "score", "created_at", "categories"]),
);
case "list":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, [
"id",
"memory",
"created_at",
"categories",
"expiration_date",
]),
pick(r, ["id", "memory", "created_at", "categories"]),
);
case "get": {
const r = data as Record<string, unknown>;
@@ -310,12 +297,11 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
"updated_at",
"categories",
"metadata",
"expiration_date",
]);
}
case "update": {
const r = data as Record<string, unknown>;
return pick(r, ["id", "memory", "expiration_date"]);
return pick(r, ["id", "memory"]);
}
case "delete":
case "delete-all":
-13
View File
@@ -3,8 +3,6 @@
* read by commands and branding functions.
*/
import fs from "node:fs";
let _agentMode = false;
let _currentCommand = "";
let _pendingNotice = "";
@@ -40,14 +38,3 @@ export function takeNotice(): string {
_pendingNotice = "";
return msg;
}
/** True only when stdin is an actual pipe or file redirect (never in agent mode). */
export function stdinIsPiped(): boolean {
if (isAgentMode()) return false;
try {
const stat = fs.fstatSync(0);
return stat.isFIFO() || stat.isFile();
} catch {
return false;
}
}
+5 -5
View File
@@ -145,11 +145,11 @@ export function captureEvent(
anonDistinctIdToAlias: anonIdToAlias,
};
const child = spawn(process.execPath, [SENDER_SCRIPT], {
detached: true,
stdio: ["pipe", "ignore", "ignore"],
});
child.stdin?.end(JSON.stringify(context));
const child = spawn(
process.execPath,
[SENDER_SCRIPT, JSON.stringify(context)],
{ detached: true, stdio: "ignore" },
);
child.unref();
} catch {
/* silently swallow */
+2 -28
View File
@@ -1,8 +1,7 @@
/**
* Standalone telemetry sender — runs as a detached child process.
*
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single
* argv argument is still accepted as a legacy fallback)
* Usage: node telemetry-sender.cjs '<json context>'
*
* This script is spawned by telemetry.captureEvent() and runs independently
* of the parent CLI process. It:
@@ -20,31 +19,6 @@
const https = require("https");
const fs = require("fs");
function loadContext() {
return new Promise((resolve, reject) => {
if (process.argv[2]) {
try {
resolve(JSON.parse(process.argv[2]));
} catch (err) {
reject(err);
}
return;
}
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
process.stdin.on("error", reject);
});
}
function httpsRequest(url, method, headers, body) {
return new Promise((resolve, reject) => {
const u = new URL(url);
@@ -134,7 +108,7 @@ async function sendIdentifyEvent(ctx, payload, anonId) {
}
async function main() {
const ctx = await loadContext();
const ctx = JSON.parse(process.argv[2]);
const payload = ctx.payload;
if (ctx.needsEmail && ctx.mem0ApiKey) {
+7 -17
View File
@@ -44,25 +44,15 @@ describe("CLI Integration — help and version", () => {
expect(result.stdout).toContain("search");
});
it("prints the version with --version, and has no version subcommand", () => {
const flag = run(["--version"]);
expect(flag.exitCode).toBe(0);
expect(flag.stdout).toContain("Mem0");
expect(run(["version"]).exitCode).not.toBe(0);
it("help --json produces valid JSON", () => {
const result = run(["help", "--json"]);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
// spec may have cli.name or top-level name
const name = parsed.name ?? parsed.cli?.name;
expect(name).toBe("mem0");
});
it.each([["help", "--json"], ["--json", "help"], ["--agent", "help"]])(
"%s %s produces valid JSON",
(...args) => {
const result = run(args);
expect(result.exitCode).toBe(0);
const parsed = JSON.parse(result.stdout);
// spec may have cli.name or top-level name
const name = parsed.name ?? parsed.cli?.name;
expect(name).toBe("mem0");
},
);
it("shows add help", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
File diff suppressed because it is too large Load Diff
-163
View File
@@ -1,163 +0,0 @@
/**
* Drift test: every documented v3 add/search/list param must be reachable from the Node CLI.
*/
import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
const OPENAPI_PATH = path.join(
__dirname,
"..",
"..",
"..",
"docs",
"openapi.json",
);
const KNOWN_UNSURFACED: Record<string, Record<string, string>> = {
"/v3/memories/add/": {
includes: "extraction hint, no CLI flag yet",
excludes: "extraction hint, no CLI flag yet",
enable_graph: "graph memory toggle, no CLI flag yet",
output_format: "response envelope is pinned by the CLI",
prompt_profile_id: "no CLI flag yet",
temporal_reasoning: "no CLI flag yet",
timezone: "no CLI flag yet",
observation_datetime: "no CLI flag yet, --timestamp backdates instead",
observation_date: "no CLI flag yet, --timestamp backdates instead",
},
"/v3/memories/search/": {
categories: "expressible through --filter",
metadata: "expressible through --filter",
},
"/v3/memories/": {
start_date: "covered by --after via filters.created_at.gte",
end_date: "covered by --before via filters.created_at.lte",
categories: "covered by --category via filters.categories",
fields: "no CLI flag yet",
keywords: "no CLI flag yet",
},
};
const ADD_MAPPING: Record<string, string[]> = {
messages: ["--messages", "--file", "text"],
user_id: ["--user-id"],
agent_id: ["--agent-id"],
app_id: ["--app-id"],
run_id: ["--run-id"],
metadata: ["--metadata"],
expiration_date: ["--expires"],
custom_instructions: ["--custom-instructions"],
custom_categories: ["--custom-categories"],
infer: ["--no-infer"],
immutable: ["--immutable"],
structured_data_schema: ["--structured-data-schema"],
timestamp: ["--timestamp"],
};
const SEARCH_MAPPING: Record<string, string[]> = {
query: ["query"],
filters: ["--filter", "--user-id", "--agent-id", "--run-id"],
show_expired: ["--show-expired"],
top_k: ["--top-k"],
threshold: ["--threshold"],
rerank: ["--rerank"],
reference_date: ["--reference-date"],
fields: ["--fields"],
};
const LIST_MAPPING: Record<string, string[]> = {
filters: [
"--user-id",
"--agent-id",
"--run-id",
"--category",
"--after",
"--before",
],
show_expired: ["--show-expired"],
page: ["--page"],
page_size: ["--page-size"],
};
function documentedFields(endpoint: string): string[] {
const spec = JSON.parse(fs.readFileSync(OPENAPI_PATH, "utf-8"));
const schema =
spec.paths[endpoint].post.requestBody.content["application/json"].schema;
return Object.keys(schema.properties);
}
function helpText(command: string): string {
return execSync(`npx tsx src/index.ts ${command} --help`, {
cwd: path.join(__dirname, ".."),
encoding: "utf-8",
timeout: 15000,
});
}
function assertAllReachable(
endpoint: string,
mapping: Record<string, string[]>,
command: string,
) {
const documented = documentedFields(endpoint);
const help = helpText(command);
for (const field of documented) {
if (KNOWN_UNSURFACED[endpoint]?.[field]) continue;
const candidates = mapping[field];
expect(
candidates,
`${endpoint}: documented field "${field}" has no mapping entry for command "${command}"`,
).toBeDefined();
const reachable = candidates.some((flag) =>
flag.startsWith("--") ? help.includes(flag) : true,
);
expect(
reachable,
`${endpoint}: documented field "${field}" not reachable via any of ${JSON.stringify(candidates)} on command "${command}"`,
).toBe(true);
}
}
describe("Option parity: Node CLI reachability of documented v3 params", () => {
it("add covers documented fields", () => {
assertAllReachable("/v3/memories/add/", ADD_MAPPING, "add");
});
it("search covers documented fields", () => {
assertAllReachable("/v3/memories/search/", SEARCH_MAPPING, "search");
});
it("list covers documented fields", () => {
assertAllReachable("/v3/memories/", LIST_MAPPING, "list");
});
});
describe("stdin fallback uses the shared piped-stdin guard", () => {
const SOURCES = ["src/index.ts", "src/commands/memory.ts"];
for (const rel of SOURCES) {
it(`${rel} never checks process.stdin.isTTY directly`, () => {
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
expect(
src.includes("process.stdin.isTTY"),
`${rel}: use stdinIsPiped() from state.ts. A bare !isTTY check is also true for /dev/null and sockets, so readFileSync(0) crashes with EAGAIN in scripts, CI, and agent mode.`,
).toBe(false);
});
it(`${rel} guards every readFileSync(0) with stdinIsPiped()`, () => {
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
const lines = src.split("\n");
for (const [i, line] of lines.entries()) {
if (!line.includes("readFileSync(0")) continue;
const guard = lines.slice(Math.max(0, i - 3), i).join("\n");
expect(
guard.includes("stdinIsPiped()"),
`${rel}:${i + 1}: readFileSync(0) must be guarded by stdinIsPiped()`,
).toBe(true);
}
});
}
});
-237
View File
@@ -1,237 +0,0 @@
/**
* Tests for the Platform backend (mem0 Platform API client).
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PlatformBackend } from "../src/backend/platform.js";
import { createDefaultConfig } from "../src/config.js";
function makeBackend(): PlatformBackend {
return new PlatformBackend(createDefaultConfig().platform);
}
function mockFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: vi.fn().mockReturnValue(null) },
json: vi.fn().mockResolvedValue({ message: "ok" }),
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("deleteEntities", () => {
it("returns all results keyed by entity type for a multi-entity delete", async () => {
const backend = makeBackend();
const responses: Record<string, unknown> = {
"/v2/entities/user/alice/": { message: "user deleted" },
"/v2/entities/agent/bob/": { message: "agent deleted" },
};
const spy = vi
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
.spyOn(backend as any, "_request")
.mockImplementation(
async (_method: string, path: string) => responses[path],
);
const result = await backend.deleteEntities({
userId: "alice",
agentId: "bob",
});
expect(result).toEqual({
user: { message: "user deleted" },
agent: { message: "agent deleted" },
});
expect(spy).toHaveBeenCalledTimes(2);
});
it("keys a single-entity delete by its type", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
vi.spyOn(backend as any, "_request").mockResolvedValue({
message: "user deleted",
});
const result = await backend.deleteEntities({ userId: "alice" });
expect(result).toEqual({ user: { message: "user deleted" } });
});
it("throws when no entity id is provided", async () => {
const backend = makeBackend();
await expect(backend.deleteEntities({})).rejects.toThrow(
"At least one entity ID is required",
);
});
});
describe("PlatformBackend option-parity payloads (MEM-5893)", () => {
it("add: custom_instructions, custom_categories, structured_data_schema, timestamp reach the payload alongside existing fields", async () => {
const backend = makeBackend();
const spy = vi
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
.spyOn(backend as any, "_request")
.mockResolvedValue({ results: [] });
await backend.add("hello", undefined, {
userId: "alice",
metadata: { source: "test" },
expires: "2099-01-01",
customInstructions: "Extract only preferences.",
customCategories: [{ prefs: "user preferences" }],
structuredDataSchema: { type: "object" },
timestamp: 1700000000,
});
const payload = spy.mock.calls[0][2].json;
expect(payload.custom_instructions).toBe("Extract only preferences.");
expect(payload.custom_categories).toEqual([{ prefs: "user preferences" }]);
expect(payload.structured_data_schema).toEqual({ type: "object" });
expect(payload.timestamp).toBe(1700000000);
expect(payload.metadata).toEqual({ source: "test" });
expect(payload.expiration_date).toBe("2099-01-01");
});
it("add: omitted optional fields are absent from the payload", async () => {
const backend = makeBackend();
const spy = vi
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
.spyOn(backend as any, "_request")
.mockResolvedValue({ results: [] });
await backend.add("hello", undefined, { userId: "alice" });
const payload = spy.mock.calls[0][2].json;
expect(payload).not.toHaveProperty("custom_instructions");
expect(payload).not.toHaveProperty("custom_categories");
expect(payload).not.toHaveProperty("structured_data_schema");
expect(payload).not.toHaveProperty("timestamp");
});
it("search: show_expired, reference_date, latest_only reach the payload", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
await backend.search("query", {
showExpired: true,
referenceDate: "2024-01-01",
latestOnly: true,
});
const payload = spy.mock.calls[0][2].json;
expect(payload.show_expired).toBe(true);
expect(payload.reference_date).toBe("2024-01-01");
expect(payload.latest_only).toBe(true);
});
it("search: keyword_search and fields reach the payload", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
await backend.search("query", {
keyword: true,
fields: ["memory", "score"],
});
const payload = spy.mock.calls[0][2].json;
expect(payload.keyword_search).toBe(true);
expect(payload.fields).toEqual(["memory", "score"]);
});
it("search: omitted keyword and fields are absent from the payload", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
await backend.search("query", {});
const payload = spy.mock.calls[0][2].json;
expect(payload).not.toHaveProperty("keyword_search");
expect(payload).not.toHaveProperty("fields");
});
it("listMemories: show_expired and latest_only are top-level, not nested inside filters", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
await backend.listMemories({
userId: "alice",
showExpired: true,
latestOnly: true,
});
const payload = spy.mock.calls[0][2].json;
expect(payload.show_expired).toBe(true);
expect(payload.latest_only).toBe(true);
expect(payload.filters ?? {}).not.toHaveProperty("show_expired");
expect(payload.filters ?? {}).not.toHaveProperty("latest_only");
});
it("update: expiration_date and timestamp reach the payload", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
await backend.update("mem-123", undefined, undefined, {
expirationDate: "2099-01-01",
timestamp: 1700000000,
});
const payload = spy.mock.calls[0][2].json;
expect(payload.expiration_date).toBe("2099-01-01");
expect(payload.timestamp).toBe(1700000000);
});
it("delete: delete_linked is a query param, not part of the JSON body", async () => {
const backend = makeBackend();
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
await backend.delete("mem-123", { deleteLinked: true });
const opts = spy.mock.calls[0][2];
expect(opts.params.delete_linked).toBe("true");
expect(opts.json).toBeUndefined();
});
});
describe("PlatformBackend path encoding", () => {
it("encodes memory IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.get("mem/a?b#c");
await backend.update("mem/a?b#c", "updated");
await backend.delete("mem/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
]);
});
it("encodes entity and event IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.deleteEntities({ userId: "org/team?active#frag" });
await backend.getEvent("evt/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
]);
});
});
-59
View File
@@ -1,59 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockLoadConfig = vi.fn();
const mockSaveConfig = vi.fn();
const mockSpawn = vi.fn();
vi.mock("../src/config.js", () => ({
CONFIG_FILE: "/tmp/mem0-config.json",
loadConfig: mockLoadConfig,
saveConfig: mockSaveConfig,
}));
vi.mock("node:child_process", () => ({
spawn: mockSpawn,
}));
describe("captureEvent", () => {
beforeEach(() => {
vi.resetModules();
mockLoadConfig.mockReset();
mockSaveConfig.mockReset();
mockSpawn.mockReset();
delete process.env.MEM0_TELEMETRY;
});
it("pipes the telemetry context through stdin instead of argv", async () => {
mockLoadConfig.mockReturnValue({
platform: {
apiKey: "m0-node-secret",
baseUrl: "https://api.mem0.ai",
userEmail: "",
},
telemetry: {
anonymousId: "cli-anon-node",
},
});
const stdin = { end: vi.fn() };
const child = { stdin, unref: vi.fn() };
mockSpawn.mockReturnValue(child);
const { captureEvent } = await import("../src/telemetry.js");
captureEvent("node_test_event", { case: "stdin-secret" });
expect(mockSpawn).toHaveBeenCalledTimes(1);
const [execPath, args, options] = mockSpawn.mock.calls[0];
expect(execPath).toBe(process.execPath);
expect(args).toHaveLength(1);
expect(String(args[0])).toContain("telemetry-sender.cjs");
expect(JSON.stringify(args)).not.toContain("m0-node-secret");
expect(options).toMatchObject({ detached: true, stdio: ["pipe", "ignore", "ignore"] });
expect(stdin.end).toHaveBeenCalledTimes(1);
const payload = JSON.parse(stdin.end.mock.calls[0][0]);
expect(payload.mem0ApiKey).toBe("m0-node-secret");
expect(payload.payload.event).toBe("node_test_event");
expect(child.unref).toHaveBeenCalledTimes(1);
});
});
-6
View File
@@ -8,10 +8,4 @@ export default defineConfig({
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
test: {
// Integration tests spawn the CLI via `npx tsx` (15s subprocess
// timeout); the first spawn in a file pays a cold-start cost that can
// exceed vitest's 5s default on CI runners.
testTimeout: 30_000,
},
});
+36
View File
@@ -0,0 +1,36 @@
# Changelog
All notable changes to `mem0-cli` (Python) are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.7] — 2026-05-20
### Added
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
leaderboard identifier). Reads from local config, no network call.
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
platform error codes into actionable hints (e.g. `agentrush_search_first`
→ "Run 3 'mem0 agent-rush search' commands before adding.").
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
explicit `y` to acknowledge that AGENTRUSH memories are public; the
acknowledgement is persisted in `~/.mem0/config.json` under
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
Non-interactive (agent) invocations surface the warning to stderr without
blocking.
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
empty until first interactive acknowledgement).
### Changed
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
in addition to the existing source headers, so platform telemetry can split
game traffic from regular CLI usage.
## [0.2.6] and earlier
Unlogged historical releases. See git history under `cli/python/`.
+8 -2
View File
@@ -241,6 +241,14 @@ Verify your API connection and display the current project.
mem0 status
```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
@@ -299,8 +307,6 @@ These flags are available on all commands:
| `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format |
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
## Environment variables
| Variable | Description |
+1 -1
View File
@@ -80,7 +80,7 @@ mem0 --help
# Using Python directly (with venv activated)
source .venv/bin/activate
mem0 --help
mem0 --version
mem0 version
# Or run without activating
.venv/bin/mem0 --help
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.11"
version = "0.2.7"
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.11"
__version__ = "0.2.4"
+6 -89
View File
@@ -273,21 +273,7 @@ def add(
no_infer: bool = typer.Option(False, "--no-infer", help="Skip inference, store raw."),
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
categories: str | None = typer.Option(
None, "--categories", help="Not supported on add, use --custom-categories instead."
),
custom_instructions: str | None = typer.Option(
None, "--custom-instructions", help="Custom instructions for fact extraction."
),
custom_categories: str | None = typer.Option(
None,
"--custom-categories",
help="Custom categories as a JSON array of {name: description} objects.",
),
structured_data_schema: str | None = typer.Option(
None, "--structured-data-schema", help="Schema for structured data extraction, as JSON."
),
timestamp: int | None = typer.Option(
None, "--timestamp", help="Unix timestamp for the memory."
None, "--categories", help="Categories (JSON array or comma-separated)."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
@@ -326,10 +312,6 @@ def add(
no_infer=no_infer,
expires=expires,
categories=categories,
custom_instructions=custom_instructions,
custom_categories=custom_categories,
structured_data_schema=structured_data_schema,
timestamp=timestamp,
output=output,
)
@@ -373,21 +355,6 @@ def search(
help="Specific fields to return (comma-separated).",
rich_help_panel="Search",
),
show_expired: bool = typer.Option(
False, "--show-expired", help="Include expired memories.", rich_help_panel="Search"
),
reference_date: str | None = typer.Option(
None,
"--reference-date",
help="Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
rich_help_panel="Search",
),
latest_only: bool = typer.Option(
False,
"--latest-only",
help="Only return the latest version of each memory.",
rich_help_panel="Search",
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -431,9 +398,6 @@ def search(
keyword=keyword,
filter_json=filter_json,
fields=fields,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
output=output,
)
@@ -500,15 +464,6 @@ def list_cmd(
before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
),
show_expired: bool = typer.Option(
False, "--show-expired", help="Include expired memories.", rich_help_panel="Filters"
),
latest_only: bool = typer.Option(
False,
"--latest-only",
help="Only return the latest version of each memory.",
rich_help_panel="Filters",
),
output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -542,8 +497,6 @@ def list_cmd(
category=category,
after=after,
before=before,
show_expired=show_expired,
latest_only=latest_only,
output=output,
)
@@ -556,10 +509,6 @@ def update(
memory_id: str = typer.Argument(..., help="Memory ID to update."),
text: str | None = typer.Argument(None, help="New memory text."),
metadata: str | None = typer.Option(None, "--metadata", "-m", help="Update metadata (JSON)."),
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
timestamp: int | None = typer.Option(
None, "--timestamp", help="Unix timestamp for the memory."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
),
@@ -588,15 +537,7 @@ def update(
text = _read_stdin()
backend = _get_backend(api_key, base_url)
cmd_update(
backend,
memory_id,
text,
metadata=metadata,
expires=expires,
timestamp=timestamp,
output=output,
)
cmd_update(backend, memory_id, text, metadata=metadata, output=output)
# ── Memory: delete ────────────────────────────────────────────────────────
@@ -618,9 +559,6 @@ def delete(
False, "--dry-run", help="Show what would be deleted without deleting."
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
delete_linked: bool = typer.Option(
False, "--delete-linked", help="Also delete memories linked to this memory."
),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
),
@@ -678,14 +616,7 @@ def delete(
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,
delete_linked=delete_linked,
output=output,
)
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
elif all_:
_fire_telemetry("delete", {"delete_mode": "all"})
@@ -1137,11 +1068,7 @@ def _build_help_json() -> dict:
"--immutable": "Prevent future updates.",
"--no-infer": "Skip inference, store raw.",
"--expires": "Expiration date (YYYY-MM-DD).",
"--categories": "Not supported on add, use --custom-categories instead.",
"--custom-instructions": "Custom instructions for fact extraction.",
"--custom-categories": "Custom categories as a JSON array of {name: description} objects.",
"--structured-data-schema": "Schema for structured data extraction, as JSON.",
"--timestamp": "Unix timestamp for the memory.",
"--categories": "Categories (JSON array or comma-separated).",
"--graph": "Enable graph memory extraction.",
"--no-graph": "Disable graph memory extraction.",
"--output, -o": "Output format: text, json, quiet.",
@@ -1160,9 +1087,6 @@ def _build_help_json() -> dict:
"--keyword": "Use keyword search instead of semantic.",
"--filter": "Advanced filter expression (JSON).",
"--fields": "Specific fields to return (comma-separated).",
"--show-expired": "Include expired memories.",
"--reference-date": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
"--latest-only": "Only return the latest version of each memory.",
"--graph": "Enable graph in search.",
"--no-graph": "Disable graph in search.",
"--output, -o": "Output format: text, json, table.",
@@ -1186,8 +1110,6 @@ def _build_help_json() -> dict:
"--category": "Filter by category.",
"--after": "Created after (YYYY-MM-DD).",
"--before": "Created before (YYYY-MM-DD).",
"--show-expired": "Include expired memories.",
"--latest-only": "Only return the latest version of each memory.",
"--graph": "Enable graph in listing.",
"--no-graph": "Disable graph in listing.",
"--output, -o": "Output format: text, json, table.",
@@ -1202,8 +1124,6 @@ def _build_help_json() -> dict:
},
"options": {
"--metadata, -m": "Update metadata (JSON).",
"--expires": "Expiration date (YYYY-MM-DD).",
"--timestamp": "Unix timestamp for the memory.",
"--output, -o": "Output format: text, json, quiet.",
},
},
@@ -1220,7 +1140,6 @@ def _build_help_json() -> dict:
"--all": "Delete all memories matching scope filters.",
"--entity": "Delete the entity itself and all its memories (cascade).",
"--project": "With --all: delete ALL memories project-wide.",
"--delete-linked": "Also delete memories linked to this memory.",
"--dry-run": "Show what would be deleted without deleting.",
"--force": "Skip confirmation.",
"--user-id, -u": "Scope to user.",
@@ -1350,10 +1269,8 @@ def help(
mem0 help
mem0 help --json
"""
from mem0_cli.state import is_agent_mode
if json or is_agent_mode():
console.print_json(_json.dumps(_build_help_json()))
if json:
console.print(_json.dumps(_build_help_json(), indent=2))
else:
console.print(
f"[{BRAND_COLOR}]◆ mem0 CLI[/] v{__version__} — The Memory Layer for AI Agents\n"
+2 -17
View File
@@ -25,10 +25,7 @@ class Backend(ABC):
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
categories: list[str] | None = None,
) -> dict: ...
@abstractmethod
@@ -46,9 +43,6 @@ class Backend(ABC):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]: ...
@abstractmethod
@@ -67,19 +61,11 @@ class Backend(ABC):
category: str | None = None,
after: str | None = None,
before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]: ...
@abstractmethod
def update(
self,
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
self, memory_id: str, content: str | None = None, metadata: dict | None = None
) -> dict: ...
@abstractmethod
@@ -92,7 +78,6 @@ class Backend(ABC):
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
delete_linked: bool = False,
) -> dict: ...
@abstractmethod
+13 -72
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from typing import Any
from urllib.parse import quote
import httpx
@@ -12,10 +11,6 @@ from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
def _encode_path_segment(value: Any) -> str:
return quote(str(value), safe="")
class PlatformBackend(Backend):
"""Backend that talks to the mem0 Platform API."""
@@ -87,10 +82,7 @@ class PlatformBackend(Backend):
immutable: bool = False,
infer: bool = True,
expires: str | None = None,
custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
categories: list[str] | None = None,
) -> dict:
payload: dict[str, Any] = {}
@@ -115,14 +107,8 @@ class PlatformBackend(Backend):
payload["infer"] = False
if expires:
payload["expiration_date"] = expires
if custom_instructions:
payload["custom_instructions"] = custom_instructions
if custom_categories:
payload["custom_categories"] = custom_categories
if structured_data_schema:
payload["structured_data_schema"] = structured_data_schema
if timestamp is not None:
payload["timestamp"] = timestamp
if categories:
payload["categories"] = categories
payload["source"] = "CLI"
return self._request("POST", "/v3/memories/add/", json=payload)
@@ -182,9 +168,6 @@ class PlatformBackend(Backend):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
@@ -203,12 +186,6 @@ class PlatformBackend(Backend):
payload["keyword_search"] = True
if fields:
payload["fields"] = fields
if show_expired:
payload["show_expired"] = True
if reference_date is not None:
payload["reference_date"] = reference_date
if latest_only:
payload["latest_only"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/search/", json=payload)
@@ -219,11 +196,7 @@ class PlatformBackend(Backend):
)
def get(self, memory_id: str) -> dict:
return self._request(
"GET",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
def list_memories(
self,
@@ -237,8 +210,6 @@ class PlatformBackend(Backend):
category: str | None = None,
after: str | None = None,
before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {}
params = {"page": str(page), "page_size": str(page_size)}
@@ -261,10 +232,6 @@ class PlatformBackend(Backend):
)
if api_filters:
payload["filters"] = api_filters
if show_expired:
payload["show_expired"] = True
if latest_only:
payload["latest_only"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/", json=payload, params=params)
@@ -275,29 +242,15 @@ class PlatformBackend(Backend):
)
def update(
self,
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
self, memory_id: str, content: str | None = None, metadata: dict | None = None
) -> dict:
payload: dict[str, Any] = {}
if content:
payload["text"] = content
if metadata:
payload["metadata"] = metadata
if expiration_date:
payload["expiration_date"] = expiration_date
if timestamp is not None:
payload["timestamp"] = timestamp
payload["source"] = "CLI"
return self._request(
"PUT",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
json=payload,
)
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
def delete(
self,
@@ -308,7 +261,6 @@ class PlatformBackend(Backend):
agent_id: str | None = None,
app_id: str | None = None,
run_id: str | None = None,
delete_linked: bool = False,
) -> dict:
if all:
params: dict[str, str] = {"source": "CLI"}
@@ -322,14 +274,7 @@ class PlatformBackend(Backend):
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
params = {"source": "CLI"}
if delete_linked:
params["delete_linked"] = "true"
return self._request(
"DELETE",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params=params,
)
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
else:
raise ValueError("Either memory_id or --all is required")
@@ -351,17 +296,13 @@ class PlatformBackend(Backend):
entities = {t: v for t, v in type_map.items() if v}
if not entities:
raise ValueError("At least one entity ID is required for delete_entities.")
# Delete each provided entity via the v2 path-based endpoint. Key each
# response by entity type so a multi-entity delete (e.g. --user-id and
# --agent-id together) doesn't discard everything but the last result.
results: dict = {}
# Delete each provided entity via the v2 path-based endpoint
result: dict = {}
for entity_type, entity_id in entities.items():
results[entity_type] = self._request(
"DELETE",
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
params={"source": "CLI"},
result = self._request(
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
)
return results
return result
def ping(self, timeout: float | None = None) -> dict:
"""Call the ping endpoint and return the raw response.
@@ -405,7 +346,7 @@ class PlatformBackend(Backend):
return result if isinstance(result, list) else result.get("results", [])
def get_event(self, event_id: str) -> dict:
return self._request("GET", f"/v1/event/{_encode_path_segment(event_id)}/")
return self._request("GET", f"/v1/event/{event_id}/")
class AuthError(Exception):
+20 -67
View File
@@ -4,11 +4,9 @@ from __future__ import annotations
import json
import os
import re
import stat as _stat_mod
import sys
import time as _time
from datetime import date
from pathlib import Path
import typer
@@ -49,18 +47,6 @@ def _stdin_is_piped() -> bool:
return False
def _validate_expires(value: str) -> None:
"""Exit 1 if value is not a future YYYY-MM-DD date."""
if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
if date.fromisoformat(value) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
def cmd_add(
backend: Backend,
text: str | None,
@@ -76,10 +62,6 @@ def cmd_add(
no_infer: bool,
expires: str | None,
categories: str | None,
custom_instructions: str | None = None,
custom_categories: str | None = None,
structured_data_schema: str | None = None,
timestamp: int | None = None,
output: str = "text",
) -> None:
"""Add a memory."""
@@ -88,13 +70,6 @@ def cmd_add(
set_current_command("add")
if is_agent_mode():
output = "agent"
if categories:
print_error(
err_console, "--categories is not supported on add. Use --custom-categories instead."
)
raise typer.Exit(1)
msgs = None
content = text
@@ -133,24 +108,27 @@ def cmd_add(
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
custom_cats = None
if custom_categories:
cats = None
if categories:
try:
custom_cats = json.loads(custom_categories)
cats = json.loads(categories)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --custom-categories.")
raise typer.Exit(1) from None
schema = None
if structured_data_schema:
try:
schema = json.loads(structured_data_schema)
except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --structured-data-schema.")
raise typer.Exit(1) from None
cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
_validate_expires(expires)
import re
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts:
try:
@@ -165,10 +143,7 @@ def cmd_add(
immutable=immutable,
infer=not no_infer,
expires=expires,
custom_instructions=custom_instructions,
custom_categories=custom_cats,
structured_data_schema=schema,
timestamp=timestamp,
categories=cats,
)
except Exception as e:
ts.error_msg = str(e)
@@ -249,9 +224,6 @@ def cmd_search(
keyword: bool,
filter_json: str | None,
fields: str | None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
output: str = "text",
) -> None:
"""Search memories."""
@@ -294,9 +266,6 @@ def cmd_search(
keyword=keyword,
filters=filters,
fields=field_list,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
)
except Exception as e:
print_error(err_console, str(e))
@@ -383,8 +352,6 @@ def cmd_list(
category: str | None,
after: str | None,
before: str | None,
show_expired: bool = False,
latest_only: bool = False,
output: str = "table",
) -> None:
"""List memories."""
@@ -413,8 +380,6 @@ def cmd_list(
category=category,
after=after,
before=before,
show_expired=show_expired,
latest_only=latest_only,
)
except Exception as e:
print_error(err_console, str(e))
@@ -481,8 +446,6 @@ def cmd_update(
text: str | None,
*,
metadata: str | None,
expires: str | None = None,
timestamp: int | None = None,
output: str,
) -> None:
"""Update a memory."""
@@ -499,19 +462,10 @@ def cmd_update(
print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None
if expires:
_validate_expires(expires)
_start = _time.perf_counter()
with timed_status(err_console, "Updating memory...") as _ts:
try:
result = backend.update(
memory_id,
content=text,
metadata=meta,
expiration_date=expires,
timestamp=timestamp,
)
result = backend.update(memory_id, content=text, metadata=meta)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
@@ -536,7 +490,6 @@ def cmd_delete(
*,
dry_run: bool = False,
force: bool = False,
delete_linked: bool = False,
output: str,
) -> None:
"""Delete a single memory by ID."""
@@ -559,7 +512,7 @@ def cmd_delete(
_start = _time.perf_counter()
with timed_status(err_console, "Deleting...") as _ts:
try:
result = backend.delete(memory_id=memory_id, delete_linked=delete_linked)
result = backend.delete(memory_id=memory_id)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
+1 -4
View File
@@ -235,10 +235,7 @@ def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
if isinstance(current, bool):
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
elif isinstance(current, int):
try:
value = int(value) # type: ignore[assignment]
except ValueError:
return False
value = int(value) # type: ignore[assignment]
setattr(obj, final_key, value)
return True
+10 -26
View File
@@ -20,8 +20,8 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
for i, mem in enumerate(memories, 1):
memory_text = mem.get("memory") or mem.get("text") or ""
mem_id = (mem.get("id") or "")[:8]
memory_text = mem.get("memory", mem.get("text", ""))
mem_id = mem.get("id", "")[:8]
score = mem.get("score")
created = _format_date(mem.get("created_at"))
category = mem.get("categories", [None])
@@ -67,8 +67,8 @@ def format_memories_table(
table.add_column("Created", max_width=12)
for mem in memories:
mem_id = mem.get("id") or ""
memory_text = mem.get("memory") or mem.get("text") or ""
mem_id = mem.get("id", "")
memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60:
memory_text = memory_text[:57] + "..."
categories = mem.get("categories", [])
@@ -104,8 +104,8 @@ def format_single_memory(console: Console, mem: dict, output: str = "text") -> N
format_json(console, mem)
return
memory_text = mem.get("memory") or mem.get("text") or ""
mem_id = mem.get("id") or ""
memory_text = mem.get("memory", mem.get("text", ""))
mem_id = mem.get("id", "")
lines = []
lines.append(f" [white bold]{memory_text}[/]")
@@ -262,32 +262,16 @@ def sanitize_agent_data(command: str, data: Any) -> Any:
return result
if command == "search":
return [
pick(r, ["id", "memory", "score", "created_at", "categories", "expiration_date"])
for r in data
]
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
if command == "list":
return [
pick(r, ["id", "memory", "created_at", "categories", "expiration_date"]) for r in data
]
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
if command == "get":
return pick(
data,
[
"id",
"memory",
"created_at",
"updated_at",
"categories",
"metadata",
"expiration_date",
],
)
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
if command == "update":
return pick(data, ["id", "memory", "expiration_date"])
return pick(data, ["id", "memory"])
if command in ("delete", "delete-all", "entity delete"):
return data
+2 -9
View File
@@ -137,19 +137,12 @@ def capture_event(
"anon_distinct_id_to_alias": anon_id_to_alias,
}
child = subprocess.Popen(
[sys.executable, "-m", "mem0_cli.telemetry_sender"],
stdin=subprocess.PIPE,
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,
text=True,
)
if child.stdin:
with contextlib.suppress(Exception):
child.stdin.write(json.dumps(context))
with contextlib.suppress(Exception):
child.stdin.close()
except Exception:
pass
+2 -13
View File
@@ -1,7 +1,6 @@
"""Standalone telemetry sender — runs as a detached subprocess.
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin;
a single argv argument is still accepted as a legacy fallback)
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:
@@ -21,18 +20,8 @@ import sys
import urllib.request
def _load_context() -> dict:
"""Load telemetry context from stdin, falling back to argv for compatibility."""
raw = ""
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if not raw and len(sys.argv) > 1:
raw = sys.argv[1]
return json.loads(raw)
def main() -> None:
ctx = _load_context()
ctx = json.loads(sys.argv[1])
payload = ctx["payload"]
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
+3 -6
View File
@@ -32,13 +32,12 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir:
env["HOME"] = home_dir
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
encoding="utf-8",
text=True,
env=env,
timeout=15,
)
@@ -100,13 +99,12 @@ class TestArgvPreprocessing:
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
capture_output=True,
encoding="utf-8",
text=True,
env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
"FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
},
timeout=15,
)
@@ -135,13 +133,12 @@ class TestJsonEnvelopeParity:
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
capture_output=True,
encoding="utf-8",
text=True,
env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1",
"FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
},
timeout=15,
)
+1 -28
View File
@@ -7,7 +7,6 @@ boundaries).
from __future__ import annotations
import json
import os
import re
import subprocess
@@ -50,7 +49,6 @@ def _run(
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir:
env["HOME"] = home_dir
if env_override:
@@ -58,7 +56,7 @@ def _run(
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
encoding="utf-8",
text=True,
env=env,
)
return subprocess.CompletedProcess(
@@ -85,31 +83,6 @@ class TestCLIIntegration:
assert "add" in result.stdout
assert "search" in result.stdout
def test_version_flag_only(self):
from mem0_cli import __version__
flag = _run(["--version"])
assert flag.returncode == 0
assert __version__ in flag.stdout
assert _run(["version"]).returncode != 0
@pytest.mark.parametrize(
"args",
[["help", "--json"], ["--json", "help"], ["help", "--agent"], ["--agent", "help"]],
)
def test_help_json_produces_valid_json(self, args):
result = _run(args)
assert result.returncode == 0
spec = json.loads(result.stdout)
assert spec["name"] == "mem0"
assert "add" in spec["commands"]
def test_help_without_json_is_text(self):
result = _run(["help"])
assert result.returncode == 0
with pytest.raises(json.JSONDecodeError):
json.loads(result.stdout)
def test_add_help(self):
result = _run(["add", "--help"])
assert result.returncode == 0
+10 -184
View File
@@ -8,8 +8,8 @@ from io import StringIO
from unittest.mock import patch
import pytest
from click.exceptions import Exit as ClickExit
from rich.console import Console
from typer import Exit as TyperExit
from mem0_cli.commands.config_cmd import (
cmd_config_get,
@@ -181,7 +181,7 @@ class TestAddCommand:
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_add(
mock_backend,
@@ -206,7 +206,7 @@ class TestAddCommand:
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_add(
mock_backend,
@@ -252,13 +252,12 @@ class TestAddCommand:
)
mock_backend.add.assert_called_once()
def test_add_categories_rejected(self, mock_backend):
def test_add_categories_csv(self, mock_backend):
console, _buf = _make_console()
err_console, err_buf = _make_err_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
):
cmd_add(
mock_backend,
@@ -276,93 +275,7 @@ class TestAddCommand:
categories="health,prefs",
output="text",
)
assert "--custom-categories" in err_buf.getvalue()
mock_backend.add.assert_not_called()
def test_add_invalid_custom_categories_json(self, mock_backend):
console, _buf = _make_console()
err_console, err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
):
cmd_add(
mock_backend,
"test",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
custom_categories="not-json",
output="text",
)
assert "--custom-categories" in err_buf.getvalue()
mock_backend.add.assert_not_called()
def test_add_invalid_structured_data_schema_json(self, mock_backend):
console, _buf = _make_console()
err_console, err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
):
cmd_add(
mock_backend,
"test",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata=None,
immutable=False,
no_infer=False,
expires=None,
categories=None,
structured_data_schema="not-json",
output="text",
)
assert "--structured-data-schema" in err_buf.getvalue()
mock_backend.add.assert_not_called()
def test_add_regression_metadata_expiration_custom_categories_together(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_add(
mock_backend,
"test",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
messages=None,
file=None,
metadata='{"source": "test"}',
immutable=False,
no_infer=False,
expires="2099-01-01",
categories=None,
custom_categories='[{"prefs": "user preferences"}]',
output="text",
)
call_kwargs = mock_backend.add.call_args.kwargs
assert call_kwargs["metadata"] == {"source": "test"}
assert call_kwargs["expires"] == "2099-01-01"
assert call_kwargs["custom_categories"] == [{"prefs": "user preferences"}]
mock_backend.add.assert_called_once()
class TestAddDeduplicatesPending:
@@ -551,36 +464,6 @@ class TestSearchCommand:
)
mock_backend.search.assert_called_once()
def test_search_new_flags_reach_backend(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_search(
mock_backend,
"preferences",
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
top_k=10,
threshold=0.3,
rerank=False,
keyword=False,
filter_json=None,
fields=None,
show_expired=True,
reference_date="2024-01-01",
latest_only=True,
output="text",
)
call_kwargs = mock_backend.search.call_args.kwargs
assert call_kwargs["show_expired"] is True
assert call_kwargs["reference_date"] == "2024-01-01"
assert call_kwargs["latest_only"] is True
class TestGetCommand:
def test_get_text(self, mock_backend):
@@ -678,32 +561,6 @@ class TestListCommand:
output = buf.getvalue()
assert "No memories found" in output
def test_list_new_flags_reach_backend(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_list(
mock_backend,
user_id="alice",
agent_id=None,
app_id=None,
run_id=None,
page=1,
page_size=100,
category=None,
after=None,
before=None,
show_expired=True,
latest_only=True,
output="table",
)
call_kwargs = mock_backend.list_memories.call_args.kwargs
assert call_kwargs["show_expired"] is True
assert call_kwargs["latest_only"] is True
class TestUpdateCommand:
def test_update(self, mock_backend):
@@ -728,26 +585,6 @@ class TestUpdateCommand:
output = buf.getvalue()
assert '"memory"' in output
def test_update_new_fields_reach_backend(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_update(
mock_backend,
"abc-123",
"New text",
metadata=None,
expires="2099-01-01",
timestamp=1700000000,
output="text",
)
call_kwargs = mock_backend.update.call_args.kwargs
assert call_kwargs["expiration_date"] == "2099-01-01"
assert call_kwargs["timestamp"] == 1700000000
class TestDeleteCommand:
def test_delete_single(self, mock_backend):
@@ -773,17 +610,6 @@ class TestDeleteCommand:
assert "dry run" in output.lower()
mock_backend.delete.assert_not_called()
def test_delete_linked_reaches_backend(self, mock_backend):
console, _buf = _make_console()
err_console, _err_buf = _make_err_console()
with (
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
):
cmd_delete(mock_backend, "abc-123", delete_linked=True, output="text")
call_kwargs = mock_backend.delete.call_args.kwargs
assert call_kwargs["delete_linked"] is True
class TestDeleteAllCommand:
def test_delete_all_force(self, mock_backend):
@@ -938,7 +764,7 @@ class TestImportCommand:
with (
patch("mem0_cli.commands.utils.console", console),
patch("mem0_cli.commands.utils.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
@@ -975,7 +801,7 @@ class TestEntitiesListCommand:
with (
patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_entities_list(mock_backend, "invalid", output="table")
@@ -1118,7 +944,7 @@ class TestEntitiesDeleteCommand:
with (
patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_entities_delete(
mock_backend,
@@ -1482,7 +1308,7 @@ class TestAgentMode:
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("sys.stdout", captured_stdout),
pytest.raises((SystemExit, TyperExit)),
pytest.raises((SystemExit, ClickExit)),
):
cmd_get(mock_backend, "bad-id", output="text")
+1 -7
View File
@@ -67,8 +67,7 @@ class TestConfig:
from mem0_cli.config import CONFIG_FILE
mode = os.stat(CONFIG_FILE).st_mode & 0o777
if os.name != "nt":
assert mode == 0o600
assert mode == 0o600
def test_defaults_save_and_load(self, isolate_config):
config = Mem0Config()
@@ -139,11 +138,6 @@ class TestNestedAccess:
assert set_nested_value(config, "platform.api_key", "new-key")
assert config.platform.api_key == "new-key"
def test_set_int_value_rejects_invalid_input(self):
config = Mem0Config()
assert set_nested_value(config, "version", "abc") is False
assert config.version == 1
def test_set_nonexistent_key(self):
config = Mem0Config()
assert set_nested_value(config, "nonexistent.key", "val") is False
-102
View File
@@ -1,102 +0,0 @@
"""Drift test: every documented v3 add/search/list param must be reachable from the Python CLI."""
import json
from pathlib import Path
import typer.main
from mem0_cli.app import app
REPO_ROOT = Path(__file__).resolve().parents[3]
OPENAPI_PATH = REPO_ROOT / "docs" / "openapi.json"
KNOWN_UNSURFACED: dict[tuple[str, str], str] = {
("/v3/memories/add/", "includes"): "extraction hint, no CLI flag yet",
("/v3/memories/add/", "excludes"): "extraction hint, no CLI flag yet",
("/v3/memories/add/", "enable_graph"): "graph memory toggle, no CLI flag yet",
("/v3/memories/add/", "output_format"): "response envelope is pinned by the CLI",
("/v3/memories/add/", "prompt_profile_id"): "no CLI flag yet",
("/v3/memories/add/", "temporal_reasoning"): "no CLI flag yet",
("/v3/memories/add/", "timezone"): "no CLI flag yet",
("/v3/memories/add/", "observation_datetime"): "no CLI flag yet, --timestamp backdates instead",
("/v3/memories/add/", "observation_date"): "no CLI flag yet, --timestamp backdates instead",
("/v3/memories/search/", "categories"): "expressible through --filter",
("/v3/memories/search/", "metadata"): "expressible through --filter",
("/v3/memories/", "start_date"): "covered by --after via filters.created_at.gte",
("/v3/memories/", "end_date"): "covered by --before via filters.created_at.lte",
("/v3/memories/", "categories"): "covered by --category via filters.categories",
("/v3/memories/", "fields"): "no CLI flag yet",
("/v3/memories/", "keywords"): "no CLI flag yet",
}
ADD_MAPPING: dict[str, list[str]] = {
"messages": ["messages", "file", "text"],
"user_id": ["user_id"],
"agent_id": ["agent_id"],
"app_id": ["app_id"],
"run_id": ["run_id"],
"metadata": ["metadata"],
"expiration_date": ["expires"],
"custom_instructions": ["custom_instructions"],
"custom_categories": ["custom_categories"],
"infer": ["no_infer"],
"immutable": ["immutable"],
"structured_data_schema": ["structured_data_schema"],
"timestamp": ["timestamp"],
}
SEARCH_MAPPING: dict[str, list[str]] = {
"query": ["query"],
"filters": ["filter_json", "user_id", "agent_id", "run_id"],
"show_expired": ["show_expired"],
"top_k": ["top_k"],
"threshold": ["threshold"],
"rerank": ["rerank"],
"reference_date": ["reference_date"],
"fields": ["fields"],
}
LIST_MAPPING: dict[str, list[str]] = {
"filters": ["user_id", "agent_id", "run_id", "category", "after", "before"],
"show_expired": ["show_expired"],
"page": ["page"],
"page_size": ["page_size"],
}
def _documented_fields(endpoint: str) -> set[str]:
spec = json.loads(OPENAPI_PATH.read_text())
schema = spec["paths"][endpoint]["post"]["requestBody"]["content"]["application/json"]["schema"]
return set(schema["properties"])
def _cli_param_names(command_name: str) -> set[str]:
click_app = typer.main.get_command(app)
command = click_app.commands[command_name]
return {param.name for param in command.params}
def _assert_all_reachable(endpoint: str, mapping: dict[str, list[str]], command_name: str) -> None:
documented = _documented_fields(endpoint)
reachable = _cli_param_names(command_name)
for field in documented:
if (endpoint, field) in KNOWN_UNSURFACED:
continue
candidates = mapping.get(field)
assert candidates, (
f"{endpoint}: documented field {field!r} has no mapping entry for command {command_name!r}"
)
assert any(candidate in reachable for candidate in candidates), (
f"{endpoint}: documented field {field!r} not reachable via any of {candidates} on command {command_name!r}"
)
class TestOptionParity:
def test_add_covers_documented_fields(self):
_assert_all_reachable("/v3/memories/add/", ADD_MAPPING, "add")
def test_search_covers_documented_fields(self):
_assert_all_reachable("/v3/memories/search/", SEARCH_MAPPING, "search")
def test_list_covers_documented_fields(self):
_assert_all_reachable("/v3/memories/", LIST_MAPPING, "list")
-23
View File
@@ -54,11 +54,6 @@ class TestTextFormat:
output = buf.getvalue()
assert "Found 0" in output
def test_format_memories_text_handles_null_fields(self):
console, buf = _make_console()
format_memories_text(console, [{"id": None, "memory": None, "created_at": None}])
assert "Found 1 memories" in buf.getvalue()
class TestTableFormat:
def test_format_memories_table(self):
@@ -75,13 +70,6 @@ class TestTableFormat:
# Should still render (empty table)
assert "ID" in output
def test_format_memories_table_handles_null_fields(self):
console, buf = _make_console()
format_memories_table(console, [{"id": None, "memory": None, "created_at": None}])
output = buf.getvalue()
assert "ID" in output
assert "Memory" in output
class TestSingleMemory:
def test_format_single_memory_text(self):
@@ -99,17 +87,6 @@ class TestSingleMemory:
output = buf.getvalue()
assert '"memory"' in output
def test_format_single_memory_handles_null_fields(self):
console, buf = _make_console()
format_single_memory(
console,
{"id": None, "memory": None, "text": "Fallback memory", "created_at": None},
"text",
)
output = buf.getvalue()
assert "Fallback memory" in output
assert "ID:" not in output
class TestAddResult:
def test_format_add_result_text(self):
-46
View File
@@ -1,46 +0,0 @@
"""Tests for the Platform backend (mem0 Platform API client)."""
from __future__ import annotations
from unittest.mock import patch
from mem0_cli.backend.platform import PlatformBackend
from mem0_cli.config import PlatformConfig
def _make_backend() -> PlatformBackend:
# api_key/base_url are only used to build the httpx client; every test here
# patches _request, so no real network calls are made.
return PlatformBackend(PlatformConfig(api_key="test-key", base_url="https://api.mem0.ai"))
class TestDeleteEntities:
def test_multiple_entities_returns_all_results(self):
backend = _make_backend()
responses = {
"/v2/entities/user/alice/": {"message": "user deleted"},
"/v2/entities/agent/bob/": {"message": "agent deleted"},
}
with patch.object(backend, "_request") as mock_request:
mock_request.side_effect = lambda method, path, **kw: responses[path]
result = backend.delete_entities(user_id="alice", agent_id="bob")
# Regression: previously only the last entity's response survived.
assert result == {
"user": {"message": "user deleted"},
"agent": {"message": "agent deleted"},
}
assert mock_request.call_count == 2
def test_single_entity_keyed_by_type(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value={"message": "user deleted"}):
result = backend.delete_entities(user_id="alice")
assert result == {"user": {"message": "user deleted"}}
def test_no_entities_raises(self):
backend = _make_backend()
import pytest
with pytest.raises(ValueError):
backend.delete_entities()
@@ -1,109 +0,0 @@
"""Tests that the MEM-5893 option-parity flags reach the correct request payload/params."""
from __future__ import annotations
from unittest.mock import patch
from mem0_cli.backend.platform import PlatformBackend
from mem0_cli.config import PlatformConfig
def _make_backend() -> PlatformBackend:
return PlatformBackend(PlatformConfig(api_key="test-key", base_url="https://api.mem0.ai"))
class TestAddOptions:
def test_new_fields_and_existing_fields_land_in_payload_together(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
backend.add(
content="hello",
user_id="alice",
metadata={"source": "test"},
expires="2099-01-01",
custom_instructions="Extract only preferences.",
custom_categories=[{"prefs": "user preferences"}],
structured_data_schema={"type": "object"},
timestamp=1700000000,
)
payload = mock_request.call_args.kwargs["json"]
assert payload["custom_instructions"] == "Extract only preferences."
assert payload["custom_categories"] == [{"prefs": "user preferences"}]
assert payload["structured_data_schema"] == {"type": "object"}
assert payload["timestamp"] == 1700000000
assert payload["metadata"] == {"source": "test"}
assert payload["expiration_date"] == "2099-01-01"
def test_omitted_fields_are_absent_from_payload(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
backend.add(content="hello", user_id="alice")
payload = mock_request.call_args.kwargs["json"]
assert "custom_instructions" not in payload
assert "custom_categories" not in payload
assert "structured_data_schema" not in payload
assert "timestamp" not in payload
class TestSearchOptions:
def test_show_expired_reference_date_latest_only_reach_payload(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value=[]) as mock_request:
backend.search(
"query",
show_expired=True,
reference_date="2024-01-01",
latest_only=True,
)
payload = mock_request.call_args.kwargs["json"]
assert payload["show_expired"] is True
assert payload["reference_date"] == "2024-01-01"
assert payload["latest_only"] is True
def test_keyword_and_fields_reach_payload(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value=[]) as mock_request:
backend.search("query", keyword=True, fields=["memory", "score"])
payload = mock_request.call_args.kwargs["json"]
assert payload["keyword_search"] is True
assert payload["fields"] == ["memory", "score"]
def test_keyword_and_fields_omitted_are_absent_from_payload(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value=[]) as mock_request:
backend.search("query")
payload = mock_request.call_args.kwargs["json"]
assert "keyword_search" not in payload
assert "fields" not in payload
class TestListOptions:
def test_show_expired_and_latest_only_are_top_level_not_in_filters(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value=[]) as mock_request:
backend.list_memories(user_id="alice", show_expired=True, latest_only=True)
payload = mock_request.call_args.kwargs["json"]
assert payload["show_expired"] is True
assert payload["latest_only"] is True
assert "show_expired" not in payload.get("filters", {})
assert "latest_only" not in payload.get("filters", {})
class TestUpdateOptions:
def test_expires_and_timestamp_reach_payload(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value={}) as mock_request:
backend.update("mem-123", expiration_date="2099-01-01", timestamp=1700000000)
payload = mock_request.call_args.kwargs["json"]
assert payload["expiration_date"] == "2099-01-01"
assert payload["timestamp"] == 1700000000
class TestDeleteOptions:
def test_delete_linked_is_a_query_param_not_json_body(self):
backend = _make_backend()
with patch.object(backend, "_request", return_value={}) as mock_request:
backend.delete(memory_id="mem-123", delete_linked=True)
call = mock_request.call_args
assert call.kwargs["params"]["delete_linked"] == "true"
assert "json" not in call.kwargs
@@ -1,43 +0,0 @@
from unittest.mock import MagicMock
from mem0_cli.backend.platform import PlatformBackend
def _backend(sample_config):
backend = PlatformBackend(sample_config.platform)
backend._client = MagicMock()
backend._client.request.return_value = MagicMock(
status_code=200,
json=lambda: {"message": "ok"},
headers={},
raise_for_status=lambda: None,
)
return backend
def test_memory_id_path_segments_are_encoded(sample_config):
backend = _backend(sample_config)
backend.get("mem/a?b#c")
backend.update("mem/a?b#c", content="updated")
backend.delete("mem/a?b#c")
paths = [call.args[1] for call in backend._client.request.call_args_list]
assert paths == [
"/v1/memories/mem%2Fa%3Fb%23c/",
"/v1/memories/mem%2Fa%3Fb%23c/",
"/v1/memories/mem%2Fa%3Fb%23c/",
]
def test_entity_and_event_path_segments_are_encoded(sample_config):
backend = _backend(sample_config)
backend.delete_entities(user_id="org/team?active#frag")
backend.get_event("evt/a?b#c")
paths = [call.args[1] for call in backend._client.request.call_args_list]
assert paths == [
"/v2/entities/user/org%2Fteam%3Factive%23frag/",
"/v1/event/evt%2Fa%3Fb%23c/",
]
-80
View File
@@ -1,80 +0,0 @@
"""Tests for telemetry subprocess secret handling."""
from __future__ import annotations
import io
import json
import subprocess
import sys
from mem0_cli.config import Mem0Config, save_config
from mem0_cli.telemetry import capture_event
from mem0_cli.telemetry_sender import _load_context
class _CaptureStdin:
def __init__(self):
self.buffer = ""
self.closed = False
def write(self, value: str) -> None:
self.buffer += value
def close(self) -> None:
self.closed = True
class _DummyProcess:
def __init__(self):
self.stdin = _CaptureStdin()
def test_capture_event_writes_context_to_stdin_not_argv(isolate_config, monkeypatch):
config = Mem0Config()
config.platform.api_key = "m0-test-secret"
config.telemetry.anonymous_id = "cli-anon-test"
save_config(config)
captured: dict[str, object] = {}
proc = _DummyProcess()
def fake_popen(args, **kwargs):
captured["args"] = args
captured["kwargs"] = kwargs
return proc
monkeypatch.setattr("mem0_cli.telemetry.subprocess.Popen", fake_popen)
capture_event("unit_test_event", {"case": "stdin-secret"})
argv = captured["args"]
assert argv == [sys.executable, "-m", "mem0_cli.telemetry_sender"]
assert all("m0-test-secret" not in arg for arg in argv)
kwargs = captured["kwargs"]
assert kwargs["stdin"] == subprocess.PIPE
assert kwargs["text"] is True
ctx = json.loads(proc.stdin.buffer)
assert ctx["mem0_api_key"] == "m0-test-secret"
assert ctx["payload"]["event"] == "unit_test_event"
assert proc.stdin.closed
def test_load_context_reads_from_stdin(monkeypatch):
monkeypatch.setattr("sys.argv", ["telemetry_sender"])
monkeypatch.setattr("sys.stdin", io.StringIO('{"payload": {"event": "stdin"}}'))
ctx = _load_context()
assert ctx["payload"]["event"] == "stdin"
def test_load_context_falls_back_to_argv(monkeypatch):
monkeypatch.setattr("sys.argv", ["telemetry_sender", '{"payload": {"event": "argv"}}'])
monkeypatch.setattr("sys.stdin", io.StringIO(""))
ctx = _load_context()
assert ctx["payload"]["event"] == "argv"
@@ -555,7 +555,7 @@
"# - Enables creation of AI agents with long-term memory and learning abilities.\n",
"# - Improves consistency and reduces repetition in user-agent interactions.\n",
"\n",
"from helper.mem0_teachability import Mem0Teachability\n",
"from cookbooks.helper.mem0_teachability import Mem0Teachability\n",
"\n",
"teachability = Mem0Teachability(\n",
" verbosity=2, # for visibility of what's happening\n",
+1 -1
View File
@@ -24,7 +24,7 @@ mintlify dev
### Publishing Changes
Install our GitHub App to auto-propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
#### Troubleshooting
-5
View File
@@ -1,5 +0,0 @@
{/* Subtle, value-anchored nudge to star the repo. Drop in at peak-end "win" moments in the OSS docs (after a successful add/search, a server bootstrap, etc.). Keep it off the Platform/API pages. */}
{/* Clicks are tracked via PostHog autocapture: the data-ph-capture-attribute-cta below tags each click with cta="star-on-github" so it's filterable as an event property. Metric = count of $autocapture where cta = star-on-github; break down by Current URL to see which win-moment converts. */}
<Callout icon="star" iconType="solid" color="#FACC15">
**Using Mem0?** <a href="https://github.com/mem0ai/mem0" data-ph-capture-attribute-cta="star-on-github">Star us on GitHub</a> to help more developers discover memory for AI apps.
</Callout>
+1 -5
View File
@@ -97,7 +97,7 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
## Next Steps
<CardGroup cols={3}>
<CardGroup cols={2}>
<Card title="Add Your First Memory" icon="rocket" href="/api-reference/memory/add-memories">
Start storing memories via the REST API
</Card>
@@ -105,8 +105,4 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
<Card title="Search with Filters" icon="filter" href="/api-reference/memory/search-memories">
Learn advanced search and filtering techniques
</Card>
<Card title="Build with cookbooks" icon="book-open" href="/cookbooks/overview">
See the API used end to end in real projects.
</Card>
</CardGroup>
+4 -12
View File
@@ -4,7 +4,7 @@ description: "Add facts, messages, or metadata to a user memory store with async
openapi: post /v3/memories/add/
---
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction: one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
## Endpoint
@@ -50,7 +50,6 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
| `app_id` | string | No* | Associates the memory with an app. |
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
@@ -65,8 +64,9 @@ The request is queued for background processing. The response contains an `event
<CodeGroup>
```json 200 response
{
"event_id": "evt-uuid",
"status": "PENDING"
"message": "Memory processing has been queued for background execution",
"status": "PENDING",
"event_id": "evt-uuid"
}
```
@@ -83,11 +83,3 @@ The request is queued for background processing. The response contains an `event
<Info>
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
</Info>
<Info>
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
</Info>
<Info>
Python uses `expiration_date`; TypeScript uses `expirationDate`.
</Info>
@@ -4,4 +4,4 @@ description: "Submit an export job to create a structured memory export using a
openapi: post /v1/exports/
---
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `app_id`, or `run_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `run_id`, or `session_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
+1 -8
View File
@@ -4,11 +4,7 @@ description: "Retrieve memories with paginated results and advanced filtering us
openapi: post /v3/memories/
---
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object: top-level entity IDs are rejected with 400.
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
Python uses `show_expired`; TypeScript uses `showExpired`.
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
@@ -36,7 +32,6 @@ memories = client.get_all(
}
]
},
show_expired=False,
page=1,
page_size=50
)
@@ -51,14 +46,12 @@ memories = client.get_all(
{
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
"expiration_date": null,
"created_at": "2024-07-01T12:00:00Z",
"updated_at": "2024-07-01T12:00:00Z"
},
{
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"memory": "Alex prefers vegetarian restaurants",
"expiration_date": null,
"created_at": "2024-07-05T15:30:00Z",
"updated_at": "2024-07-05T15:30:00Z"
}

Some files were not shown because too many files have changed in this diff Show More