Compare commits
308 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1112be3e5e | |||
| 6052252250 | |||
| deca4bd3a5 | |||
| 4cfcd0241a | |||
| b54710a3c3 | |||
| 4cfa98f626 | |||
| b830b99abe | |||
| 45208feebb | |||
| 3ac9ba4b50 | |||
| 965140eb19 | |||
| 6e6f5b8d59 | |||
| dd54e387de | |||
| fd32b980b4 | |||
| 21aae599be | |||
| ea6fd3b457 | |||
| 8ca9a0f2c0 | |||
| 5f77d86caf | |||
| 9ef409222f | |||
| c90bdbdce0 | |||
| 50bdaaea0c | |||
| 38e47ac261 | |||
| d06ea1875c | |||
| c2c3a12838 | |||
| 07e58c54ae | |||
| cf3355e2ca | |||
| 54328ffd97 | |||
| 29fa41558c | |||
| 8d45fb3c9a | |||
| 760dca6f39 | |||
| 74f6dc6f0d | |||
| bcca72e3f0 | |||
| 9c2d6222ce | |||
| 790e190486 | |||
| d4869d24ec | |||
| 1ac3aa7256 | |||
| e168d48e04 | |||
| ea2ee07586 | |||
| 540d23d610 | |||
| 3274390f82 | |||
| b357a5a1b0 | |||
| d653b63fac | |||
| cc4671579f | |||
| 01afdde3e7 | |||
| d6d89c987b | |||
| c2150e8f1a | |||
| 19c7bb84a2 | |||
| e6281ab724 | |||
| a71d7bdbe3 | |||
| 56ec7d20f1 | |||
| ca2abca2b8 | |||
| 0e582adc6c | |||
| 9caffeaa7b | |||
| a58e0586ad | |||
| a9cb4bb644 | |||
| 7bf84b8d38 | |||
| 5e7adc4d12 | |||
| 14393b5962 | |||
| f590c9596a | |||
| dd5f7e39a8 | |||
| 70ab76a053 | |||
| 2af3a72f73 | |||
| 8005e18aec | |||
| 39551145b8 | |||
| c2bc28e589 | |||
| fec2fe6a2c | |||
| d0c23a5950 | |||
| b05cce581b | |||
| 8a57967c8a | |||
| 756b0b1b6d | |||
| 726bcc80b2 | |||
| 9383e9a255 | |||
| ddaa655edf | |||
| 739534c0a3 | |||
| 633b035342 | |||
| ccbe5861a1 | |||
| 50c3cf44f1 | |||
| d6d2588ef5 | |||
| 6c1741e3a4 | |||
| 42cf18c4e6 | |||
| d89793b666 | |||
| 17836748d7 | |||
| c9af55986e | |||
| f69f8dcc7b | |||
| 28e4d819f8 | |||
| 1d383c4ee2 | |||
| 8488abe603 | |||
| 49863e9a7a | |||
| 770ce97bd9 | |||
| 44bcfbe1f3 | |||
| 33a0ed7559 | |||
| ba9054e0c8 | |||
| df9d5cc4b1 | |||
| 3b2357bfe0 | |||
| 573b20cec8 | |||
| a781800d3f | |||
| 4470803fe5 | |||
| 6a801bfe2f | |||
| 2a4aa232b2 | |||
| 99206f0c64 | |||
| 5dbf071356 | |||
| b26469e006 | |||
| e72ae96ad4 | |||
| fbdbab805d | |||
| 22f70d50e1 | |||
| f89edb45dc | |||
| 846f25bd39 | |||
| 9b04509433 | |||
| 803ff13bb8 | |||
| 94e46526bc | |||
| d122479687 | |||
| cc52f0e367 | |||
| 87276ef968 | |||
| 002fe46ab3 | |||
| 9d36b2c94d | |||
| c944bed460 | |||
| 2cc060fd76 | |||
| 2bc2f763d9 | |||
| 7fb3feb5cd | |||
| fec7cdf118 | |||
| 03b41ab00f | |||
| 4c974c8fa8 | |||
| b0bee551cb | |||
| b8141aaea8 | |||
| a7ecf781cd | |||
| 6dc4606dcf | |||
| 580d390e4d | |||
| 2bd3ff1eff | |||
| cd79fa8914 | |||
| 59484f066f | |||
| 8a5c0729e5 | |||
| 3b9aed866a | |||
| fb2593e10d | |||
| 207f65deda | |||
| f2532f072f | |||
| 41c8f00851 | |||
| a36a392cd3 | |||
| 152d1e66f7 | |||
| bc05fd9623 | |||
| ad7e09851c | |||
| c325bd3b8e | |||
| 2add7fd57d | |||
| b2ff3aeda5 | |||
| 754034abbc | |||
| bedf862d64 | |||
| cc59d122db | |||
| 3619fd77ae | |||
| 2dcb3542f8 | |||
| 31cec11a79 | |||
| 4c0ea22d31 | |||
| f59320df65 | |||
| ad57cbb8d6 | |||
| ee0c38e081 | |||
| d5b64ccec9 | |||
| 8d6b7c1d67 | |||
| b44ce4dcc3 | |||
| e9c930c430 | |||
| 4b39d01ccb | |||
| 49061718bd | |||
| 7e7682a06d | |||
| f38608fb50 | |||
| fb11cdffbb | |||
| ee600705c2 | |||
| f4ccef5157 | |||
| 08da741a31 | |||
| e4efdd2e29 | |||
| a87c9ce367 | |||
| d258b638ef | |||
| bbbfcfea07 | |||
| fbef369b91 | |||
| a7ed68e697 | |||
| 8a92cf0306 | |||
| 0fbbb2f525 | |||
| 818c2981b7 | |||
| 1f66aadfa3 | |||
| b91c745fbc | |||
| af70668308 | |||
| 890473f891 | |||
| d2ff83cf72 | |||
| 0e02effaf7 | |||
| 9269a0ad6e | |||
| 3d06006f36 | |||
| 6bb1d328ad | |||
| ac296f7534 | |||
| ac8f862ff7 | |||
| b33fa5427c | |||
| 5d573dd2ae | |||
| 98dbf90864 | |||
| 6ddf1669f4 | |||
| d3d2e89fd5 | |||
| 43175d85f2 | |||
| 661ecb9f0f | |||
| 09f181c577 | |||
| 3497f26a00 | |||
| e9c0547423 | |||
| 25bc1b7426 | |||
| fee344db85 | |||
| b611f69381 | |||
| c2862831db | |||
| 1678e682ee | |||
| ced4af681f | |||
| 565db27121 | |||
| c0ac9f81fa | |||
| 879c68555c | |||
| c2e723352e | |||
| 716f021df8 | |||
| 7fa996261d | |||
| 87bd2d91e0 | |||
| 15a930dac2 | |||
| fa9abc77a6 | |||
| 4e448269bc | |||
| 29d131f7aa | |||
| 42fe129330 | |||
| bd5996f41e | |||
| 299c423213 | |||
| ce0531a13e | |||
| 513b56159f | |||
| 1676b3168d | |||
| 8a786bf72d | |||
| a48f34cf77 | |||
| 650b734b1b | |||
| 871a1de7d2 | |||
| e615cc66de | |||
| ca86a164bd | |||
| 2ac3f3956a | |||
| 7a9f03af3f | |||
| 48f1d6f010 | |||
| f0ccd99924 | |||
| c5971193a2 | |||
| ff53fd60b7 | |||
| ffa334537a | |||
| bd7ce2c13c | |||
| 5d767219ff | |||
| 6b744845c3 | |||
| 5b4478458b | |||
| 1751e7bff9 | |||
| 7ae6a8c36a | |||
| 1dcee153b9 | |||
| 4065e846f6 | |||
| 466249113c | |||
| 3e2ae734e7 | |||
| 96b31c4bc0 | |||
| 0117d5838b | |||
| 42a3b4043c | |||
| 158e9111cb | |||
| 9ed1983b85 | |||
| 703e8a035d | |||
| 7ed2faab84 | |||
| 0d66d3d127 | |||
| 137b7519f7 | |||
| e34f5835bd | |||
| f122eb7c65 | |||
| a5123b8a5e | |||
| 6aa9bffa55 | |||
| d772f9a961 | |||
| 7c841a2bce | |||
| 6a6dfb4935 | |||
| 8b370def80 | |||
| d46464282c | |||
| bb4a239cb1 | |||
| e30f0d91fe | |||
| 9f34e858c7 | |||
| 94bbc13de0 | |||
| a2f01a8fcc | |||
| 30d172e826 | |||
| bb69b036b5 | |||
| b55c51e004 | |||
| 4492e75d04 | |||
| 4d949022f2 | |||
| 3ef034a9e4 | |||
| b90e3c0b76 | |||
| a8eeddde64 | |||
| 09a9e34382 | |||
| 66c4394b40 | |||
| 32575a65fc | |||
| 66901d7393 | |||
| a1eefc31bc | |||
| de471799d1 | |||
| 3951ad4705 | |||
| 9315e3036f | |||
| b3ede5b7c0 | |||
| 3553fc79dd | |||
| f322cf82b9 | |||
| 73c975ba68 | |||
| 931d579ba5 | |||
| f4773a0baf | |||
| 06d33f6cc4 | |||
| 8f3b60f3e1 | |||
| a6e27dcc9c | |||
| 4f10c986b5 | |||
| 821152bd14 | |||
| f48b133101 | |||
| 1d56f85705 | |||
| ced852033b | |||
| b9ad8fa8b2 | |||
| e3f5ce7b41 | |||
| f681889b14 | |||
| b5ec46be5b | |||
| 168ad358d5 | |||
| 2c796d144f | |||
| c676c2c458 | |||
| b36847622d | |||
| 32c8849044 | |||
| 2dd2872c08 | |||
| cf268da19d | |||
| 7a5df64746 | |||
| 4c41f6deeb | |||
| f84aa1eb31 | |||
| 9226ee2229 |
@@ -8,7 +8,7 @@
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./mem0-plugin"
|
||||
"path": "./integrations/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.2.9"
|
||||
"version": "0.2.14"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./mem0-plugin"
|
||||
"path": "./integrations/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.2.9"
|
||||
"version": "0.2.14"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,12 +9,10 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
|
||||
@@ -9,14 +9,11 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
# Maps dropdown selections to GitHub labels
|
||||
# Used by the advanced-issue-labeler GitHub Action
|
||||
|
||||
component:
|
||||
- label: "sdk-python"
|
||||
matcher: "Core / Python SDK"
|
||||
- label: "sdk-typescript"
|
||||
matcher: "TypeScript SDK"
|
||||
- label: "vector-store"
|
||||
matcher: "Vector Store"
|
||||
- label: "graph-memory"
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
matcher: "REST API"
|
||||
policy:
|
||||
- section:
|
||||
- id: ['component']
|
||||
block-list: ['Other']
|
||||
label:
|
||||
- name: 'sdk-python'
|
||||
keys: ['Python SDK']
|
||||
- name: 'sdk-typescript'
|
||||
keys: ['TypeScript SDK']
|
||||
- name: 'vector-store'
|
||||
keys: ['Vector Store']
|
||||
- name: 'plugin'
|
||||
keys: ['Plugin']
|
||||
- name: 'rest-api'
|
||||
keys: ['REST API']
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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'
|
||||
@@ -0,0 +1,44 @@
|
||||
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 };
|
||||
@@ -0,0 +1,102 @@
|
||||
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.`);
|
||||
@@ -0,0 +1,208 @@
|
||||
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."
|
||||
@@ -1,9 +1,11 @@
|
||||
name: ci
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
changelog_check:
|
||||
@@ -104,7 +106,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||
pip install ruff
|
||||
pip install ruff==0.16.0
|
||||
- name: Run Linting
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: make lint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: CLI Node CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,10 +9,7 @@ on:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/node/**'
|
||||
- '.github/workflows/cli-node-ci.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
name: CLI Python CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -7,10 +9,7 @@ on:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'cli/python/**'
|
||||
- '.github/workflows/cli-python-ci.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
@@ -6,13 +6,10 @@ name: docs - llms.txt check
|
||||
# python scripts/check-llms-txt-coverage.py # read-only
|
||||
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# manual runs remain standalone.
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**/*.mdx'
|
||||
- 'docs/llms.txt'
|
||||
- 'scripts/check-llms-txt-coverage.py'
|
||||
- 'scripts/llms-txt-ignore.txt'
|
||||
workflow_call:
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -12,28 +12,56 @@ jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: issue-parser
|
||||
continue-on-error: true
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
- name: Infer component from text when the form was not used
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
script: |
|
||||
const {
|
||||
componentLabels,
|
||||
inferComponentLabels,
|
||||
loadKeywords,
|
||||
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`);
|
||||
const known = componentLabels(keywords);
|
||||
const existing = issue.labels.map((label) => label.name || label);
|
||||
if (existing.some((name) => known.includes(name))) {
|
||||
core.info(`Component label already present: ${existing.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = inferComponentLabels(`${issue.title}\n\n${issue.body || ''}`, keywords);
|
||||
|
||||
if (labels.length === 0) {
|
||||
core.info('No component could be inferred from the issue text');
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Inferred: ${labels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
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
|
||||
@@ -0,0 +1,60 @@
|
||||
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
|
||||
@@ -0,0 +1,88 @@
|
||||
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)
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: openclaw
|
||||
working-directory: integrations/openclaw
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
name: openclaw checks
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main and manual runs remain standalone.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openclaw/**'
|
||||
- 'integrations/openclaw/**'
|
||||
- '.github/workflows/openclaw-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -28,13 +27,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd openclaw && pnpm exec tsc --noEmit
|
||||
run: cd integrations/openclaw && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -54,20 +53,20 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: cd openclaw && pnpm exec vitest run --coverage
|
||||
run: cd integrations/openclaw && pnpm exec vitest run --coverage
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.node-version == 20
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
flags: openclaw
|
||||
directory: openclaw/coverage
|
||||
directory: integrations/openclaw/coverage
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -86,15 +85,15 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: openclaw/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/openclaw/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd openclaw && pnpm install --frozen-lockfile
|
||||
run: cd integrations/openclaw && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd openclaw && pnpm build
|
||||
run: cd integrations/openclaw && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f integrations/openclaw/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mem0-plugin/.opencode-plugin
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
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:
|
||||
- 'mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0-plugin/.opencode-plugin/**'
|
||||
- 'integrations/mem0-plugin/.opencode-plugin/**'
|
||||
- '.github/workflows/opencode-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: mem0-plugin/.opencode-plugin
|
||||
working-directory: integrations/mem0-plugin/.opencode-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: pi-agent-plugin
|
||||
working-directory: integrations/pi-agent-plugin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
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:
|
||||
- 'pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'pi-agent-plugin/**'
|
||||
- 'integrations/pi-agent-plugin/**'
|
||||
- '.github/workflows/pi-agent-plugin-checks.yml'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
@@ -28,13 +27,13 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Type check
|
||||
run: cd pi-agent-plugin && pnpm exec tsc --noEmit
|
||||
run: cd integrations/pi-agent-plugin && pnpm exec tsc --noEmit
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -54,13 +53,13 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run tests
|
||||
run: cd pi-agent-plugin && pnpm exec vitest run
|
||||
run: cd integrations/pi-agent-plugin && pnpm exec vitest run
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -77,17 +76,17 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: pi-agent-plugin/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: cd pi-agent-plugin && pnpm build
|
||||
run: cd integrations/pi-agent-plugin && pnpm build
|
||||
|
||||
- name: Verify dist output exists
|
||||
run: |
|
||||
test -f pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
|
||||
test -f pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
|
||||
test -f pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
|
||||
test -f pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
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],
|
||||
});
|
||||
}
|
||||
@@ -45,6 +45,7 @@ jobs:
|
||||
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."
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
name: TypeScript SDK CI
|
||||
|
||||
# On PRs this is invoked by ci-gate.yml (the single required check);
|
||||
# push-to-main runs remain standalone.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
- '.github/workflows/ts-sdk-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'mem0-ts/**'
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
check_changes:
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
id-token: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: vercel-ai-sdk
|
||||
working-directory: integrations/vercel-ai-sdk
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
node-version: '22'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: vercel-ai-sdk/pnpm-lock.yaml
|
||||
cache-dependency-path: integrations/vercel-ai-sdk/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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
|
||||
@@ -0,0 +1,47 @@
|
||||
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
|
||||
@@ -189,3 +189,5 @@ eval/
|
||||
qdrant_storage/
|
||||
.crossnote
|
||||
testing.ipynb
|
||||
.weave/
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[submodule "evaluation"]
|
||||
path = evaluation
|
||||
url = https://github.com/mem0ai/memory-benchmarks
|
||||
branch = main
|
||||
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
|
||||
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
|
||||
|
||||
### Key Directories
|
||||
|
||||
@@ -22,17 +22,19 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
|
||||
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
|
||||
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
|
||||
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
|
||||
| `vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/` | **Agent & editor integrations**, one directory per integration (see "Adding a New Integration") |
|
||||
| `integrations/mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills. Contains nested `.opencode-plugin/` (`@mem0/opencode-plugin`) |
|
||||
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
|
||||
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `integrations/n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` — n8n community node; add / search / get / update / delete memories |
|
||||
| `integrations/zapier-mem0/` | `@mem0/zapier` — Zapier Platform CLI app (deploys to Zapier, not npm); add / search / get / delete memories |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `mem0-plugin/` | AI editor plugins (Claude Code, Cursor, Codex) — MCP server connection, lifecycle hooks, skills |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/` |
|
||||
| `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/` |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
| `evaluation/` | Benchmarking framework — LOCOMO evals, experiment runner, score generation |
|
||||
| `examples/` | Sample projects — demo apps, Chrome extension, multi-agent patterns |
|
||||
| `cookbooks/` | Jupyter notebooks — customer support chatbot, AutoGen integration |
|
||||
| `evaluation/` | Submodule → [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) — benchmarking (LOCOMO, LongMemEval, BEAM) lives in that repo |
|
||||
| `examples/` | Sample projects & runnable demos — apps, Chrome extension, multi-agent patterns, and Jupyter notebooks (`notebooks/`) |
|
||||
| `pr-reviews/` | Pull request review materials |
|
||||
| `scripts/` | Repo-wide utility scripts (e.g., `check-llms-txt-coverage.py` for docs/llms.txt sync) |
|
||||
|
||||
@@ -49,8 +51,8 @@ mem0 (Python SDK) mem0-ts (TypeScript SDK)
|
||||
|
||||
cli/python/ ──▶ mem0ai (optional, for OSS mode)
|
||||
cli/node/ ──▶ mem0ai (npm, for API calls)
|
||||
vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
openclaw/ ──▶ mem0ai (npm)
|
||||
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
|
||||
integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
```
|
||||
|
||||
## Development Setup
|
||||
@@ -61,7 +63,7 @@ openclaw/ ──▶ mem0ai (npm)
|
||||
- **Node.js**: v18+ (v20 or v22 recommended)
|
||||
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
|
||||
- **Hatch**: Python build/environment tool (`pip install hatch`)
|
||||
- **Docker**: Required for `server/` and `openmemory/` development
|
||||
- **Docker**: Required for `server/` development
|
||||
|
||||
### Initial Setup
|
||||
|
||||
@@ -73,8 +75,8 @@ pre-commit install # install git hooks
|
||||
# TypeScript packages
|
||||
cd mem0-ts && pnpm install # TS SDK
|
||||
cd cli/node && pnpm install # Node CLI
|
||||
cd vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd openclaw && pnpm install # OpenClaw plugin
|
||||
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
|
||||
cd integrations/openclaw && pnpm install # OpenClaw plugin
|
||||
```
|
||||
|
||||
## Build, Lint, and Test Commands
|
||||
@@ -162,10 +164,10 @@ pnpm run dev # tsx src/index.ts (development)
|
||||
- **Test:** vitest (not jest)
|
||||
- **Framework:** Commander + Chalk + ora + cli-table3
|
||||
|
||||
### Vercel AI SDK Provider (`vercel-ai-sdk/`)
|
||||
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
|
||||
|
||||
```bash
|
||||
cd vercel-ai-sdk
|
||||
cd integrations/vercel-ai-sdk
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run lint # eslint
|
||||
@@ -180,10 +182,10 @@ pnpm run test:node # vitest (node runtime)
|
||||
- **Lint:** ESLint + Prettier
|
||||
- **Test:** jest + vitest (edge/node configs)
|
||||
|
||||
### OpenClaw Plugin (`openclaw/`)
|
||||
### OpenClaw Plugin (`integrations/openclaw/`)
|
||||
|
||||
```bash
|
||||
cd openclaw
|
||||
cd integrations/openclaw
|
||||
pnpm install
|
||||
pnpm run build # tsup
|
||||
pnpm run test # vitest run
|
||||
@@ -213,28 +215,6 @@ docker-compose up # starts all 3 services
|
||||
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
|
||||
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
|
||||
|
||||
### OpenMemory (`openmemory/`)
|
||||
|
||||
```bash
|
||||
# Full stack via Docker Compose
|
||||
cd openmemory
|
||||
docker-compose up
|
||||
# Qdrant: localhost:6333
|
||||
# API (MCP): localhost:8765
|
||||
# UI: localhost:3000
|
||||
|
||||
# Individual development
|
||||
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
|
||||
cd openmemory/ui && npm run dev # Next.js frontend
|
||||
|
||||
# Tests
|
||||
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
|
||||
```
|
||||
|
||||
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
|
||||
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
|
||||
- **Vector store:** Qdrant
|
||||
|
||||
### Documentation (`docs/`)
|
||||
|
||||
```bash
|
||||
@@ -245,18 +225,19 @@ make docs # or: cd docs && mintlify dev
|
||||
- **API spec:** `docs/openapi.json`
|
||||
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
|
||||
|
||||
### Evaluation (`evaluation/`)
|
||||
### Evaluation / Benchmarking
|
||||
|
||||
Benchmarking lives in the external [`mem0ai/memory-benchmarks`](https://github.com/mem0ai/memory-benchmarks) repo (LOCOMO + LongMemEval + BEAM). The in-repo `evaluation/` path is a **git submodule** pinned to that repo's `main` — populate it with `git submodule update --init evaluation` (or clone mem0 with `--recurse-submodules`), or clone the benchmarks repo standalone:
|
||||
|
||||
```bash
|
||||
cd evaluation
|
||||
make run-mem0-add # Run mem0 add experiments
|
||||
make run-mem0-search # Run mem0 search experiments
|
||||
make run-mem0-plus-add # With graph memory
|
||||
make run-mem0-plus-search # With graph memory
|
||||
make run-rag # RAG baseline
|
||||
make run-full-context # Full context baseline
|
||||
make run-langmem # LangMem comparison
|
||||
make run-openai # OpenAI comparison
|
||||
git clone https://github.com/mem0ai/memory-benchmarks.git
|
||||
cd memory-benchmarks
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run a benchmark (Mem0 Cloud; use docker compose for OSS)
|
||||
python -m benchmarks.locomo.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY
|
||||
python -m benchmarks.longmemeval.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --all-questions
|
||||
python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-key $MEM0_API_KEY --chat-sizes 100K --conversations 0-9
|
||||
```
|
||||
|
||||
## Core APIs
|
||||
@@ -329,7 +310,6 @@ make run-openai # OpenAI comparison
|
||||
- Root SDK: line length **120**
|
||||
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||
- **isort** with `profile = "black"` for import sorting.
|
||||
- Ruff excludes `openmemory/` from root config.
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
@@ -342,8 +322,8 @@ make run-openai # OpenAI comparison
|
||||
|---------|--------|-----------|---------------|
|
||||
| `mem0-ts/` | — | Prettier | jest |
|
||||
| `cli/node/` | Biome | Biome | vitest |
|
||||
| `vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `openclaw/` | — | — | vitest |
|
||||
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
|
||||
| `integrations/openclaw/` | — | — | vitest |
|
||||
|
||||
### Type Checking
|
||||
|
||||
@@ -380,15 +360,14 @@ Optional layer on top of vector memory for relationship-aware retrieval. Configu
|
||||
Model Context Protocol support in multiple places:
|
||||
|
||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||
- **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:** MCP tools in `integrations/mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||
|
||||
### Plugin & Skills System
|
||||
|
||||
- `mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||
- `integrations/mem0-plugin/` provides integrations for Claude Code, Cursor, and Codex via MCP server connections and lifecycle hooks for automatic memory capture.
|
||||
- `skills/` contains structured skill definitions for AI agents, split into two categories:
|
||||
- **Reference skills** (always-on SDK knowledge): `mem0` (Python + TS SDKs, framework integrations), `mem0-cli` (terminal workflows), `mem0-vercel-ai-sdk` (Vercel AI provider).
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch. The two are loosely coupled via `.mem0-integration/` artifacts.
|
||||
- **Pipeline skills** (run on demand): `mem0-integrate` wires Mem0 into an existing repo via a TDD pipeline; `mem0-test-integration` verifies what the integrator produced on the same branch (the two are loosely coupled via `.mem0-integration/` artifacts); `mem0-oss-to-platform` migrates an existing project from Mem0 OSS to the hosted Platform SDK (plan, then execute on approval).
|
||||
|
||||
### Adding a New Provider
|
||||
|
||||
@@ -402,19 +381,39 @@ To add a new LLM, embedding, vector store, or reranker provider:
|
||||
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
|
||||
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
|
||||
|
||||
### Adding a New Integration
|
||||
|
||||
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
|
||||
|
||||
1. Create `integrations/<name>/` and build the integration there.
|
||||
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
|
||||
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
|
||||
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
|
||||
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
|
||||
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
|
||||
|
||||
## CI/CD
|
||||
|
||||
### CI Workflows (automated testing)
|
||||
|
||||
| Workflow | File | Triggers | Tests |
|
||||
|----------|------|----------|-------|
|
||||
| Python SDK | `ci.yml` | Push to main, PRs on `mem0/`, `tests/`, `pyproject.toml` | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main, PRs on `mem0-ts/` | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to `cli/python/`, PRs, manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to `cli/node/`, PRs, manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to `openclaw/`, PRs, manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to `mem0-plugin/.opencode-plugin/`, PRs, manual | Bun: tsc type-check + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to `pi-agent-plugin/`, PRs, manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
|
||||
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
|
||||
|
||||
| Workflow | File | Standalone Triggers | Tests |
|
||||
|----------|------|---------------------|-------|
|
||||
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
|
||||
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
|
||||
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
|
||||
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
|
||||
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
|
||||
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
|
||||
| Mem0 Plugin | `mem0-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/`, excluding `.opencode-plugin/`), manual | pytest + hook entry-point exec bits + JSON manifest validation on Python 3.10, 3.11, 3.12 |
|
||||
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
|
||||
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (on `integrations/n8n-nodes-mem0/`), manual | ESLint (n8n-nodes-base) + tsc build (dist artifact check) on Node 20 |
|
||||
| Zapier App | `zapier-mem0-checks.yml` | Push to main (on `integrations/zapier-mem0/`), manual | build (tsc) + `zapier validate` + offline unit tests on Node 22 |
|
||||
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
|
||||
|
||||
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
|
||||
|
||||
### CD Workflows (automated publishing)
|
||||
|
||||
@@ -431,11 +430,13 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
|
||||
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
|
||||
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
|
||||
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
|
||||
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/n8n-nodes-mem0`) |
|
||||
|
||||
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
|
||||
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
|
||||
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
|
||||
- The **Zapier app** (`integrations/zapier-mem0`) deploys to Zapier's own platform, not npm, so it is **not** in the release router. Deploy it manually: `gh workflow run zapier-mem0-cd.yml --ref main` (requires the `ZAPIER_DEPLOY_KEY` secret).
|
||||
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
|
||||
|
||||
### Utility Workflows
|
||||
@@ -443,6 +444,7 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
|
||||
| PR Labeler | `pr-labeler.yml` | Path-based PR labeling plus propagating labels from linked issues |
|
||||
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
|
||||
|
||||
@@ -565,7 +567,7 @@ N/A
|
||||
|
||||
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
|
||||
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
|
||||
- For `server/` and `openmemory/` work, use Docker Compose for local development.
|
||||
- For `server/` work, use Docker Compose for local development.
|
||||
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
|
||||
|
||||
### Contributing Guides
|
||||
@@ -588,5 +590,4 @@ N/A
|
||||
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
|
||||
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
|
||||
- Change public APIs without updating documentation in `docs/`.
|
||||
|
||||
+134
-47
@@ -1,72 +1,157 @@
|
||||
# Contributing to mem0
|
||||
# Contributing to Mem0
|
||||
|
||||
Let us make contribution easy, collaborative and fun.
|
||||
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
|
||||
community-driven project and we welcome contributions of all kinds — bug fixes,
|
||||
new features, documentation, examples, and integrations.
|
||||
|
||||
## Submit your Contribution through PR
|
||||
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
|
||||
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
|
||||
|
||||
To make a contribution, follow these steps:
|
||||
## Before You Start
|
||||
|
||||
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
|
||||
### 1. Open an Issue First
|
||||
|
||||
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).
|
||||
**Always open an issue before opening a pull request.** This lets us discuss the
|
||||
change, avoid duplicate effort, and agree on the approach before you invest time
|
||||
in code.
|
||||
|
||||
- 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.
|
||||
|
||||
### 📦 Development Environment
|
||||
Every pull request must link to an issue using `Closes #<issue-number>`.
|
||||
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
### 2. Sign the Contributor License Agreement (CLA)
|
||||
|
||||
**We cannot accept or merge any pull request until you have signed our Contributor
|
||||
License Agreement (CLA).**
|
||||
|
||||
When you open your first PR, the CLA bot will automatically comment with a link to
|
||||
sign. Signing takes less than a minute and only needs to be done once. Pull
|
||||
requests from contributors who have not signed the CLA will be blocked from
|
||||
merging.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
The two most common contribution targets are the SDKs:
|
||||
|
||||
| Package | Path | Language | Package manager |
|
||||
| --------------------- | ---------- | ------------ | --------------- |
|
||||
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
|
||||
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
|
||||
|
||||
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
|
||||
(`integrations/`), the self-hosted `server/`, and the docs site
|
||||
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Fork** the repository and **clone** your fork.
|
||||
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
|
||||
`fix/issue-1234`).
|
||||
3. Make your changes — add **tests**, **documentation**, and **examples** as
|
||||
appropriate.
|
||||
4. Run **linting and tests** for every package you touched (see below).
|
||||
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
|
||||
6. Push and open a **pull request** against `main`, linking the issue with
|
||||
`Closes #<number>` and filling out the
|
||||
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
### Contributing to the Python SDK (`mem0/`)
|
||||
|
||||
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
|
||||
**Do not use `pip` or `conda` for dependency management.**
|
||||
|
||||
```bash
|
||||
# Activate environment for specific Python version:
|
||||
hatch shell dev_py_3_9 # Python 3.9
|
||||
hatch shell dev_py_3_10 # Python 3.10
|
||||
hatch shell dev_py_3_11 # Python 3.11
|
||||
hatch shell dev_py_3_12 # Python 3.12
|
||||
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
|
||||
hatch shell dev_py_3_11
|
||||
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
|
||||
To ensure our standards, make sure to install pre-commit before starting to contribute.
|
||||
|
||||
```bash
|
||||
# Install pre-commit hooks (runs ruff + isort on commit)
|
||||
pre-commit install
|
||||
|
||||
# Lint, format, and sort imports
|
||||
make lint
|
||||
make format
|
||||
make sort
|
||||
|
||||
# Run the test suite (run `make install_all` first if deps are missing)
|
||||
make test
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
- **Linter / formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Tests:** pytest (in `tests/`)
|
||||
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
|
||||
environment details.
|
||||
|
||||
### Contributing to the TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
|
||||
`npm` or `yarn`.**
|
||||
|
||||
```bash
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
|
||||
# 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
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
pnpm run build # tsup (CJS + ESM)
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # unit tests with coverage
|
||||
```
|
||||
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
- **Build:** tsup
|
||||
- **Formatter:** Prettier
|
||||
- **Tests:** jest
|
||||
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
|
||||
- Use ES module `import` syntax — never `require()`.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
## Good Contribution Practices
|
||||
|
||||
### 🚀 Releasing
|
||||
- **Keep PRs small and focused.** One logical change per PR is easier to review and
|
||||
merge.
|
||||
- **Follow existing patterns.** Match the style, structure, and conventions of the
|
||||
code around you. Don't introduce new frameworks or abstractions without
|
||||
discussion.
|
||||
- **Write tests** that would fail without your change — regression tests for bugs,
|
||||
coverage for new features.
|
||||
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
|
||||
must be added to `docs/llms.txt` (run
|
||||
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
|
||||
- **Add examples** when introducing new user-facing behavior.
|
||||
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
|
||||
via the CI Gate.
|
||||
- **Never commit secrets** — no `.env` files, API keys, or credentials.
|
||||
- **Don't add core dependencies lightly.** New Python dependencies belong in an
|
||||
optional group in `pyproject.toml`, not the core `dependencies` list.
|
||||
- **Be responsive** to review feedback and keep your branch up to date with `main`.
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
## Pull Request Checklist
|
||||
|
||||
#### Tag Prefixes
|
||||
Before requesting review, make sure:
|
||||
|
||||
- [ ] An issue exists and is linked with `Closes #<number>`
|
||||
- [ ] You have signed the CLA
|
||||
- [ ] Your code follows the project's style guidelines (lint passes)
|
||||
- [ ] You performed a self-review of your changes
|
||||
- [ ] Tests are added/updated and pass locally
|
||||
- [ ] Documentation is updated if needed
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
||||
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.
|
||||
|
||||
### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
@@ -77,15 +162,17 @@ All packages are published automatically via GitHub Actions when a GitHub Releas
|
||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||
|
||||
#### How to Release
|
||||
### How to Release
|
||||
|
||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||
|
||||
#### Publishing Details
|
||||
### Publishing Details
|
||||
|
||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
@@ -1026,7 +1026,8 @@ def get_user_preferences(user_id: str):
|
||||
### AutoGen Integration
|
||||
|
||||
```python
|
||||
from cookbooks.helper.mem0_teachability import Mem0Teachability
|
||||
# Mem0Teachability lives in examples/notebooks/helper/ — see examples/notebooks/mem0-autogen.ipynb
|
||||
from helper.mem0_teachability import Mem0Teachability
|
||||
from mem0 import Memory
|
||||
|
||||
# Add memory capability to AutoGen agents
|
||||
|
||||
@@ -11,7 +11,7 @@ install:
|
||||
hatch env create
|
||||
|
||||
install_all:
|
||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
pip install ruff==0.16.0 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
|
||||
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@
|
||||
|
||||
| Benchmark | Old | New | Tokens | Latency p50 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **LoCoMo** | 71.4 | **91.6** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **94.8** | 6.8K | 1.09s |
|
||||
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
|
||||
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
|
||||
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
|
||||
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
|
||||
|
||||
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops).
|
||||
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.
|
||||
|
||||
**What changed:**
|
||||
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
|
||||
@@ -63,8 +63,8 @@ All benchmarks run on the same production-representative model stack. Single-pas
|
||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
|
||||
|
||||
## Research Highlights
|
||||
- **91.6 on LoCoMo** -- +20 points over the previous algorithm
|
||||
- **94.8 on LongMemEval** -- +27 points, with +53.6 on assistant memory recall
|
||||
- **92.5 on LoCoMo** -- +21 points over the previous algorithm
|
||||
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall
|
||||
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
|
||||
- [Read the full paper](https://mem0.ai/research)
|
||||
|
||||
@@ -186,9 +186,10 @@ npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
|
||||
```
|
||||
|
||||
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. 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. Use `/mem0-oss-to-platform` to migrate an existing project from Mem0 OSS to the hosted Platform SDK. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# 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.
|
||||
+1
-2
@@ -60,9 +60,8 @@ mem0 delete <memory-id>
|
||||
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
|
||||
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
|
||||
| `mem0 status` | Verify API connection and display current project |
|
||||
| `mem0 version` | Print the CLI version |
|
||||
|
||||
Run `mem0 <command> --help` for detailed usage on any command.
|
||||
Run `mem0 <command> --help` for detailed usage on any command, or `mem0 --version` to print the CLI version.
|
||||
|
||||
## Agent mode
|
||||
|
||||
|
||||
+13
-1
@@ -207,7 +207,11 @@
|
||||
{ "name": "immutable", "flags": ["--immutable"], "type": "boolean", "default": false, "help": "Prevent future updates." },
|
||||
{ "name": "no_infer", "flags": ["--no-infer"], "type": "boolean", "default": false, "help": "Skip inference, store raw." },
|
||||
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Categories (JSON array or comma-separated)." },
|
||||
{ "name": "categories", "flags": ["--categories"], "type": "string", "help": "Not supported on add, use --custom-categories instead." },
|
||||
{ "name": "custom_instructions", "flags": ["--custom-instructions"], "type": "string", "help": "Custom instructions for fact extraction." },
|
||||
{ "name": "custom_categories", "flags": ["--custom-categories"], "type": "string", "help": "Custom categories as a JSON array of {name: description} objects." },
|
||||
{ "name": "structured_data_schema", "flags": ["--structured-data-schema"], "type": "string", "help": "Schema for structured data extraction, as JSON." },
|
||||
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph memory extraction.", "panel": "Scope" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output format: text, json, quiet.", "panel": "Output" }
|
||||
@@ -244,6 +248,9 @@
|
||||
{ "name": "keyword", "flags": ["--keyword"], "type": "boolean", "default": false, "help": "Use keyword search.", "panel": "Search" },
|
||||
{ "name": "filter_json", "flags": ["--filter"], "type": "string", "help": "Advanced filter expression (JSON).", "panel": "Search" },
|
||||
{ "name": "fields", "flags": ["--fields"], "type": "string", "help": "Specific fields to return (comma-separated).", "panel": "Search" },
|
||||
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Search" },
|
||||
{ "name": "reference_date", "flags": ["--reference-date"], "type": "string", "help": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).", "panel": "Search" },
|
||||
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Search" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in search.", "panel": "Search" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in search.", "panel": "Search" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
@@ -296,6 +303,8 @@
|
||||
{ "name": "category", "flags": ["--category"], "type": "string", "help": "Filter by category.", "panel": "Filters" },
|
||||
{ "name": "after", "flags": ["--after"], "type": "string", "help": "Created after (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "before", "flags": ["--before"], "type": "string", "help": "Created before (YYYY-MM-DD).", "panel": "Filters" },
|
||||
{ "name": "show_expired", "flags": ["--show-expired"], "type": "boolean", "default": false, "help": "Include expired memories.", "panel": "Filters" },
|
||||
{ "name": "latest_only", "flags": ["--latest-only"], "type": "boolean", "default": false, "help": "Only return the latest version of each memory.", "panel": "Filters" },
|
||||
{ "name": "graph", "flags": ["--graph"], "type": "boolean", "default": false, "help": "Enable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "no_graph", "flags": ["--no-graph"], "type": "boolean", "default": false, "help": "Disable graph in listing.", "panel": "Filters" },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "table", "help": "Output: text, json, table.", "panel": "Output" }
|
||||
@@ -329,6 +338,8 @@
|
||||
],
|
||||
"options": [
|
||||
{ "name": "metadata", "flags": ["--metadata", "-m"], "type": "string", "help": "Update metadata (JSON)." },
|
||||
{ "name": "expires", "flags": ["--expires"], "type": "string", "help": "Expiration date (YYYY-MM-DD)." },
|
||||
{ "name": "timestamp", "flags": ["--timestamp"], "type": "integer", "help": "Unix timestamp for the memory." },
|
||||
{ "name": "output", "flags": ["--output", "-o"], "type": "string", "default": "text", "help": "Output: text, json, quiet.", "panel": "Output" }
|
||||
],
|
||||
"apiEndpoint": "update"
|
||||
@@ -356,6 +367,7 @@
|
||||
{ "name": "all", "flags": ["--all"], "type": "boolean", "default": false, "help": "Delete all memories matching scope filters." },
|
||||
{ "name": "entity", "flags": ["--entity"], "type": "boolean", "default": false, "help": "Delete the entity itself and all its memories (cascade)." },
|
||||
{ "name": "project", "flags": ["--project"], "type": "boolean", "default": false, "help": "With --all: delete ALL memories project-wide." },
|
||||
{ "name": "delete_linked", "flags": ["--delete-linked"], "type": "boolean", "default": false, "help": "Also delete memories linked to this memory." },
|
||||
{ "name": "dry_run", "flags": ["--dry-run"], "type": "boolean", "default": false, "help": "Show what would be deleted without deleting." },
|
||||
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Skip confirmation." },
|
||||
{ "name": "user_id", "flags": ["--user-id", "-u"], "type": "string", "help": "Scope to user.", "panel": "Scope" },
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `@mem0/cli` are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.8] — 2026-06-01
|
||||
|
||||
### Security
|
||||
|
||||
- Pinned transitive dependencies via pnpm overrides to remediate high-severity CVEs:
|
||||
- `jws` → 4.0.1 (CVE-2025-65945)
|
||||
- `langsmith` → ^0.6.0 (CVE-2026-45134)
|
||||
- `tar-fs` → ^2.1.4 (CVE-2025-48387, CVE-2025-59343)
|
||||
- `picomatch` → ^2.3.2 (CVE-2026-33671)
|
||||
- `minimatch` → ^3.1.3 / ^5.1.8 / ^9.0.7 (CVE-2026-27903, CVE-2026-27904, CVE-2026-26996)
|
||||
- `path-to-regexp` → ^8.4.0 (CVE-2026-4926)
|
||||
- `rollup` → ^4.59.0 (CVE-2026-27606)
|
||||
- `glob` → ^10.5.0 (CVE-2025-64756)
|
||||
- `@modelcontextprotocol/sdk` → ^1.25.4 (CVE-2025-66414, CVE-2026-0621)
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/node/`.
|
||||
+2
-8
@@ -232,14 +232,6 @@ Verify your API connection and display the current project.
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
@@ -298,6 +290,8 @@ These flags are available on all commands:
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -51,7 +51,7 @@ pnpm link --global
|
||||
|
||||
# Now use it like a normal CLI
|
||||
mem0 --help
|
||||
mem0 version
|
||||
mem0 --version
|
||||
```
|
||||
|
||||
> **Warning:** If you also have the Python CLI installed, both register the `mem0` command. The last one linked/installed wins. Unlink with `pnpm unlink --global`.
|
||||
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.12",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -44,5 +44,15 @@
|
||||
"vitest": "^4.1.0",
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"jws@4.0.0": "4.0.1",
|
||||
"langsmith@<0.6.0": "^0.6.0",
|
||||
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
||||
"picomatch@<2.3.2": "^2.3.2",
|
||||
"esbuild": ">=0.28.1",
|
||||
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+132
-398
@@ -9,6 +9,8 @@ overrides:
|
||||
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'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -38,7 +40,7 @@ importers:
|
||||
version: 20.19.37
|
||||
tsup:
|
||||
specifier: ^8.0.0
|
||||
version: 8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)
|
||||
version: 8.5.1(postcss@8.5.23)(tsx@4.21.0)(typescript@5.9.3)
|
||||
tsx:
|
||||
specifier: ^4.7.0
|
||||
version: 4.21.0
|
||||
@@ -115,314 +117,158 @@ packages:
|
||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==}
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -657,7 +503,7 @@ packages:
|
||||
resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
peerDependencies:
|
||||
esbuild: '>=0.18'
|
||||
esbuild: '>=0.28.1'
|
||||
|
||||
cac@6.7.14:
|
||||
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
|
||||
@@ -737,13 +583,8 @@ packages:
|
||||
es-module-lexer@2.1.0:
|
||||
resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==}
|
||||
|
||||
esbuild@0.25.12:
|
||||
resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.4:
|
||||
resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==}
|
||||
esbuild@0.28.1:
|
||||
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -829,8 +670,8 @@ packages:
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
nanoid@3.3.16:
|
||||
resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
@@ -871,7 +712,7 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
postcss: '>=8.5.18 <9.0.0'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
@@ -884,8 +725,8 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
postcss@8.5.8:
|
||||
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
|
||||
postcss@8.5.23:
|
||||
resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
readdirp@4.1.2:
|
||||
@@ -997,7 +838,7 @@ packages:
|
||||
peerDependencies:
|
||||
'@microsoft/api-extractor': ^7.36.0
|
||||
'@swc/core': ^1
|
||||
postcss: ^8.4.12
|
||||
postcss: '>=8.5.18 <9.0.0'
|
||||
typescript: '>=4.5.0'
|
||||
peerDependenciesMeta:
|
||||
'@microsoft/api-extractor':
|
||||
@@ -1163,160 +1004,82 @@ snapshots:
|
||||
'@colors/colors@1.5.0':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.12':
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.4':
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.12':
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.4':
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.12':
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.4':
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.12':
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.4':
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.12':
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.4':
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.12':
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.4':
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.12':
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.4':
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.12':
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.4':
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.12':
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.4':
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.12':
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.4':
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.12':
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.4':
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.12':
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.4':
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.12':
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.4':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.12':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.4':
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
@@ -1491,9 +1254,9 @@ snapshots:
|
||||
widest-line: 4.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
|
||||
bundle-require@5.1.0(esbuild@0.27.4):
|
||||
bundle-require@5.1.0(esbuild@0.28.1):
|
||||
dependencies:
|
||||
esbuild: 0.27.4
|
||||
esbuild: 0.28.1
|
||||
load-tsconfig: 0.2.5
|
||||
|
||||
cac@6.7.14: {}
|
||||
@@ -1546,63 +1309,34 @@ snapshots:
|
||||
|
||||
es-module-lexer@2.1.0: {}
|
||||
|
||||
esbuild@0.25.12:
|
||||
esbuild@0.28.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.12
|
||||
'@esbuild/android-arm': 0.25.12
|
||||
'@esbuild/android-arm64': 0.25.12
|
||||
'@esbuild/android-x64': 0.25.12
|
||||
'@esbuild/darwin-arm64': 0.25.12
|
||||
'@esbuild/darwin-x64': 0.25.12
|
||||
'@esbuild/freebsd-arm64': 0.25.12
|
||||
'@esbuild/freebsd-x64': 0.25.12
|
||||
'@esbuild/linux-arm': 0.25.12
|
||||
'@esbuild/linux-arm64': 0.25.12
|
||||
'@esbuild/linux-ia32': 0.25.12
|
||||
'@esbuild/linux-loong64': 0.25.12
|
||||
'@esbuild/linux-mips64el': 0.25.12
|
||||
'@esbuild/linux-ppc64': 0.25.12
|
||||
'@esbuild/linux-riscv64': 0.25.12
|
||||
'@esbuild/linux-s390x': 0.25.12
|
||||
'@esbuild/linux-x64': 0.25.12
|
||||
'@esbuild/netbsd-arm64': 0.25.12
|
||||
'@esbuild/netbsd-x64': 0.25.12
|
||||
'@esbuild/openbsd-arm64': 0.25.12
|
||||
'@esbuild/openbsd-x64': 0.25.12
|
||||
'@esbuild/openharmony-arm64': 0.25.12
|
||||
'@esbuild/sunos-x64': 0.25.12
|
||||
'@esbuild/win32-arm64': 0.25.12
|
||||
'@esbuild/win32-ia32': 0.25.12
|
||||
'@esbuild/win32-x64': 0.25.12
|
||||
|
||||
esbuild@0.27.4:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.4
|
||||
'@esbuild/android-arm': 0.27.4
|
||||
'@esbuild/android-arm64': 0.27.4
|
||||
'@esbuild/android-x64': 0.27.4
|
||||
'@esbuild/darwin-arm64': 0.27.4
|
||||
'@esbuild/darwin-x64': 0.27.4
|
||||
'@esbuild/freebsd-arm64': 0.27.4
|
||||
'@esbuild/freebsd-x64': 0.27.4
|
||||
'@esbuild/linux-arm': 0.27.4
|
||||
'@esbuild/linux-arm64': 0.27.4
|
||||
'@esbuild/linux-ia32': 0.27.4
|
||||
'@esbuild/linux-loong64': 0.27.4
|
||||
'@esbuild/linux-mips64el': 0.27.4
|
||||
'@esbuild/linux-ppc64': 0.27.4
|
||||
'@esbuild/linux-riscv64': 0.27.4
|
||||
'@esbuild/linux-s390x': 0.27.4
|
||||
'@esbuild/linux-x64': 0.27.4
|
||||
'@esbuild/netbsd-arm64': 0.27.4
|
||||
'@esbuild/netbsd-x64': 0.27.4
|
||||
'@esbuild/openbsd-arm64': 0.27.4
|
||||
'@esbuild/openbsd-x64': 0.27.4
|
||||
'@esbuild/openharmony-arm64': 0.27.4
|
||||
'@esbuild/sunos-x64': 0.27.4
|
||||
'@esbuild/win32-arm64': 0.27.4
|
||||
'@esbuild/win32-ia32': 0.27.4
|
||||
'@esbuild/win32-x64': 0.27.4
|
||||
'@esbuild/aix-ppc64': 0.28.1
|
||||
'@esbuild/android-arm': 0.28.1
|
||||
'@esbuild/android-arm64': 0.28.1
|
||||
'@esbuild/android-x64': 0.28.1
|
||||
'@esbuild/darwin-arm64': 0.28.1
|
||||
'@esbuild/darwin-x64': 0.28.1
|
||||
'@esbuild/freebsd-arm64': 0.28.1
|
||||
'@esbuild/freebsd-x64': 0.28.1
|
||||
'@esbuild/linux-arm': 0.28.1
|
||||
'@esbuild/linux-arm64': 0.28.1
|
||||
'@esbuild/linux-ia32': 0.28.1
|
||||
'@esbuild/linux-loong64': 0.28.1
|
||||
'@esbuild/linux-mips64el': 0.28.1
|
||||
'@esbuild/linux-ppc64': 0.28.1
|
||||
'@esbuild/linux-riscv64': 0.28.1
|
||||
'@esbuild/linux-s390x': 0.28.1
|
||||
'@esbuild/linux-x64': 0.28.1
|
||||
'@esbuild/netbsd-arm64': 0.28.1
|
||||
'@esbuild/netbsd-x64': 0.28.1
|
||||
'@esbuild/openbsd-arm64': 0.28.1
|
||||
'@esbuild/openbsd-x64': 0.28.1
|
||||
'@esbuild/openharmony-arm64': 0.28.1
|
||||
'@esbuild/sunos-x64': 0.28.1
|
||||
'@esbuild/win32-arm64': 0.28.1
|
||||
'@esbuild/win32-ia32': 0.28.1
|
||||
'@esbuild/win32-x64': 0.28.1
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
@@ -1671,7 +1405,7 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
nanoid@3.3.16: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -1707,16 +1441,16 @@ snapshots:
|
||||
mlly: 1.8.2
|
||||
pathe: 2.0.3
|
||||
|
||||
postcss-load-config@6.0.1(postcss@8.5.8)(tsx@4.21.0):
|
||||
postcss-load-config@6.0.1(postcss@8.5.23)(tsx@4.21.0):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
postcss: 8.5.8
|
||||
postcss: 8.5.23
|
||||
tsx: 4.21.0
|
||||
|
||||
postcss@8.5.8:
|
||||
postcss@8.5.23:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
nanoid: 3.3.16
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
@@ -1837,18 +1571,18 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
tsup@8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3):
|
||||
tsup@8.5.1(postcss@8.5.23)(tsx@4.21.0)(typescript@5.9.3):
|
||||
dependencies:
|
||||
bundle-require: 5.1.0(esbuild@0.27.4)
|
||||
bundle-require: 5.1.0(esbuild@0.28.1)
|
||||
cac: 6.7.14
|
||||
chokidar: 4.0.3
|
||||
consola: 3.4.2
|
||||
debug: 4.4.3
|
||||
esbuild: 0.27.4
|
||||
esbuild: 0.28.1
|
||||
fix-dts-default-cjs-exports: 1.0.1
|
||||
joycon: 3.1.1
|
||||
picocolors: 1.1.1
|
||||
postcss-load-config: 6.0.1(postcss@8.5.8)(tsx@4.21.0)
|
||||
postcss-load-config: 6.0.1(postcss@8.5.23)(tsx@4.21.0)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.60.0
|
||||
source-map: 0.7.6
|
||||
@@ -1857,7 +1591,7 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tree-kill: 1.2.2
|
||||
optionalDependencies:
|
||||
postcss: 8.5.8
|
||||
postcss: 8.5.23
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
@@ -1867,7 +1601,7 @@ snapshots:
|
||||
|
||||
tsx@4.21.0:
|
||||
dependencies:
|
||||
esbuild: 0.27.4
|
||||
esbuild: 0.28.1
|
||||
get-tsconfig: 4.13.7
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
@@ -1882,10 +1616,10 @@ snapshots:
|
||||
|
||||
vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
esbuild: 0.28.1
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
picomatch: 4.0.4
|
||||
postcss: 8.5.8
|
||||
postcss: 8.5.23
|
||||
rollup: 4.60.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
|
||||
@@ -10,3 +10,5 @@ overrides:
|
||||
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"
|
||||
|
||||
@@ -14,7 +14,10 @@ export interface AddOptions {
|
||||
immutable?: boolean;
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string[];
|
||||
customInstructions?: string;
|
||||
customCategories?: Record<string, string>[];
|
||||
structuredDataSchema?: Record<string, unknown>;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface SearchOptions {
|
||||
@@ -28,6 +31,9 @@ export interface SearchOptions {
|
||||
keyword?: boolean;
|
||||
filters?: Record<string, unknown>;
|
||||
fields?: string[];
|
||||
showExpired?: boolean;
|
||||
referenceDate?: string | number;
|
||||
latestOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface ListOptions {
|
||||
@@ -40,6 +46,8 @@ export interface ListOptions {
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
showExpired?: boolean;
|
||||
latestOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteOptions {
|
||||
@@ -48,6 +56,12 @@ export interface DeleteOptions {
|
||||
agentId?: string;
|
||||
appId?: string;
|
||||
runId?: string;
|
||||
deleteLinked?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateOptions {
|
||||
expirationDate?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface EntityIds {
|
||||
@@ -77,6 +91,7 @@ export interface Backend {
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
opts?: UpdateOptions,
|
||||
): Promise<Record<string, unknown>>;
|
||||
|
||||
delete(
|
||||
|
||||
@@ -15,8 +15,13 @@ import {
|
||||
type ListOptions,
|
||||
NotFoundError,
|
||||
type SearchOptions,
|
||||
type UpdateOptions,
|
||||
} from "./base.js";
|
||||
|
||||
function encodePathSegment(value: unknown): string {
|
||||
return encodeURIComponent(String(value));
|
||||
}
|
||||
|
||||
export class PlatformBackend implements Backend {
|
||||
private baseUrl: string;
|
||||
private headers: Record<string, string>;
|
||||
@@ -146,7 +151,13 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.immutable) payload.immutable = true;
|
||||
if (opts.infer === false) payload.infer = false;
|
||||
if (opts.expires) payload.expiration_date = opts.expires;
|
||||
if (opts.categories) payload.categories = opts.categories;
|
||||
if (opts.customInstructions)
|
||||
payload.custom_instructions = opts.customInstructions;
|
||||
if (opts.customCategories)
|
||||
payload.custom_categories = opts.customCategories;
|
||||
if (opts.structuredDataSchema)
|
||||
payload.structured_data_schema = opts.structuredDataSchema;
|
||||
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
|
||||
payload.source = "CLI";
|
||||
|
||||
return (await this._request("POST", "/v3/memories/add/", {
|
||||
@@ -207,6 +218,10 @@ export class PlatformBackend implements Backend {
|
||||
if (opts.rerank) payload.rerank = true;
|
||||
if (opts.keyword) payload.keyword_search = true;
|
||||
if (opts.fields) payload.fields = opts.fields;
|
||||
if (opts.showExpired) payload.show_expired = true;
|
||||
if (opts.referenceDate !== undefined)
|
||||
payload.reference_date = opts.referenceDate;
|
||||
if (opts.latestOnly) payload.latest_only = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/search/", {
|
||||
@@ -218,9 +233,13 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async get(memoryId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
return (await this._request(
|
||||
"GET",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{
|
||||
params: { source: "CLI" },
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async listMemories(
|
||||
@@ -257,6 +276,8 @@ export class PlatformBackend implements Backend {
|
||||
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
|
||||
});
|
||||
if (apiFilters) payload.filters = apiFilters;
|
||||
if (opts.showExpired) payload.show_expired = true;
|
||||
if (opts.latestOnly) payload.latest_only = true;
|
||||
payload.source = "CLI";
|
||||
|
||||
const result = (await this._request("POST", "/v3/memories/", {
|
||||
@@ -272,14 +293,21 @@ export class PlatformBackend implements Backend {
|
||||
memoryId: string,
|
||||
content?: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
opts: UpdateOptions = {},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const payload: Record<string, unknown> = {};
|
||||
if (content) payload.text = content;
|
||||
if (metadata) payload.metadata = metadata;
|
||||
if (opts.expirationDate) payload.expiration_date = opts.expirationDate;
|
||||
if (opts.timestamp !== undefined) payload.timestamp = opts.timestamp;
|
||||
payload.source = "CLI";
|
||||
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
|
||||
json: payload,
|
||||
})) as Record<string, unknown>;
|
||||
return (await this._request(
|
||||
"PUT",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{
|
||||
json: payload,
|
||||
},
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async delete(
|
||||
@@ -297,9 +325,13 @@ export class PlatformBackend implements Backend {
|
||||
})) as Record<string, unknown>;
|
||||
}
|
||||
if (memoryId) {
|
||||
return (await this._request("DELETE", `/v1/memories/${memoryId}/`, {
|
||||
params: { source: "CLI" },
|
||||
})) as Record<string, unknown>;
|
||||
const params: Record<string, string> = { source: "CLI" };
|
||||
if (opts.deleteLinked) params.delete_linked = "true";
|
||||
return (await this._request(
|
||||
"DELETE",
|
||||
`/v1/memories/${encodePathSegment(memoryId)}/`,
|
||||
{ params },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
throw new Error("Either memoryId or --all is required");
|
||||
}
|
||||
@@ -316,16 +348,18 @@ export class PlatformBackend implements Backend {
|
||||
if (entities.length === 0) {
|
||||
throw new Error("At least one entity ID is required for deleteEntities.");
|
||||
}
|
||||
// Delete each provided entity via the v2 path-based endpoint
|
||||
let result: Record<string, unknown> = {};
|
||||
// Delete each provided entity via the v2 path-based endpoint. Key each
|
||||
// response by entity type so a multi-entity delete (e.g. --user-id and
|
||||
// --agent-id together) doesn't discard everything but the last result.
|
||||
const results: Record<string, unknown> = {};
|
||||
for (const [entityType, entityId] of entities) {
|
||||
result = (await this._request(
|
||||
results[entityType] = (await this._request(
|
||||
"DELETE",
|
||||
`/v2/entities/${entityType}/${entityId}/`,
|
||||
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`,
|
||||
{ params: { source: "CLI" } },
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
return result;
|
||||
return results;
|
||||
}
|
||||
|
||||
async ping(): Promise<Record<string, unknown>> {
|
||||
@@ -384,9 +418,9 @@ export class PlatformBackend implements Backend {
|
||||
}
|
||||
|
||||
async getEvent(eventId: string): Promise<Record<string, unknown>> {
|
||||
return (await this._request("GET", `/v1/event/${eventId}/`)) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
return (await this._request(
|
||||
"GET",
|
||||
`/v1/event/${encodePathSegment(eventId)}/`,
|
||||
)) as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,19 @@ import {
|
||||
formatSingleMemory,
|
||||
printResultSummary,
|
||||
} from "../output.js";
|
||||
import { isAgentMode, setCurrentCommand } from "../state.js";
|
||||
import { isAgentMode, setCurrentCommand, stdinIsPiped } from "../state.js";
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect — never in agent mode. */
|
||||
function _stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
/** Exit 1 if value is not a future YYYY-MM-DD date. */
|
||||
function _validateExpires(value: string): void {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
printError(
|
||||
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (new Date(value) <= new Date()) {
|
||||
printError("--expires date must be in the future.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,10 +52,22 @@ export async function cmdAdd(
|
||||
infer?: boolean;
|
||||
expires?: string;
|
||||
categories?: string;
|
||||
customInstructions?: string;
|
||||
customCategories?: string;
|
||||
structuredDataSchema?: string;
|
||||
timestamp?: number;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("add");
|
||||
|
||||
if (opts.categories) {
|
||||
printError(
|
||||
"--categories is not supported on add. Use --custom-categories instead.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let msgs: Record<string, unknown>[] | undefined;
|
||||
let content = text;
|
||||
|
||||
@@ -78,7 +93,7 @@ export async function cmdAdd(
|
||||
}
|
||||
}
|
||||
// Read from stdin only if stdin is an actual pipe or file redirect
|
||||
else if (!content && _stdinIsPiped()) {
|
||||
else if (!content && stdinIsPiped()) {
|
||||
content = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
|
||||
@@ -93,20 +108,6 @@ export async function cmdAdd(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate --expires
|
||||
if (opts.expires) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.expires)) {
|
||||
printError(
|
||||
"Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31).",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (new Date(opts.expires) <= new Date()) {
|
||||
printError("--expires date must be in the future.");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
if (opts.metadata) {
|
||||
try {
|
||||
@@ -117,15 +118,28 @@ export async function cmdAdd(
|
||||
}
|
||||
}
|
||||
|
||||
let cats: string[] | undefined;
|
||||
if (opts.categories) {
|
||||
let customCats: Record<string, string>[] | undefined;
|
||||
if (opts.customCategories) {
|
||||
try {
|
||||
cats = JSON.parse(opts.categories);
|
||||
customCats = JSON.parse(opts.customCategories);
|
||||
} catch {
|
||||
cats = opts.categories.split(",").map((c) => c.trim());
|
||||
printError("Invalid JSON in --custom-categories.");
|
||||
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>;
|
||||
try {
|
||||
result = await timedStatus("Adding memory...", async () => {
|
||||
@@ -138,7 +152,10 @@ export async function cmdAdd(
|
||||
immutable: opts.immutable,
|
||||
infer: opts.infer !== false,
|
||||
expires: opts.expires,
|
||||
categories: cats,
|
||||
customInstructions: opts.customInstructions,
|
||||
customCategories: customCats,
|
||||
structuredDataSchema: schema,
|
||||
timestamp: opts.timestamp,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -223,6 +240,9 @@ export async function cmdSearch(
|
||||
keyword: boolean;
|
||||
filterJson?: string;
|
||||
fields?: string;
|
||||
showExpired?: boolean;
|
||||
referenceDate?: string;
|
||||
latestOnly?: boolean;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
@@ -271,6 +291,9 @@ export async function cmdSearch(
|
||||
keyword: opts.keyword,
|
||||
filters,
|
||||
fields: fieldList,
|
||||
showExpired: opts.showExpired,
|
||||
referenceDate: opts.referenceDate,
|
||||
latestOnly: opts.latestOnly,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -364,6 +387,8 @@ export async function cmdList(
|
||||
category?: string;
|
||||
after?: string;
|
||||
before?: string;
|
||||
showExpired?: boolean;
|
||||
latestOnly?: boolean;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
@@ -391,6 +416,8 @@ export async function cmdList(
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
showExpired: opts.showExpired,
|
||||
latestOnly: opts.latestOnly,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -450,7 +477,12 @@ export async function cmdUpdate(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
text: string | undefined,
|
||||
opts: { metadata?: string; output: string },
|
||||
opts: {
|
||||
metadata?: string;
|
||||
expires?: string;
|
||||
timestamp?: number;
|
||||
output: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("update");
|
||||
let meta: Record<string, unknown> | undefined;
|
||||
@@ -463,11 +495,16 @@ export async function cmdUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.expires) _validateExpires(opts.expires);
|
||||
|
||||
const start = performance.now();
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Updating memory...", async () => {
|
||||
return backend.update(memoryId, text, meta);
|
||||
return backend.update(memoryId, text, meta, {
|
||||
expirationDate: opts.expires,
|
||||
timestamp: opts.timestamp,
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
@@ -493,7 +530,12 @@ export async function cmdUpdate(
|
||||
export async function cmdDelete(
|
||||
backend: Backend,
|
||||
memoryId: string,
|
||||
opts: { output: string; dryRun?: boolean; force?: boolean },
|
||||
opts: {
|
||||
output: string;
|
||||
dryRun?: boolean;
|
||||
force?: boolean;
|
||||
deleteLinked?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
setCurrentCommand("delete");
|
||||
if (opts.dryRun) {
|
||||
@@ -514,7 +556,7 @@ export async function cmdDelete(
|
||||
let result: Record<string, unknown>;
|
||||
try {
|
||||
result = await timedStatus("Deleting...", async () => {
|
||||
return backend.delete(memoryId);
|
||||
return backend.delete(memoryId, { deleteLinked: opts.deleteLinked });
|
||||
});
|
||||
} catch (e) {
|
||||
printError(e instanceof Error ? e.message : String(e));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Utility commands: status, version, import.
|
||||
* Utility commands: status, import.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
@@ -8,7 +8,6 @@ import type { Backend } from "../backend/base.js";
|
||||
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
|
||||
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
|
||||
import { setCurrentCommand } from "../state.js";
|
||||
import { CLI_VERSION } from "../version.js";
|
||||
|
||||
const { brand, dim, success, error: errorColor } = colors;
|
||||
|
||||
@@ -82,10 +81,6 @@ export async function cmdStatus(
|
||||
console.log();
|
||||
}
|
||||
|
||||
export function cmdVersion(): void {
|
||||
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
|
||||
}
|
||||
|
||||
export async function cmdImport(
|
||||
backend: Backend,
|
||||
filePath: string,
|
||||
|
||||
+57
-5
@@ -17,6 +17,7 @@ import {
|
||||
isAgentMode,
|
||||
setAgentMode,
|
||||
setCurrentCommand,
|
||||
stdinIsPiped,
|
||||
takeNotice,
|
||||
} from "./state.js";
|
||||
import { captureEvent } from "./telemetry.js";
|
||||
@@ -319,7 +320,25 @@ program
|
||||
.option("--immutable", "Prevent future updates.", false)
|
||||
.option("--no-infer", "Skip inference, store raw.")
|
||||
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||
.option("--categories <value>", "Categories (JSON array or comma-separated).")
|
||||
.option(
|
||||
"--categories <value>",
|
||||
"Not supported on add, use --custom-categories instead.",
|
||||
)
|
||||
.option(
|
||||
"--custom-instructions <text>",
|
||||
"Custom instructions for fact extraction.",
|
||||
)
|
||||
.option(
|
||||
"--custom-categories <json>",
|
||||
"Custom categories as a JSON array of {name: description} objects.",
|
||||
)
|
||||
.option(
|
||||
"--structured-data-schema <json>",
|
||||
"Schema for structured data extraction, as JSON.",
|
||||
)
|
||||
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
|
||||
Number.parseInt(v),
|
||||
)
|
||||
.option("-o, --output <format>", "Output format: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -366,6 +385,16 @@ program
|
||||
.option("--keyword", "Use keyword search.", false)
|
||||
.option("--filter <json>", "Advanced filter expression (JSON).")
|
||||
.option("--fields <list>", "Specific fields to return (comma-separated).")
|
||||
.option("--show-expired", "Include expired memories.", false)
|
||||
.option(
|
||||
"--reference-date <date>",
|
||||
"Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
)
|
||||
.option(
|
||||
"--latest-only",
|
||||
"Only return the latest version of each memory.",
|
||||
false,
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -375,7 +404,7 @@ program
|
||||
)
|
||||
.action(async (query, opts) => {
|
||||
let resolvedQuery = query;
|
||||
if (!resolvedQuery && !process.stdin.isTTY) {
|
||||
if (!resolvedQuery && stdinIsPiped()) {
|
||||
resolvedQuery = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
if (!resolvedQuery) {
|
||||
@@ -398,6 +427,9 @@ program
|
||||
keyword: opts.keyword,
|
||||
filterJson: opts.filter,
|
||||
fields: opts.fields,
|
||||
showExpired: opts.showExpired,
|
||||
referenceDate: opts.referenceDate,
|
||||
latestOnly: opts.latestOnly,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -441,6 +473,12 @@ program
|
||||
.option("--category <name>", "Filter by category.")
|
||||
.option("--after <date>", "Created after (YYYY-MM-DD).")
|
||||
.option("--before <date>", "Created before (YYYY-MM-DD).")
|
||||
.option("--show-expired", "Include expired memories.", false)
|
||||
.option(
|
||||
"--latest-only",
|
||||
"Only return the latest version of each memory.",
|
||||
false,
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, table.", "table")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -464,6 +502,8 @@ program
|
||||
category: opts.category,
|
||||
after: opts.after,
|
||||
before: opts.before,
|
||||
showExpired: opts.showExpired,
|
||||
latestOnly: opts.latestOnly,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -474,6 +514,10 @@ program
|
||||
.command("update <memoryId> [text]")
|
||||
.description("Update a memory's text or metadata.")
|
||||
.option("-m, --metadata <json>", "Update metadata (JSON).")
|
||||
.option("--expires <date>", "Expiration date (YYYY-MM-DD).")
|
||||
.option("--timestamp <unix>", "Unix timestamp for the memory.", (v) =>
|
||||
Number.parseInt(v),
|
||||
)
|
||||
.option("-o, --output <format>", "Output: text, json, quiet.", "text")
|
||||
.option("--api-key <key>", "Override API key.")
|
||||
.option("--base-url <url>", "Override API base URL.")
|
||||
@@ -483,7 +527,7 @@ program
|
||||
)
|
||||
.action(async (memoryId, text, opts) => {
|
||||
let resolvedText = text;
|
||||
if (!resolvedText && !opts.metadata && !process.stdin.isTTY) {
|
||||
if (!resolvedText && stdinIsPiped()) {
|
||||
resolvedText = fs.readFileSync(0, "utf-8").trim();
|
||||
}
|
||||
const { cmdUpdate } = await import("./commands/memory.js");
|
||||
@@ -492,6 +536,8 @@ program
|
||||
const output = isAgent ? "agent" : opts.output;
|
||||
await cmdUpdate(backend, memoryId, resolvedText, {
|
||||
metadata: opts.metadata,
|
||||
expires: opts.expires,
|
||||
timestamp: opts.timestamp,
|
||||
output,
|
||||
});
|
||||
});
|
||||
@@ -510,6 +556,11 @@ program
|
||||
.option("--project", "With --all: delete ALL memories project-wide.", false)
|
||||
.option("--dry-run", "Show what would be deleted without deleting.", false)
|
||||
.option("--force", "Skip confirmation.", false)
|
||||
.option(
|
||||
"--delete-linked",
|
||||
"Also delete memories linked to this memory.",
|
||||
false,
|
||||
)
|
||||
.option("-u, --user-id <id>", "Scope to user.")
|
||||
.option("--agent-id <id>", "Scope to agent.")
|
||||
.option("--app-id <id>", "Scope to app.")
|
||||
@@ -563,6 +614,7 @@ program
|
||||
output,
|
||||
dryRun: opts.dryRun,
|
||||
force: opts.force,
|
||||
deleteLinked: opts.deleteLinked,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -806,8 +858,8 @@ program
|
||||
.addHelpText("after", "\nExamples:\n $ mem0 help\n $ mem0 help --json")
|
||||
.action((opts) => {
|
||||
// opts.json is set when `mem0 help --json` is used (subcommand flag).
|
||||
// program.opts().json is set when the root --json global flag was used first.
|
||||
if (opts.json || program.opts().json) {
|
||||
// program.opts().json/.agent is set when a root global flag was used first.
|
||||
if (opts.json || program.opts().json || program.opts().agent) {
|
||||
// Load spec from parent directory
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const specPath = path.join(__dirname, "..", "..", "cli-spec.json");
|
||||
|
||||
+17
-3
@@ -282,11 +282,24 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
}
|
||||
case "search":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories"]),
|
||||
pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"score",
|
||||
"created_at",
|
||||
"categories",
|
||||
"expiration_date",
|
||||
]),
|
||||
);
|
||||
case "list":
|
||||
return (data as Record<string, unknown>[]).map((r) =>
|
||||
pick(r, ["id", "memory", "created_at", "categories"]),
|
||||
pick(r, [
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"categories",
|
||||
"expiration_date",
|
||||
]),
|
||||
);
|
||||
case "get": {
|
||||
const r = data as Record<string, unknown>;
|
||||
@@ -297,11 +310,12 @@ export function sanitizeAgentData(command: string, data: unknown): unknown {
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
"expiration_date",
|
||||
]);
|
||||
}
|
||||
case "update": {
|
||||
const r = data as Record<string, unknown>;
|
||||
return pick(r, ["id", "memory"]);
|
||||
return pick(r, ["id", "memory", "expiration_date"]);
|
||||
}
|
||||
case "delete":
|
||||
case "delete-all":
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* read by commands and branding functions.
|
||||
*/
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
let _agentMode = false;
|
||||
let _currentCommand = "";
|
||||
let _pendingNotice = "";
|
||||
@@ -38,3 +40,14 @@ export function takeNotice(): string {
|
||||
_pendingNotice = "";
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** True only when stdin is an actual pipe or file redirect (never in agent mode). */
|
||||
export function stdinIsPiped(): boolean {
|
||||
if (isAgentMode()) return false;
|
||||
try {
|
||||
const stat = fs.fstatSync(0);
|
||||
return stat.isFIFO() || stat.isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,11 +145,11 @@ export function captureEvent(
|
||||
anonDistinctIdToAlias: anonIdToAlias,
|
||||
};
|
||||
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
[SENDER_SCRIPT, JSON.stringify(context)],
|
||||
{ detached: true, stdio: "ignore" },
|
||||
);
|
||||
const child = spawn(process.execPath, [SENDER_SCRIPT], {
|
||||
detached: true,
|
||||
stdio: ["pipe", "ignore", "ignore"],
|
||||
});
|
||||
child.stdin?.end(JSON.stringify(context));
|
||||
child.unref();
|
||||
} catch {
|
||||
/* silently swallow */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Standalone telemetry sender — runs as a detached child process.
|
||||
*
|
||||
* Usage: node telemetry-sender.cjs '<json context>'
|
||||
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single
|
||||
* argv argument is still accepted as a legacy fallback)
|
||||
*
|
||||
* This script is spawned by telemetry.captureEvent() and runs independently
|
||||
* of the parent CLI process. It:
|
||||
@@ -19,6 +20,31 @@
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
|
||||
function loadContext() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (process.argv[2]) {
|
||||
try {
|
||||
resolve(JSON.parse(process.argv[2]));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => (data += chunk));
|
||||
process.stdin.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function httpsRequest(url, method, headers, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const u = new URL(url);
|
||||
@@ -108,7 +134,7 @@ async function sendIdentifyEvent(ctx, payload, anonId) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const ctx = JSON.parse(process.argv[2]);
|
||||
const ctx = await loadContext();
|
||||
const payload = ctx.payload;
|
||||
|
||||
if (ctx.needsEmail && ctx.mem0ApiKey) {
|
||||
|
||||
@@ -44,15 +44,25 @@ describe("CLI Integration — help and version", () => {
|
||||
expect(result.stdout).toContain("search");
|
||||
});
|
||||
|
||||
it("help --json produces valid JSON", () => {
|
||||
const result = run(["help", "--json"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
// spec may have cli.name or top-level name
|
||||
const name = parsed.name ?? parsed.cli?.name;
|
||||
expect(name).toBe("mem0");
|
||||
it("prints the version with --version, and has no version subcommand", () => {
|
||||
const flag = run(["--version"]);
|
||||
expect(flag.exitCode).toBe(0);
|
||||
expect(flag.stdout).toContain("Mem0");
|
||||
expect(run(["version"]).exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
it.each([["help", "--json"], ["--json", "help"], ["--agent", "help"]])(
|
||||
"%s %s produces valid JSON",
|
||||
(...args) => {
|
||||
const result = run(args);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
// spec may have cli.name or top-level name
|
||||
const name = parsed.name ?? parsed.cli?.name;
|
||||
expect(name).toBe("mem0");
|
||||
},
|
||||
);
|
||||
|
||||
it("shows add help", () => {
|
||||
const result = run(["add", "--help"]);
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
||||
+580
-373
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Drift test: every documented v3 add/search/list param must be reachable from the Node CLI.
|
||||
*/
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const OPENAPI_PATH = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"docs",
|
||||
"openapi.json",
|
||||
);
|
||||
|
||||
const KNOWN_UNSURFACED: Record<string, Record<string, string>> = {
|
||||
"/v3/memories/add/": {
|
||||
includes: "extraction hint, no CLI flag yet",
|
||||
excludes: "extraction hint, no CLI flag yet",
|
||||
enable_graph: "graph memory toggle, no CLI flag yet",
|
||||
output_format: "response envelope is pinned by the CLI",
|
||||
prompt_profile_id: "no CLI flag yet",
|
||||
temporal_reasoning: "no CLI flag yet",
|
||||
timezone: "no CLI flag yet",
|
||||
observation_datetime: "no CLI flag yet, --timestamp backdates instead",
|
||||
observation_date: "no CLI flag yet, --timestamp backdates instead",
|
||||
},
|
||||
"/v3/memories/search/": {
|
||||
categories: "expressible through --filter",
|
||||
metadata: "expressible through --filter",
|
||||
},
|
||||
"/v3/memories/": {
|
||||
start_date: "covered by --after via filters.created_at.gte",
|
||||
end_date: "covered by --before via filters.created_at.lte",
|
||||
categories: "covered by --category via filters.categories",
|
||||
fields: "no CLI flag yet",
|
||||
keywords: "no CLI flag yet",
|
||||
},
|
||||
};
|
||||
|
||||
const ADD_MAPPING: Record<string, string[]> = {
|
||||
messages: ["--messages", "--file", "text"],
|
||||
user_id: ["--user-id"],
|
||||
agent_id: ["--agent-id"],
|
||||
app_id: ["--app-id"],
|
||||
run_id: ["--run-id"],
|
||||
metadata: ["--metadata"],
|
||||
expiration_date: ["--expires"],
|
||||
custom_instructions: ["--custom-instructions"],
|
||||
custom_categories: ["--custom-categories"],
|
||||
infer: ["--no-infer"],
|
||||
immutable: ["--immutable"],
|
||||
structured_data_schema: ["--structured-data-schema"],
|
||||
timestamp: ["--timestamp"],
|
||||
};
|
||||
|
||||
const SEARCH_MAPPING: Record<string, string[]> = {
|
||||
query: ["query"],
|
||||
filters: ["--filter", "--user-id", "--agent-id", "--run-id"],
|
||||
show_expired: ["--show-expired"],
|
||||
top_k: ["--top-k"],
|
||||
threshold: ["--threshold"],
|
||||
rerank: ["--rerank"],
|
||||
reference_date: ["--reference-date"],
|
||||
fields: ["--fields"],
|
||||
};
|
||||
|
||||
const LIST_MAPPING: Record<string, string[]> = {
|
||||
filters: [
|
||||
"--user-id",
|
||||
"--agent-id",
|
||||
"--run-id",
|
||||
"--category",
|
||||
"--after",
|
||||
"--before",
|
||||
],
|
||||
show_expired: ["--show-expired"],
|
||||
page: ["--page"],
|
||||
page_size: ["--page-size"],
|
||||
};
|
||||
|
||||
function documentedFields(endpoint: string): string[] {
|
||||
const spec = JSON.parse(fs.readFileSync(OPENAPI_PATH, "utf-8"));
|
||||
const schema =
|
||||
spec.paths[endpoint].post.requestBody.content["application/json"].schema;
|
||||
return Object.keys(schema.properties);
|
||||
}
|
||||
|
||||
function helpText(command: string): string {
|
||||
return execSync(`npx tsx src/index.ts ${command} --help`, {
|
||||
cwd: path.join(__dirname, ".."),
|
||||
encoding: "utf-8",
|
||||
timeout: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
function assertAllReachable(
|
||||
endpoint: string,
|
||||
mapping: Record<string, string[]>,
|
||||
command: string,
|
||||
) {
|
||||
const documented = documentedFields(endpoint);
|
||||
const help = helpText(command);
|
||||
for (const field of documented) {
|
||||
if (KNOWN_UNSURFACED[endpoint]?.[field]) continue;
|
||||
const candidates = mapping[field];
|
||||
expect(
|
||||
candidates,
|
||||
`${endpoint}: documented field "${field}" has no mapping entry for command "${command}"`,
|
||||
).toBeDefined();
|
||||
const reachable = candidates.some((flag) =>
|
||||
flag.startsWith("--") ? help.includes(flag) : true,
|
||||
);
|
||||
expect(
|
||||
reachable,
|
||||
`${endpoint}: documented field "${field}" not reachable via any of ${JSON.stringify(candidates)} on command "${command}"`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
describe("Option parity: Node CLI reachability of documented v3 params", () => {
|
||||
it("add covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/add/", ADD_MAPPING, "add");
|
||||
});
|
||||
|
||||
it("search covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/search/", SEARCH_MAPPING, "search");
|
||||
});
|
||||
|
||||
it("list covers documented fields", () => {
|
||||
assertAllReachable("/v3/memories/", LIST_MAPPING, "list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stdin fallback uses the shared piped-stdin guard", () => {
|
||||
const SOURCES = ["src/index.ts", "src/commands/memory.ts"];
|
||||
|
||||
for (const rel of SOURCES) {
|
||||
it(`${rel} never checks process.stdin.isTTY directly`, () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
|
||||
expect(
|
||||
src.includes("process.stdin.isTTY"),
|
||||
`${rel}: use stdinIsPiped() from state.ts. A bare !isTTY check is also true for /dev/null and sockets, so readFileSync(0) crashes with EAGAIN in scripts, CI, and agent mode.`,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it(`${rel} guards every readFileSync(0) with stdinIsPiped()`, () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, "..", rel), "utf-8");
|
||||
const lines = src.split("\n");
|
||||
for (const [i, line] of lines.entries()) {
|
||||
if (!line.includes("readFileSync(0")) continue;
|
||||
const guard = lines.slice(Math.max(0, i - 3), i).join("\n");
|
||||
expect(
|
||||
guard.includes("stdinIsPiped()"),
|
||||
`${rel}:${i + 1}: readFileSync(0) must be guarded by stdinIsPiped()`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Tests for the Platform backend (mem0 Platform API client).
|
||||
*/
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { PlatformBackend } from "../src/backend/platform.js";
|
||||
import { createDefaultConfig } from "../src/config.js";
|
||||
|
||||
function makeBackend(): PlatformBackend {
|
||||
return new PlatformBackend(createDefaultConfig().platform);
|
||||
}
|
||||
|
||||
function mockFetch() {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: vi.fn().mockReturnValue(null) },
|
||||
json: vi.fn().mockResolvedValue({ message: "ok" }),
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
return fetchMock;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("deleteEntities", () => {
|
||||
it("returns all results keyed by entity type for a multi-entity delete", async () => {
|
||||
const backend = makeBackend();
|
||||
const responses: Record<string, unknown> = {
|
||||
"/v2/entities/user/alice/": { message: "user deleted" },
|
||||
"/v2/entities/agent/bob/": { message: "agent deleted" },
|
||||
};
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockImplementation(
|
||||
async (_method: string, path: string) => responses[path],
|
||||
);
|
||||
|
||||
const result = await backend.deleteEntities({
|
||||
userId: "alice",
|
||||
agentId: "bob",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
user: { message: "user deleted" },
|
||||
agent: { message: "agent deleted" },
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keys a single-entity delete by its type", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
vi.spyOn(backend as any, "_request").mockResolvedValue({
|
||||
message: "user deleted",
|
||||
});
|
||||
|
||||
const result = await backend.deleteEntities({ userId: "alice" });
|
||||
expect(result).toEqual({ user: { message: "user deleted" } });
|
||||
});
|
||||
|
||||
it("throws when no entity id is provided", async () => {
|
||||
const backend = makeBackend();
|
||||
await expect(backend.deleteEntities({})).rejects.toThrow(
|
||||
"At least one entity ID is required",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PlatformBackend option-parity payloads (MEM-5893)", () => {
|
||||
it("add: custom_instructions, custom_categories, structured_data_schema, timestamp reach the payload alongside existing fields", async () => {
|
||||
const backend = makeBackend();
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockResolvedValue({ results: [] });
|
||||
|
||||
await backend.add("hello", undefined, {
|
||||
userId: "alice",
|
||||
metadata: { source: "test" },
|
||||
expires: "2099-01-01",
|
||||
customInstructions: "Extract only preferences.",
|
||||
customCategories: [{ prefs: "user preferences" }],
|
||||
structuredDataSchema: { type: "object" },
|
||||
timestamp: 1700000000,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.custom_instructions).toBe("Extract only preferences.");
|
||||
expect(payload.custom_categories).toEqual([{ prefs: "user preferences" }]);
|
||||
expect(payload.structured_data_schema).toEqual({ type: "object" });
|
||||
expect(payload.timestamp).toBe(1700000000);
|
||||
expect(payload.metadata).toEqual({ source: "test" });
|
||||
expect(payload.expiration_date).toBe("2099-01-01");
|
||||
});
|
||||
|
||||
it("add: omitted optional fields are absent from the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
const spy = vi
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
.spyOn(backend as any, "_request")
|
||||
.mockResolvedValue({ results: [] });
|
||||
|
||||
await backend.add("hello", undefined, { userId: "alice" });
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload).not.toHaveProperty("custom_instructions");
|
||||
expect(payload).not.toHaveProperty("custom_categories");
|
||||
expect(payload).not.toHaveProperty("structured_data_schema");
|
||||
expect(payload).not.toHaveProperty("timestamp");
|
||||
});
|
||||
|
||||
it("search: show_expired, reference_date, latest_only reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {
|
||||
showExpired: true,
|
||||
referenceDate: "2024-01-01",
|
||||
latestOnly: true,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.show_expired).toBe(true);
|
||||
expect(payload.reference_date).toBe("2024-01-01");
|
||||
expect(payload.latest_only).toBe(true);
|
||||
});
|
||||
|
||||
it("search: keyword_search and fields reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {
|
||||
keyword: true,
|
||||
fields: ["memory", "score"],
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.keyword_search).toBe(true);
|
||||
expect(payload.fields).toEqual(["memory", "score"]);
|
||||
});
|
||||
|
||||
it("search: omitted keyword and fields are absent from the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.search("query", {});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload).not.toHaveProperty("keyword_search");
|
||||
expect(payload).not.toHaveProperty("fields");
|
||||
});
|
||||
|
||||
it("listMemories: show_expired and latest_only are top-level, not nested inside filters", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue([]);
|
||||
|
||||
await backend.listMemories({
|
||||
userId: "alice",
|
||||
showExpired: true,
|
||||
latestOnly: true,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.show_expired).toBe(true);
|
||||
expect(payload.latest_only).toBe(true);
|
||||
expect(payload.filters ?? {}).not.toHaveProperty("show_expired");
|
||||
expect(payload.filters ?? {}).not.toHaveProperty("latest_only");
|
||||
});
|
||||
|
||||
it("update: expiration_date and timestamp reach the payload", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
|
||||
|
||||
await backend.update("mem-123", undefined, undefined, {
|
||||
expirationDate: "2099-01-01",
|
||||
timestamp: 1700000000,
|
||||
});
|
||||
|
||||
const payload = spy.mock.calls[0][2].json;
|
||||
expect(payload.expiration_date).toBe("2099-01-01");
|
||||
expect(payload.timestamp).toBe(1700000000);
|
||||
});
|
||||
|
||||
it("delete: delete_linked is a query param, not part of the JSON body", async () => {
|
||||
const backend = makeBackend();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
|
||||
const spy = vi.spyOn(backend as any, "_request").mockResolvedValue({});
|
||||
|
||||
await backend.delete("mem-123", { deleteLinked: true });
|
||||
|
||||
const opts = spy.mock.calls[0][2];
|
||||
expect(opts.params.delete_linked).toBe("true");
|
||||
expect(opts.json).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PlatformBackend path encoding", () => {
|
||||
it("encodes memory IDs before interpolating them into paths", async () => {
|
||||
const fetchMock = mockFetch();
|
||||
const backend = makeBackend();
|
||||
|
||||
await backend.get("mem/a?b#c");
|
||||
await backend.update("mem/a?b#c", "updated");
|
||||
await backend.delete("mem/a?b#c");
|
||||
|
||||
const urls = fetchMock.mock.calls.map((call) => call[0]);
|
||||
expect(urls).toEqual([
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
|
||||
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
|
||||
]);
|
||||
});
|
||||
|
||||
it("encodes entity and event IDs before interpolating them into paths", async () => {
|
||||
const fetchMock = mockFetch();
|
||||
const backend = makeBackend();
|
||||
|
||||
await backend.deleteEntities({ userId: "org/team?active#frag" });
|
||||
await backend.getEvent("evt/a?b#c");
|
||||
|
||||
const urls = fetchMock.mock.calls.map((call) => call[0]);
|
||||
expect(urls).toEqual([
|
||||
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
|
||||
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockLoadConfig = vi.fn();
|
||||
const mockSaveConfig = vi.fn();
|
||||
const mockSpawn = vi.fn();
|
||||
|
||||
vi.mock("../src/config.js", () => ({
|
||||
CONFIG_FILE: "/tmp/mem0-config.json",
|
||||
loadConfig: mockLoadConfig,
|
||||
saveConfig: mockSaveConfig,
|
||||
}));
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawn: mockSpawn,
|
||||
}));
|
||||
|
||||
describe("captureEvent", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
mockLoadConfig.mockReset();
|
||||
mockSaveConfig.mockReset();
|
||||
mockSpawn.mockReset();
|
||||
delete process.env.MEM0_TELEMETRY;
|
||||
});
|
||||
|
||||
it("pipes the telemetry context through stdin instead of argv", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
platform: {
|
||||
apiKey: "m0-node-secret",
|
||||
baseUrl: "https://api.mem0.ai",
|
||||
userEmail: "",
|
||||
},
|
||||
telemetry: {
|
||||
anonymousId: "cli-anon-node",
|
||||
},
|
||||
});
|
||||
|
||||
const stdin = { end: vi.fn() };
|
||||
const child = { stdin, unref: vi.fn() };
|
||||
mockSpawn.mockReturnValue(child);
|
||||
|
||||
const { captureEvent } = await import("../src/telemetry.js");
|
||||
captureEvent("node_test_event", { case: "stdin-secret" });
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
||||
const [execPath, args, options] = mockSpawn.mock.calls[0];
|
||||
expect(execPath).toBe(process.execPath);
|
||||
expect(args).toHaveLength(1);
|
||||
expect(String(args[0])).toContain("telemetry-sender.cjs");
|
||||
expect(JSON.stringify(args)).not.toContain("m0-node-secret");
|
||||
expect(options).toMatchObject({ detached: true, stdio: ["pipe", "ignore", "ignore"] });
|
||||
|
||||
expect(stdin.end).toHaveBeenCalledTimes(1);
|
||||
const payload = JSON.parse(stdin.end.mock.calls[0][0]);
|
||||
expect(payload.mem0ApiKey).toBe("m0-node-secret");
|
||||
expect(payload.payload.event).toBe("node_test_event");
|
||||
expect(child.unref).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,4 +8,10 @@ export default defineConfig({
|
||||
define: {
|
||||
__CLI_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
test: {
|
||||
// Integration tests spawn the CLI via `npx tsx` (15s subprocess
|
||||
// timeout); the first spawn in a file pays a cold-start cost that can
|
||||
// exceed vitest's 5s default on CI runners.
|
||||
testTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to `mem0-cli` (Python) are documented here.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.2.7] — 2026-05-20
|
||||
|
||||
### Added
|
||||
|
||||
- `mem0 whoami` — print the active agent's `default_user_id` (the AGENTRUSH
|
||||
leaderboard identifier). Reads from local config, no network call.
|
||||
- `mem0 agent-rush <add | search>` — subcommand group that wraps the new
|
||||
`/v1/agent-rush/` platform endpoints for the 7-day AGENTRUSH game. Project
|
||||
routing is implicit (resolved server-side); no flags exposed. Pretty-prints
|
||||
platform error codes into actionable hints (e.g. `agentrush_search_first`
|
||||
→ "Run 3 'mem0 agent-rush search' commands before adding.").
|
||||
- PII safety prompt on first `mem0 agent-rush add`. Interactive runs require
|
||||
explicit `y` to acknowledge that AGENTRUSH memories are public; the
|
||||
acknowledgement is persisted in `~/.mem0/config.json` under
|
||||
`agent_rush.acknowledged_at` so the prompt only appears once per machine.
|
||||
Non-interactive (agent) invocations surface the warning to stderr without
|
||||
blocking.
|
||||
- New config schema field: `agent_rush.acknowledged_at` (ISO timestamp,
|
||||
empty until first interactive acknowledgement).
|
||||
|
||||
### Changed
|
||||
|
||||
- HTTP requests from the new agent-rush commands send `X-Mem0-Mode: agent-rush`
|
||||
in addition to the existing source headers, so platform telemetry can split
|
||||
game traffic from regular CLI usage.
|
||||
|
||||
## [0.2.6] and earlier
|
||||
|
||||
Unlogged historical releases. See git history under `cli/python/`.
|
||||
@@ -241,14 +241,6 @@ Verify your API connection and display the current project.
|
||||
mem0 status
|
||||
```
|
||||
|
||||
### `mem0 version`
|
||||
|
||||
Print the CLI version.
|
||||
|
||||
```bash
|
||||
mem0 version
|
||||
```
|
||||
|
||||
## Agent mode
|
||||
|
||||
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
|
||||
@@ -307,6 +299,8 @@ These flags are available on all commands:
|
||||
| `--base-url` | Override the configured API base URL for this request |
|
||||
| `-o, --output` | Set the output format |
|
||||
|
||||
`mem0 --version` prints the CLI version. It is only valid before a subcommand, not after one.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|
||||
@@ -80,7 +80,7 @@ mem0 --help
|
||||
# Using Python directly (with venv activated)
|
||||
source .venv/bin/activate
|
||||
mem0 --help
|
||||
mem0 version
|
||||
mem0 --version
|
||||
|
||||
# Or run without activating
|
||||
.venv/bin/mem0 --help
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.7"
|
||||
version = "0.2.11"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.4"
|
||||
__version__ = "0.2.11"
|
||||
|
||||
@@ -273,7 +273,21 @@ def add(
|
||||
no_infer: bool = typer.Option(False, "--no-infer", help="Skip inference, store raw."),
|
||||
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
|
||||
categories: str | None = typer.Option(
|
||||
None, "--categories", help="Categories (JSON array or comma-separated)."
|
||||
None, "--categories", help="Not supported on add, use --custom-categories instead."
|
||||
),
|
||||
custom_instructions: str | None = typer.Option(
|
||||
None, "--custom-instructions", help="Custom instructions for fact extraction."
|
||||
),
|
||||
custom_categories: str | None = typer.Option(
|
||||
None,
|
||||
"--custom-categories",
|
||||
help="Custom categories as a JSON array of {name: description} objects.",
|
||||
),
|
||||
structured_data_schema: str | None = typer.Option(
|
||||
None, "--structured-data-schema", help="Schema for structured data extraction, as JSON."
|
||||
),
|
||||
timestamp: int | None = typer.Option(
|
||||
None, "--timestamp", help="Unix timestamp for the memory."
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
|
||||
@@ -312,6 +326,10 @@ def add(
|
||||
no_infer=no_infer,
|
||||
expires=expires,
|
||||
categories=categories,
|
||||
custom_instructions=custom_instructions,
|
||||
custom_categories=custom_categories,
|
||||
structured_data_schema=structured_data_schema,
|
||||
timestamp=timestamp,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -355,6 +373,21 @@ def search(
|
||||
help="Specific fields to return (comma-separated).",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
show_expired: bool = typer.Option(
|
||||
False, "--show-expired", help="Include expired memories.", rich_help_panel="Search"
|
||||
),
|
||||
reference_date: str | None = typer.Option(
|
||||
None,
|
||||
"--reference-date",
|
||||
help="Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
latest_only: bool = typer.Option(
|
||||
False,
|
||||
"--latest-only",
|
||||
help="Only return the latest version of each memory.",
|
||||
rich_help_panel="Search",
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -398,6 +431,9 @@ def search(
|
||||
keyword=keyword,
|
||||
filter_json=filter_json,
|
||||
fields=fields,
|
||||
show_expired=show_expired,
|
||||
reference_date=reference_date,
|
||||
latest_only=latest_only,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -464,6 +500,15 @@ def list_cmd(
|
||||
before: str | None = typer.Option(
|
||||
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
|
||||
),
|
||||
show_expired: bool = typer.Option(
|
||||
False, "--show-expired", help="Include expired memories.", rich_help_panel="Filters"
|
||||
),
|
||||
latest_only: bool = typer.Option(
|
||||
False,
|
||||
"--latest-only",
|
||||
help="Only return the latest version of each memory.",
|
||||
rich_help_panel="Filters",
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -497,6 +542,8 @@ def list_cmd(
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
show_expired=show_expired,
|
||||
latest_only=latest_only,
|
||||
output=output,
|
||||
)
|
||||
|
||||
@@ -509,6 +556,10 @@ def update(
|
||||
memory_id: str = typer.Argument(..., help="Memory ID to update."),
|
||||
text: str | None = typer.Argument(None, help="New memory text."),
|
||||
metadata: str | None = typer.Option(None, "--metadata", "-m", help="Update metadata (JSON)."),
|
||||
expires: str | None = typer.Option(None, "--expires", help="Expiration date (YYYY-MM-DD)."),
|
||||
timestamp: int | None = typer.Option(
|
||||
None, "--timestamp", help="Unix timestamp for the memory."
|
||||
),
|
||||
output: str = typer.Option(
|
||||
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
|
||||
),
|
||||
@@ -537,7 +588,15 @@ def update(
|
||||
text = _read_stdin()
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_update(backend, memory_id, text, metadata=metadata, output=output)
|
||||
cmd_update(
|
||||
backend,
|
||||
memory_id,
|
||||
text,
|
||||
metadata=metadata,
|
||||
expires=expires,
|
||||
timestamp=timestamp,
|
||||
output=output,
|
||||
)
|
||||
|
||||
|
||||
# ── Memory: delete ────────────────────────────────────────────────────────
|
||||
@@ -559,6 +618,9 @@ def delete(
|
||||
False, "--dry-run", help="Show what would be deleted without deleting."
|
||||
),
|
||||
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
|
||||
delete_linked: bool = typer.Option(
|
||||
False, "--delete-linked", help="Also delete memories linked to this memory."
|
||||
),
|
||||
user_id: str | None = typer.Option(
|
||||
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
|
||||
),
|
||||
@@ -616,7 +678,14 @@ def delete(
|
||||
from mem0_cli.commands.memory import cmd_delete
|
||||
|
||||
backend = _get_backend(api_key, base_url)
|
||||
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
|
||||
cmd_delete(
|
||||
backend,
|
||||
memory_id,
|
||||
dry_run=dry_run,
|
||||
force=force,
|
||||
delete_linked=delete_linked,
|
||||
output=output,
|
||||
)
|
||||
|
||||
elif all_:
|
||||
_fire_telemetry("delete", {"delete_mode": "all"})
|
||||
@@ -1068,7 +1137,11 @@ def _build_help_json() -> dict:
|
||||
"--immutable": "Prevent future updates.",
|
||||
"--no-infer": "Skip inference, store raw.",
|
||||
"--expires": "Expiration date (YYYY-MM-DD).",
|
||||
"--categories": "Categories (JSON array or comma-separated).",
|
||||
"--categories": "Not supported on add, use --custom-categories instead.",
|
||||
"--custom-instructions": "Custom instructions for fact extraction.",
|
||||
"--custom-categories": "Custom categories as a JSON array of {name: description} objects.",
|
||||
"--structured-data-schema": "Schema for structured data extraction, as JSON.",
|
||||
"--timestamp": "Unix timestamp for the memory.",
|
||||
"--graph": "Enable graph memory extraction.",
|
||||
"--no-graph": "Disable graph memory extraction.",
|
||||
"--output, -o": "Output format: text, json, quiet.",
|
||||
@@ -1087,6 +1160,9 @@ def _build_help_json() -> dict:
|
||||
"--keyword": "Use keyword search instead of semantic.",
|
||||
"--filter": "Advanced filter expression (JSON).",
|
||||
"--fields": "Specific fields to return (comma-separated).",
|
||||
"--show-expired": "Include expired memories.",
|
||||
"--reference-date": "Reference date for relative queries (YYYY-MM-DD or unix timestamp).",
|
||||
"--latest-only": "Only return the latest version of each memory.",
|
||||
"--graph": "Enable graph in search.",
|
||||
"--no-graph": "Disable graph in search.",
|
||||
"--output, -o": "Output format: text, json, table.",
|
||||
@@ -1110,6 +1186,8 @@ def _build_help_json() -> dict:
|
||||
"--category": "Filter by category.",
|
||||
"--after": "Created after (YYYY-MM-DD).",
|
||||
"--before": "Created before (YYYY-MM-DD).",
|
||||
"--show-expired": "Include expired memories.",
|
||||
"--latest-only": "Only return the latest version of each memory.",
|
||||
"--graph": "Enable graph in listing.",
|
||||
"--no-graph": "Disable graph in listing.",
|
||||
"--output, -o": "Output format: text, json, table.",
|
||||
@@ -1124,6 +1202,8 @@ def _build_help_json() -> dict:
|
||||
},
|
||||
"options": {
|
||||
"--metadata, -m": "Update metadata (JSON).",
|
||||
"--expires": "Expiration date (YYYY-MM-DD).",
|
||||
"--timestamp": "Unix timestamp for the memory.",
|
||||
"--output, -o": "Output format: text, json, quiet.",
|
||||
},
|
||||
},
|
||||
@@ -1140,6 +1220,7 @@ def _build_help_json() -> dict:
|
||||
"--all": "Delete all memories matching scope filters.",
|
||||
"--entity": "Delete the entity itself and all its memories (cascade).",
|
||||
"--project": "With --all: delete ALL memories project-wide.",
|
||||
"--delete-linked": "Also delete memories linked to this memory.",
|
||||
"--dry-run": "Show what would be deleted without deleting.",
|
||||
"--force": "Skip confirmation.",
|
||||
"--user-id, -u": "Scope to user.",
|
||||
@@ -1269,8 +1350,10 @@ def help(
|
||||
mem0 help
|
||||
mem0 help --json
|
||||
"""
|
||||
if json:
|
||||
console.print(_json.dumps(_build_help_json(), indent=2))
|
||||
from mem0_cli.state import is_agent_mode
|
||||
|
||||
if json or is_agent_mode():
|
||||
console.print_json(_json.dumps(_build_help_json()))
|
||||
else:
|
||||
console.print(
|
||||
f"[{BRAND_COLOR}]◆ mem0 CLI[/] v{__version__} — The Memory Layer for AI Agents\n"
|
||||
|
||||
@@ -25,7 +25,10 @@ class Backend(ABC):
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
custom_instructions: str | None = None,
|
||||
custom_categories: list[dict] | None = None,
|
||||
structured_data_schema: dict | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -43,6 +46,9 @@ class Backend(ABC):
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -61,11 +67,19 @@ class Backend(ABC):
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]: ...
|
||||
|
||||
@abstractmethod
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
self,
|
||||
memory_id: str,
|
||||
content: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
*,
|
||||
expiration_date: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
@@ -78,6 +92,7 @@ class Backend(ABC):
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
delete_linked: bool = False,
|
||||
) -> dict: ...
|
||||
|
||||
@abstractmethod
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
@@ -11,6 +12,10 @@ from mem0_cli.backend.base import Backend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
def _encode_path_segment(value: Any) -> str:
|
||||
return quote(str(value), safe="")
|
||||
|
||||
|
||||
class PlatformBackend(Backend):
|
||||
"""Backend that talks to the mem0 Platform API."""
|
||||
|
||||
@@ -82,7 +87,10 @@ class PlatformBackend(Backend):
|
||||
immutable: bool = False,
|
||||
infer: bool = True,
|
||||
expires: str | None = None,
|
||||
categories: list[str] | None = None,
|
||||
custom_instructions: str | None = None,
|
||||
custom_categories: list[dict] | None = None,
|
||||
structured_data_schema: dict | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
|
||||
@@ -107,8 +115,14 @@ class PlatformBackend(Backend):
|
||||
payload["infer"] = False
|
||||
if expires:
|
||||
payload["expiration_date"] = expires
|
||||
if categories:
|
||||
payload["categories"] = categories
|
||||
if custom_instructions:
|
||||
payload["custom_instructions"] = custom_instructions
|
||||
if custom_categories:
|
||||
payload["custom_categories"] = custom_categories
|
||||
if structured_data_schema:
|
||||
payload["structured_data_schema"] = structured_data_schema
|
||||
if timestamp is not None:
|
||||
payload["timestamp"] = timestamp
|
||||
payload["source"] = "CLI"
|
||||
|
||||
return self._request("POST", "/v3/memories/add/", json=payload)
|
||||
@@ -168,6 +182,9 @@ class PlatformBackend(Backend):
|
||||
keyword: bool = False,
|
||||
filters: dict | None = None,
|
||||
fields: list[str] | None = None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
|
||||
|
||||
@@ -186,6 +203,12 @@ class PlatformBackend(Backend):
|
||||
payload["keyword_search"] = True
|
||||
if fields:
|
||||
payload["fields"] = fields
|
||||
if show_expired:
|
||||
payload["show_expired"] = True
|
||||
if reference_date is not None:
|
||||
payload["reference_date"] = reference_date
|
||||
if latest_only:
|
||||
payload["latest_only"] = True
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/search/", json=payload)
|
||||
@@ -196,7 +219,11 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def get(self, memory_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
return self._request(
|
||||
"GET",
|
||||
f"/v1/memories/{_encode_path_segment(memory_id)}/",
|
||||
params={"source": "CLI"},
|
||||
)
|
||||
|
||||
def list_memories(
|
||||
self,
|
||||
@@ -210,6 +237,8 @@ class PlatformBackend(Backend):
|
||||
category: str | None = None,
|
||||
after: str | None = None,
|
||||
before: str | None = None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
) -> list[dict]:
|
||||
payload: dict[str, Any] = {}
|
||||
params = {"page": str(page), "page_size": str(page_size)}
|
||||
@@ -232,6 +261,10 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
if api_filters:
|
||||
payload["filters"] = api_filters
|
||||
if show_expired:
|
||||
payload["show_expired"] = True
|
||||
if latest_only:
|
||||
payload["latest_only"] = True
|
||||
payload["source"] = "CLI"
|
||||
|
||||
result = self._request("POST", "/v3/memories/", json=payload, params=params)
|
||||
@@ -242,15 +275,29 @@ class PlatformBackend(Backend):
|
||||
)
|
||||
|
||||
def update(
|
||||
self, memory_id: str, content: str | None = None, metadata: dict | None = None
|
||||
self,
|
||||
memory_id: str,
|
||||
content: str | None = None,
|
||||
metadata: dict | None = None,
|
||||
*,
|
||||
expiration_date: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
) -> dict:
|
||||
payload: dict[str, Any] = {}
|
||||
if content:
|
||||
payload["text"] = content
|
||||
if metadata:
|
||||
payload["metadata"] = metadata
|
||||
if expiration_date:
|
||||
payload["expiration_date"] = expiration_date
|
||||
if timestamp is not None:
|
||||
payload["timestamp"] = timestamp
|
||||
payload["source"] = "CLI"
|
||||
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
|
||||
return self._request(
|
||||
"PUT",
|
||||
f"/v1/memories/{_encode_path_segment(memory_id)}/",
|
||||
json=payload,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
@@ -261,6 +308,7 @@ class PlatformBackend(Backend):
|
||||
agent_id: str | None = None,
|
||||
app_id: str | None = None,
|
||||
run_id: str | None = None,
|
||||
delete_linked: bool = False,
|
||||
) -> dict:
|
||||
if all:
|
||||
params: dict[str, str] = {"source": "CLI"}
|
||||
@@ -274,7 +322,14 @@ class PlatformBackend(Backend):
|
||||
params["run_id"] = run_id
|
||||
return self._request("DELETE", "/v1/memories/", params=params)
|
||||
elif memory_id:
|
||||
return self._request("DELETE", f"/v1/memories/{memory_id}/", params={"source": "CLI"})
|
||||
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:
|
||||
raise ValueError("Either memory_id or --all is required")
|
||||
|
||||
@@ -296,13 +351,17 @@ class PlatformBackend(Backend):
|
||||
entities = {t: v for t, v in type_map.items() if v}
|
||||
if not entities:
|
||||
raise ValueError("At least one entity ID is required for delete_entities.")
|
||||
# Delete each provided entity via the v2 path-based endpoint
|
||||
result: dict = {}
|
||||
# Delete each provided entity via the v2 path-based endpoint. Key each
|
||||
# response by entity type so a multi-entity delete (e.g. --user-id and
|
||||
# --agent-id together) doesn't discard everything but the last result.
|
||||
results: dict = {}
|
||||
for entity_type, entity_id in entities.items():
|
||||
result = self._request(
|
||||
"DELETE", f"/v2/entities/{entity_type}/{entity_id}/", params={"source": "CLI"}
|
||||
results[entity_type] = self._request(
|
||||
"DELETE",
|
||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
|
||||
params={"source": "CLI"},
|
||||
)
|
||||
return result
|
||||
return results
|
||||
|
||||
def ping(self, timeout: float | None = None) -> dict:
|
||||
"""Call the ping endpoint and return the raw response.
|
||||
@@ -346,7 +405,7 @@ class PlatformBackend(Backend):
|
||||
return result if isinstance(result, list) else result.get("results", [])
|
||||
|
||||
def get_event(self, event_id: str) -> dict:
|
||||
return self._request("GET", f"/v1/event/{event_id}/")
|
||||
return self._request("GET", f"/v1/event/{_encode_path_segment(event_id)}/")
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
|
||||
@@ -4,9 +4,11 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat as _stat_mod
|
||||
import sys
|
||||
import time as _time
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
@@ -47,6 +49,18 @@ def _stdin_is_piped() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_expires(value: str) -> None:
|
||||
"""Exit 1 if value is not a future YYYY-MM-DD date."""
|
||||
if not re.match(r"^\d{4}-\d{2}-\d{2}$", value):
|
||||
print_error(
|
||||
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
if date.fromisoformat(value) <= date.today():
|
||||
print_error(err_console, "--expires date must be in the future.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
def cmd_add(
|
||||
backend: Backend,
|
||||
text: str | None,
|
||||
@@ -62,6 +76,10 @@ def cmd_add(
|
||||
no_infer: bool,
|
||||
expires: str | None,
|
||||
categories: str | None,
|
||||
custom_instructions: str | None = None,
|
||||
custom_categories: str | None = None,
|
||||
structured_data_schema: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Add a memory."""
|
||||
@@ -70,6 +88,13 @@ def cmd_add(
|
||||
set_current_command("add")
|
||||
if is_agent_mode():
|
||||
output = "agent"
|
||||
|
||||
if categories:
|
||||
print_error(
|
||||
err_console, "--categories is not supported on add. Use --custom-categories instead."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
msgs = None
|
||||
content = text
|
||||
|
||||
@@ -108,27 +133,24 @@ def cmd_add(
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
cats = None
|
||||
if categories:
|
||||
custom_cats = None
|
||||
if custom_categories:
|
||||
try:
|
||||
cats = json.loads(categories)
|
||||
custom_cats = json.loads(custom_categories)
|
||||
except json.JSONDecodeError:
|
||||
cats = [c.strip() for c in categories.split(",")]
|
||||
print_error(err_console, "Invalid JSON in --custom-categories.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
schema = None
|
||||
if structured_data_schema:
|
||||
try:
|
||||
schema = json.loads(structured_data_schema)
|
||||
except json.JSONDecodeError:
|
||||
print_error(err_console, "Invalid JSON in --structured-data-schema.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Validate --expires
|
||||
if 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)
|
||||
_validate_expires(expires)
|
||||
|
||||
with timed_status(err_console, "Adding memory...") as ts:
|
||||
try:
|
||||
@@ -143,7 +165,10 @@ def cmd_add(
|
||||
immutable=immutable,
|
||||
infer=not no_infer,
|
||||
expires=expires,
|
||||
categories=cats,
|
||||
custom_instructions=custom_instructions,
|
||||
custom_categories=custom_cats,
|
||||
structured_data_schema=schema,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
except Exception as e:
|
||||
ts.error_msg = str(e)
|
||||
@@ -224,6 +249,9 @@ def cmd_search(
|
||||
keyword: bool,
|
||||
filter_json: str | None,
|
||||
fields: str | None,
|
||||
show_expired: bool = False,
|
||||
reference_date: str | None = None,
|
||||
latest_only: bool = False,
|
||||
output: str = "text",
|
||||
) -> None:
|
||||
"""Search memories."""
|
||||
@@ -266,6 +294,9 @@ def cmd_search(
|
||||
keyword=keyword,
|
||||
filters=filters,
|
||||
fields=field_list,
|
||||
show_expired=show_expired,
|
||||
reference_date=reference_date,
|
||||
latest_only=latest_only,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
@@ -352,6 +383,8 @@ def cmd_list(
|
||||
category: str | None,
|
||||
after: str | None,
|
||||
before: str | None,
|
||||
show_expired: bool = False,
|
||||
latest_only: bool = False,
|
||||
output: str = "table",
|
||||
) -> None:
|
||||
"""List memories."""
|
||||
@@ -380,6 +413,8 @@ def cmd_list(
|
||||
category=category,
|
||||
after=after,
|
||||
before=before,
|
||||
show_expired=show_expired,
|
||||
latest_only=latest_only,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
@@ -446,6 +481,8 @@ def cmd_update(
|
||||
text: str | None,
|
||||
*,
|
||||
metadata: str | None,
|
||||
expires: str | None = None,
|
||||
timestamp: int | None = None,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Update a memory."""
|
||||
@@ -462,10 +499,19 @@ def cmd_update(
|
||||
print_error(err_console, "Invalid JSON in --metadata.")
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
if expires:
|
||||
_validate_expires(expires)
|
||||
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Updating memory...") as _ts:
|
||||
try:
|
||||
result = backend.update(memory_id, content=text, metadata=meta)
|
||||
result = backend.update(
|
||||
memory_id,
|
||||
content=text,
|
||||
metadata=meta,
|
||||
expiration_date=expires,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
@@ -490,6 +536,7 @@ def cmd_delete(
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
delete_linked: bool = False,
|
||||
output: str,
|
||||
) -> None:
|
||||
"""Delete a single memory by ID."""
|
||||
@@ -512,7 +559,7 @@ def cmd_delete(
|
||||
_start = _time.perf_counter()
|
||||
with timed_status(err_console, "Deleting...") as _ts:
|
||||
try:
|
||||
result = backend.delete(memory_id=memory_id)
|
||||
result = backend.delete(memory_id=memory_id, delete_linked=delete_linked)
|
||||
except Exception as e:
|
||||
print_error(err_console, str(e))
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
@@ -235,7 +235,10 @@ def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
|
||||
if isinstance(current, bool):
|
||||
value = value.lower() in ("true", "1", "yes") # type: ignore[assignment]
|
||||
elif isinstance(current, int):
|
||||
value = int(value) # type: ignore[assignment]
|
||||
try:
|
||||
value = int(value) # type: ignore[assignment]
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
setattr(obj, final_key, value)
|
||||
return True
|
||||
|
||||
@@ -20,8 +20,8 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
|
||||
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
|
||||
|
||||
for i, mem in enumerate(memories, 1):
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")[:8]
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
mem_id = (mem.get("id") or "")[:8]
|
||||
score = mem.get("score")
|
||||
created = _format_date(mem.get("created_at"))
|
||||
category = mem.get("categories", [None])
|
||||
@@ -67,8 +67,8 @@ def format_memories_table(
|
||||
table.add_column("Created", max_width=12)
|
||||
|
||||
for mem in memories:
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id") or ""
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
if len(memory_text) > 60:
|
||||
memory_text = memory_text[:57] + "..."
|
||||
categories = mem.get("categories", [])
|
||||
@@ -104,8 +104,8 @@ def format_single_memory(console: Console, mem: dict, output: str = "text") -> N
|
||||
format_json(console, mem)
|
||||
return
|
||||
|
||||
memory_text = mem.get("memory", mem.get("text", ""))
|
||||
mem_id = mem.get("id", "")
|
||||
memory_text = mem.get("memory") or mem.get("text") or ""
|
||||
mem_id = mem.get("id") or ""
|
||||
|
||||
lines = []
|
||||
lines.append(f" [white bold]{memory_text}[/]")
|
||||
@@ -262,16 +262,32 @@ def sanitize_agent_data(command: str, data: Any) -> Any:
|
||||
return result
|
||||
|
||||
if command == "search":
|
||||
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
|
||||
return [
|
||||
pick(r, ["id", "memory", "score", "created_at", "categories", "expiration_date"])
|
||||
for r in data
|
||||
]
|
||||
|
||||
if command == "list":
|
||||
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
|
||||
return [
|
||||
pick(r, ["id", "memory", "created_at", "categories", "expiration_date"]) for r in data
|
||||
]
|
||||
|
||||
if command == "get":
|
||||
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
|
||||
return pick(
|
||||
data,
|
||||
[
|
||||
"id",
|
||||
"memory",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"categories",
|
||||
"metadata",
|
||||
"expiration_date",
|
||||
],
|
||||
)
|
||||
|
||||
if command == "update":
|
||||
return pick(data, ["id", "memory"])
|
||||
return pick(data, ["id", "memory", "expiration_date"])
|
||||
|
||||
if command in ("delete", "delete-all", "entity delete"):
|
||||
return data
|
||||
|
||||
@@ -137,12 +137,19 @@ def capture_event(
|
||||
"anon_distinct_id_to_alias": anon_id_to_alias,
|
||||
}
|
||||
|
||||
subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender", json.dumps(context)],
|
||||
child = subprocess.Popen(
|
||||
[sys.executable, "-m", "mem0_cli.telemetry_sender"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
close_fds=True,
|
||||
text=True,
|
||||
)
|
||||
if child.stdin:
|
||||
with contextlib.suppress(Exception):
|
||||
child.stdin.write(json.dumps(context))
|
||||
with contextlib.suppress(Exception):
|
||||
child.stdin.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Standalone telemetry sender — runs as a detached subprocess.
|
||||
|
||||
Usage: python -m mem0_cli.telemetry_sender '<json context>'
|
||||
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin;
|
||||
a single argv argument is still accepted as a legacy fallback)
|
||||
|
||||
This module is spawned by telemetry.capture_event() and runs independently
|
||||
of the parent CLI process. It:
|
||||
@@ -20,8 +21,18 @@ import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def _load_context() -> dict:
|
||||
"""Load telemetry context from stdin, falling back to argv for compatibility."""
|
||||
raw = ""
|
||||
if not sys.stdin.isatty():
|
||||
raw = sys.stdin.read().strip()
|
||||
if not raw and len(sys.argv) > 1:
|
||||
raw = sys.argv[1]
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ctx = json.loads(sys.argv[1])
|
||||
ctx = _load_context()
|
||||
payload = ctx["payload"]
|
||||
|
||||
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
|
||||
|
||||
@@ -32,12 +32,13 @@ def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedPr
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
timeout=15,
|
||||
)
|
||||
@@ -99,12 +100,13 @@ class TestArgvPreprocessing:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1", # blackhole
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
@@ -133,12 +135,13 @@ class TestJsonEnvelopeParity:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env={
|
||||
**{k: v for k, v in os.environ.items() if not k.startswith("MEM0_")},
|
||||
"HOME": clean_home,
|
||||
"MEM0_BASE_URL": "http://127.0.0.1:1",
|
||||
"FORCE_COLOR": "0",
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
},
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ boundaries).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -49,6 +50,7 @@ def _run(
|
||||
if key.startswith("MEM0_"):
|
||||
del env[key]
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
if home_dir:
|
||||
env["HOME"] = home_dir
|
||||
if env_override:
|
||||
@@ -56,7 +58,7 @@ def _run(
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "mem0_cli", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
env=env,
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
@@ -83,6 +85,31 @@ class TestCLIIntegration:
|
||||
assert "add" in result.stdout
|
||||
assert "search" in result.stdout
|
||||
|
||||
def test_version_flag_only(self):
|
||||
from mem0_cli import __version__
|
||||
|
||||
flag = _run(["--version"])
|
||||
assert flag.returncode == 0
|
||||
assert __version__ in flag.stdout
|
||||
assert _run(["version"]).returncode != 0
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args",
|
||||
[["help", "--json"], ["--json", "help"], ["help", "--agent"], ["--agent", "help"]],
|
||||
)
|
||||
def test_help_json_produces_valid_json(self, args):
|
||||
result = _run(args)
|
||||
assert result.returncode == 0
|
||||
spec = json.loads(result.stdout)
|
||||
assert spec["name"] == "mem0"
|
||||
assert "add" in spec["commands"]
|
||||
|
||||
def test_help_without_json_is_text(self):
|
||||
result = _run(["help"])
|
||||
assert result.returncode == 0
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
json.loads(result.stdout)
|
||||
|
||||
def test_add_help(self):
|
||||
result = _run(["add", "--help"])
|
||||
assert result.returncode == 0
|
||||
|
||||
@@ -8,8 +8,8 @@ from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.exceptions import Exit as ClickExit
|
||||
from rich.console import Console
|
||||
from typer import Exit as TyperExit
|
||||
|
||||
from mem0_cli.commands.config_cmd import (
|
||||
cmd_config_get,
|
||||
@@ -181,7 +181,7 @@ class TestAddCommand:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -206,7 +206,7 @@ class TestAddCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -252,12 +252,13 @@ class TestAddCommand:
|
||||
)
|
||||
mock_backend.add.assert_called_once()
|
||||
|
||||
def test_add_categories_csv(self, mock_backend):
|
||||
def test_add_categories_rejected(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
@@ -275,7 +276,93 @@ class TestAddCommand:
|
||||
categories="health,prefs",
|
||||
output="text",
|
||||
)
|
||||
mock_backend.add.assert_called_once()
|
||||
assert "--custom-categories" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_invalid_custom_categories_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
custom_categories="not-json",
|
||||
output="text",
|
||||
)
|
||||
assert "--custom-categories" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_invalid_structured_data_schema_json(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata=None,
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires=None,
|
||||
categories=None,
|
||||
structured_data_schema="not-json",
|
||||
output="text",
|
||||
)
|
||||
assert "--structured-data-schema" in err_buf.getvalue()
|
||||
mock_backend.add.assert_not_called()
|
||||
|
||||
def test_add_regression_metadata_expiration_custom_categories_together(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_add(
|
||||
mock_backend,
|
||||
"test",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
messages=None,
|
||||
file=None,
|
||||
metadata='{"source": "test"}',
|
||||
immutable=False,
|
||||
no_infer=False,
|
||||
expires="2099-01-01",
|
||||
categories=None,
|
||||
custom_categories='[{"prefs": "user preferences"}]',
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.add.call_args.kwargs
|
||||
assert call_kwargs["metadata"] == {"source": "test"}
|
||||
assert call_kwargs["expires"] == "2099-01-01"
|
||||
assert call_kwargs["custom_categories"] == [{"prefs": "user preferences"}]
|
||||
|
||||
|
||||
class TestAddDeduplicatesPending:
|
||||
@@ -464,6 +551,36 @@ class TestSearchCommand:
|
||||
)
|
||||
mock_backend.search.assert_called_once()
|
||||
|
||||
def test_search_new_flags_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_search(
|
||||
mock_backend,
|
||||
"preferences",
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
top_k=10,
|
||||
threshold=0.3,
|
||||
rerank=False,
|
||||
keyword=False,
|
||||
filter_json=None,
|
||||
fields=None,
|
||||
show_expired=True,
|
||||
reference_date="2024-01-01",
|
||||
latest_only=True,
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.search.call_args.kwargs
|
||||
assert call_kwargs["show_expired"] is True
|
||||
assert call_kwargs["reference_date"] == "2024-01-01"
|
||||
assert call_kwargs["latest_only"] is True
|
||||
|
||||
|
||||
class TestGetCommand:
|
||||
def test_get_text(self, mock_backend):
|
||||
@@ -561,6 +678,32 @@ class TestListCommand:
|
||||
output = buf.getvalue()
|
||||
assert "No memories found" in output
|
||||
|
||||
def test_list_new_flags_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_list(
|
||||
mock_backend,
|
||||
user_id="alice",
|
||||
agent_id=None,
|
||||
app_id=None,
|
||||
run_id=None,
|
||||
page=1,
|
||||
page_size=100,
|
||||
category=None,
|
||||
after=None,
|
||||
before=None,
|
||||
show_expired=True,
|
||||
latest_only=True,
|
||||
output="table",
|
||||
)
|
||||
call_kwargs = mock_backend.list_memories.call_args.kwargs
|
||||
assert call_kwargs["show_expired"] is True
|
||||
assert call_kwargs["latest_only"] is True
|
||||
|
||||
|
||||
class TestUpdateCommand:
|
||||
def test_update(self, mock_backend):
|
||||
@@ -585,6 +728,26 @@ class TestUpdateCommand:
|
||||
output = buf.getvalue()
|
||||
assert '"memory"' in output
|
||||
|
||||
def test_update_new_fields_reach_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_update(
|
||||
mock_backend,
|
||||
"abc-123",
|
||||
"New text",
|
||||
metadata=None,
|
||||
expires="2099-01-01",
|
||||
timestamp=1700000000,
|
||||
output="text",
|
||||
)
|
||||
call_kwargs = mock_backend.update.call_args.kwargs
|
||||
assert call_kwargs["expiration_date"] == "2099-01-01"
|
||||
assert call_kwargs["timestamp"] == 1700000000
|
||||
|
||||
|
||||
class TestDeleteCommand:
|
||||
def test_delete_single(self, mock_backend):
|
||||
@@ -610,6 +773,17 @@ class TestDeleteCommand:
|
||||
assert "dry run" in output.lower()
|
||||
mock_backend.delete.assert_not_called()
|
||||
|
||||
def test_delete_linked_reaches_backend(self, mock_backend):
|
||||
console, _buf = _make_console()
|
||||
err_console, _err_buf = _make_err_console()
|
||||
with (
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
):
|
||||
cmd_delete(mock_backend, "abc-123", delete_linked=True, output="text")
|
||||
call_kwargs = mock_backend.delete.call_args.kwargs
|
||||
assert call_kwargs["delete_linked"] is True
|
||||
|
||||
|
||||
class TestDeleteAllCommand:
|
||||
def test_delete_all_force(self, mock_backend):
|
||||
@@ -764,7 +938,7 @@ class TestImportCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.utils.console", console),
|
||||
patch("mem0_cli.commands.utils.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
|
||||
|
||||
@@ -801,7 +975,7 @@ class TestEntitiesListCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_entities_list(mock_backend, "invalid", output="table")
|
||||
|
||||
@@ -944,7 +1118,7 @@ class TestEntitiesDeleteCommand:
|
||||
with (
|
||||
patch("mem0_cli.commands.entities.console", console),
|
||||
patch("mem0_cli.commands.entities.err_console", err_console),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_entities_delete(
|
||||
mock_backend,
|
||||
@@ -1308,7 +1482,7 @@ class TestAgentMode:
|
||||
patch("mem0_cli.commands.memory.console", console),
|
||||
patch("mem0_cli.commands.memory.err_console", err_console),
|
||||
patch("sys.stdout", captured_stdout),
|
||||
pytest.raises((SystemExit, ClickExit)),
|
||||
pytest.raises((SystemExit, TyperExit)),
|
||||
):
|
||||
cmd_get(mock_backend, "bad-id", output="text")
|
||||
|
||||
|
||||
@@ -67,7 +67,8 @@ class TestConfig:
|
||||
from mem0_cli.config import CONFIG_FILE
|
||||
|
||||
mode = os.stat(CONFIG_FILE).st_mode & 0o777
|
||||
assert mode == 0o600
|
||||
if os.name != "nt":
|
||||
assert mode == 0o600
|
||||
|
||||
def test_defaults_save_and_load(self, isolate_config):
|
||||
config = Mem0Config()
|
||||
@@ -138,6 +139,11 @@ class TestNestedAccess:
|
||||
assert set_nested_value(config, "platform.api_key", "new-key")
|
||||
assert config.platform.api_key == "new-key"
|
||||
|
||||
def test_set_int_value_rejects_invalid_input(self):
|
||||
config = Mem0Config()
|
||||
assert set_nested_value(config, "version", "abc") is False
|
||||
assert config.version == 1
|
||||
|
||||
def test_set_nonexistent_key(self):
|
||||
config = Mem0Config()
|
||||
assert set_nested_value(config, "nonexistent.key", "val") is False
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Drift test: every documented v3 add/search/list param must be reachable from the Python CLI."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import typer.main
|
||||
|
||||
from mem0_cli.app import app
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
OPENAPI_PATH = REPO_ROOT / "docs" / "openapi.json"
|
||||
|
||||
KNOWN_UNSURFACED: dict[tuple[str, str], str] = {
|
||||
("/v3/memories/add/", "includes"): "extraction hint, no CLI flag yet",
|
||||
("/v3/memories/add/", "excludes"): "extraction hint, no CLI flag yet",
|
||||
("/v3/memories/add/", "enable_graph"): "graph memory toggle, no CLI flag yet",
|
||||
("/v3/memories/add/", "output_format"): "response envelope is pinned by the CLI",
|
||||
("/v3/memories/add/", "prompt_profile_id"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "temporal_reasoning"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "timezone"): "no CLI flag yet",
|
||||
("/v3/memories/add/", "observation_datetime"): "no CLI flag yet, --timestamp backdates instead",
|
||||
("/v3/memories/add/", "observation_date"): "no CLI flag yet, --timestamp backdates instead",
|
||||
("/v3/memories/search/", "categories"): "expressible through --filter",
|
||||
("/v3/memories/search/", "metadata"): "expressible through --filter",
|
||||
("/v3/memories/", "start_date"): "covered by --after via filters.created_at.gte",
|
||||
("/v3/memories/", "end_date"): "covered by --before via filters.created_at.lte",
|
||||
("/v3/memories/", "categories"): "covered by --category via filters.categories",
|
||||
("/v3/memories/", "fields"): "no CLI flag yet",
|
||||
("/v3/memories/", "keywords"): "no CLI flag yet",
|
||||
}
|
||||
|
||||
ADD_MAPPING: dict[str, list[str]] = {
|
||||
"messages": ["messages", "file", "text"],
|
||||
"user_id": ["user_id"],
|
||||
"agent_id": ["agent_id"],
|
||||
"app_id": ["app_id"],
|
||||
"run_id": ["run_id"],
|
||||
"metadata": ["metadata"],
|
||||
"expiration_date": ["expires"],
|
||||
"custom_instructions": ["custom_instructions"],
|
||||
"custom_categories": ["custom_categories"],
|
||||
"infer": ["no_infer"],
|
||||
"immutable": ["immutable"],
|
||||
"structured_data_schema": ["structured_data_schema"],
|
||||
"timestamp": ["timestamp"],
|
||||
}
|
||||
|
||||
SEARCH_MAPPING: dict[str, list[str]] = {
|
||||
"query": ["query"],
|
||||
"filters": ["filter_json", "user_id", "agent_id", "run_id"],
|
||||
"show_expired": ["show_expired"],
|
||||
"top_k": ["top_k"],
|
||||
"threshold": ["threshold"],
|
||||
"rerank": ["rerank"],
|
||||
"reference_date": ["reference_date"],
|
||||
"fields": ["fields"],
|
||||
}
|
||||
|
||||
LIST_MAPPING: dict[str, list[str]] = {
|
||||
"filters": ["user_id", "agent_id", "run_id", "category", "after", "before"],
|
||||
"show_expired": ["show_expired"],
|
||||
"page": ["page"],
|
||||
"page_size": ["page_size"],
|
||||
}
|
||||
|
||||
|
||||
def _documented_fields(endpoint: str) -> set[str]:
|
||||
spec = json.loads(OPENAPI_PATH.read_text())
|
||||
schema = spec["paths"][endpoint]["post"]["requestBody"]["content"]["application/json"]["schema"]
|
||||
return set(schema["properties"])
|
||||
|
||||
|
||||
def _cli_param_names(command_name: str) -> set[str]:
|
||||
click_app = typer.main.get_command(app)
|
||||
command = click_app.commands[command_name]
|
||||
return {param.name for param in command.params}
|
||||
|
||||
|
||||
def _assert_all_reachable(endpoint: str, mapping: dict[str, list[str]], command_name: str) -> None:
|
||||
documented = _documented_fields(endpoint)
|
||||
reachable = _cli_param_names(command_name)
|
||||
for field in documented:
|
||||
if (endpoint, field) in KNOWN_UNSURFACED:
|
||||
continue
|
||||
candidates = mapping.get(field)
|
||||
assert candidates, (
|
||||
f"{endpoint}: documented field {field!r} has no mapping entry for command {command_name!r}"
|
||||
)
|
||||
assert any(candidate in reachable for candidate in candidates), (
|
||||
f"{endpoint}: documented field {field!r} not reachable via any of {candidates} on command {command_name!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestOptionParity:
|
||||
def test_add_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/add/", ADD_MAPPING, "add")
|
||||
|
||||
def test_search_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/search/", SEARCH_MAPPING, "search")
|
||||
|
||||
def test_list_covers_documented_fields(self):
|
||||
_assert_all_reachable("/v3/memories/", LIST_MAPPING, "list")
|
||||
@@ -54,6 +54,11 @@ class TestTextFormat:
|
||||
output = buf.getvalue()
|
||||
assert "Found 0" in output
|
||||
|
||||
def test_format_memories_text_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_memories_text(console, [{"id": None, "memory": None, "created_at": None}])
|
||||
assert "Found 1 memories" in buf.getvalue()
|
||||
|
||||
|
||||
class TestTableFormat:
|
||||
def test_format_memories_table(self):
|
||||
@@ -70,6 +75,13 @@ class TestTableFormat:
|
||||
# Should still render (empty table)
|
||||
assert "ID" in output
|
||||
|
||||
def test_format_memories_table_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_memories_table(console, [{"id": None, "memory": None, "created_at": None}])
|
||||
output = buf.getvalue()
|
||||
assert "ID" in output
|
||||
assert "Memory" in output
|
||||
|
||||
|
||||
class TestSingleMemory:
|
||||
def test_format_single_memory_text(self):
|
||||
@@ -87,6 +99,17 @@ class TestSingleMemory:
|
||||
output = buf.getvalue()
|
||||
assert '"memory"' in output
|
||||
|
||||
def test_format_single_memory_handles_null_fields(self):
|
||||
console, buf = _make_console()
|
||||
format_single_memory(
|
||||
console,
|
||||
{"id": None, "memory": None, "text": "Fallback memory", "created_at": None},
|
||||
"text",
|
||||
)
|
||||
output = buf.getvalue()
|
||||
assert "Fallback memory" in output
|
||||
assert "ID:" not in output
|
||||
|
||||
|
||||
class TestAddResult:
|
||||
def test_format_add_result_text(self):
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Tests that the MEM-5893 option-parity flags reach the correct request payload/params."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from mem0_cli.backend.platform import PlatformBackend
|
||||
from mem0_cli.config import PlatformConfig
|
||||
|
||||
|
||||
def _make_backend() -> PlatformBackend:
|
||||
return PlatformBackend(PlatformConfig(api_key="test-key", base_url="https://api.mem0.ai"))
|
||||
|
||||
|
||||
class TestAddOptions:
|
||||
def test_new_fields_and_existing_fields_land_in_payload_together(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
|
||||
backend.add(
|
||||
content="hello",
|
||||
user_id="alice",
|
||||
metadata={"source": "test"},
|
||||
expires="2099-01-01",
|
||||
custom_instructions="Extract only preferences.",
|
||||
custom_categories=[{"prefs": "user preferences"}],
|
||||
structured_data_schema={"type": "object"},
|
||||
timestamp=1700000000,
|
||||
)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["custom_instructions"] == "Extract only preferences."
|
||||
assert payload["custom_categories"] == [{"prefs": "user preferences"}]
|
||||
assert payload["structured_data_schema"] == {"type": "object"}
|
||||
assert payload["timestamp"] == 1700000000
|
||||
assert payload["metadata"] == {"source": "test"}
|
||||
assert payload["expiration_date"] == "2099-01-01"
|
||||
|
||||
def test_omitted_fields_are_absent_from_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={"results": []}) as mock_request:
|
||||
backend.add(content="hello", user_id="alice")
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert "custom_instructions" not in payload
|
||||
assert "custom_categories" not in payload
|
||||
assert "structured_data_schema" not in payload
|
||||
assert "timestamp" not in payload
|
||||
|
||||
|
||||
class TestSearchOptions:
|
||||
def test_show_expired_reference_date_latest_only_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search(
|
||||
"query",
|
||||
show_expired=True,
|
||||
reference_date="2024-01-01",
|
||||
latest_only=True,
|
||||
)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["show_expired"] is True
|
||||
assert payload["reference_date"] == "2024-01-01"
|
||||
assert payload["latest_only"] is True
|
||||
|
||||
def test_keyword_and_fields_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search("query", keyword=True, fields=["memory", "score"])
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["keyword_search"] is True
|
||||
assert payload["fields"] == ["memory", "score"]
|
||||
|
||||
def test_keyword_and_fields_omitted_are_absent_from_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.search("query")
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert "keyword_search" not in payload
|
||||
assert "fields" not in payload
|
||||
|
||||
|
||||
class TestListOptions:
|
||||
def test_show_expired_and_latest_only_are_top_level_not_in_filters(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value=[]) as mock_request:
|
||||
backend.list_memories(user_id="alice", show_expired=True, latest_only=True)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["show_expired"] is True
|
||||
assert payload["latest_only"] is True
|
||||
assert "show_expired" not in payload.get("filters", {})
|
||||
assert "latest_only" not in payload.get("filters", {})
|
||||
|
||||
|
||||
class TestUpdateOptions:
|
||||
def test_expires_and_timestamp_reach_payload(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={}) as mock_request:
|
||||
backend.update("mem-123", expiration_date="2099-01-01", timestamp=1700000000)
|
||||
payload = mock_request.call_args.kwargs["json"]
|
||||
assert payload["expiration_date"] == "2099-01-01"
|
||||
assert payload["timestamp"] == 1700000000
|
||||
|
||||
|
||||
class TestDeleteOptions:
|
||||
def test_delete_linked_is_a_query_param_not_json_body(self):
|
||||
backend = _make_backend()
|
||||
with patch.object(backend, "_request", return_value={}) as mock_request:
|
||||
backend.delete(memory_id="mem-123", delete_linked=True)
|
||||
call = mock_request.call_args
|
||||
assert call.kwargs["params"]["delete_linked"] == "true"
|
||||
assert "json" not in call.kwargs
|
||||
@@ -0,0 +1,43 @@
|
||||
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/",
|
||||
]
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Tests for telemetry subprocess secret handling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from mem0_cli.config import Mem0Config, save_config
|
||||
from mem0_cli.telemetry import capture_event
|
||||
from mem0_cli.telemetry_sender import _load_context
|
||||
|
||||
|
||||
class _CaptureStdin:
|
||||
def __init__(self):
|
||||
self.buffer = ""
|
||||
self.closed = False
|
||||
|
||||
def write(self, value: str) -> None:
|
||||
self.buffer += value
|
||||
|
||||
def close(self) -> None:
|
||||
self.closed = True
|
||||
|
||||
|
||||
class _DummyProcess:
|
||||
def __init__(self):
|
||||
self.stdin = _CaptureStdin()
|
||||
|
||||
|
||||
def test_capture_event_writes_context_to_stdin_not_argv(isolate_config, monkeypatch):
|
||||
config = Mem0Config()
|
||||
config.platform.api_key = "m0-test-secret"
|
||||
config.telemetry.anonymous_id = "cli-anon-test"
|
||||
save_config(config)
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
proc = _DummyProcess()
|
||||
|
||||
def fake_popen(args, **kwargs):
|
||||
captured["args"] = args
|
||||
captured["kwargs"] = kwargs
|
||||
return proc
|
||||
|
||||
monkeypatch.setattr("mem0_cli.telemetry.subprocess.Popen", fake_popen)
|
||||
|
||||
capture_event("unit_test_event", {"case": "stdin-secret"})
|
||||
|
||||
argv = captured["args"]
|
||||
assert argv == [sys.executable, "-m", "mem0_cli.telemetry_sender"]
|
||||
assert all("m0-test-secret" not in arg for arg in argv)
|
||||
|
||||
kwargs = captured["kwargs"]
|
||||
assert kwargs["stdin"] == subprocess.PIPE
|
||||
assert kwargs["text"] is True
|
||||
|
||||
ctx = json.loads(proc.stdin.buffer)
|
||||
assert ctx["mem0_api_key"] == "m0-test-secret"
|
||||
assert ctx["payload"]["event"] == "unit_test_event"
|
||||
|
||||
assert proc.stdin.closed
|
||||
|
||||
|
||||
def test_load_context_reads_from_stdin(monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["telemetry_sender"])
|
||||
monkeypatch.setattr("sys.stdin", io.StringIO('{"payload": {"event": "stdin"}}'))
|
||||
|
||||
ctx = _load_context()
|
||||
|
||||
assert ctx["payload"]["event"] == "stdin"
|
||||
|
||||
|
||||
def test_load_context_falls_back_to_argv(monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["telemetry_sender", '{"payload": {"event": "argv"}}'])
|
||||
monkeypatch.setattr("sys.stdin", io.StringIO(""))
|
||||
|
||||
ctx = _load_context()
|
||||
|
||||
assert ctx["payload"]["event"] == "argv"
|
||||
+1
-1
@@ -24,7 +24,7 @@ mintlify dev
|
||||
|
||||
### Publishing Changes
|
||||
|
||||
Install our Github App to auto propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
|
||||
Install our GitHub App to auto-propagate changes from your repo to your deployment. Changes will be deployed to production automatically after pushing to the default branch. Find the link to install on your dashboard.
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{/* Subtle, value-anchored nudge to star the repo. Drop in at peak-end "win" moments in the OSS docs (after a successful add/search, a server bootstrap, etc.). Keep it off the Platform/API pages. */}
|
||||
{/* Clicks are tracked via PostHog autocapture: the data-ph-capture-attribute-cta below tags each click with cta="star-on-github" so it's filterable as an event property. Metric = count of $autocapture where cta = star-on-github; break down by Current URL to see which win-moment converts. */}
|
||||
<Callout icon="star" iconType="solid" color="#FACC15">
|
||||
**Using Mem0?** <a href="https://github.com/mem0ai/mem0" data-ph-capture-attribute-cta="star-on-github">Star us on GitHub</a> to help more developers discover memory for AI apps.
|
||||
</Callout>
|
||||
@@ -97,7 +97,7 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
|
||||
|
||||
## Next Steps
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<CardGroup cols={3}>
|
||||
<Card title="Add Your First Memory" icon="rocket" href="/api-reference/memory/add-memories">
|
||||
Start storing memories via the REST API
|
||||
</Card>
|
||||
@@ -105,4 +105,8 @@ Get your API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_so
|
||||
<Card title="Search with Filters" icon="filter" href="/api-reference/memory/search-memories">
|
||||
Learn advanced search and filtering techniques
|
||||
</Card>
|
||||
|
||||
<Card title="Build with cookbooks" icon="book-open" href="/cookbooks/overview">
|
||||
See the API used end to end in real projects.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@ description: "Add facts, messages, or metadata to a user memory store with async
|
||||
openapi: post /v3/memories/add/
|
||||
---
|
||||
|
||||
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
|
||||
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction: one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
|
||||
|
||||
## Endpoint
|
||||
|
||||
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
|
||||
| `app_id` | string | No* | Associates the memory with an app. |
|
||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
||||
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
||||
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
|
||||
|
||||
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
||||
|
||||
@@ -64,9 +65,8 @@ The request is queued for background processing. The response contains an `event
|
||||
<CodeGroup>
|
||||
```json 200 response
|
||||
{
|
||||
"message": "Memory processing has been queued for background execution",
|
||||
"status": "PENDING",
|
||||
"event_id": "evt-uuid"
|
||||
"event_id": "evt-uuid",
|
||||
"status": "PENDING"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,3 +83,11 @@ The request is queued for background processing. The response contains an `event
|
||||
<Info>
|
||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
|
||||
</Info>
|
||||
|
||||
<Info>
|
||||
Python uses `expiration_date`; TypeScript uses `expirationDate`.
|
||||
</Info>
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Submit an export job to create a structured memory export using a
|
||||
openapi: post /v1/exports/
|
||||
---
|
||||
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `run_id`, or `session_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `app_id`, or `run_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
|
||||
@@ -4,7 +4,11 @@ description: "Retrieve memories with paginated results and advanced filtering us
|
||||
openapi: post /v3/memories/
|
||||
---
|
||||
|
||||
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
|
||||
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object: top-level entity IDs are rejected with 400.
|
||||
|
||||
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
||||
|
||||
Python uses `show_expired`; TypeScript uses `showExpired`.
|
||||
|
||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||
|
||||
@@ -32,6 +36,7 @@ memories = client.get_all(
|
||||
}
|
||||
]
|
||||
},
|
||||
show_expired=False,
|
||||
page=1,
|
||||
page_size=50
|
||||
)
|
||||
@@ -46,12 +51,14 @@ memories = client.get_all(
|
||||
{
|
||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-01T12:00:00Z",
|
||||
"updated_at": "2024-07-01T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
||||
"memory": "Alex prefers vegetarian restaurants",
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-05T15:30:00Z",
|
||||
"updated_at": "2024-07-05T15:30:00Z"
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Retrieve the latest structured memory export after submitting an e
|
||||
openapi: post /v1/exports/get
|
||||
---
|
||||
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `run_id`, `session_id`, or `app_id` to get the most recent export matching your filters.
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, or `updated_at` to get the most recent export matching your filters.
|
||||
@@ -4,9 +4,13 @@ description: "Search memories with hybrid retrieval (semantic + BM25 + entity ma
|
||||
openapi: post /v3/memories/search/
|
||||
---
|
||||
|
||||
Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retrieval — semantic, BM25 keyword, and entity matching scored in parallel and fused. The returned `score` is a combined `[0, 1]` value.
|
||||
Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retrieval: semantic, BM25 keyword, and entity matching scored in parallel and fused. The returned `score` is a combined `[0, 1]` value.
|
||||
|
||||
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
|
||||
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object: top-level entity IDs are rejected with 400. At least one entity ID is required.
|
||||
|
||||
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
||||
|
||||
Python uses `show_expired`; TypeScript uses `showExpired`.
|
||||
|
||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||
- `in`: Matches any of the values specified
|
||||
@@ -20,16 +24,17 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
|
||||
|
||||
### Search parameter defaults
|
||||
|
||||
| Parameter | V1/V2 | V3 |
|
||||
| --- | --- | --- |
|
||||
| `top_k` | Supported (default 10) | Supported (1-1000, default 10) |
|
||||
| `threshold` | No default | Default `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | Default `true` | Default `false` (pass `true` to enable) |
|
||||
| Parameter | Default |
|
||||
| --- | --- |
|
||||
| `top_k` | `10` (range 1–1000) |
|
||||
| `threshold` | `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | `false` (pass `true` to enable) |
|
||||
|
||||
<CodeGroup>
|
||||
```python Platform API Example
|
||||
related_memories = client.search(
|
||||
query="What are Alice's hobbies?",
|
||||
show_expired=False,
|
||||
filters={
|
||||
"OR": [
|
||||
{
|
||||
@@ -54,6 +59,7 @@ related_memories = client.search(
|
||||
"category": "hobbies"
|
||||
},
|
||||
"score": 0.82,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||
"updated_at": null,
|
||||
"categories": ["hobbies"]
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
---
|
||||
title: 'Update Memory'
|
||||
description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
|
||||
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
|
||||
openapi: put /v1/memories/{memory_id}/
|
||||
---
|
||||
---
|
||||
|
||||
Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
|
||||
|
||||
```python
|
||||
client.update("mem_123", expiration_date="2030-01-31")
|
||||
client.update("mem_123", expiration_date=None)
|
||||
```
|
||||
|
||||
TypeScript uses `expirationDate`.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Remove Organization Member"
|
||||
description: "Remove a member from an organization to revoke their access to its projects and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Organization Member"
|
||||
description: "Update an existing member's role within an organization to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -14,7 +14,7 @@ Organizations and projects are **optional** features. You can use Mem0 without t
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **Multi-org/project Support**: Specify organization and project when initializing the Mem0 client to attribute API usage appropriately
|
||||
- **Multi-org/project Support**: Organization and project are resolved automatically from your API key via `/v1/ping/`: no org or project params are accepted by `MemoryClient.__init__`. Use a project-specific API key to target a particular project.
|
||||
- **Member Management**: Control access to data through organization and project membership
|
||||
- **Access Control**: Only members can access memories and data within their organization/project scope
|
||||
- **Team Isolation**: Maintain data separation between different teams and projects for secure collaboration
|
||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, and language preferences:
|
||||
Modify project configuration including custom instructions, categories, language preferences, and memory decay:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
@@ -98,6 +98,9 @@ client.project.update(
|
||||
# Use the input language for memory storage and retrieval
|
||||
client.project.update(multilingual=True)
|
||||
|
||||
# Enable Memory Decay (boosts recently-accessed memories at search time)
|
||||
client.project.update(decay=True)
|
||||
|
||||
# Update multiple settings at once
|
||||
client.project.update(
|
||||
custom_instructions="...",
|
||||
@@ -111,7 +114,7 @@ client.project.update(
|
||||
|
||||
#### Toggle Memory Decay
|
||||
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay) — a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay): a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
|
||||
```bash cURL
|
||||
curl -X PATCH https://api.mem0.ai/api/v1/orgs/organizations/$ORG_ID/projects/$PROJECT_ID/ \
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Remove Project Member"
|
||||
description: "Remove a member from a project to revoke their access to its memories, configuration, and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Project Member"
|
||||
description: "Update an existing member's role within a project to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user