Compare commits

..
Author SHA1 Message Date
Mgeeeek e975025747 chore: touch pyproject.toml to fire Python SDK CI for plugin-only PR
Plugin paths are filtered out of every CI workflow. Same path-filter
trap and same workaround as #5094.
2026-05-15 02:26:17 +05:30
Mgeeeek 767795d5de chore(plugin): bump to v0.1.3 + add CHANGELOG
Version bump to ship the user_id revert. Adds the missing CHANGELOG
file for the plugin and back-fills 0.1.2 (which went undocumented)
so users upgrading have a clear "what changed and how to recover
stranded memories" entry.
2026-05-15 02:09:46 +05:30
Mgeeeek d7e2a0e7b0 fix(plugin): drop API-key-derived user_id, restore $USER fallback
The deterministic resolver from #5076 silently rebucketed every user
who hadn't set MEM0_USER_ID. With MEM0_API_KEY present (the normal
case — hooks early-exit without it), the resolver picked
"mem0-<sha256(api_key)[:12]>" instead of $USER, so an existing
"deshraj" bucket suddenly became "mem0-f34dd1cba657" on update with
no migration. The old memories aren't reachable from the plugin
anymore, and the user gets a fresh empty bucket without warning.

Resolution priority is now back to:
  1. MEM0_USER_ID env var
  2. \$USER, else "default"

The ~/.mem0/identity.json cache is no longer written or read; any
existing cache file becomes inert. Users on multiple machines who
want a single bucket can still set MEM0_USER_ID explicitly — that's
the supported path, not silent derivation.

Also dropped the "regardless of which machine you're on" line from
the session bootstrap header since it no longer applies.
2026-05-15 01:54:34 +05:30
1865 changed files with 87252 additions and 117287 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "mem0", "name": "mem0",
"source": { "source": {
"source": "local", "source": "local",
"path": "./integrations/mem0-plugin" "path": "./mem0-plugin"
}, },
"policy": { "policy": {
"installation": "AVAILABLE", "installation": "AVAILABLE",
+2 -2
View File
@@ -10,9 +10,9 @@
"plugins": [ "plugins": [
{ {
"name": "mem0", "name": "mem0",
"source": "./integrations/mem0-plugin", "source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.14" "version": "0.1.3"
} }
] ]
} }
-20
View File
@@ -1,20 +0,0 @@
{
"name": "mem0-plugins",
"interface": {
"displayName": "Mem0 Plugins"
},
"plugins": [
{
"name": "mem0",
"source": {
"source": "local",
"path": "./integrations/mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+2 -2
View File
@@ -10,9 +10,9 @@
"plugins": [ "plugins": [
{ {
"name": "mem0", "name": "mem0",
"source": "./integrations/mem0-plugin", "source": "./mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.14" "version": "0.1.1"
} }
] ]
} }
+5 -3
View File
@@ -9,10 +9,12 @@ body:
label: Component label: Component
description: Which part of mem0 is affected? description: Which part of mem0 is affected?
options: options:
- Python SDK - Core / Python SDK
- TypeScript SDK - TypeScript SDK
- Vector Store - Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Plugin - Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenClaw
- REST API - REST API
- Other - Other
validations: validations:
+6 -3
View File
@@ -9,11 +9,14 @@ body:
label: Component label: Component
description: Which part of mem0 does this relate to? description: Which part of mem0 does this relate to?
options: options:
- Python SDK - Core / Python SDK
- TypeScript SDK - TypeScript SDK
- Vector Store - Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
- Plugin - Graph Memory (Neo4j, Memgraph, etc.)
- Ollama / Local Models
- OpenClaw
- REST API - REST API
- Benchmarks / Evals
- Other - Other
validations: validations:
required: true required: true
+18 -15
View File
@@ -1,15 +1,18 @@
policy: # Maps dropdown selections to GitHub labels
- section: # Used by the advanced-issue-labeler GitHub Action
- id: ['component']
block-list: ['Other'] component:
label: - label: "sdk-python"
- name: 'sdk-python' matcher: "Core / Python SDK"
keys: ['Python SDK'] - label: "sdk-typescript"
- name: 'sdk-typescript' matcher: "TypeScript SDK"
keys: ['TypeScript SDK'] - label: "vector-store"
- name: 'vector-store' matcher: "Vector Store"
keys: ['Vector Store'] - label: "graph-memory"
- name: 'plugin' matcher: "Graph Memory"
keys: ['Plugin'] - label: "ollama"
- name: 'rest-api' matcher: "Ollama"
keys: ['REST API'] - 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 name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
# Dispatched by release.yml (Release Router) when a release tagged v* is
# published. Can also be dispatched manually to re-publish a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. v1.2.3)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags if: startsWith(github.event.release.tag_name, 'v')
# like vercel-ai-v* that also start with 'v'
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
ref: ${{ inputs.tag }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
@@ -54,6 +39,7 @@ jobs:
# packages_dir: dist/ # packages_dir: dist/
- name: Publish distribution 📦 to PyPI - name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
packages_dir: dist/ packages_dir: dist/
-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."
+60 -19
View File
@@ -1,11 +1,20 @@
name: ci name: ci
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main runs remain standalone.
on: on:
push: push:
branches: [main] branches: [main]
workflow_call: paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- '.github/workflows/**'
- 'pyproject.toml'
pull_request:
paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- 'pyproject.toml'
jobs: jobs:
changelog_check: changelog_check:
@@ -51,8 +60,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
mem0_changed: ${{ steps.filter.outputs.mem0 }} mem0_changed: ${{ steps.filter.outputs.mem0 }}
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: dorny/paths-filter@v2 - uses: dorny/paths-filter@v2
id: filter id: filter
with: with:
@@ -60,28 +70,25 @@ jobs:
mem0: mem0:
- 'mem0/**' - 'mem0/**'
- 'tests/**' - 'tests/**'
- '.github/workflows/ci.yml' - '.github/workflows/**'
- 'pyproject.toml' - 'pyproject.toml'
embedchain:
- 'embedchain/**'
build_mem0: build_mem0:
needs: check_changes needs: check_changes
if: needs.check_changes.outputs.mem0_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ["3.10", "3.11", "3.12"] python-version: ["3.10", "3.11", "3.12"]
steps: steps:
- name: Skip — no relevant changes - uses: actions/checkout@v3
if: needs.check_changes.outputs.mem0_changed != 'true'
run: echo "No changes in mem0/, tests/, pyproject.toml, or ci.yml — skipping"
- uses: actions/checkout@v4
if: needs.check_changes.outputs.mem0_changed == 'true'
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
if: needs.check_changes.outputs.mem0_changed == 'true'
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Clean up disk space - name: Clean up disk space
if: needs.check_changes.outputs.mem0_changed == 'true'
run: | run: |
df -h df -h
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
@@ -89,27 +96,61 @@ jobs:
sudo docker builder prune -a sudo docker builder prune -a
df -h df -h
- name: Install Hatch - name: Install Hatch
if: needs.check_changes.outputs.mem0_changed == 'true'
run: pip install hatch run: pip install hatch
- name: Load cached venv - name: Load cached venv
if: needs.check_changes.outputs.mem0_changed == 'true'
id: cached-hatch-dependencies id: cached-hatch-dependencies
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
path: .venv path: .venv
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install GEOS Libraries - name: Install GEOS Libraries
if: needs.check_changes.outputs.mem0_changed == 'true'
run: sudo apt-get update && sudo apt-get install -y libgeos-dev run: sudo apt-get update && sudo apt-get install -y libgeos-dev
- name: Install dependencies - name: Install dependencies
if: needs.check_changes.outputs.mem0_changed == 'true' && steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
run: | run: |
pip install --upgrade pip pip install --upgrade pip
pip install -e ".[test,graph,vector_stores,llms,extras]" pip install -e ".[test,graph,vector_stores,llms,extras]"
pip install ruff==0.16.0 pip install ruff
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Linting - name: Run Linting
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make lint run: make lint
- name: Run tests and generate coverage report - name: Run tests and generate coverage report
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make test run: make test
build_embedchain:
needs: check_changes
if: needs.check_changes.outputs.embedchain_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Hatch
run: pip install hatch
- name: Load cached venv
id: cached-hatch-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-embedchain-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install dependencies
run: cd embedchain && make install_all
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Formatting
run: |
mkdir -p embedchain/.ruff_cache && chmod -R 777 embedchain/.ruff_cache
cd embedchain && hatch run format
- name: Lint with ruff
run: cd embedchain && make lint
- name: Run tests and generate coverage report
run: cd embedchain && make coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+4 -18
View File
@@ -1,25 +1,13 @@
name: Publish @mem0/cli 📦 to npm name: Publish @mem0/cli 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# cli-node-v* is published. Can also be dispatched manually to re-publish
# a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish @mem0/cli 📦 to npm name: Build and publish @mem0/cli 📦 to npm
if: startsWith(inputs.tag, 'cli-node-v') if: startsWith(github.event.release.tag_name, 'cli-node-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
@@ -28,8 +16,6 @@ jobs:
working-directory: cli/node working-directory: cli/node
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm - name: Publish to npm
run: | run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]") PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID" npx npm@latest publish --provenance --access public --tag "$PREID"
else else
+4 -3
View File
@@ -1,7 +1,5 @@
name: CLI Node CI name: CLI Node CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@@ -9,7 +7,10 @@ on:
paths: paths:
- 'cli/node/**' - 'cli/node/**'
- '.github/workflows/cli-node-ci.yml' - '.github/workflows/cli-node-ci.yml'
workflow_call: pull_request:
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
jobs: jobs:
lint: lint:
+3 -16
View File
@@ -1,24 +1,13 @@
name: Publish mem0-cli 🐍 distributions 📦 to PyPI name: Publish mem0-cli 🐍 distributions 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged cli-v* is
# published. Can also be dispatched manually to re-publish a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish mem0-cli 📦 to PyPI name: Build and publish mem0-cli 📦 to PyPI
if: startsWith(inputs.tag, 'cli-v') if: startsWith(github.event.release.tag_name, 'cli-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
@@ -27,8 +16,6 @@ jobs:
working-directory: cli/python working-directory: cli/python
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
+4 -3
View File
@@ -1,7 +1,5 @@
name: CLI Python CI name: CLI Python CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@@ -9,7 +7,10 @@ on:
paths: paths:
- 'cli/python/**' - 'cli/python/**'
- '.github/workflows/cli-python-ci.yml' - '.github/workflows/cli-python-ci.yml'
workflow_call: pull_request:
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
jobs: jobs:
lint: lint:
+6 -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 # read-only
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders # python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
# On PRs this is invoked by ci-gate.yml (the single required check);
# manual runs remain standalone.
on: on:
workflow_call: pull_request:
paths:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
- 'scripts/check-llms-txt-coverage.py'
- 'scripts/llms-txt-ignore.txt'
workflow_dispatch: {} workflow_dispatch: {}
permissions: permissions:
+12 -40
View File
@@ -12,56 +12,28 @@ jobs:
label: label:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4
- uses: stefanbuck/github-issue-parser@v3 - uses: stefanbuck/github-issue-parser@v3
id: issue-parser id: issue-parser
continue-on-error: true
with: with:
template-path: .github/ISSUE_TEMPLATE/bug_report.yml template-path: .github/ISSUE_TEMPLATE/bug_report.yml
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3 - uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
continue-on-error: true
with: with:
issue-form: ${{ steps.issue-parser.outputs.jsonString }} issue-form: ${{ steps.issue-parser.outputs.jsonString }}
section: component
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
config-path: .github/advanced-issue-labeler.yml config-path: .github/advanced-issue-labeler.yml
- name: Infer component from text when the form was not used - uses: stefanbuck/github-issue-parser@v3
uses: actions/github-script@v7 id: feature-parser
if: contains(github.event.issue.labels.*.name, 'enhancement')
with: with:
script: | template-path: .github/ISSUE_TEMPLATE/feature_request.yml
const {
componentLabels,
inferComponentLabels,
loadKeywords,
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
const { data: issue } = await github.rest.issues.get({ - uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
owner: context.repo.owner, if: contains(github.event.issue.labels.*.name, 'enhancement')
repo: context.repo.repo, with:
issue_number: context.issue.number, issue-form: ${{ steps.feature-parser.outputs.jsonString }}
}); section: component
token: ${{ secrets.GITHUB_TOKEN }}
const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`); config-path: .github/advanced-issue-labeler.yml
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,
});
-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 name: Publish @mem0/openclaw-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# openclaw-v* is published. Can also be dispatched manually to re-publish
# a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish @mem0/openclaw-mem0 📦 to npm name: Build and publish @mem0/openclaw-mem0 📦 to npm
if: startsWith(inputs.tag, 'openclaw-v') if: startsWith(github.event.release.tag_name, 'openclaw-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
defaults: defaults:
run: run:
working-directory: integrations/openclaw working-directory: openclaw
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -42,7 +28,7 @@ jobs:
node-version: '22' node-version: '22'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm - name: Publish to npm
run: | run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]") PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID" npx npm@latest publish --provenance --access public --tag "$PREID"
else else
+17 -16
View File
@@ -1,15 +1,16 @@
name: openclaw checks name: openclaw checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: [main] branches: [main]
paths: paths:
- 'integrations/openclaw/**' - 'openclaw/**'
- '.github/workflows/openclaw-checks.yml'
pull_request:
paths:
- 'openclaw/**'
- '.github/workflows/openclaw-checks.yml' - '.github/workflows/openclaw-checks.yml'
workflow_call:
jobs: jobs:
lint: lint:
@@ -27,13 +28,13 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile run: cd openclaw && pnpm install --frozen-lockfile
- name: Type check - name: Type check
run: cd integrations/openclaw && pnpm exec tsc --noEmit run: cd openclaw && pnpm exec tsc --noEmit
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -53,20 +54,20 @@ jobs:
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile run: cd openclaw && pnpm install --frozen-lockfile
- name: Run tests with coverage - name: Run tests with coverage
run: cd integrations/openclaw && pnpm exec vitest run --coverage run: cd openclaw && pnpm exec vitest run --coverage
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: matrix.node-version == 20 if: matrix.node-version == 20
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4
with: with:
flags: openclaw flags: openclaw
directory: integrations/openclaw/coverage directory: openclaw/coverage
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -85,15 +86,15 @@ jobs:
with: with:
node-version: 20 node-version: 20
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml cache-dependency-path: openclaw/pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: cd integrations/openclaw && pnpm install --frozen-lockfile run: cd openclaw && pnpm install --frozen-lockfile
- name: Build - name: Build
run: cd integrations/openclaw && pnpm build run: cd openclaw && pnpm build
- name: Verify dist output exists - name: Verify dist output exists
run: | run: |
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1) test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1) test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
-58
View File
@@ -1,58 +0,0 @@
name: Publish @mem0/opencode-plugin 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# opencode-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. opencode-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/opencode-plugin 📦 to npm
if: startsWith(inputs.tag, 'opencode-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: bun run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -1,39 +0,0 @@
name: opencode-plugin checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Type check
run: bun run type-check
- name: Build
run: bun run build
- name: Verify dist output exists
run: |
test -f dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
-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 name: Publish mem0ai 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged ts-v* is
# published. Can also be dispatched manually to re-publish a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish mem0ai 📦 to npm name: Build and publish mem0ai 📦 to npm
if: startsWith(inputs.tag, 'ts-v') if: startsWith(github.event.release.tag_name, 'ts-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
@@ -27,8 +16,6 @@ jobs:
working-directory: mem0-ts working-directory: mem0-ts
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -51,7 +38,7 @@ jobs:
- name: Publish to npm - name: Publish to npm
run: | run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]") PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID" npx npm@latest publish --provenance --access public --tag "$PREID"
else else
+3 -3
View File
@@ -1,14 +1,14 @@
name: TypeScript SDK CI name: TypeScript SDK CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main runs remain standalone.
on: on:
push: push:
branches: [main] branches: [main]
paths: paths:
- 'mem0-ts/**' - 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml' - '.github/workflows/ts-sdk-ci.yml'
workflow_call: pull_request:
paths:
- 'mem0-ts/**'
jobs: jobs:
check_changes: check_changes:
+6 -20
View File
@@ -1,35 +1,21 @@
name: Publish @mem0/vercel-ai-provider 📦 to npm name: Publish @mem0/vercel-ai-provider 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# vercel-ai-v* is published. Can also be dispatched manually to re-publish
# a tag.
on: on:
workflow_dispatch: release:
inputs: types: [published]
tag:
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs: jobs:
build-n-publish: build-n-publish:
name: Build and publish @mem0/vercel-ai-provider 📦 to npm name: Build and publish @mem0/vercel-ai-provider 📦 to npm
if: startsWith(inputs.tag, 'vercel-ai-v') if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write
defaults: defaults:
run: run:
working-directory: integrations/vercel-ai-sdk working-directory: vercel-ai-sdk
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -42,7 +28,7 @@ jobs:
node-version: '22' node-version: '22'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@@ -52,7 +38,7 @@ jobs:
- name: Publish to npm - name: Publish to npm
run: | run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then if [ "${{ github.event.release.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]") PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID" npx npm@latest publish --provenance --access public --tag "$PREID"
else else
-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
+1 -2
View File
@@ -170,6 +170,7 @@ cython_debug/
# Database # Database
db db
test-db test-db
!embedchain/embedchain/core/db/
.vscode .vscode
.idea/ .idea/
@@ -189,5 +190,3 @@ eval/
qdrant_storage/ qdrant_storage/
.crossnote .crossnote
testing.ipynb testing.ipynb
.weave/
-4
View File
@@ -1,4 +0,0 @@
[submodule "evaluation"]
path = evaluation
url = https://github.com/mem0ai/memory-benchmarks
branch = main
-15
View File
@@ -1,15 +0,0 @@
{
"name": "mem0-plugins",
"version": "1",
"plugins": [
{
"id": "mem0",
"displayName": "Mem0",
"version": "0.1.0",
"description": "Persistent memory for Kimi Code. Remembers decisions, patterns, and preferences across sessions.",
"homepage": "https://mem0.ai",
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
"source": "https://github.com/mem0ai/mem0/tree/main/integrations/mem0-plugin"
}
]
}
+69 -77
View File
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
## Repository Structure ## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation. This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
### Key Directories ### Key Directories
@@ -22,19 +22,18 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory | | `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` | | `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` | | `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") | | `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) | | `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
| `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 |
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) | | `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
| `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) | | `docs/` | Documentation site (Mintlify) |
| `tests/` | Python SDK tests (pytest) | | `tests/` | Python SDK tests (pytest) |
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo | | `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) | | `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
| `pr-reviews/` | Pull request review materials | | `pr-reviews/` | Pull request review materials |
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) | | `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
@@ -51,8 +50,8 @@ mem0 (Python SDK) mem0-ts (TypeScript SDK)
cli/python/ ──▶ mem0ai (optional, for OSS mode) cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls) cli/node/ ──▶ mem0ai (npm, for API calls)
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
integrations/openclaw/ ──▶ mem0ai (npm) openclaw/ ──▶ mem0ai (npm)
``` ```
## Development Setup ## Development Setup
@@ -63,7 +62,7 @@ integrations/openclaw/ ──▶ mem0ai (npm)
- **Node.js**: v18+ (v20 or v22 recommended) - **Node.js**: v18+ (v20 or v22 recommended)
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages - **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
- **Hatch**: Python build/environment tool (`pip install hatch`) - **Hatch**: Python build/environment tool (`pip install hatch`)
- **Docker**: Required for `server/` development - **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup ### Initial Setup
@@ -75,8 +74,8 @@ pre-commit install # install git hooks
# TypeScript packages # TypeScript packages
cd mem0-ts && pnpm install # TS SDK cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI cd cli/node && pnpm install # Node CLI
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider cd vercel-ai-sdk && pnpm install # Vercel AI provider
cd integrations/openclaw && pnpm install # OpenClaw plugin cd openclaw && pnpm install # OpenClaw plugin
``` ```
## Build, Lint, and Test Commands ## Build, Lint, and Test Commands
@@ -164,10 +163,10 @@ pnpm run dev # tsx src/index.ts (development)
- **Test:** vitest (not jest) - **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3 - **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`) ### Vercel AI SDK Provider (`vercel-ai-sdk/`)
```bash ```bash
cd integrations/vercel-ai-sdk cd vercel-ai-sdk
pnpm install pnpm install
pnpm run build # tsup pnpm run build # tsup
pnpm run lint # eslint pnpm run lint # eslint
@@ -182,10 +181,10 @@ pnpm run test:node # vitest (node runtime)
- **Lint:** ESLint + Prettier - **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs) - **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`integrations/openclaw/`) ### OpenClaw Plugin (`openclaw/`)
```bash ```bash
cd integrations/openclaw cd openclaw
pnpm install pnpm install
pnpm run build # tsup pnpm run build # tsup
pnpm run test # vitest run pnpm run test # vitest run
@@ -215,6 +214,28 @@ docker-compose up # starts all 3 services
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin - **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes - **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/`) ### Documentation (`docs/`)
```bash ```bash
@@ -225,19 +246,18 @@ make docs # or: cd docs && mintlify dev
- **API spec:** `docs/openapi.json` - **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/` - **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation / Benchmarking ### Evaluation (`evaluation/`)
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
```bash ```bash
git clone https://github.com/mem0ai/memory-benchmarks.git cd evaluation
cd memory-benchmarks make run-mem0-add # Run mem0 add experiments
pip install -r requirements.txt make run-mem0-search # Run mem0 search experiments
make run-mem0-plus-add # With graph memory
# Run a benchmark (Mem0 Cloud; use docker compose for OSS) make run-mem0-plus-search # With graph memory
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY make run-rag # RAG baseline
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions make run-full-context # Full context baseline
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9 make run-langmem # LangMem comparison
make run-openai # OpenAI comparison
``` ```
## Core APIs ## Core APIs
@@ -310,6 +330,7 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
- Root SDK: line length **120** - Root SDK: line length **120**
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF) - Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
- **isort** with `profile = "black"` for import sorting. - **isort** with `profile = "black"` for import sorting.
- Ruff excludes `embedchain/` and `openmemory/` from root config.
### TypeScript Conventions ### TypeScript Conventions
@@ -322,8 +343,8 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
|---------|--------|-----------|---------------| |---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest | | `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest | | `cli/node/` | Biome | Biome | vitest |
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest | | `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `integrations/openclaw/` | — | — | vitest | | `openclaw/` | — | — | vitest |
### Type Checking ### Type Checking
@@ -360,14 +381,15 @@ Optional layer on top of vector memory for relationship-aware retrieval. Configu
Model Context Protocol support in multiple places: Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai` - **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 ### Plugin & Skills System
- `integrations/mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture. - `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
- `skills/` contains structured skill definitions for AI agents, split into two categories: - `skills/` contains structured skill definitions for AI agents, 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). - **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 ### Adding a New Provider
@@ -381,70 +403,38 @@ To add a new LLM, embedding, vector store, or reranker provider:
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`) 6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure 7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
### Adding a New Integration
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
1. Create `integrations/<name>/` and build the integration there.
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
## CI/CD ## CI/CD
### CI Workflows (automated testing) ### CI Workflows (automated testing)
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters. | Workflow | File | Triggers | Tests |
|----------|------|----------|-------|
| Workflow | File | Standalone Triggers | Tests | | Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|----------|------|---------------------|-------| | TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below | | Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 | | Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 | | OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 | | Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| 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.
### CD Workflows (automated publishing) ### CD Workflows (automated publishing)
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
| Workflow | File | Tag Prefix | Target | | Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------| |----------|------|------------|--------|
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) | | Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) | | TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) | | Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) | | Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) | | Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) | | OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
| 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. - All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions. - First publish of a new npm package must be done manually; OIDC works for subsequent versions.
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
- 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 ### Utility Workflows
| Workflow | File | Purpose | | Workflow | File | Purpose |
|----------|------|---------| |----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling | | 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 | | 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`. | | 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. - Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript). - 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`). - Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
### Contributing Guides ### Contributing Guides
@@ -586,8 +576,10 @@ N/A
- Modify CI/CD workflows without explicit approval. - Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead. - Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials. - Commit `.env` files, API keys, or credentials.
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
- Skip pre-commit hooks. - Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively. - Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax. - Use `require()` for imports in TypeScript — use ES module `import` syntax.
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff). - 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/`. - 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 Let us make contribution easy, collaborative and fun.
community-driven project and we welcome contributions of all kinds — bug fixes,
new features, documentation, examples, and integrations.
Mem0 is a polyglot monorepo, and this guide covers contributing to both the ## Submit your Contribution through PR
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
## Before You Start To make a contribution, follow these steps:
### 1. Open an Issue First 1. Fork and clone this repository
2. Do the changes on your fork with dedicated feature branch `feature/f1`
3. If you modified the code (new feature or bug-fix), please add tests for it
4. Include proper documentation / docstring and examples to run the feature
5. Ensure that all tests pass
6. Submit a pull request
**Always open an issue before opening a pull request.** This lets us discuss the For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
change, avoid duplicate effort, and agree on the approach before you invest time
in code.
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
your bug or idea already exists.
- If it doesn't, open a
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
before starting significant work.
Every pull request must link to an issue using `Closes #<issue-number>`. ### 📦 Development Environment
### 2. Sign the Contributor License Agreement (CLA) We use `hatch` for managing development environments. To set up:
**We cannot accept or merge any pull request until you have signed our Contributor
License Agreement (CLA).**
When you open your first PR, the CLA bot will automatically comment with a link to
sign. Signing takes less than a minute and only needs to be done once. Pull
requests from contributors who have not signed the CLA will be blocked from
merging.
## Repository Layout
The two most common contribution targets are the SDKs:
| Package | Path | Language | Package manager |
| --------------------- | ---------- | ------------ | --------------- |
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
(`integrations/`), the self-hosted `server/`, and the docs site
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
## Development Workflow
1. **Fork** the repository and **clone** your fork.
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
`fix/issue-1234`).
3. Make your changes — add **tests**, **documentation**, and **examples** as
appropriate.
4. Run **linting and tests** for every package you touched (see below).
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
6. Push and open a **pull request** against `main`, linking the issue with
`Closes #<number>` and filling out the
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
### Contributing to the Python SDK (`mem0/`)
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
**Do not use `pip` or `conda` for dependency management.**
```bash ```bash
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12) # Activate environment for specific Python version:
hatch shell dev_py_3_11 hatch shell dev_py_3_9 # Python 3.9
hatch shell dev_py_3_10 # Python 3.10
hatch shell dev_py_3_11 # Python 3.11
hatch shell dev_py_3_12 # Python 3.12
# Install pre-commit hooks (runs ruff + isort on commit) # The environment will automatically install all dev dependencies
pre-commit install # Run tests within the activated shell:
# Lint, format, and sort imports
make lint
make format
make sort
# Run the test suite (run `make install_all` first if deps are missing)
make test make test
``` ```
- **Linter / formatter:** Ruff (line length **120**) ### 📌 Pre-commit
- **Import sorting:** isort (`profile = "black"`)
- **Tests:** pytest (in `tests/`)
See the full [Development guide](https://docs.mem0.ai/contributing/development) for To ensure our standards, make sure to install pre-commit before starting to contribute.
environment details.
### Contributing to the TypeScript SDK (`mem0-ts/`)
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
`npm` or `yarn`.**
```bash ```bash
cd mem0-ts pre-commit install
pnpm install
pnpm run build # tsup (CJS + ESM)
pnpm run test # jest (all tests)
pnpm run test:unit # unit tests with coverage
``` ```
- **Build:** tsup ### 🧪 Testing
- **Formatter:** Prettier
- **Tests:** jest
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
- Use ES module `import` syntax — never `require()`.
## Good Contribution Practices We use `pytest` to test our code across multiple Python versions. You can run tests using:
- **Keep PRs small and focused.** One logical change per PR is easier to review and ```bash
merge. # Run tests with default Python version
- **Follow existing patterns.** Match the style, structure, and conventions of the make test
code around you. Don't introduce new frameworks or abstractions without
discussion.
- **Write tests** that would fail without your change — regression tests for bugs,
coverage for new features.
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
must be added to `docs/llms.txt` (run
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
- **Add examples** when introducing new user-facing behavior.
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
via the CI Gate.
- **Never commit secrets** — no `.env` files, API keys, or credentials.
- **Don't add core dependencies lightly.** New Python dependencies belong in an
optional group in `pyproject.toml`, not the core `dependencies` list.
- **Be responsive** to review feedback and keep your branch up to date with `main`.
## Pull Request Checklist # Test specific Python versions:
make test-py-3.9 # Python 3.9 environment
make test-py-3.10 # Python 3.10 environment
make test-py-3.11 # Python 3.11 environment
make test-py-3.12 # Python 3.12 environment
Before requesting review, make sure: # When using hatch shells, run tests with:
make test # After activating a shell with hatch shell test_XX
```
- [ ] An issue exists and is linked with `Closes #<number>` Make sure that all tests pass across all supported Python versions before submitting a pull request.
- [ ] You have signed the CLA
- [ ] Your code follows the project's style guidelines (lint passes)
- [ ] You performed a self-review of your changes
- [ ] Tests are added/updated and pass locally
- [ ] Documentation is updated if needed
## Reporting Security Issues We look forward to your pull requests and can't wait to see your contributions!
**Do not report security vulnerabilities through public issues or pull requests.** ### 🚀 Releasing
Please follow our [Security Policy](./SECURITY.md) to report them privately.
## Releasing All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
All packages are published automatically via GitHub Actions when a GitHub Release #### Tag Prefixes
is created with the correct tag prefix.
### Tag Prefixes
| Package | Registry | Tag Prefix | Example | | Package | Registry | Tag Prefix | Example |
|---------|----------|------------|---------| |---------|----------|------------|---------|
@@ -162,17 +77,15 @@ is created with the correct tag prefix.
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` | | `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` | | `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
### How to Release #### How to Release
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node) 1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix 2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions) 3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
### Publishing Details #### Publishing Details
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish` - **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required - **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
- All workflows require `permissions: id-token: write` for OIDC authentication - All workflows require `permissions: id-token: write` for OIDC authentication
- First publish of a new npm package must be done manually; OIDC works for subsequent versions - First publish of a new npm package must be done manually; OIDC works for subsequent versions
We look forward to your pull requests and can't wait to see your contributions!
+1 -2
View File
@@ -1026,8 +1026,7 @@ def get_user_preferences(user_id: str):
### AutoGen Integration ### AutoGen Integration
```python ```python
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb from cookbooks.helper.mem0_teachability import Mem0Teachability
from helper.mem0_teachability import Mem0Teachability
from mem0 import Memory from mem0 import Memory
# Add memory capability to AutoGen agents # Add memory capability to AutoGen agents
+221
View File
@@ -0,0 +1,221 @@
# Migration Guide: Upgrading to mem0 1.0.0
## TL;DR
**What changed?** We simplified the API by removing confusing version parameters. Now everything returns a consistent format: `{"results": [...]}`.
**What you need to do:**
1. Upgrade: `pip install mem0ai==1.0.0`
2. Remove `version` and `output_format` parameters from your code
3. Update response handling to use `result["results"]` instead of treating responses as lists
**Time needed:** ~5-10 minutes for most projects
---
## Quick Migration Guide
### 1. Install the Update
```bash
pip install mem0ai==1.0.0
```
### 2. Update Your Code
**If you're using the Memory API:**
```python
# Before
memory = Memory(config=MemoryConfig(version="v1.1"))
result = memory.add("I like pizza")
# After
memory = Memory() # That's it - version is automatic now
result = memory.add("I like pizza")
```
**If you're using the Client API:**
```python
# Before
client.add(messages, output_format="v1.1")
client.search(query, version="v2", output_format="v1.1")
# After
client.add(messages) # Just remove those extra parameters
client.search(query)
```
### 3. Update How You Handle Responses
All responses now use the same format: a dictionary with `"results"` key.
```python
# Before - you might have done this
result = memory.add("I like pizza")
for item in result: # Treating it as a list
print(item)
# After - do this instead
result = memory.add("I like pizza")
for item in result["results"]: # Access the results key
print(item)
# Graph relations (if you use them)
if "relations" in result:
for relation in result["relations"]:
print(relation)
```
---
## Enhanced Message Handling
The platform client (MemoryClient) now supports the same flexible message formats as the OSS version:
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-key")
# All three formats now work:
# 1. Single string (automatically converted to user message)
client.add("I like pizza", user_id="alice")
# 2. Single message dictionary
client.add({"role": "user", "content": "I like pizza"}, user_id="alice")
# 3. List of messages (conversation)
client.add([
{"role": "user", "content": "I like pizza"},
{"role": "assistant", "content": "I'll remember that!"}
], user_id="alice")
```
### Async Mode Configuration
The `async_mode` parameter now defaults to `True` but can be configured:
```python
# Default behavior (async_mode=True)
client.add(messages, user_id="alice")
# Explicitly set async mode
client.add(messages, user_id="alice", async_mode=True)
# Disable async mode if needed
client.add(messages, user_id="alice", async_mode=False)
```
**Note:** `async_mode=True` provides better performance for most use cases. Only set it to `False` if you have specific synchronous processing requirements.
---
## That's It!
For most users, that's all you need to know. The changes are:
- ✅ No more `version` or `output_format` parameters
- ✅ Consistent `{"results": [...]}` response format
- ✅ Cleaner, simpler API
---
## Common Issues
**Getting `KeyError: 'results'`?**
Your code is still treating the response as a list. Update it:
```python
# Change this:
for memory in response:
# To this:
for memory in response["results"]:
```
**Getting `TypeError: unexpected keyword argument`?**
You're still passing old parameters. Remove them:
```python
# Change this:
client.add(messages, output_format="v1.1")
# To this:
client.add(messages)
```
**Seeing deprecation warnings?**
Remove any explicit `version="v1.0"` from your config:
```python
# Change this:
memory = Memory(config=MemoryConfig(version="v1.0"))
# To this:
memory = Memory()
```
---
## What's New in 1.0.0
- **Better vector stores:** Fixed OpenSearch and improved reliability across all stores
- **Cleaner API:** One way to do things, no more confusing options
- **Enhanced GCP support:** Better Vertex AI configuration options
- **Flexible message input:** Platform client now accepts strings, dicts, and lists (aligned with OSS)
- **Configurable async_mode:** Now defaults to `True` but users can override if needed
---
## Need Help?
- Check [GitHub Issues](https://github.com/mem0ai/mem0/issues)
- Read the [documentation](https://docs.mem0.ai/)
- Open a new issue if you're stuck
---
## Advanced: Configuration Changes
**If you configured vector stores with version:**
```python
# Before
config = MemoryConfig(
version="v1.1",
vector_store=VectorStoreConfig(...)
)
# After
config = MemoryConfig(
vector_store=VectorStoreConfig(...)
)
```
---
## Testing Your Migration
Quick sanity check:
```python
from mem0 import Memory
memory = Memory()
# Add should return a dict with "results"
result = memory.add("I like pizza", user_id="test")
assert "results" in result
# Search should return a dict with "results"
search = memory.search("food", user_id="test")
assert "results" in search
# Get all should return a dict with "results"
all_memories = memory.get_all(user_id="test")
assert "results" in all_memories
print("✅ Migration successful!")
```
+1 -1
View File
@@ -11,7 +11,7 @@ install:
hatch env create hatch env create
install_all: 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 \ 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 upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
+7 -27
View File
@@ -46,12 +46,12 @@
| Benchmark | Old | New | Tokens | Latency p50 | | Benchmark | Old | New | Tokens | Latency p50 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s | | **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s | | **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s | | **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s | | **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
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:** **What changed:**
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten. - **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. See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
## Research Highlights ## Research Highlights
- **92.5 on LoCoMo** -- +21 points over the previous algorithm - **91.6 on LoCoMo** -- +20 points over the previous algorithm
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall - **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 - **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
- [Read the full paper](https://mem0.ai/research) - [Read the full paper](https://mem0.ai/research)
@@ -86,25 +86,7 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
## 🚀 Quickstart Guide <a name="quickstart"></a> ## 🚀 Quickstart Guide <a name="quickstart"></a>
### Sign up as an agent > **Agent Mode — for AI agents setting up Mem0 autonomously.** Run `mem0 init --agent --agent-caller <your-name> --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`), replacing `<your-name>` with your AI agent identity (e.g. `claude-code`, `cursor`, `codex`). This mints an evaluation Mem0 API key in <5 seconds — no email, no dashboard, no OTP. If you forgot to pass `--agent-caller`, run `mem0 identify <your-name>` after init. Use the key immediately against the Platform or the MCP server. The human owner can claim ownership later with `mem0 init --email <their-email>`: memories transfer, the same key keeps working, and the agent isn't disrupted.
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
```bash
# 1. Install
npm install -g @mem0/cli # or: pip install mem0-cli
# 2. Sign up as an agent (replace `claude-code` with your name)
mem0 init --agent --agent-caller claude-code
# 3. Add a memory
mem0 add "I am using mem0"
# 4. Search
mem0 search "am I using mem0"
```
The human owner can claim the account later with `mem0 init --email <their-email>` — same key, memories preserved. Full guide: [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup).
| | Library | Self-Hosted Server | Cloud Platform | | | Library | Self-Hosted Server | Cloud Platform |
|---|---------|-------------------|----------------| |---|---------|-------------------|----------------|
@@ -153,7 +135,6 @@ See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for config
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme) 1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
2. Embed the memory layer via SDK or API keys 2. Embed the memory layer via SDK or API keys
3. Using hosted Qdrant vectors? See the [Platform migration guide](https://docs.mem0.ai/migration/oss-to-platform) to import them into Mem0 Platform.
### CLI ### CLI
@@ -186,10 +167,9 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
```bash ```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate 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-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 ### 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 entity` | List or delete entities (users, agents, apps, runs) |
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) | | `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
| `mem0 status` | Verify API connection and display current project | | `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 ## Agent mode
+1 -13
View File
@@ -207,11 +207,7 @@
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." }, { "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": "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": "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": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
{ "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": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" }, { "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": "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" } { "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": "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": "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": "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": "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": "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" } { "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": "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": "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": "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": "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": "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" } { "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
@@ -338,8 +329,6 @@
], ],
"options": [ "options": [
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." }, { "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" } { "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
], ],
"apiEndpoint": "update" "apiEndpoint": "update"
@@ -367,7 +356,6 @@
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." }, { "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": "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": "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": "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": "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" }, { "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
-41
View File
@@ -1,41 +0,0 @@
# Node CLI (`cli/node/`)
The `@mem0/cli` package on npm. Commander-based, entry point `mem0`.
## Commands
```bash
pnpm install
pnpm run build # tsup (ESM)
pnpm run lint # biome check src/
pnpm run lint:fix # biome check --write src/
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest run
pnpm run test:watch
pnpm run dev # tsx src/index.ts
```
pnpm only. Never npm, never yarn.
## Conventions
> **Biome, not ESLint. vitest, not jest.** `mem0-ts/` uses Prettier + jest and
> `integrations/vercel-ai-sdk/` uses ESLint + jest. Running those tools here produces
> spurious diffs. Every toolchain in this repo is per-package.
- **Node 18+** required.
- **Build:** tsup, ESM output only.
- **Linter and formatter:** Biome, configured in `biome.json`.
- **Tests:** vitest.
- **TypeScript strict mode.** ES module `import` syntax only, never `require()`.
Run `pnpm run typecheck` after every change.
## Dependencies
Commander + Chalk + ora + cli-table3, and `mem0ai` (npm) for API calls.
## CI and release
- CI: `cli-node-ci.yml`, Biome + tsc + vitest + tsup build on Node 20 and 22.
- Release: tag prefix `cli-node-v*` dispatches `cli-node-cd.yml`, publishing to npm over OIDC.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+8 -2
View File
@@ -232,6 +232,14 @@ Verify your API connection and display the current project.
mem0 status mem0 status
``` ```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode ## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops: 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 | | `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format | | `-o, --output` | Set the output format |
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
## Environment variables ## Environment variables
| Variable | Description | | Variable | Description |
+1 -1
View File
@@ -51,7 +51,7 @@ pnpm link --global
# Now use it like a normal CLI # Now use it like a normal CLI
mem0 --help 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`. > **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", "name": "@mem0/cli",
"version": "0.2.12", "version": "0.2.5",
"description": "The official CLI for mem0 — the memory layer for AI agents", "description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module", "type": "module",
"bin": { "bin": {
@@ -40,19 +40,8 @@
"typescript": "^5.4.0", "typescript": "^5.4.0",
"tsup": "^8.0.0", "tsup": "^8.0.0",
"tsx": "^4.7.0", "tsx": "^4.7.0",
"vite": "^6.0.0", "vitest": "^1.5.0",
"vitest": "^4.1.0",
"@biomejs/biome": "^1.7.0", "@biomejs/biome": "^1.7.0",
"@types/node": "^20.0.0" "@types/node": "^20.0.0"
},
"pnpm": {
"overrides": {
"jws@4.0.0": "4.0.1",
"langsmith@<0.6.0": "^0.6.0",
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"picomatch@<2.3.2": "^2.3.2",
"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 -17
View File
@@ -14,11 +14,7 @@ export interface AddOptions {
immutable?: boolean; immutable?: boolean;
infer?: boolean; infer?: boolean;
expires?: string; expires?: string;
customInstructions?: string; categories?: string[];
agentCustomInstructions?: string;
customCategories?: Record<string, string>[];
structuredDataSchema?: Record<string, unknown>;
timestamp?: number;
} }
export interface SearchOptions { export interface SearchOptions {
@@ -32,9 +28,6 @@ export interface SearchOptions {
keyword?: boolean; keyword?: boolean;
filters?: Record<string, unknown>; filters?: Record<string, unknown>;
fields?: string[]; fields?: string[];
showExpired?: boolean;
referenceDate?: string | number;
latestOnly?: boolean;
} }
export interface ListOptions { export interface ListOptions {
@@ -47,8 +40,6 @@ export interface ListOptions {
category?: string; category?: string;
after?: string; after?: string;
before?: string; before?: string;
showExpired?: boolean;
latestOnly?: boolean;
} }
export interface DeleteOptions { export interface DeleteOptions {
@@ -57,12 +48,6 @@ export interface DeleteOptions {
agentId?: string; agentId?: string;
appId?: string; appId?: string;
runId?: string; runId?: string;
deleteLinked?: boolean;
}
export interface UpdateOptions {
expirationDate?: string;
timestamp?: number;
} }
export interface EntityIds { export interface EntityIds {
@@ -92,7 +77,6 @@ export interface Backend {
memoryId: string, memoryId: string,
content?: string, content?: string,
metadata?: Record<string, unknown>, metadata?: Record<string, unknown>,
opts?: UpdateOptions,
): Promise<Record<string, unknown>>; ): Promise<Record<string, unknown>>;
delete( delete(
+19 -55
View File
@@ -15,13 +15,8 @@ import {
type ListOptions, type ListOptions,
NotFoundError, NotFoundError,
type SearchOptions, type SearchOptions,
type UpdateOptions,
} from "./base.js"; } from "./base.js";
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
export class PlatformBackend implements Backend { export class PlatformBackend implements Backend {
private baseUrl: string; private baseUrl: string;
private headers: Record<string, string>; private headers: Record<string, string>;
@@ -151,15 +146,7 @@ export class PlatformBackend implements Backend {
if (opts.immutable) payload.immutable = true; if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false; if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires; if (opts.expires) payload.expiration_date = opts.expires;
if (opts.customInstructions) if (opts.categories) payload.categories = opts.categories;
payload.custom_instructions = opts.customInstructions;
if (opts.agentCustomInstructions)
payload.agent_custom_instructions = opts.agentCustomInstructions;
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;
payload.source = "CLI"; payload.source = "CLI";
return (await this._request("POST", "/v3/memories/add/", { return (await this._request("POST", "/v3/memories/add/", {
@@ -220,10 +207,6 @@ export class PlatformBackend implements Backend {
if (opts.rerank) payload.rerank = true; if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true; if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields; 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"; payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/search/", { const result = (await this._request("POST", "/v3/memories/search/", {
@@ -235,13 +218,9 @@ export class PlatformBackend implements Backend {
} }
async get(memoryId: string): Promise<Record<string, unknown>> { async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request( return (await this._request("GET", `/v1/memories/${memoryId}/`, {
"GET", params: { source: "CLI" },
`/v1/memories/${encodePathSegment(memoryId)}/`, })) as Record<string, unknown>;
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
} }
async listMemories( async listMemories(
@@ -278,8 +257,6 @@ export class PlatformBackend implements Backend {
extraFilters: Object.keys(extra).length > 0 ? extra : undefined, extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
}); });
if (apiFilters) payload.filters = apiFilters; if (apiFilters) payload.filters = apiFilters;
if (opts.showExpired) payload.show_expired = true;
if (opts.latestOnly) payload.latest_only = true;
payload.source = "CLI"; payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/", { const result = (await this._request("POST", "/v3/memories/", {
@@ -295,21 +272,14 @@ export class PlatformBackend implements Backend {
memoryId: string, memoryId: string,
content?: string, content?: string,
metadata?: Record<string, unknown>, metadata?: Record<string, unknown>,
opts: UpdateOptions = {},
): Promise<Record<string, unknown>> { ): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {}; const payload: Record<string, unknown> = {};
if (content) payload.text = content; if (content) payload.text = content;
if (metadata) payload.metadata = metadata; if (metadata) payload.metadata = metadata;
if (opts.expirationDate) payload.expiration_date = opts.expirationDate;
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
payload.source = "CLI"; payload.source = "CLI";
return (await this._request( return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
"PUT", json: payload,
`/v1/memories/${encodePathSegment(memoryId)}/`, })) as Record<string, unknown>;
{
json: payload,
},
)) as Record<string, unknown>;
} }
async delete( async delete(
@@ -327,13 +297,9 @@ export class PlatformBackend implements Backend {
})) as Record<string, unknown>; })) as Record<string, unknown>;
} }
if (memoryId) { if (memoryId) {
const params: Record<string, string> = { source: "CLI" }; return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
if (opts.deleteLinked) params.delete_linked = "true"; params: { source: "CLI" },
return (await this._request( })) as Record<string, unknown>;
"DELETE",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{ params },
)) as Record<string, unknown>;
} }
throw new Error("Either memoryId or --all is required"); throw new Error("Either memoryId or --all is required");
} }
@@ -350,18 +316,16 @@ export class PlatformBackend implements Backend {
if (entities.length === 0) { if (entities.length === 0) {
throw new Error("At least one entity ID is required for deleteEntities."); throw new Error("At least one entity ID is required for deleteEntities.");
} }
// Delete each provided entity via the v2 path-based endpoint. Key each // Delete each provided entity via the v2 path-based endpoint
// response by entity type so a multi-entity delete (e.g. --user-id and let result: Record<string, unknown> = {};
// --agent-id together) doesn't discard everything but the last result.
const results: Record<string, unknown> = {};
for (const [entityType, entityId] of entities) { for (const [entityType, entityId] of entities) {
results[entityType] = (await this._request( result = (await this._request(
"DELETE", "DELETE",
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`, `/v2/entities/${entityType}/${entityId}/`,
{ params: { source: "CLI" } }, { params: { source: "CLI" } },
)) as Record<string, unknown>; )) as Record<string, unknown>;
} }
return results; return result;
} }
async ping(): Promise<Record<string, unknown>> { async ping(): Promise<Record<string, unknown>> {
@@ -420,9 +384,9 @@ export class PlatformBackend implements Backend {
} }
async getEvent(eventId: string): Promise<Record<string, unknown>> { async getEvent(eventId: string): Promise<Record<string, unknown>> {
return (await this._request( return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
"GET", string,
`/v1/event/${encodePathSegment(eventId)}/`, unknown
)) as Record<string, unknown>; >;
} }
} }
+1 -1
View File
@@ -247,7 +247,7 @@ export async function claimViaOtp(
printError(`Claim failed: ${detail}`); printError(`Claim failed: ${detail}`);
if (errCode === "email_already_claimed") { if (errCode === "email_already_claimed") {
console.log( console.log(
` ${dim("Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.")}`, ` ${dim("Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.")}`,
); );
} }
process.exit(1); process.exit(1);
-147
View File
@@ -1,147 +0,0 @@
/**
* `mem0 agent-rush <add|search> "..."` — wraps the AGENTRUSH platform endpoints.
* Project routing is implicit (server-side); zero flags needed.
*/
import readline from "node:readline";
import { colors, printError, printSuccess } from "../branding.js";
import { loadConfig, saveConfig } from "../config.js";
import { CLI_VERSION } from "../version.js";
const PII_WARNING = [
"",
"⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.",
" Do not include real names, emails, secrets, work content, or PII.",
"",
].join("\n");
const ERROR_HINTS: Record<string, string> = {
agentrush_search_first:
"Run 3 'mem0 agent-rush search' commands before adding.",
agentrush_search_quota: "You've used your 3 lifetime searches.",
agentrush_add_quota: "You've used your 3 lifetime adds.",
agentrush_not_agent_mode:
"Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
agentrush_length: "Memory text must be 50-1000 characters.",
agentrush_no_urls: "URLs are not allowed.",
agentrush_blocklist: "Content contains a blocked term.",
agentrush_global_quota: "Event-wide cap reached. Try again later.",
agentrush_not_provisioned:
"AGENTRUSH is not provisioned in this environment.",
};
async function callEndpoint(
path: string,
body: Record<string, unknown>,
): Promise<unknown> {
const config = loadConfig();
const baseUrl = (config.platform?.baseUrl ?? "https://api.mem0.ai").replace(
/\/+$/,
"",
);
if (!config.platform?.apiKey) {
printError("Not initialized. Run `mem0 init --agent` first.");
process.exit(1);
}
const resp = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Token ${config.platform.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
"X-Mem0-Mode": "agent-rush",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
const code =
(json as { error?: { code?: string } }).error?.code ?? "unknown";
printError(`AGENTRUSH error: ${code}`);
if (ERROR_HINTS[code]) {
console.log(` ${colors.dim(ERROR_HINTS[code])}`);
}
process.exit(1);
}
return json;
}
function promptLine(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
/**
* Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC.
*
* Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt`
* so we never ask the same machine twice. On anything else, abort.
*
* Non-interactive (agent invocation, no TTY): print the warning to stderr
* for the human reading the agent's transcript and proceed — agents can't
* answer y/N prompts.
*/
async function ensureWarningAcknowledged(): Promise<void> {
const config = loadConfig();
if (config.agentRush?.acknowledgedAt) return;
if (!process.stdin.isTTY || !process.stdout.isTTY) {
// Agent context: surface the warning to stderr, don't block.
console.error(PII_WARNING);
return;
}
console.log(PII_WARNING);
const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase();
if (answer !== "y" && answer !== "yes") {
printError("Aborted.");
process.exit(1);
}
config.agentRush.acknowledgedAt = new Date().toISOString();
saveConfig(config);
}
export async function cmdAgentRushAdd(content: string): Promise<void> {
await ensureWarningAcknowledged();
const result = await callEndpoint("/v1/agent-rush/memories/", { content });
printSuccess(
`Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`,
);
}
export async function cmdAgentRushSearch(query: string): Promise<void> {
const result = (await callEndpoint("/v1/agent-rush/memories/search/", {
query,
})) as {
results?: Array<{ memory?: string }>;
memories?: Array<{ memory?: string }>;
};
const memories = result.results ?? result.memories ?? [];
if (memories.length === 0) {
console.log(colors.dim("(no results)"));
return;
}
memories.slice(0, 5).forEach((m, i) => {
console.log(` ${i + 1}. ${m.memory ?? JSON.stringify(m)}`);
});
}
+35 -79
View File
@@ -21,19 +21,16 @@ import {
formatSingleMemory, formatSingleMemory,
printResultSummary, printResultSummary,
} from "../output.js"; } 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. */ /** True only when stdin is an actual pipe or file redirect — never in agent mode. */
function _validateExpires(value: string): void { function _stdinIsPiped(): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { if (isAgentMode()) return false;
printError( try {
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).", const stat = fs.fstatSync(0);
); return stat.isFIFO() || stat.isFile();
process.exit(1); } catch {
} return false;
if (new Date(value) <= new Date()) {
printError("--expires date must be in the future.");
process.exit(1);
} }
} }
@@ -49,26 +46,13 @@ export async function cmdAdd(
file?: string; file?: string;
metadata?: string; metadata?: string;
immutable: boolean; immutable: boolean;
infer?: boolean; noInfer: boolean;
expires?: string; expires?: string;
categories?: string; categories?: string;
customInstructions?: string;
agentCustomInstructions?: string;
customCategories?: string;
structuredDataSchema?: string;
timestamp?: number;
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
setCurrentCommand("add"); 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 msgs: Record<string, unknown>[] | undefined;
let content = text; let content = text;
@@ -94,7 +78,7 @@ export async function cmdAdd(
} }
} }
// Read from stdin only if stdin is an actual pipe or file redirect // 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(); content = fs.readFileSync(0, "utf-8").trim();
} }
@@ -109,6 +93,20 @@ export async function cmdAdd(
process.exit(1); 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; let meta: Record<string, unknown> | undefined;
if (opts.metadata) { if (opts.metadata) {
try { try {
@@ -119,28 +117,15 @@ export async function cmdAdd(
} }
} }
let customCats: Record<string, string>[] | undefined; let cats: string[] | undefined;
if (opts.customCategories) { if (opts.categories) {
try { try {
customCats = JSON.parse(opts.customCategories); cats = JSON.parse(opts.categories);
} catch { } catch {
printError("Invalid JSON in --custom-categories."); cats = opts.categories.split(",").map((c) => c.trim());
process.exit(1);
} }
} }
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>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Adding memory...", async () => { result = await timedStatus("Adding memory...", async () => {
@@ -151,13 +136,9 @@ export async function cmdAdd(
runId: opts.runId, runId: opts.runId,
metadata: meta, metadata: meta,
immutable: opts.immutable, immutable: opts.immutable,
infer: opts.infer !== false, infer: !opts.noInfer,
expires: opts.expires, expires: opts.expires,
customInstructions: opts.customInstructions, categories: cats,
agentCustomInstructions: opts.agentCustomInstructions,
customCategories: customCats,
structuredDataSchema: schema,
timestamp: opts.timestamp,
}); });
}); });
} catch (e) { } catch (e) {
@@ -242,9 +223,6 @@ export async function cmdSearch(
keyword: boolean; keyword: boolean;
filterJson?: string; filterJson?: string;
fields?: string; fields?: string;
showExpired?: boolean;
referenceDate?: string;
latestOnly?: boolean;
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
@@ -293,9 +271,6 @@ export async function cmdSearch(
keyword: opts.keyword, keyword: opts.keyword,
filters, filters,
fields: fieldList, fields: fieldList,
showExpired: opts.showExpired,
referenceDate: opts.referenceDate,
latestOnly: opts.latestOnly,
}); });
}); });
} catch (e) { } catch (e) {
@@ -389,8 +364,6 @@ export async function cmdList(
category?: string; category?: string;
after?: string; after?: string;
before?: string; before?: string;
showExpired?: boolean;
latestOnly?: boolean;
output: string; output: string;
}, },
): Promise<void> { ): Promise<void> {
@@ -418,8 +391,6 @@ export async function cmdList(
category: opts.category, category: opts.category,
after: opts.after, after: opts.after,
before: opts.before, before: opts.before,
showExpired: opts.showExpired,
latestOnly: opts.latestOnly,
}); });
}); });
} catch (e) { } catch (e) {
@@ -479,12 +450,7 @@ export async function cmdUpdate(
backend: Backend, backend: Backend,
memoryId: string, memoryId: string,
text: string | undefined, text: string | undefined,
opts: { opts: { metadata?: string; output: string },
metadata?: string;
expires?: string;
timestamp?: number;
output: string;
},
): Promise<void> { ): Promise<void> {
setCurrentCommand("update"); setCurrentCommand("update");
let meta: Record<string, unknown> | undefined; let meta: Record<string, unknown> | undefined;
@@ -497,16 +463,11 @@ export async function cmdUpdate(
} }
} }
if (opts.expires) _validateExpires(opts.expires);
const start = performance.now(); const start = performance.now();
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Updating memory...", async () => { result = await timedStatus("Updating memory...", async () => {
return backend.update(memoryId, text, meta, { return backend.update(memoryId, text, meta);
expirationDate: opts.expires,
timestamp: opts.timestamp,
});
}); });
} catch (e) { } catch (e) {
printError(e instanceof Error ? e.message : String(e)); printError(e instanceof Error ? e.message : String(e));
@@ -532,12 +493,7 @@ export async function cmdUpdate(
export async function cmdDelete( export async function cmdDelete(
backend: Backend, backend: Backend,
memoryId: string, memoryId: string,
opts: { opts: { output: string; dryRun?: boolean; force?: boolean },
output: string;
dryRun?: boolean;
force?: boolean;
deleteLinked?: boolean;
},
): Promise<void> { ): Promise<void> {
setCurrentCommand("delete"); setCurrentCommand("delete");
if (opts.dryRun) { if (opts.dryRun) {
@@ -558,7 +514,7 @@ export async function cmdDelete(
let result: Record<string, unknown>; let result: Record<string, unknown>;
try { try {
result = await timedStatus("Deleting...", async () => { result = await timedStatus("Deleting...", async () => {
return backend.delete(memoryId, { deleteLinked: opts.deleteLinked }); return backend.delete(memoryId);
}); });
} catch (e) { } catch (e) {
printError(e instanceof Error ? e.message : String(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"; 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 { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js"; import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { setCurrentCommand } from "../state.js"; import { setCurrentCommand } from "../state.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors; const { brand, dim, success, error: errorColor } = colors;
@@ -81,6 +82,10 @@ export async function cmdStatus(
console.log(); console.log();
} }
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport( export async function cmdImport(
backend: Backend, backend: Backend,
filePath: string, filePath: string,
-18
View File
@@ -1,18 +0,0 @@
/**
* `mem0 whoami` — print the active agent's default_user_id (AGENTRUSH identifier).
* Reads from local config; no network call.
*/
import { colors, printError, printInfo } from "../branding.js";
import { loadConfig } from "../config.js";
export async function cmdWhoami(): Promise<void> {
const config = loadConfig();
const sessionId = config.platform?.defaultUserId;
if (!sessionId) {
printError("No default_user_id found. Run `mem0 init --agent` first.");
process.exit(1);
}
console.log(`Your AGENTRUSH identifier: ${colors.brand(sessionId)}`);
printInfo("Find your row at https://mem0.ai/agentrush");
}
-15
View File
@@ -40,18 +40,11 @@ export interface TelemetryConfig {
anonymousId: string; anonymousId: string;
} }
export interface AgentRushConfig {
// ISO timestamp the human acknowledged the "memories are public" warning.
// Empty until first interactive `mem0 agent-rush add`.
acknowledgedAt: string;
}
export interface Mem0Config { export interface Mem0Config {
version: number; version: number;
defaults: DefaultsConfig; defaults: DefaultsConfig;
platform: PlatformConfig; platform: PlatformConfig;
telemetry: TelemetryConfig; telemetry: TelemetryConfig;
agentRush: AgentRushConfig;
} }
export function createDefaultConfig(): Mem0Config { export function createDefaultConfig(): Mem0Config {
@@ -76,9 +69,6 @@ export function createDefaultConfig(): Mem0Config {
telemetry: { telemetry: {
anonymousId: "", anonymousId: "",
}, },
agentRush: {
acknowledgedAt: "",
},
}; };
} }
@@ -113,8 +103,6 @@ export function loadConfig(): Mem0Config {
config.defaults.runId = defaults.run_id ?? ""; config.defaults.runId = defaults.run_id ?? "";
const telemetry = data.telemetry ?? {}; const telemetry = data.telemetry ?? {};
config.telemetry.anonymousId = telemetry.anonymous_id ?? ""; config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
const agentRush = data.agent_rush ?? {};
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
} }
// Environment variable overrides // Environment variable overrides
@@ -155,9 +143,6 @@ export function saveConfig(config: Mem0Config): void {
telemetry: { telemetry: {
anonymous_id: config.telemetry.anonymousId, anonymous_id: config.telemetry.anonymousId,
}, },
agent_rush: {
acknowledged_at: config.agentRush.acknowledgedAt,
},
}; };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2)); fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
+5 -103
View File
@@ -17,7 +17,6 @@ import {
isAgentMode, isAgentMode,
setAgentMode, setAgentMode,
setCurrentCommand, setCurrentCommand,
stdinIsPiped,
takeNotice, takeNotice,
} from "./state.js"; } from "./state.js";
import { captureEvent } from "./telemetry.js"; import { captureEvent } from "./telemetry.js";
@@ -263,48 +262,6 @@ program
await runIdentify(name); await runIdentify(name);
}); });
// ── Setup: whoami (print active agent identifier) ────────────────────────
program
.command("whoami")
.description("Print the active agent's AGENTRUSH identifier.")
.action(async () => {
const { cmdWhoami } = await import("./commands/whoami.js");
await cmdWhoami();
});
// ── AGENTRUSH subcommand group ────────────────────────────────────────────
const agentRush = program
.command("agent-rush")
.description("AGENTRUSH game commands.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
agentRush
.command("add <content...>")
.description("Submit a memory to AGENTRUSH.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 agent-rush add "I used mem0 to build a coding agent"\n $ mem0 agent-rush add "Agents that remember are better agents"',
)
.action(async (parts: string[]) => {
const { cmdAgentRushAdd } = await import("./commands/agent-rush.js");
await cmdAgentRushAdd(parts.join(" "));
});
agentRush
.command("search <query...>")
.description("Search AGENTRUSH memories.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 agent-rush search "agents and memory and tools"\n $ mem0 agent-rush search "coding assistant"',
)
.action(async (parts: string[]) => {
const { cmdAgentRushSearch } = await import("./commands/agent-rush.js");
await cmdAgentRushSearch(parts.join(" "));
});
// ── Memory: add ─────────────────────────────────────────────────────────── // ── Memory: add ───────────────────────────────────────────────────────────
program program
@@ -320,29 +277,7 @@ program
.option("--immutable", "Prevent future updates.", false) .option("--immutable", "Prevent future updates.", false)
.option("--no-infer", "Skip inference, store raw.") .option("--no-infer", "Skip inference, store raw.")
.option("--expires <date>", "Expiration date (YYYY-MM-DD).") .option("--expires <date>", "Expiration date (YYYY-MM-DD).")
.option( .option("--categories <value>", "Categories (JSON array or comma-separated).")
"--categories <value>",
"Not supported on add, use --custom-categories instead.",
)
.option(
"--custom-instructions <text>",
"Custom instructions for fact extraction.",
)
.option(
"--agent-custom-instructions <text>",
"Extraction instructions for agent-scoped memories, overriding the project setting.",
)
.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("-o, --output <format>", "Output format: text, json, quiet.", "text") .option("-o, --output <format>", "Output format: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.") .option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.") .option("--base-url <url>", "Override API base URL.")
@@ -389,16 +324,6 @@ program
.option("--keyword", "Use keyword search.", false) .option("--keyword", "Use keyword search.", false)
.option("--filter <json>", "Advanced filter expression (JSON).") .option("--filter <json>", "Advanced filter expression (JSON).")
.option("--fields <list>", "Specific fields to return (comma-separated).") .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("-o, --output <format>", "Output: text, json, table.", "text")
.option("--api-key <key>", "Override API key.") .option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.") .option("--base-url <url>", "Override API base URL.")
@@ -408,7 +333,7 @@ program
) )
.action(async (query, opts) => { .action(async (query, opts) => {
let resolvedQuery = query; let resolvedQuery = query;
if (!resolvedQuery && stdinIsPiped()) { if (!resolvedQuery && !process.stdin.isTTY) {
resolvedQuery = fs.readFileSync(0, "utf-8").trim(); resolvedQuery = fs.readFileSync(0, "utf-8").trim();
} }
if (!resolvedQuery) { if (!resolvedQuery) {
@@ -431,9 +356,6 @@ program
keyword: opts.keyword, keyword: opts.keyword,
filterJson: opts.filter, filterJson: opts.filter,
fields: opts.fields, fields: opts.fields,
showExpired: opts.showExpired,
referenceDate: opts.referenceDate,
latestOnly: opts.latestOnly,
output, output,
}); });
}); });
@@ -477,12 +399,6 @@ program
.option("--category <name>", "Filter by category.") .option("--category <name>", "Filter by category.")
.option("--after <date>", "Created after (YYYY-MM-DD).") .option("--after <date>", "Created after (YYYY-MM-DD).")
.option("--before <date>", "Created before (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("-o, --output <format>", "Output: text, json, table.", "table")
.option("--api-key <key>", "Override API key.") .option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.") .option("--base-url <url>", "Override API base URL.")
@@ -506,8 +422,6 @@ program
category: opts.category, category: opts.category,
after: opts.after, after: opts.after,
before: opts.before, before: opts.before,
showExpired: opts.showExpired,
latestOnly: opts.latestOnly,
output, output,
}); });
}); });
@@ -518,10 +432,6 @@ program
.command("update <memoryId> [text]") .command("update <memoryId> [text]")
.description("Update a memory's text or metadata.") .description("Update a memory's text or metadata.")
.option("-m, --metadata <json>", "Update metadata (JSON).") .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("-o, --output <format>", "Output: text, json, quiet.", "text")
.option("--api-key <key>", "Override API key.") .option("--api-key <key>", "Override API key.")
.option("--base-url <url>", "Override API base URL.") .option("--base-url <url>", "Override API base URL.")
@@ -531,7 +441,7 @@ program
) )
.action(async (memoryId, text, opts) => { .action(async (memoryId, text, opts) => {
let resolvedText = text; let resolvedText = text;
if (!resolvedText && stdinIsPiped()) { if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
resolvedText = fs.readFileSync(0, "utf-8").trim(); resolvedText = fs.readFileSync(0, "utf-8").trim();
} }
const { cmdUpdate } = await import("./commands/memory.js"); const { cmdUpdate } = await import("./commands/memory.js");
@@ -540,8 +450,6 @@ program
const output = isAgent ? "agent" : opts.output; const output = isAgent ? "agent" : opts.output;
await cmdUpdate(backend, memoryId, resolvedText, { await cmdUpdate(backend, memoryId, resolvedText, {
metadata: opts.metadata, metadata: opts.metadata,
expires: opts.expires,
timestamp: opts.timestamp,
output, output,
}); });
}); });
@@ -560,11 +468,6 @@ program
.option("--project", "With --all: delete ALL memories project-wide.", false) .option("--project", "With --all: delete ALL memories project-wide.", false)
.option("--dry-run", "Show what would be deleted without deleting.", false) .option("--dry-run", "Show what would be deleted without deleting.", false)
.option("--force", "Skip confirmation.", 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("-u, --user-id <id>", "Scope to user.")
.option("--agent-id <id>", "Scope to agent.") .option("--agent-id <id>", "Scope to agent.")
.option("--app-id <id>", "Scope to app.") .option("--app-id <id>", "Scope to app.")
@@ -618,7 +521,6 @@ program
output, output,
dryRun: opts.dryRun, dryRun: opts.dryRun,
force: opts.force, force: opts.force,
deleteLinked: opts.deleteLinked,
}); });
return; return;
} }
@@ -862,8 +764,8 @@ program
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json") .addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
.action((opts) => { .action((opts) => {
// opts.json is set when `mem0 help --json` is used (subcommand flag). // 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. // program.opts().json is set when the root --json global flag was used first.
if (opts.json || program.opts().json || program.opts().agent) { if (opts.json || program.opts().json) {
// Load spec from parent directory // Load spec from parent directory
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const specPath = path.join(__dirname, "..", "..", "cli-spec.json"); 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": case "search":
return (data as Record<string, unknown>[]).map((r) => return (data as Record<string, unknown>[]).map((r) =>
pick(r, [ pick(r, ["id", "memory", "score", "created_at", "categories"]),
"id",
"memory",
"score",
"created_at",
"categories",
"expiration_date",
]),
); );
case "list": case "list":
return (data as Record<string, unknown>[]).map((r) => return (data as Record<string, unknown>[]).map((r) =>
pick(r, [ pick(r, ["id", "memory", "created_at", "categories"]),
"id",
"memory",
"created_at",
"categories",
"expiration_date",
]),
); );
case "get": { case "get": {
const r = data as Record<string, unknown>; const r = data as Record<string, unknown>;
@@ -310,12 +297,11 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
"updated_at", "updated_at",
"categories", "categories",
"metadata", "metadata",
"expiration_date",
]); ]);
} }
case "update": { case "update": {
const r = data as Record<string, unknown>; const r = data as Record<string, unknown>;
return pick(r, ["id", "memory", "expiration_date"]); return pick(r, ["id", "memory"]);
} }
case "delete": case "delete":
case "delete-all": case "delete-all":
-13
View File
@@ -3,8 +3,6 @@
* read by commands and branding functions. * read by commands and branding functions.
*/ */
import fs from "node:fs";
let _agentMode = false; let _agentMode = false;
let _currentCommand = ""; let _currentCommand = "";
let _pendingNotice = ""; let _pendingNotice = "";
@@ -40,14 +38,3 @@ export function takeNotice(): string {
_pendingNotice = ""; _pendingNotice = "";
return msg; 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, anonDistinctIdToAlias: anonIdToAlias,
}; };
const child = spawn(process.execPath, [SENDER_SCRIPT], { const child = spawn(
detached: true, process.execPath,
stdio: ["pipe", "ignore", "ignore"], [SENDER_SCRIPT, JSON.stringify(context)],
}); { detached: true, stdio: "ignore" },
child.stdin?.end(JSON.stringify(context)); );
child.unref(); child.unref();
} catch { } catch {
/* silently swallow */ /* silently swallow */
+2 -28
View File
@@ -1,8 +1,7 @@
/** /**
* Standalone telemetry sender — runs as a detached child process. * Standalone telemetry sender — runs as a detached child process.
* *
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single * Usage: node telemetry-sender.cjs '<json context>'
* argv argument is still accepted as a legacy fallback)
* *
* This script is spawned by telemetry.captureEvent() and runs independently * This script is spawned by telemetry.captureEvent() and runs independently
* of the parent CLI process. It: * of the parent CLI process. It:
@@ -20,31 +19,6 @@
const https = require("https"); const https = require("https");
const fs = require("fs"); const fs = require("fs");
function loadContext() {
return new Promise((resolve, reject) => {
if (process.argv[2]) {
try {
resolve(JSON.parse(process.argv[2]));
} catch (err) {
reject(err);
}
return;
}
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => (data += chunk));
process.stdin.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
process.stdin.on("error", reject);
});
}
function httpsRequest(url, method, headers, body) { function httpsRequest(url, method, headers, body) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const u = new URL(url); const u = new URL(url);
@@ -134,7 +108,7 @@ async function sendIdentifyEvent(ctx, payload, anonId) {
} }
async function main() { async function main() {
const ctx = await loadContext(); const ctx = JSON.parse(process.argv[2]);
const payload = ctx.payload; const payload = ctx.payload;
if (ctx.needsEmail && ctx.mem0ApiKey) { if (ctx.needsEmail && ctx.mem0ApiKey) {
+7 -17
View File
@@ -44,25 +44,15 @@ describe("CLI Integration — help and version", () => {
expect(result.stdout).toContain("search"); expect(result.stdout).toContain("search");
}); });
it("prints the version with --version, and has no version subcommand", () => { it("help --json produces valid JSON", () => {
const flag = run(["--version"]); const result = run(["help", "--json"]);
expect(flag.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(flag.stdout).toContain("Mem0"); const parsed = JSON.parse(result.stdout);
expect(run(["version"]).exitCode).not.toBe(0); // 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", () => { it("shows add help", () => {
const result = run(["add", "--help"]); const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
File diff suppressed because it is too large Load Diff
-164
View File
@@ -1,164 +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"],
agent_custom_instructions: ["--agent-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);
}
});
}
});
-242
View File
@@ -1,242 +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.",
agentCustomInstructions: "Extract only tool outcomes.",
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.agent_custom_instructions).toBe(
"Extract only tool outcomes.",
);
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("agent_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: { define: {
__CLI_VERSION__: JSON.stringify(pkg.version), __CLI_VERSION__: JSON.stringify(pkg.version),
}, },
test: {
// Integration tests spawn the CLI via `npx tsx` (15s subprocess
// timeout); the first spawn in a file pays a cold-start cost that can
// exceed vitest's 5s default on CI runners.
testTimeout: 30_000,
},
}); });
-47
View File
@@ -1,47 +0,0 @@
# Python CLI (`cli/python/`)
The `mem0-cli` package on PyPI. Typer-based, entry point `mem0`.
## Commands
```bash
pip install -e ".[dev]" # dev install: ruff + pytest
ruff check . # lint
ruff format . # format
pytest # test
hatch build # build
```
## Conventions
> **Line length is 100 here, not 120.** The root Python SDK uses 120. Running the root
> `make format` over this directory reformats every file and fails CI. Use the local
> `ruff` invocations above.
- **Python 3.10+.** Not 3.9, unlike the root SDK.
- **Ruff** with an extended rule set: `E`, `F`, `I`, `W`, `UP`, `B`, `SIM`, `RUF`.
Ignores `E501` (formatter handles it), `B008` (required by Typer's argument defaults),
and `SIM108`.
- **Ruff format:** double quotes, space indent, `docstring-code-format = true`.
- **isort** first-party is `mem0_cli` only.
- **pytest** for tests.
- Target version pinned to `py310`.
## Layout
```
cli/python/
├── src/mem0_cli/ package source (src layout)
└── tests/
```
Entry point: `mem0 = "mem0_cli.app:main"`.
## Dependencies
Typer + Rich + httpx. `mem0ai` is **optional**, exposed through the `[oss]` extra for OSS mode. Do not promote it to a required dependency.
## CI and release
- CI: `cli-python-ci.yml`, ruff + pytest + `hatch build` on Python 3.10, 3.11, 3.12.
- Release: tag prefix `cli-v*` dispatches `cli-python-cd.yml`, publishing to PyPI over OIDC.
-1
View File
@@ -1 +0,0 @@
AGENTS.md
+8 -2
View File
@@ -241,6 +241,14 @@ Verify your API connection and display the current project.
mem0 status mem0 status
``` ```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode ## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops: 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 | | `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format | | `-o, --output` | Set the output format |
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
## Environment variables ## Environment variables
| Variable | Description | | Variable | Description |
+1 -1
View File
@@ -80,7 +80,7 @@ mem0 --help
# Using Python directly (with venv activated) # Using Python directly (with venv activated)
source .venv/bin/activate source .venv/bin/activate
mem0 --help mem0 --help
mem0 --version mem0 version
# Or run without activating # Or run without activating
.venv/bin/mem0 --help .venv/bin/mem0 --help
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mem0-cli" name = "mem0-cli"
version = "0.2.11" version = "0.2.5"
description = "The official CLI for mem0 — the memory layer for AI agents" description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md" readme = "README.md"
license = "Apache-2.0" license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer.""" """mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.11" __version__ = "0.2.4"
+6 -155
View File
@@ -273,26 +273,7 @@ def add(
no_infer: bool = typer.Option(False, "--no-infer", help="Skip inference, store raw."), 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)."), expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
categories: str | None = typer.Option( categories: str | None = typer.Option(
None, "--categories", help="Not supported on add, use --custom-categories instead." None, "--categories", help="Categories (JSON array or comma-separated)."
),
custom_instructions: str | None = typer.Option(
None, "--custom-instructions", help="Custom instructions for fact extraction."
),
agent_custom_instructions: str | None = typer.Option(
None,
"--agent-custom-instructions",
help="Extraction instructions for agent-scoped memories, overriding the project setting.",
),
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."
), ),
output: str = typer.Option( output: str = typer.Option(
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output" "text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
@@ -331,11 +312,6 @@ def add(
no_infer=no_infer, no_infer=no_infer,
expires=expires, expires=expires,
categories=categories, categories=categories,
custom_instructions=custom_instructions,
agent_custom_instructions=agent_custom_instructions,
custom_categories=custom_categories,
structured_data_schema=structured_data_schema,
timestamp=timestamp,
output=output, output=output,
) )
@@ -379,21 +355,6 @@ def search(
help="Specific fields to return (comma-separated).", help="Specific fields to return (comma-separated).",
rich_help_panel="Search", 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( output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" "text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
), ),
@@ -437,9 +398,6 @@ def search(
keyword=keyword, keyword=keyword,
filter_json=filter_json, filter_json=filter_json,
fields=fields, fields=fields,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
output=output, output=output,
) )
@@ -506,15 +464,6 @@ def list_cmd(
before: str | None = typer.Option( before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters" 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( output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output" "table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
), ),
@@ -548,8 +497,6 @@ def list_cmd(
category=category, category=category,
after=after, after=after,
before=before, before=before,
show_expired=show_expired,
latest_only=latest_only,
output=output, output=output,
) )
@@ -562,10 +509,6 @@ def update(
memory_id: str = typer.Argument(..., help="Memory ID to update."), memory_id: str = typer.Argument(..., help="Memory ID to update."),
text: str | None = typer.Argument(None, help="New memory text."), text: str | None = typer.Argument(None, help="New memory text."),
metadata: str | None = typer.Option(None, "--metadata", "-m", help="Update metadata (JSON)."), 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( output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output" "text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
), ),
@@ -594,15 +537,7 @@ def update(
text = _read_stdin() text = _read_stdin()
backend = _get_backend(api_key, base_url) backend = _get_backend(api_key, base_url)
cmd_update( cmd_update(backend, memory_id, text, metadata=metadata, output=output)
backend,
memory_id,
text,
metadata=metadata,
expires=expires,
timestamp=timestamp,
output=output,
)
# ── Memory: delete ──────────────────────────────────────────────────────── # ── Memory: delete ────────────────────────────────────────────────────────
@@ -624,9 +559,6 @@ def delete(
False, "--dry-run", help="Show what would be deleted without deleting." False, "--dry-run", help="Show what would be deleted without deleting."
), ),
force: bool = typer.Option(False, "--force", help="Skip confirmation."), 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( user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope" None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
), ),
@@ -684,14 +616,7 @@ def delete(
from mem0_cli.commands.memory import cmd_delete from mem0_cli.commands.memory import cmd_delete
backend = _get_backend(api_key, base_url) backend = _get_backend(api_key, base_url)
cmd_delete( cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
backend,
memory_id,
dry_run=dry_run,
force=force,
delete_linked=delete_linked,
output=output,
)
elif all_: elif all_:
_fire_telemetry("delete", {"delete_mode": "all"}) _fire_telemetry("delete", {"delete_mode": "all"})
@@ -989,65 +914,6 @@ def identify(
run_identify(name) run_identify(name)
@app.command(name="whoami", rich_help_panel="Setup")
def whoami_cmd() -> None:
"""Print your AGENTRUSH identifier (default_user_id).
Example:
mem0 whoami
"""
from mem0_cli.commands.whoami_cmd import run_whoami
run_whoami()
# ── AGENTRUSH sub-app ─────────────────────────────────────────────────────
agent_rush_app = typer.Typer(
name="agent-rush",
help="AGENTRUSH game commands",
no_args_is_help=True,
rich_markup_mode="rich",
)
@agent_rush_app.callback(invoke_without_command=True)
def _agent_rush_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"agent-rush.{ctx.invoked_subcommand}")
@agent_rush_app.command(name="add")
def agent_rush_add(
content: str = typer.Argument(..., help="Memory content (50-1000 characters, no URLs)."),
) -> None:
"""Submit a memory to AGENTRUSH.
Example:
mem0 agent-rush add "I enjoy solving constraint-satisfaction problems."
"""
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_add
run_agent_rush_add(content)
@agent_rush_app.command(name="search")
def agent_rush_search(
query: str = typer.Argument(..., help="Search query."),
) -> None:
"""Search AGENTRUSH memories.
Example:
mem0 agent-rush search "constraint satisfaction"
"""
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_search
run_agent_rush_search(query)
app.add_typer(agent_rush_app, name="agent-rush", rich_help_panel="Setup")
# (entity_app registered at module level, below sub-group definitions) # (entity_app registered at module level, below sub-group definitions)
@@ -1143,12 +1009,7 @@ def _build_help_json() -> dict:
"--immutable": "Prevent future updates.", "--immutable": "Prevent future updates.",
"--no-infer": "Skip inference, store raw.", "--no-infer": "Skip inference, store raw.",
"--expires": "Expiration date (YYYY-MM-DD).", "--expires": "Expiration date (YYYY-MM-DD).",
"--categories": "Not supported on add, use --custom-categories instead.", "--categories": "Categories (JSON array or comma-separated).",
"--custom-instructions": "Custom instructions for fact extraction.",
"--agent-custom-instructions": "Extraction instructions for agent-scoped memories, overriding the project setting.",
"--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.",
"--graph": "Enable graph memory extraction.", "--graph": "Enable graph memory extraction.",
"--no-graph": "Disable graph memory extraction.", "--no-graph": "Disable graph memory extraction.",
"--output, -o": "Output format: text, json, quiet.", "--output, -o": "Output format: text, json, quiet.",
@@ -1167,9 +1028,6 @@ def _build_help_json() -> dict:
"--keyword": "Use keyword search instead of semantic.", "--keyword": "Use keyword search instead of semantic.",
"--filter": "Advanced filter expression (JSON).", "--filter": "Advanced filter expression (JSON).",
"--fields": "Specific fields to return (comma-separated).", "--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.", "--graph": "Enable graph in search.",
"--no-graph": "Disable graph in search.", "--no-graph": "Disable graph in search.",
"--output, -o": "Output format: text, json, table.", "--output, -o": "Output format: text, json, table.",
@@ -1193,8 +1051,6 @@ def _build_help_json() -> dict:
"--category": "Filter by category.", "--category": "Filter by category.",
"--after": "Created after (YYYY-MM-DD).", "--after": "Created after (YYYY-MM-DD).",
"--before": "Created before (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.", "--graph": "Enable graph in listing.",
"--no-graph": "Disable graph in listing.", "--no-graph": "Disable graph in listing.",
"--output, -o": "Output format: text, json, table.", "--output, -o": "Output format: text, json, table.",
@@ -1209,8 +1065,6 @@ def _build_help_json() -> dict:
}, },
"options": { "options": {
"--metadata, -m": "Update metadata (JSON).", "--metadata, -m": "Update metadata (JSON).",
"--expires": "Expiration date (YYYY-MM-DD).",
"--timestamp": "Unix timestamp for the memory.",
"--output, -o": "Output format: text, json, quiet.", "--output, -o": "Output format: text, json, quiet.",
}, },
}, },
@@ -1227,7 +1081,6 @@ def _build_help_json() -> dict:
"--all": "Delete all memories matching scope filters.", "--all": "Delete all memories matching scope filters.",
"--entity": "Delete the entity itself and all its memories (cascade).", "--entity": "Delete the entity itself and all its memories (cascade).",
"--project": "With --all: delete ALL memories project-wide.", "--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.", "--dry-run": "Show what would be deleted without deleting.",
"--force": "Skip confirmation.", "--force": "Skip confirmation.",
"--user-id, -u": "Scope to user.", "--user-id, -u": "Scope to user.",
@@ -1357,10 +1210,8 @@ def help(
mem0 help mem0 help
mem0 help --json mem0 help --json
""" """
from mem0_cli.state import is_agent_mode if json:
console.print(_json.dumps(_build_help_json(), indent=2))
if json or is_agent_mode():
console.print_json(_json.dumps(_build_help_json()))
else: else:
console.print( console.print(
f"[{BRAND_COLOR}]◆ mem0 CLI[/] v{__version__} — The Memory Layer for AI Agents\n" f"[{BRAND_COLOR}]◆ mem0 CLI[/] v{__version__} — The Memory Layer for AI Agents\n"
+2 -18
View File
@@ -25,11 +25,7 @@ class Backend(ABC):
immutable: bool = False, immutable: bool = False,
infer: bool = True, infer: bool = True,
expires: str | None = None, expires: str | None = None,
custom_instructions: str | None = None, categories: list[str] | None = None,
agent_custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
) -> dict: ... ) -> dict: ...
@abstractmethod @abstractmethod
@@ -47,9 +43,6 @@ class Backend(ABC):
keyword: bool = False, keyword: bool = False,
filters: dict | None = None, filters: dict | None = None,
fields: list[str] | None = None, fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]: ... ) -> list[dict]: ...
@abstractmethod @abstractmethod
@@ -68,19 +61,11 @@ class Backend(ABC):
category: str | None = None, category: str | None = None,
after: str | None = None, after: str | None = None,
before: str | None = None, before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]: ... ) -> list[dict]: ...
@abstractmethod @abstractmethod
def update( def update(
self, self, memory_id: str, content: str | None = None, metadata: dict | None = None
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
) -> dict: ... ) -> dict: ...
@abstractmethod @abstractmethod
@@ -93,7 +78,6 @@ class Backend(ABC):
agent_id: str | None = None, agent_id: str | None = None,
app_id: str | None = None, app_id: str | None = None,
run_id: str | None = None, run_id: str | None = None,
delete_linked: bool = False,
) -> dict: ... ) -> dict: ...
@abstractmethod @abstractmethod
+13 -75
View File
@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from urllib.parse import quote
import httpx import httpx
@@ -12,10 +11,6 @@ from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig from mem0_cli.config import PlatformConfig
def _encode_path_segment(value: Any) -> str:
return quote(str(value), safe="")
class PlatformBackend(Backend): class PlatformBackend(Backend):
"""Backend that talks to the mem0 Platform API.""" """Backend that talks to the mem0 Platform API."""
@@ -87,11 +82,7 @@ class PlatformBackend(Backend):
immutable: bool = False, immutable: bool = False,
infer: bool = True, infer: bool = True,
expires: str | None = None, expires: str | None = None,
custom_instructions: str | None = None, categories: list[str] | None = None,
agent_custom_instructions: str | None = None,
custom_categories: list[dict] | None = None,
structured_data_schema: dict | None = None,
timestamp: int | None = None,
) -> dict: ) -> dict:
payload: dict[str, Any] = {} payload: dict[str, Any] = {}
@@ -116,16 +107,8 @@ class PlatformBackend(Backend):
payload["infer"] = False payload["infer"] = False
if expires: if expires:
payload["expiration_date"] = expires payload["expiration_date"] = expires
if custom_instructions: if categories:
payload["custom_instructions"] = custom_instructions payload["categories"] = categories
if agent_custom_instructions:
payload["agent_custom_instructions"] = agent_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
payload["source"] = "CLI" payload["source"] = "CLI"
return self._request("POST", "/v3/memories/add/", json=payload) return self._request("POST", "/v3/memories/add/", json=payload)
@@ -185,9 +168,6 @@ class PlatformBackend(Backend):
keyword: bool = False, keyword: bool = False,
filters: dict | None = None, filters: dict | None = None,
fields: list[str] | None = None, fields: list[str] | None = None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
) -> list[dict]: ) -> list[dict]:
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold} payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
@@ -206,12 +186,6 @@ class PlatformBackend(Backend):
payload["keyword_search"] = True payload["keyword_search"] = True
if fields: if fields:
payload["fields"] = 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" payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/search/", json=payload) result = self._request("POST", "/v3/memories/search/", json=payload)
@@ -222,11 +196,7 @@ class PlatformBackend(Backend):
) )
def get(self, memory_id: str) -> dict: def get(self, memory_id: str) -> dict:
return self._request( return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
"GET",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
def list_memories( def list_memories(
self, self,
@@ -240,8 +210,6 @@ class PlatformBackend(Backend):
category: str | None = None, category: str | None = None,
after: str | None = None, after: str | None = None,
before: str | None = None, before: str | None = None,
show_expired: bool = False,
latest_only: bool = False,
) -> list[dict]: ) -> list[dict]:
payload: dict[str, Any] = {} payload: dict[str, Any] = {}
params = {"page": str(page), "page_size": str(page_size)} params = {"page": str(page), "page_size": str(page_size)}
@@ -264,10 +232,6 @@ class PlatformBackend(Backend):
) )
if api_filters: if api_filters:
payload["filters"] = api_filters payload["filters"] = api_filters
if show_expired:
payload["show_expired"] = True
if latest_only:
payload["latest_only"] = True
payload["source"] = "CLI" payload["source"] = "CLI"
result = self._request("POST", "/v3/memories/", json=payload, params=params) result = self._request("POST", "/v3/memories/", json=payload, params=params)
@@ -278,29 +242,15 @@ class PlatformBackend(Backend):
) )
def update( def update(
self, self, memory_id: str, content: str | None = None, metadata: dict | None = None
memory_id: str,
content: str | None = None,
metadata: dict | None = None,
*,
expiration_date: str | None = None,
timestamp: int | None = None,
) -> dict: ) -> dict:
payload: dict[str, Any] = {} payload: dict[str, Any] = {}
if content: if content:
payload["text"] = content payload["text"] = content
if metadata: if metadata:
payload["metadata"] = metadata payload["metadata"] = metadata
if expiration_date:
payload["expiration_date"] = expiration_date
if timestamp is not None:
payload["timestamp"] = timestamp
payload["source"] = "CLI" payload["source"] = "CLI"
return self._request( return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
"PUT",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
json=payload,
)
def delete( def delete(
self, self,
@@ -311,7 +261,6 @@ class PlatformBackend(Backend):
agent_id: str | None = None, agent_id: str | None = None,
app_id: str | None = None, app_id: str | None = None,
run_id: str | None = None, run_id: str | None = None,
delete_linked: bool = False,
) -> dict: ) -> dict:
if all: if all:
params: dict[str, str] = {"source": "CLI"} params: dict[str, str] = {"source": "CLI"}
@@ -325,14 +274,7 @@ class PlatformBackend(Backend):
params["run_id"] = run_id params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params) return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id: elif memory_id:
params = {"source": "CLI"} return self._request("DELETE", f"/v1/memories/{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,
)
else: else:
raise ValueError("Either memory_id or --all is required") raise ValueError("Either memory_id or --all is required")
@@ -354,17 +296,13 @@ class PlatformBackend(Backend):
entities = {t: v for t, v in type_map.items() if v} entities = {t: v for t, v in type_map.items() if v}
if not entities: if not entities:
raise ValueError("At least one entity ID is required for delete_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 # Delete each provided entity via the v2 path-based endpoint
# response by entity type so a multi-entity delete (e.g. --user-id and result: dict = {}
# --agent-id together) doesn't discard everything but the last result.
results: dict = {}
for entity_type, entity_id in entities.items(): for entity_type, entity_id in entities.items():
results[entity_type] = self._request( result = self._request(
"DELETE", "DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
params={"source": "CLI"},
) )
return results return result
def ping(self, timeout: float | None = None) -> dict: def ping(self, timeout: float | None = None) -> dict:
"""Call the ping endpoint and return the raw response. """Call the ping endpoint and return the raw response.
@@ -408,7 +346,7 @@ class PlatformBackend(Backend):
return result if isinstance(result, list) else result.get("results", []) return result if isinstance(result, list) else result.get("results", [])
def get_event(self, event_id: str) -> dict: 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): class AuthError(Exception):
@@ -218,7 +218,7 @@ def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) ->
print_error(err_console, f"Claim failed: {detail}") print_error(err_console, f"Claim failed: {detail}")
if code_str == "email_already_claimed": if code_str == "email_already_claimed":
console.print( console.print(
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.[/]" f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in there and run `mem0 link <key>` to attach this agent.[/]"
) )
raise typer.Exit(1) raise typer.Exit(1)
@@ -1,132 +0,0 @@
"""mem0 agent-rush — AGENTRUSH game commands.
Wraps the platform's /v1/agent-rush/{memories/, memories/search/} endpoints.
Hardcoded routing; no flags needed.
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone
import httpx
import typer
from rich.console import Console
from mem0_cli.branding import print_error, print_success
from mem0_cli.config import load_config, save_config
console = Console()
err_console = Console(stderr=True)
_PII_WARNING_LINES = (
"",
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
"",
)
_SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Mode": "agent-rush",
}
_ERROR_HINTS = {
"agentrush_search_first": "Run 3 'mem0 agent-rush search' commands before adding.",
"agentrush_search_quota": "You've used your 3 lifetime searches.",
"agentrush_add_quota": "You've used your 3 lifetime adds.",
"agentrush_not_agent_mode": "Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
"agentrush_length": "Memory text must be 50-1000 characters.",
"agentrush_no_urls": "URLs are not allowed.",
"agentrush_blocklist": "Content contains a blocked term.",
"agentrush_global_quota": "Event-wide cap reached. Try again later.",
"agentrush_not_provisioned": "AGENTRUSH is not provisioned in this environment.",
}
def _call(path: str, body: dict) -> dict:
config = load_config()
if not config.platform.api_key:
print_error(err_console, "Not initialized. Run `mem0 init --agent` first.")
raise typer.Exit(1)
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
try:
with httpx.Client(timeout=30.0) as client:
resp = client.post(
f"{base_url}{path}",
headers={
**_SOURCE_HEADERS,
"Authorization": f"Token {config.platform.api_key}",
"Content-Type": "application/json",
},
json=body,
)
except httpx.HTTPError as exc:
print_error(err_console, f"Network error: {exc}")
raise typer.Exit(1) from exc
try:
data = resp.json()
except Exception:
data = {}
if resp.status_code >= 400:
code = (
(data.get("error") or {}).get("code", "unknown")
if isinstance(data, dict)
else "unknown"
)
print_error(err_console, f"AGENTRUSH error: {code}")
hint = _ERROR_HINTS.get(code)
if hint:
console.print(f" [dim]{hint}[/dim]")
raise typer.Exit(1)
return data
def _ensure_warning_acknowledged() -> None:
"""Block the first interactive add on the PII warning; pass-through for agents.
Interactive (TTY): show prompt, require explicit 'y', persist
`agent_rush.acknowledged_at` so we never ask the same machine twice.
Non-interactive (no TTY — typical when an agent runs the CLI): surface
the warning to stderr for the human reading the agent transcript and
proceed without prompting (agents can't answer y/N).
"""
config = load_config()
if config.agent_rush.acknowledged_at:
return
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
if not is_tty:
for line in _PII_WARNING_LINES:
err_console.print(line)
return
for line in _PII_WARNING_LINES:
console.print(line)
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
if answer not in ("y", "yes"):
print_error(err_console, "Aborted.")
raise typer.Exit(1)
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
save_config(config)
def run_agent_rush_add(content: str) -> None:
_ensure_warning_acknowledged()
result = _call("/v1/agent-rush/memories/", {"content": content})
event_id = result.get("event_id", "?")
print_success(console, f"Memory submitted (event_id: {event_id})")
def run_agent_rush_search(query: str) -> None:
result = _call("/v1/agent-rush/memories/search/", {"query": query})
memories = result.get("results") or result.get("memories") or []
if not memories:
console.print("[dim](no results)[/dim]")
return
for i, m in enumerate(memories[:5], start=1):
text = m.get("memory") if isinstance(m, dict) else str(m)
console.print(f" {i}. {text}")
+20 -69
View File
@@ -4,11 +4,9 @@ from __future__ import annotations
import json import json
import os import os
import re
import stat as _stat_mod import stat as _stat_mod
import sys import sys
import time as _time import time as _time
from datetime import date
from pathlib import Path from pathlib import Path
import typer import typer
@@ -49,18 +47,6 @@ def _stdin_is_piped() -> bool:
return False 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( def cmd_add(
backend: Backend, backend: Backend,
text: str | None, text: str | None,
@@ -76,11 +62,6 @@ def cmd_add(
no_infer: bool, no_infer: bool,
expires: str | None, expires: str | None,
categories: str | None, categories: str | None,
custom_instructions: str | None = None,
agent_custom_instructions: str | None = None,
custom_categories: str | None = None,
structured_data_schema: str | None = None,
timestamp: int | None = None,
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Add a memory.""" """Add a memory."""
@@ -89,13 +70,6 @@ def cmd_add(
set_current_command("add") set_current_command("add")
if is_agent_mode(): if is_agent_mode():
output = "agent" output = "agent"
if categories:
print_error(
err_console, "--categories is not supported on add. Use --custom-categories instead."
)
raise typer.Exit(1)
msgs = None msgs = None
content = text content = text
@@ -134,24 +108,27 @@ def cmd_add(
print_error(err_console, "Invalid JSON in --metadata.") print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None raise typer.Exit(1) from None
custom_cats = None cats = None
if custom_categories: if categories:
try: try:
custom_cats = json.loads(custom_categories) cats = json.loads(categories)
except json.JSONDecodeError: except json.JSONDecodeError:
print_error(err_console, "Invalid JSON in --custom-categories.") cats = [c.strip() for c in categories.split(",")]
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
# Validate --expires
if 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: with timed_status(err_console, "Adding memory...") as ts:
try: try:
@@ -166,11 +143,7 @@ def cmd_add(
immutable=immutable, immutable=immutable,
infer=not no_infer, infer=not no_infer,
expires=expires, expires=expires,
custom_instructions=custom_instructions, categories=cats,
agent_custom_instructions=agent_custom_instructions,
custom_categories=custom_cats,
structured_data_schema=schema,
timestamp=timestamp,
) )
except Exception as e: except Exception as e:
ts.error_msg = str(e) ts.error_msg = str(e)
@@ -251,9 +224,6 @@ def cmd_search(
keyword: bool, keyword: bool,
filter_json: str | None, filter_json: str | None,
fields: str | None, fields: str | None,
show_expired: bool = False,
reference_date: str | None = None,
latest_only: bool = False,
output: str = "text", output: str = "text",
) -> None: ) -> None:
"""Search memories.""" """Search memories."""
@@ -296,9 +266,6 @@ def cmd_search(
keyword=keyword, keyword=keyword,
filters=filters, filters=filters,
fields=field_list, fields=field_list,
show_expired=show_expired,
reference_date=reference_date,
latest_only=latest_only,
) )
except Exception as e: except Exception as e:
print_error(err_console, str(e)) print_error(err_console, str(e))
@@ -385,8 +352,6 @@ def cmd_list(
category: str | None, category: str | None,
after: str | None, after: str | None,
before: str | None, before: str | None,
show_expired: bool = False,
latest_only: bool = False,
output: str = "table", output: str = "table",
) -> None: ) -> None:
"""List memories.""" """List memories."""
@@ -415,8 +380,6 @@ def cmd_list(
category=category, category=category,
after=after, after=after,
before=before, before=before,
show_expired=show_expired,
latest_only=latest_only,
) )
except Exception as e: except Exception as e:
print_error(err_console, str(e)) print_error(err_console, str(e))
@@ -483,8 +446,6 @@ def cmd_update(
text: str | None, text: str | None,
*, *,
metadata: str | None, metadata: str | None,
expires: str | None = None,
timestamp: int | None = None,
output: str, output: str,
) -> None: ) -> None:
"""Update a memory.""" """Update a memory."""
@@ -501,19 +462,10 @@ def cmd_update(
print_error(err_console, "Invalid JSON in --metadata.") print_error(err_console, "Invalid JSON in --metadata.")
raise typer.Exit(1) from None raise typer.Exit(1) from None
if expires:
_validate_expires(expires)
_start = _time.perf_counter() _start = _time.perf_counter()
with timed_status(err_console, "Updating memory...") as _ts: with timed_status(err_console, "Updating memory...") as _ts:
try: try:
result = backend.update( result = backend.update(memory_id, content=text, metadata=meta)
memory_id,
content=text,
metadata=meta,
expiration_date=expires,
timestamp=timestamp,
)
except Exception as e: except Exception as e:
print_error(err_console, str(e)) print_error(err_console, str(e))
raise typer.Exit(1) from None raise typer.Exit(1) from None
@@ -538,7 +490,6 @@ def cmd_delete(
*, *,
dry_run: bool = False, dry_run: bool = False,
force: bool = False, force: bool = False,
delete_linked: bool = False,
output: str, output: str,
) -> None: ) -> None:
"""Delete a single memory by ID.""" """Delete a single memory by ID."""
@@ -561,7 +512,7 @@ def cmd_delete(
_start = _time.perf_counter() _start = _time.perf_counter()
with timed_status(err_console, "Deleting...") as _ts: with timed_status(err_console, "Deleting...") as _ts:
try: try:
result = backend.delete(memory_id=memory_id, delete_linked=delete_linked) result = backend.delete(memory_id=memory_id)
except Exception as e: except Exception as e:
print_error(err_console, str(e)) print_error(err_console, str(e))
raise typer.Exit(1) from None raise typer.Exit(1) from None
@@ -1,25 +0,0 @@
"""mem0 whoami — print the active agent's default_user_id (AGENTRUSH identifier)."""
from __future__ import annotations
import typer
from rich.console import Console
from mem0_cli.branding import BRAND_COLOR, print_error, print_info
from mem0_cli.config import load_config
console = Console()
err_console = Console(stderr=True)
def run_whoami() -> None:
config = load_config()
session_id = config.platform.default_user_id if config.platform else None
if not session_id:
print_error(
err_console,
"No default_user_id found. Run `mem0 init --agent` first.",
)
raise typer.Exit(1)
console.print(f"Your AGENTRUSH identifier: [{BRAND_COLOR}]{session_id}[/{BRAND_COLOR}]")
print_info(console, "Find your row at https://mem0.ai/agentrush")
+1 -18
View File
@@ -51,20 +51,12 @@ class TelemetryConfig:
anonymous_id: str = "" anonymous_id: str = ""
@dataclass
class AgentRushConfig:
# ISO timestamp the human acknowledged the "memories are public" warning.
# Empty until first interactive `mem0 agent-rush add`.
acknowledged_at: str = ""
@dataclass @dataclass
class Mem0Config: class Mem0Config:
version: int = CONFIG_VERSION version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig) defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig) platform: PlatformConfig = field(default_factory=PlatformConfig)
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig) telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
SHORT_KEY_ALIASES: dict[str, str] = { SHORT_KEY_ALIASES: dict[str, str] = {
@@ -113,9 +105,6 @@ def load_config() -> Mem0Config:
telemetry = data.get("telemetry", {}) telemetry = data.get("telemetry", {})
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "") config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
agent_rush = data.get("agent_rush", {})
config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "")
# Environment variable overrides # Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY") env_key = os.environ.get("MEM0_API_KEY")
if env_key: if env_key:
@@ -169,9 +158,6 @@ def save_config(config: Mem0Config) -> None:
"telemetry": { "telemetry": {
"anonymous_id": config.telemetry.anonymous_id, "anonymous_id": config.telemetry.anonymous_id,
}, },
"agent_rush": {
"acknowledged_at": config.agent_rush.acknowledged_at,
},
} }
with open(CONFIG_FILE, "w") as f: with open(CONFIG_FILE, "w") as f:
@@ -235,10 +221,7 @@ def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
if isinstance(current, bool): if isinstance(current, bool):
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment] value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
elif isinstance(current, int): elif isinstance(current, int):
try: value = int(value) # type: ignore[assignment]
value = int(value) # type: ignore[assignment]
except ValueError:
return False
setattr(obj, final_key, value) setattr(obj, final_key, value)
return True 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") console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
for i, mem in enumerate(memories, 1): for i, mem in enumerate(memories, 1):
memory_text = mem.get("memory") or mem.get("text") or "" memory_text = mem.get("memory", mem.get("text", ""))
mem_id = (mem.get("id") or "")[:8] mem_id = mem.get("id", "")[:8]
score = mem.get("score") score = mem.get("score")
created = _format_date(mem.get("created_at")) created = _format_date(mem.get("created_at"))
category = mem.get("categories", [None]) category = mem.get("categories", [None])
@@ -67,8 +67,8 @@ def format_memories_table(
table.add_column("Created", max_width=12) table.add_column("Created", max_width=12)
for mem in memories: for mem in memories:
mem_id = mem.get("id") or "" mem_id = mem.get("id", "")
memory_text = mem.get("memory") or mem.get("text") or "" memory_text = mem.get("memory", mem.get("text", ""))
if len(memory_text) > 60: if len(memory_text) > 60:
memory_text = memory_text[:57] + "..." memory_text = memory_text[:57] + "..."
categories = mem.get("categories", []) categories = mem.get("categories", [])
@@ -104,8 +104,8 @@ def format_single_memory(console: Console, mem: dict, output: str = "text") -> N
format_json(console, mem) format_json(console, mem)
return return
memory_text = mem.get("memory") or mem.get("text") or "" memory_text = mem.get("memory", mem.get("text", ""))
mem_id = mem.get("id") or "" mem_id = mem.get("id", "")
lines = [] lines = []
lines.append(f" [white bold]{memory_text}[/]") lines.append(f" [white bold]{memory_text}[/]")
@@ -262,32 +262,16 @@ def sanitize_agent_data(command: str, data: Any) -> Any:
return result return result
if command == "search": if command == "search":
return [ return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
pick(r, ["id", "memory", "score", "created_at", "categories", "expiration_date"])
for r in data
]
if command == "list": if command == "list":
return [ return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
pick(r, ["id", "memory", "created_at", "categories", "expiration_date"]) for r in data
]
if command == "get": if command == "get":
return pick( return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
data,
[
"id",
"memory",
"created_at",
"updated_at",
"categories",
"metadata",
"expiration_date",
],
)
if command == "update": if command == "update":
return pick(data, ["id", "memory", "expiration_date"]) return pick(data, ["id", "memory"])
if command in ("delete", "delete-all", "entity delete"): if command in ("delete", "delete-all", "entity delete"):
return data return data
+2 -9
View File
@@ -137,19 +137,12 @@ def capture_event(
"anon_distinct_id_to_alias": anon_id_to_alias, "anon_distinct_id_to_alias": anon_id_to_alias,
} }
child = subprocess.Popen( subprocess.Popen(
[sys.executable, "-m", "mem0_cli.telemetry_sender"], [sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
start_new_session=True, start_new_session=True,
close_fds=True, close_fds=True,
text=True,
) )
if child.stdin:
with contextlib.suppress(Exception):
child.stdin.write(json.dumps(context))
with contextlib.suppress(Exception):
child.stdin.close()
except Exception: except Exception:
pass pass
+2 -13
View File
@@ -1,7 +1,6 @@
"""Standalone telemetry sender — runs as a detached subprocess. """Standalone telemetry sender — runs as a detached subprocess.
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin; Usage: python -m mem0_cli.telemetry_sender '<json context>'
a single argv argument is still accepted as a legacy fallback)
This module is spawned by telemetry.capture_event() and runs independently This module is spawned by telemetry.capture_event() and runs independently
of the parent CLI process. It: of the parent CLI process. It:
@@ -21,18 +20,8 @@ import sys
import urllib.request import urllib.request
def _load_context() -> dict:
"""Load telemetry context from stdin, falling back to argv for compatibility."""
raw = ""
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if not raw and len(sys.argv) > 1:
raw = sys.argv[1]
return json.loads(raw)
def main() -> None: def main() -> None:
ctx = _load_context() ctx = json.loads(sys.argv[1])
payload = ctx["payload"] payload = ctx["payload"]
if ctx.get("needs_email") and ctx.get("mem0_api_key"): if ctx.get("needs_email") and ctx.get("mem0_api_key"):
+3 -6
View File
@@ -32,13 +32,12 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
if key.startswith("MEM0_"): if key.startswith("MEM0_"):
del env[key] del env[key]
env.pop("FORCE_COLOR", None) env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir: if home_dir:
env["HOME"] = home_dir env["HOME"] = home_dir
result = subprocess.run( result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args], [sys.executable, "-m", "mem0_cli", *args],
capture_output=True, capture_output=True,
encoding="utf-8", text=True,
env=env, env=env,
timeout=15, timeout=15,
) )
@@ -100,13 +99,12 @@ class TestArgvPreprocessing:
result = subprocess.run( result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent"], [sys.executable, "-m", "mem0_cli", "init", "--agent"],
capture_output=True, capture_output=True,
encoding="utf-8", text=True,
env={ env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")}, **{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home, "HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole "MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
"FORCE_COLOR": "0", "FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
}, },
timeout=15, timeout=15,
) )
@@ -135,13 +133,12 @@ class TestJsonEnvelopeParity:
result = subprocess.run( result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"], [sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
capture_output=True, capture_output=True,
encoding="utf-8", text=True,
env={ env={
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")}, **{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
"HOME": clean_home, "HOME": clean_home,
"MEM0_BASE_URL": "http://127.0.0.1:1", "MEM0_BASE_URL": "http://127.0.0.1:1",
"FORCE_COLOR": "0", "FORCE_COLOR": "0",
"PYTHONIOENCODING": "utf-8",
}, },
timeout=15, timeout=15,
) )
+1 -28
View File
@@ -7,7 +7,6 @@ boundaries).
from __future__ import annotations from __future__ import annotations
import json
import os import os
import re import re
import subprocess import subprocess
@@ -50,7 +49,6 @@ def _run(
if key.startswith("MEM0_"): if key.startswith("MEM0_"):
del env[key] del env[key]
env.pop("FORCE_COLOR", None) env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir: if home_dir:
env["HOME"] = home_dir env["HOME"] = home_dir
if env_override: if env_override:
@@ -58,7 +56,7 @@ def _run(
result = subprocess.run( result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args], [sys.executable, "-m", "mem0_cli", *args],
capture_output=True, capture_output=True,
encoding="utf-8", text=True,
env=env, env=env,
) )
return subprocess.CompletedProcess( return subprocess.CompletedProcess(
@@ -85,31 +83,6 @@ class TestCLIIntegration:
assert "add" in result.stdout assert "add" in result.stdout
assert "search" 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): def test_add_help(self):
result = _run(["add", "--help"]) result = _run(["add", "--help"])
assert result.returncode == 0 assert result.returncode == 0
+10 -184
View File
@@ -8,8 +8,8 @@ from io import StringIO
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from click.exceptions import Exit as ClickExit
from rich.console import Console from rich.console import Console
from typer import Exit as TyperExit
from mem0_cli.commands.config_cmd import ( from mem0_cli.commands.config_cmd import (
cmd_config_get, cmd_config_get,
@@ -181,7 +181,7 @@ class TestAddCommand:
patch("mem0_cli.commands.memory.console", console), patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console), patch("mem0_cli.commands.memory.err_console", err_console),
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False), patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_add( cmd_add(
mock_backend, mock_backend,
@@ -206,7 +206,7 @@ class TestAddCommand:
with ( with (
patch("mem0_cli.commands.memory.console", console), patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console), patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_add( cmd_add(
mock_backend, mock_backend,
@@ -252,13 +252,12 @@ class TestAddCommand:
) )
mock_backend.add.assert_called_once() 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() console, _buf = _make_console()
err_console, err_buf = _make_err_console() err_console, _err_buf = _make_err_console()
with ( with (
patch("mem0_cli.commands.memory.console", console), patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console), patch("mem0_cli.commands.memory.err_console", err_console),
pytest.raises((SystemExit, TyperExit)),
): ):
cmd_add( cmd_add(
mock_backend, mock_backend,
@@ -276,93 +275,7 @@ class TestAddCommand:
categories="health,prefs", categories="health,prefs",
output="text", output="text",
) )
assert "--custom-categories" in err_buf.getvalue() mock_backend.add.assert_called_once()
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"}]
class TestAddDeduplicatesPending: class TestAddDeduplicatesPending:
@@ -551,36 +464,6 @@ class TestSearchCommand:
) )
mock_backend.search.assert_called_once() 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: class TestGetCommand:
def test_get_text(self, mock_backend): def test_get_text(self, mock_backend):
@@ -678,32 +561,6 @@ class TestListCommand:
output = buf.getvalue() output = buf.getvalue()
assert "No memories found" in output 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: class TestUpdateCommand:
def test_update(self, mock_backend): def test_update(self, mock_backend):
@@ -728,26 +585,6 @@ class TestUpdateCommand:
output = buf.getvalue() output = buf.getvalue()
assert '"memory"' in output 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: class TestDeleteCommand:
def test_delete_single(self, mock_backend): def test_delete_single(self, mock_backend):
@@ -773,17 +610,6 @@ class TestDeleteCommand:
assert "dry run" in output.lower() assert "dry run" in output.lower()
mock_backend.delete.assert_not_called() 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: class TestDeleteAllCommand:
def test_delete_all_force(self, mock_backend): def test_delete_all_force(self, mock_backend):
@@ -938,7 +764,7 @@ class TestImportCommand:
with ( with (
patch("mem0_cli.commands.utils.console", console), patch("mem0_cli.commands.utils.console", console),
patch("mem0_cli.commands.utils.err_console", err_console), patch("mem0_cli.commands.utils.err_console", err_console),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None) cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
@@ -975,7 +801,7 @@ class TestEntitiesListCommand:
with ( with (
patch("mem0_cli.commands.entities.console", console), patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console), patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_entities_list(mock_backend, "invalid", output="table") cmd_entities_list(mock_backend, "invalid", output="table")
@@ -1118,7 +944,7 @@ class TestEntitiesDeleteCommand:
with ( with (
patch("mem0_cli.commands.entities.console", console), patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console), patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_entities_delete( cmd_entities_delete(
mock_backend, mock_backend,
@@ -1482,7 +1308,7 @@ class TestAgentMode:
patch("mem0_cli.commands.memory.console", console), patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console), patch("mem0_cli.commands.memory.err_console", err_console),
patch("sys.stdout", captured_stdout), patch("sys.stdout", captured_stdout),
pytest.raises((SystemExit, TyperExit)), pytest.raises((SystemExit, ClickExit)),
): ):
cmd_get(mock_backend, "bad-id", output="text") cmd_get(mock_backend, "bad-id", output="text")
+1 -7
View File
@@ -67,8 +67,7 @@ class TestConfig:
from mem0_cli.config import CONFIG_FILE from mem0_cli.config import CONFIG_FILE
mode = os.stat(CONFIG_FILE).st_mode & 0o777 mode = os.stat(CONFIG_FILE).st_mode & 0o777
if os.name != "nt": assert mode == 0o600
assert mode == 0o600
def test_defaults_save_and_load(self, isolate_config): def test_defaults_save_and_load(self, isolate_config):
config = Mem0Config() config = Mem0Config()
@@ -139,11 +138,6 @@ class TestNestedAccess:
assert set_nested_value(config, "platform.api_key", "new-key") assert set_nested_value(config, "platform.api_key", "new-key")
assert 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): def test_set_nonexistent_key(self):
config = Mem0Config() config = Mem0Config()
assert set_nested_value(config, "nonexistent.key", "val") is False assert set_nested_value(config, "nonexistent.key", "val") is False
-103
View File
@@ -1,103 +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"],
"agent_custom_instructions": ["agent_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() output = buf.getvalue()
assert "Found 0" in output 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: class TestTableFormat:
def test_format_memories_table(self): def test_format_memories_table(self):
@@ -75,13 +70,6 @@ class TestTableFormat:
# Should still render (empty table) # Should still render (empty table)
assert "ID" in output 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: class TestSingleMemory:
def test_format_single_memory_text(self): def test_format_single_memory_text(self):
@@ -99,17 +87,6 @@ class TestSingleMemory:
output = buf.getvalue() output = buf.getvalue()
assert '"memory"' in output 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: class TestAddResult:
def test_format_add_result_text(self): 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,112 +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.",
agent_custom_instructions="Extract only tool outcomes.",
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["agent_custom_instructions"] == "Extract only tool outcomes."
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 "agent_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/",
]

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