Compare commits

...

249 Commits

Author SHA1 Message Date
Kartik f2532f072f chore: update changelog, bump SDK versions to Python 2.0.11 and TypeScript 3.0.13 (#6031) 2026-07-01 22:17:41 +05:30
Kartik 41c8f00851 chore(integrations): plugin updates, pi-agent auto-recall, and version bumps (#6011) 2026-07-01 20:57:32 +05:30
Hrushikesh Yadav a36a392cd3 fix(opensearch): validate filter values to prevent term query injection (#5986) 2026-07-01 20:47:45 +05:30
Bartok 152d1e66f7 fix(embeddings): guard embed_batch count mismatch in OpenAI and Azure OpenAI (#5966) 2026-07-01 18:53:28 +05:30
Hrushikesh Yadav bc05fd9623 fix(neptune): escape filter values in openCypher queries to prevent injection (#5982)
Signed-off-by: Hrushikesh Yadav <yadavhrushikesh65@gmail.com>
2026-07-01 18:48:04 +05:30
Bartok ad7e09851c fix(memory): re-raise LLM extraction failures instead of returning [] (salvage of #5178) (#5878) 2026-07-01 18:36:19 +05:30
Kartik c325bd3b8e docs(changelog): consolidate per-package changelogs into the SDK changelog page (#6007) 2026-06-30 14:09:41 +05:30
rudrajmehta-mem0 2add7fd57d docs(graph-memory): gate Graph view visualization to Pro/Enterprise (#6000)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 18:12:50 -07:00
Kartik b2ff3aeda5 Clean up release highlights copy and removing emdash from the docs (#5984) 2026-06-29 21:27:53 +05:30
Kartik 754034abbc Revert "fix(memory): accept llm kwarg in sync Memory.add()/_create_procedural_memory (#5911)" (#5990) 2026-06-29 21:27:16 +05:30
ly-wang19 bedf862d64 fix(memory): accept llm kwarg in sync Memory.add()/_create_procedural_memory (#5911) (#5953)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-29 20:43:35 +05:30
Terrasse cc59d122db docs: remove instructions for unavailable Cursor marketplace plugin (#5971) 2026-06-29 20:35:07 +05:30
Hrushikesh Yadav 3619fd77ae fix(azure-ai-search): validate filter value types and escape quotes in OData (#5983) 2026-06-29 20:31:26 +05:30
Hrushikesh Yadav 2dcb3542f8 fix(databricks): validate catalog/schema/table identifiers to prevent SQL injection (#5988) 2026-06-29 20:30:27 +05:30
Abhay Singh 31cec11a79 fix(cli-node): keep every result in entity delete, not just the last (#5970) 2026-06-29 15:26:57 +05:30
冯基魁 4c0ea22d31 fix(ts): handle empty Google chat candidates (#5817) 2026-06-29 15:08:44 +05:30
Muhammad Furqan f59320df65 fix(faiss): normalize vectors for cosine distance strategy (#5960) 2026-06-29 15:03:55 +05:30
Abhay Singh ad57cbb8d6 fix(cli): keep every result in entity delete, not just the last (#5936) 2026-06-29 14:58:16 +05:30
Barry ee0c38e081 fix(cli): handle null memory fields in output formatters (#5957) 2026-06-29 14:53:22 +05:30
Barry d5b64ccec9 fix(cli): reject invalid int config values without traceback (#5956) 2026-06-29 14:47:32 +05:30
Kartik 8d6b7c1d67 chore: update changelog, bump SDK versions to Python 2.0.10 and TypeScript 3.0.12 (#5927) 2026-06-27 22:33:14 +05:30
Kartik b44ce4dcc3 docs(cookbooks): fix v3 filters, response shapes & dead snippet in cookbooks (#5841) 2026-06-27 18:55:08 +05:30
Kartik e9c930c430 docs(components): fix LLM & embedder model IDs and TS support lists (#5838) 2026-06-27 18:42:02 +05:30
Kartik 4b39d01ccb docs(platform): align Platform docs with v3 SDK behavior (#5849) 2026-06-27 18:41:47 +05:30
Kartik 49061718bd docs(cookbooks): fix v3 filters & update() in companions/essentials (#5844) 2026-06-27 18:41:35 +05:30
Kartik 7e7682a06d docs(vectordbs): correct vector store config defaults & imports (#5843) 2026-06-27 12:40:16 +05:30
Kartik f38608fb50 docs(api-reference): align API docs & openapi.json with v3 spec (#5848) 2026-06-26 22:07:53 +05:30
Kartik fb11cdffbb docs(changelog): fix score_details fields, graph-store note & dead link (#5845) 2026-06-26 22:05:14 +05:30
Kartik ee600705c2 docs: remove inaccurate api-changes migration page (#5857) 2026-06-26 22:03:53 +05:30
Hrushikesh Yadav f4ccef5157 fix(ts/redis): use nullish coalescing for hash/timestamps in insert/update (#5860)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-26 19:01:58 +05:30
mintlify[bot] 08da741a31 SEO & metadata audit: expand short API reference descriptions (#5897)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-06-26 16:34:52 +05:30
Kartik e4efdd2e29 docs: add SECURITY.md and improve contribution guidelines (#5855) 2026-06-26 16:02:53 +05:30
Kartik a87c9ce367 docs: fix mangled Python formatting in pgvector & Neon vector store guides (#5853) 2026-06-26 16:00:58 +05:30
Kartik d258b638ef docs: document FastEmbed embedder + missing org/project API endpoints (#5852) 2026-06-26 16:00:12 +05:30
Harsh Vardhan Gupta bbbfcfea07 fix(deps): bump undici to >=6.27.0 (CVE-2026-12151) (#5861) 2026-06-26 15:32:09 +05:30
Kartik fbef369b91 docs(open-source): fix OSS feature docs for v3 SDK (#5847) 2026-06-26 14:14:33 +05:30
Kartik a7ed68e697 docs(integrations): fix retired model IDs, v3 response shapes & vercel deps (#5842) 2026-06-26 13:59:33 +05:30
Kartik 8a92cf0306 docs(integrations): sync agent-plugin hook tables with actual hooks.json (#5839) 2026-06-26 13:58:28 +05:30
soumil-rathi 0fbbb2f525 feat(memory): expose expiration controls in client docs (#5874)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
2026-06-25 17:14:33 -07:00
Barry Collins 818c2981b7 feat(ts-sdk): add MiniMax LLM provider (#5858)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-25 17:01:50 +05:30
Rod Boev 1f66aadfa3 fix(openclaw): normalize Windows skill-loader URLs before fileURLToPath (#5679) 2026-06-25 16:36:11 +05:30
Hrushikesh Yadav b91c745fbc fix: apply remove_code_blocks() to LangChain path in async _create_procedural_memory (#5711) 2026-06-25 16:34:28 +05:30
Muhammad Furqan af70668308 fix(reranker): score HuggingFace reranker with sigmoid, not min-max (#5715) 2026-06-25 16:18:02 +05:30
rafid001 890473f891 fix(core): validate and trim entity IDs in delete_all() (#5735)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-25 16:14:30 +05:30
Hrushikesh Yadav d2ff83cf72 fix(redis): use .get() for hash/created_at in insert() to handle entity payloads (#5709) 2026-06-25 16:13:44 +05:30
Rod Boev 0e02effaf7 fix(notices): derive scale counts for Redis and search backends (#5687) 2026-06-25 16:04:44 +05:30
Rod Boev 9269a0ad6e feat(ts-sdk): add LiteLLM as LLM provider (#5830)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-25 15:52:46 +05:30
Hrushikesh Yadav 3d06006f36 fix(valkey): escape special chars in FT.SEARCH tag filter values (#5750) 2026-06-25 15:20:13 +05:30
Rod Boev 6bb1d328ad fix(mem0-ts): support pgvector connection strings and ssl (#5789) 2026-06-25 11:57:37 +05:30
Taranjeet Singh ac296f7534 docs: make example code fences copy-safe (#5833) 2026-06-24 22:19:40 -07:00
Kartik ac8f862ff7 fix(mem0-plugin): store files_touched as a list to stop double JSON-encoding (#5806) 2026-06-25 09:06:11 +05:30
soumil-rathi b33fa5427c fix(memory): align entity extraction precision (#5829)
Making the entity extraction function cleaner


Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
2026-06-24 15:48:37 -07:00
Ashutosh Kasudhan 5d573dd2ae docs: added typescript support to deepseek provider (#5723) 2026-06-24 20:55:14 +05:30
Kartik 98dbf90864 chore: update changelog, bump SDK versions to Python 2.0.8 and TypeScript 3.0.10 (#5825) 2026-06-24 20:21:06 +05:30
David Shrader 6ddf1669f4 fix(vector_stores): point fastembed-missing warning at mem0ai[extras] (#5622) 2026-06-24 14:47:02 +05:30
David Shrader d3d2e89fd5 fix(llms): skip JSON response_format for Groq compound models (#5513) 2026-06-24 14:42:22 +05:30
Xiaoju 43175d85f2 fix: preserve empty Azure AI Search update values (#5524)
Signed-off-by: Xiaoju <xiaojuchh@gmail.com>
2026-06-24 14:41:55 +05:30
Kartik 661ecb9f0f docs(hermes): update for v3 API, OSS self-hosted mode, and new tools (#5807) 2026-06-24 11:59:13 +05:30
冯基魁 09f181c577 fix(server): fetch filtered dashboard memories beyond default page (#5753) 2026-06-24 11:49:06 +05:30
Zaid 3497f26a00 fix: add auto_refresh option for OpenSearch Serverless compatibility (#3893)
Co-authored-by: Zaid Malhis <Malhis@users.noreply.github.com>
2026-06-24 11:48:52 +05:30
Bartok e9c0547423 fix(ts-sdk): preserve customCategories names through key conversion (#5741) 2026-06-24 11:15:19 +05:30
Yash Singh 25bc1b7426 fix(memory): guard against malformed image_url in parse_vision_messages (#5631) 2026-06-24 11:10:36 +05:30
Hrushikesh Yadav fee344db85 fix(chroma): wrap scalar vector_id in list for delete() (#5703) 2026-06-24 10:54:36 +05:30
Bartok b611f69381 fix(chroma): wrap update() ids/embeddings/metadatas in lists (#5757) 2026-06-24 10:54:15 +05:30
Muhammad Furqan c2862831db fix(reranker): log reranking failures instead of swallowing them silently (#5717) 2026-06-24 10:44:37 +05:30
홍찬희 1678e682ee fix(ts-sdk): check message.role instead of content for system messages (#3921) 2026-06-23 17:18:00 +05:30
Bartok ced4af681f fix(claude-plugin): rerank auto-injected memory context by default (#5690) 2026-06-23 16:52:35 +05:30
Hrushikesh Yadav 565db27121 fix(milvus,baidu): sanitize filter values to prevent expression injection (#5746) 2026-06-23 16:51:05 +05:30
Hrushikesh Yadav c0ac9f81fa fix(milvus): wrap scalar vector_id in list for delete() (#5704) 2026-06-23 16:48:47 +05:30
Yash Singh 879c68555c fix(memory): return attributed_to from get/get_all/search (#5629) 2026-06-23 16:43:30 +05:30
Abhishek Chauhan c2e723352e fix(ts-oss): return attributedTo from get/search/getAll (#5675) 2026-06-23 16:42:57 +05:30
Hrushikesh Yadav 716f021df8 fix(pinecone): map all comparison operators in _create_filter() (#5707) 2026-06-23 16:38:28 +05:30
Jiangtian Feng 7fa996261d perf: batch BM25 sparse encoding in Qdrant insert (#5592) 2026-06-23 16:29:16 +05:30
Yash Singh 87bd2d91e0 fix(llms): preserve reasoning fields in base-to-provider config conversion (#5638) 2026-06-23 16:27:43 +05:30
Yash Singh 15a930dac2 fix(llms): pass configured anthropic_base_url to the Anthropic client (#5626) 2026-06-23 16:26:22 +05:30
Bartok fa9abc77a6 fix(ts-oss): honor configured baseURL in AnthropicLLM (#5740) 2026-06-23 16:25:51 +05:30
Hrushikesh Yadav 4e448269bc fix(mongodb): reject dict filter values to prevent NoSQL operator injection (#5748) 2026-06-22 18:09:01 +05:30
Hrushikesh Yadav 29d131f7aa fix(chroma): return None instead of {} from _generate_where_clause for empty filters (#5713) 2026-06-22 12:05:28 +05:30
Bartok 42fe129330 fix(opensearch): return [[]] from list() error path to honor list() contract (#5727) 2026-06-22 11:58:37 +05:30
Hrushikesh Yadav bd5996f41e fix(pinecone): return [[]] from list() error path instead of dict (#5706) 2026-06-22 11:57:52 +05:30
Bartok 299c423213 fix(faiss): return [[]] for uninitialized index to honor list() contract (#5725) 2026-06-22 11:57:06 +05:30
Hrushikesh Yadav ce0531a13e fix(mongodb): wrap list() return in outer list to match interface contract (#5729) 2026-06-22 11:54:16 +05:30
Lucas Kim 513b56159f fix(embeddings): forward embedding_dims to Titan V2 in AWS Bedrock embedder (#5671)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:49:23 +05:30
Yash Singh 1676b3168d fix(server): return 404/400 instead of 502 for not-found and invalid input (#5634) 2026-06-22 11:47:48 +05:30
Davide Leopardi 8a786bf72d fix(azure): stop mutating and corrupting caller messages in content rewrite (#5731) 2026-06-22 11:45:46 +05:30
Yash Singh a48f34cf77 fix(vector_stores): deep-copy Redis DEFAULT_FIELDS so instances keep distinct dims (#5633) 2026-06-22 11:29:25 +05:30
Hrushikesh Yadav 650b734b1b fix: reset() only drops history table, leaving stale messages (#5541) 2026-06-22 11:28:17 +05:30
youneshima 871a1de7d2 docs: fix add memory v3 behavior (#5694) 2026-06-21 12:18:58 -07:00
fran3cc e615cc66de docs: rebrand Keywords AI integration to Respan (#5098)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-20 23:10:15 +05:30
Harsh Vardhan Gupta ca86a164bd fix(pi-agent-plugin): resolve undici CVE-2026-9697 / CVE-2026-9678 (#5669) 2026-06-19 16:21:18 +05:30
Kartik 2ac3f3956a fix(cli): pass telemetry context via stdin instead of argv (#5668)
Co-authored-by: JunghwanNA <70629228+shaun0927@users.noreply.github.com>
2026-06-19 13:57:53 +05:30
Yash Raj Pandey 7a9f03af3f fix(llms,embeddings): repair HTTP proxy support (httpx>=0.28) and preserve proxies in LlmFactory (#5447)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-19 13:45:21 +05:30
Yash Singh 48f1d6f010 fix(reranker): clamp out-of-range LLM scores instead of mis-parsing them (#5635) 2026-06-19 12:49:50 +05:30
Yash Singh f0ccd99924 fix(vertex): pass required vectors arg in list and similarity search (#5627) 2026-06-19 12:44:48 +05:30
ly-wang19 c5971193a2 fix(vector_stores): return None from Redis.get() for missing IDs (#5625)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-19 12:25:24 +05:30
Yash Singh ff53fd60b7 fix(graph): keep distinct entities that share a substring prefix (#5630) 2026-06-19 12:23:15 +05:30
Yash Singh ffa334537a fix(client): check HTTP status before parsing ping response in _validate_api_key (#5639) 2026-06-19 12:19:13 +05:30
Yash Singh bd7ce2c13c fix(vector_stores): drop stray print in Weaviate list_cols (#5637) 2026-06-19 12:07:10 +05:30
Yash Singh 5d767219ff fix(reranker): export all five rerankers from package root (#5636) 2026-06-19 12:06:12 +05:30
Haochen 6b744845c3 fix(oss-ts): preserve message roles in extraction input so assistant facts aren't attributed to the user (#5643) 2026-06-19 09:17:52 +05:30
Yash Singh 5b4478458b fix(server): return 404 not 500 for malformed api key id on revoke (#5640) 2026-06-18 17:45:05 +05:30
Bartok 1751e7bff9 fix(ts-oss): reject empty/blank messages in Memory.add() to prevent hallucinated memories (#5545) 2026-06-18 17:36:19 +05:30
Bartok 7ae6a8c36a fix(memory): guard entity embed_batch count mismatch in v3 add pipeline (#5604)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-18 17:22:44 +05:30
Harsh Vardhan Gupta 1dcee153b9 fix(deps): patch js-yaml, ai, python-dotenv vulnerabilities (CVE-2026-53550, CVE-2025-48985, CVE-2026-28684) (#5641)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 17:13:15 +05:30
Md. Mamun Hossain 4065e846f6 fix(ts-sdk): prevent hallucinated memories on empty messages payload … (#5613) 2026-06-18 17:01:02 +05:30
Hrushikesh Yadav 466249113c fix: async delete_all race condition corrupts entity store linked_memory_ids (#5553) 2026-06-18 16:57:03 +05:30
Alok Tripathi 3e2ae734e7 feat(embeddings): add native embed_batch to 5 embedders (LMStudio, Together, HuggingFace, VertexAI, GoogleGenAI) (#5609) 2026-06-18 16:46:31 +05:30
Harsh Vardhan Gupta 96b31c4bc0 fix(form-data): upgrade to >=4.0.6 across pnpm workspaces (CVE-2026-12143) (#5618)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:44:05 +05:30
Rocke Dong 0117d5838b fix(server): use 127.0.0.1 in dashboard healthcheck to avoid IPv6 localhost resolution (#5612) 2026-06-18 11:53:06 +05:30
Abhishek Chauhan 42a3b4043c fix(ts-sdk): preserve user metadata keys across the case-conversion round-trip (#5515) 2026-06-18 11:35:20 +05:30
Kartik 158e9111cb chore: update changelog, bump SDK versions to Python 2.0.7 and TypeScript 3.0.9 (#5615) 2026-06-17 21:45:28 +05:30
ChrisFloofyKitsune 9ed1983b85 refactor(opencode): use existing mem0 SDK instead of delegating to MCP, load skills properly instead of dumping them in .opencode (#5323)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-17 21:18:39 +05:30
Yash Raj Pandey 703e8a035d fix: FAISS filtered search drops over-fetched candidates before filtering (#5453)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-17 16:34:27 +05:30
Hrushikesh Yadav 7ed2faab84 fix: api_error_handler silently drops return values from async methods (#5540) 2026-06-17 14:46:08 +05:30
Abhishek Chauhan 0d66d3d127 fix(ts-sdk): preserve user-defined schema keys in createMemoryExport (#5594) 2026-06-17 14:40:33 +05:30
Lucas Kim 137b7519f7 fix(embeddings): honor aws_session_token in AWS Bedrock embeddings (#5566)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:36:33 +05:30
Alok Tripathi e34f5835bd feat(embeddings): add native embed_batch to OllamaEmbedding (#5415)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-17 14:12:53 +05:30
Yash Raj Pandey f122eb7c65 fix(weaviate): pass embedding dims in reset() so it does not crash (#5570) 2026-06-17 13:29:13 +05:30
mintlify[bot] a5123b8a5e docs: tighten Graph Memory description for SEO (#5603)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-06-17 05:10:38 +00:00
rudrajmehta-mem0 6aa9bffa55 docs: reinstate graph memory terminology (native entity linking) (#5601) 2026-06-16 21:22:55 -07:00
Aayush Soni d772f9a961 feat: support Gemini via Vertex AI as LLM provider (#4030)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-16 16:59:06 +05:30
Yash Raj Pandey 7c841a2bce fix(redis): do not crash on empty or None filters in search and list (#5446)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-16 16:10:57 +05:30
Hrushikesh Yadav 6a6dfb4935 fix(huggingface): use self.config instead of raw config parameter (#5538) 2026-06-16 15:51:27 +05:30
Hrushikesh Yadav 8b370def80 fix: AsyncMemory.reset() does not reset entity store (#5535) 2026-06-16 15:50:30 +05:30
Hrushikesh Yadav d46464282c fix(pinecone): hybrid search crashes when filters is None (#5533) 2026-06-16 15:49:26 +05:30
Hrushikesh Yadav bb4a239cb1 fix(mongodb): reset() passes wrong argument to create_col() (#5532) 2026-06-16 15:48:45 +05:30
Hrushikesh Yadav e30f0d91fe fix(weaviate): reset() crashes with missing vector_size argument (#5531) 2026-06-16 15:45:47 +05:30
Hrushikesh Yadav 9f34e858c7 fix(ollama): json format mutates caller's messages list in-place (#5539) 2026-06-16 15:37:22 +05:30
Bartok 94bbc13de0 fix(memory): skip messages without a content key in message parsers (#5575) 2026-06-16 15:29:53 +05:30
Hrushikesh Yadav a2f01a8fcc fix: async delete_all aborts on first error, leaving partial deletion (#5529) 2026-06-16 11:59:33 +05:30
Hrushikesh Yadav 30d172e826 fix: omit None config values from Gemini GenerateContentConfig (#5528) 2026-06-16 11:54:42 +05:30
ly-wang19 bb69b036b5 fix(vector_stores): return None from get() for missing IDs (milvus/weaviate/supabase) (#5562)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-16 11:52:59 +05:30
Hrushikesh Yadav b55c51e004 fix(anthropic): tool_choice format and tool response parsing (#5537)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 17:28:45 +05:30
Harsh Vardhan Gupta 4492e75d04 fix(deps): bump esbuild >=0.28.1 across all npm packages (#5563)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:04:11 +05:30
Hrushikesh Yadav 4d949022f2 fix: preserve custom metadata fields during memory update (#5480) 2026-06-15 16:29:44 +05:30
ly-wang19 3ef034a9e4 fix(vector_stores): return None from ChromaDB.get() for missing IDs (#5561)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-15 16:11:03 +05:30
Yash Singh b90e3c0b76 fix(reranker): respect config.top_k in Cohere and ZeroEntropy fallback paths (#5560) 2026-06-15 16:10:00 +05:30
ly-wang19 a8eeddde64 fix(llms): honor reasoning-model params in AzureOpenAIStructuredLLM (#5548)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
2026-06-15 16:07:19 +05:30
Hrushikesh Yadav 09a9e34382 fix(litellm): function-calling check blocks all calls on non-tool models (#5536) 2026-06-15 15:59:46 +05:30
anish 66c4394b40 fix(pyproject): rename vector_stores extra to vector-stores for PEP 503/508 compliance (#4934)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 12:38:23 +05:30
ly-wang19 32575a65fc fix(llms): honor reasoning-model params in OpenAIStructuredLLM (#5458)
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-15 12:36:15 +05:30
Yash Singh 66901d7393 fix(llms): accept and forward **kwargs in Together/LangChain/Sarvam providers (#5556) 2026-06-15 12:23:04 +05:30
Hrushikesh Yadav a1eefc31bc fix(bedrock): use dict literal instead of set in AI21 response parse default (#5527) 2026-06-15 12:09:34 +05:30
Davide Leopardi de471799d1 fix(llms): send max_completion_tokens for the GPT-5 family across providers (#5547) 2026-06-15 12:04:19 +05:30
Rod Boev 3951ad4705 fix(openclaw): reduce skills-mode triage prompt footprint (#5502) 2026-06-15 11:18:39 +05:30
Kartik 9315e3036f chore: retire in-repo evaluation/ in favor of mem0ai/memory-benchmarks (#5520) 2026-06-14 00:43:02 +05:30
Kartik b3ede5b7c0 chore: update changelog, bump SDK and package versions to 3.0.8 and 2.0.6 (#5522) 2026-06-13 20:59:53 +05:30
youneshima 3553fc79dd feat(memory): add OSS-to-Platform notices (#5494) 2026-06-13 18:34:20 +05:30
Kartik f322cf82b9 chore: consolidate cookbooks/ into an indexed examples/ directory (#5517) 2026-06-13 18:25:50 +05:30
Kartik 73c975ba68 chore: bump version to 0.1.3, update mem0ai to ^3.0.7, and adjust CHANGELOG (#5521) 2026-06-13 18:10:50 +05:30
Kartik 931d579ba5 chore(openclaw): release v1.0.13 and backfill v1.0.12 changelog (#5519) 2026-06-13 17:59:04 +05:30
Kartik f4773a0baf fix(mem0-plugin): accurate per-editor telemetry attribution + OpenCode telemetry (#5518) 2026-06-13 16:48:29 +05:30
Kartik 06d33f6cc4 fix: relax flaky entity boost parallelism timing threshold (#5511) 2026-06-12 21:07:47 +05:30
Hrushikesh Yadav 8f3b60f3e1 fix: prevent crash in parse_vision_messages when vision is disabled (#5487) 2026-06-12 20:41:18 +05:30
Harsh Vardhan Gupta a6e27dcc9c fix(@mem0/community): upgrade @langchain/community to ^1.1.18 (CVE-2026-27795, CVE-2026-26019) (#5510) 2026-06-12 20:07:03 +05:30
Yufeng He 4f10c986b5 fix: expose Qdrant https option (#5380) 2026-06-12 20:06:26 +05:30
mjzcng 821152bd14 Fix OpenClaw Mem0 custom categories payload (#5345)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-12 19:48:53 +05:30
youneshima f48b133101 feat(skills): add mem0-oss-to-platform migration skill (#5455)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-12 19:48:37 +05:30
UmranPros 1d56f85705 fix(cli): resolve Windows environment compatibility issues in python CLI tests (#5464)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-12 19:40:26 +05:30
Hrushikesh Yadav ced852033b fix: return 400 instead of 502 for invalid search filters (#5482) 2026-06-12 19:35:24 +05:30
Yufeng He b9ad8fa8b2 fix(openclaw): skip runtime setup during metadata registration (#5383) 2026-06-12 19:32:59 +05:30
Yufeng He e3f5ce7b41 fix: use valid S3 entity index names (#5416)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-12 19:22:32 +05:30
Kartik f681889b14 fix(pi-agent-plugin): make command results visible and relevance-filtered (#5504) 2026-06-12 19:13:31 +05:30
Kartik b5ec46be5b fix(plugin): guard bare $USER refs in on_session_start.sh for Windows (#5492) 2026-06-12 19:13:19 +05:30
Harsh Vardhan Gupta 168ad358d5 fix(deps): resolve all open MEDIUM Dependabot alerts (npm overrides + Python pins) (#5489)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 15:15:26 +05:30
Kartik 2c796d144f refactor: consolidate agent/editor plugins under integrations/ (#5491)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-12 10:31:35 +05:30
Rocke Dong c676c2c458 fix(dashboard): pin pnpm to 10.34.2 so docker build works on node:20-alpine (#5483)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-11 22:45:52 +05:30
Oleg Ovcharuk b36847622d fix(langchain): search() crashes with TypeError when score is None (#5072)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-11 22:34:06 +05:30
Hrushikesh Yadav 32c8849044 fix: remove dead _process_config method in Memory and AsyncMemory (#5486) 2026-06-11 22:26:53 +05:30
Hrushikesh Yadav 2dd2872c08 fix: use 'is not None' instead of truthiness for vector/payload in pgvector update (#5488) 2026-06-11 22:12:38 +05:30
Harshit Anand cf268da19d fix(demo): guard against undefined data in useMemories hook (v2 async response) (#5029)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-11 21:50:35 +05:30
Sense_wang 7a5df64746 fix: allow dashboard refresh cookie on http deployments (#5026) 2026-06-11 21:29:54 +05:30
Eldar Shlomi 4c41f6deeb fix(vector-stores): index Valkey 'memory' field as TEXT not TAG (#5443)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-11 21:28:42 +05:30
Hrushikesh Yadav f84aa1eb31 fix: implement $not filter support in ChromaDB vector store (#5485) 2026-06-11 20:12:39 +05:30
Saket Aryan 9226ee2229 ci: aggregate all PR testing behind a single required CI Gate workflow (#5476) 2026-06-11 15:24:51 +05:30
Kartik 8399b088a5 chore: release Python SDK v2.0.5 and TypeScript SDK v3.0.7 (#5470) 2026-06-10 22:17:16 +05:30
Saket Aryan 437f0b5495 ci: route all release publishing through a single Release Router workflow (#5475) 2026-06-10 22:16:58 +05:30
Saket Aryan 0ffaffa88c fix(pi-agent-plugin): correct repository.url for npm provenance validation (#5473) 2026-06-10 21:29:16 +05:30
Saket Aryan 433ff494f1 chore(pi-agent-plugin): bump version to 0.1.1 (#5471) 2026-06-10 21:16:55 +05:30
Saket Aryan de03c52ed3 ci: add CI and CD workflows for pi-agent-plugin (#5469) 2026-06-10 21:11:39 +05:30
Abhishek Chauhan b4a50e3dc8 feat(vercel-ai-sdk): migrate to Vercel AI SDK v6 (#4741)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 18:41:15 +05:30
shafdev b819d95d18 fix(pgvector): use open=False to prevent ConnectionPool hang in Docker (#5155)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 17:15:53 +05:30
Sense_wang 3ac1c9452c fix(vector-stores): filter S3 vector list results (#5018)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-10 16:55:31 +05:30
Gaurav Dubey d6347f6660 fix(vector_stores): pass namespace as top-level kwarg to UpstashVector query_many (#5202) 2026-06-10 16:41:26 +05:30
Hrushikesh Yadav e769502baa feat: warn when hybrid search silently degrades to semantic-only (#5444) 2026-06-10 16:26:48 +05:30
Aarkin Karnik 652193d599 fix(llms/xai): forward tools, add XAIConfig, parse tool_calls (#5190) 2026-06-10 12:46:27 +05:30
Chirag Arora a86c87236d fix(server): forward explain in REST search (#5423) 2026-06-10 12:15:31 +05:30
Kartik 2274b5acad feat: add @mem0/pi-agent-plugin for Pi Agent memory (#5459) 2026-06-10 01:04:09 +05:30
Harsh Vardhan Gupta 9b0705c345 fix(deps): bump mem0ai 3.0.3→3.0.6 in openclaw to remediate axios CVEs (#5460) 2026-06-09 19:00:59 +05:30
Atahan Yıldırım d31fa168eb mem0-plugin hooks: honor auto_save=false in capture entry points (#5450) 2026-06-08 21:24:24 +05:30
Ritwij Aryan Parmar f32eb4406b fix(memory): reject empty search queries (#5258) 2026-06-08 21:11:11 +05:30
youneshima 366945965d feat(server): add information on self-hosted dashboard (#5325) 2026-06-06 03:18:43 +05:30
youneshima 6702fa3e3e docs: match migration callout color styling (#5157) 2026-06-05 14:40:24 -07:00
Chirag Arora a44855af9e feat(memory): add search score explanations (#5102)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 21:30:10 +05:30
Kartik d817aa9c12 fix(oss): parallelize entity boost searches in Memory.search (#5377) 2026-06-05 21:03:42 +05:30
Kartik 7ac8ab154b fix(vector_stores): normalize scores to similarity (higher = better) across all backends (#5391) 2026-06-05 19:13:26 +05:30
Kartik b00a1a1065 docs(google-adk): rewrite integration guide to use official ADK MemoryService APIs (#5392)
Co-authored-by: Nishar <nishar@dayos.com>
Co-authored-by: Nishar Miya <miyannishar786@gmail.com>
2026-06-05 19:13:00 +05:30
Dominik K. 2e90ed4f78 docs: add Neon vector store guide (#5119)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 19:06:59 +05:30
Bartok 069ea0887c fix(llms): add is_reasoning_model config override for versioned deployments (#5327)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-05 18:42:56 +05:30
Kartik ae7f406265 fix(server): harden self-hosted server — pgvector upgrade, admin auth, endpoint security (#5360) 2026-06-05 16:35:12 +05:30
Harsh Vardhan Gupta 90f2d24e83 fix(deps): upgrade vitest 1.5→4.1 + vite 6 to patch CVE-2026-47429 (#5375)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 02:54:00 +05:30
Pragnyan Ramtha 64b9646e7d fix(ts): request float OpenAI embeddings (#5170)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-04 20:29:53 +05:30
Ren yiwei 866888df41 Fix pgvector sslmode handling for PostgreSQL URIs (#5308) 2026-06-04 19:32:47 +05:30
Abhinav 95b6f95f7b fix: replace mutable default arguments with None sentinels (B006) (#5302) 2026-06-04 18:53:03 +05:30
Kartik 74771b4e76 feat(mem0-plugin): file-context injection, stop-hook summaries & activity timeline (#5346) 2026-06-03 00:55:07 +05:30
Harsh Vardhan Gupta 8e65ce915d fix(deps): remediate high-severity vulnerabilities in npm packages (#5294) 2026-06-02 01:36:29 +05:30
Kartik a3154d59e5 fix(docs): fix broken metadata filtering examples (#5317) 2026-05-30 20:16:56 +05:30
Kartik 1019f0e17c feat(mem0-plugin): auto coding categories, global search, OpenCode parity (#5300) 2026-05-30 00:18:46 +05:30
Saket Aryan 9328c36a46 chore(opencode-plugin): bump to 0.1.1 to test CI/CD publish flow (#5288) 2026-05-28 20:35:06 +05:30
Saket Aryan eb4afc6ef7 ci(opencode-plugin): add build & publish workflows for @mem0/opencode-plugin (#5287)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:20:05 +05:30
Kartik fea748d7e6 fix(antigravity-plugin): remove unnecessary skills symlink from install steps (#5283) 2026-05-28 13:49:02 +05:30
Kartik e83297f150 fix(antigravity-plugin): fix install commands to include skills and scripts (#5282) 2026-05-28 13:28:13 +05:30
rudrajmehta-mem0 eaca45dcdb docs: remove deprecated Graph Memory references (#5277) 2026-05-28 12:58:15 +05:30
Kartik add6aad40b fix(opencode-plugin): fix tsconfig, add publishConfig and bun lockfile (#5273) 2026-05-28 11:28:21 +05:30
Kartik 116c439b1d fix(opencode-plugin): rename package to @mem0/opencode-plugin (#5272) 2026-05-28 00:16:45 +05:30
Kartik 49b7953c44 fix(opencode-plugin): add plugin array to bundled opencode.json (#5271) 2026-05-27 23:46:05 +05:30
Kartik 3e6ab39429 feat(mem0-plugin): add OpenCode & Antigravity plugins, CC parity, docs cleanup (#5268) 2026-05-27 23:17:37 +05:30
Chaithanya Kumar 75a37ec93d feat(sdk): add delete_linked option to MemoryClient.delete (Python + TS) (#5270)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:38:16 +05:30
youneshima 88934304c6 fix(cli-node): forward --no-infer flag to add (#5267)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:05:30 +05:30
youneshima 098a599579 fix: refresh stale mem0ai pins in examples and openclaw (#5212)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 23:08:45 +05:30
Kartik 5a2201d76b fix(pgvector): boolean filter casing, LIKE escaping, and TS scalar coercion (#5264) 2026-05-26 23:03:47 +05:30
Kartik ad736d9a06 fix(pgvector, server): add rich filter operators and fix /search 502 (#5263) 2026-05-26 22:47:41 +05:30
Kartik 7f6d46050e feat(mem0-plugin): v0.2.6 — fix memory visibility, remove redundant hooks, reduce latency (#5257) 2026-05-26 19:37:51 +05:30
Kartik f9c52baf21 feat(mem0-plugin): v0.2.5 — fix identity scoping, skill param bugs, add checklists (#5247) 2026-05-25 19:21:10 +05:30
Kartik 0da3359a1a feat(mem0-plugin): v0.2.4 — fix stats, session scoping, reduce noise, improve skill discovery (#5244) 2026-05-24 22:13:26 +05:30
Kartik 6b9707fee9 docs(mem0-plugin): add changelog entries for v0.2.1, v0.2.2, v0.2.3 (#5241) 2026-05-23 17:35:48 +05:30
Kartik 99beb007ab feat(mem0-plugin): improve auto-triggering — pre-fetch, dedup, skill enforcement v0.2.3 (#5237) 2026-05-23 17:20:54 +05:30
Kartik 16a7702d09 fix(mem0-plugin): v0.2.2 (#5234) 2026-05-22 21:47:14 +05:30
Saket Aryan 53a3998873 docs: fix capitalization in introduction hero subtitle (#5232)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:28:01 +05:30
Saket Aryan 08aa143db3 chore: extract embedchain to mem0ai/embedchain-archive (#5230) 2026-05-22 20:02:19 +05:30
Kartik ac141fdafe chore(mem0-plugin): bump marketplace versions to v0.2.1 (#5231) 2026-05-22 19:55:21 +05:30
Kartik b1188d6044 fix(mem0-plugin): reduce memory noise, match openclaw storage pattern (#5229) 2026-05-22 19:25:59 +05:30
Kartik 0d61af60c2 feat(mem0-plugin): plugin v0.2.1 — Tiers 1-8 + PostHog telemetry + review fixes (#5215) 2026-05-22 19:01:14 +05:30
Kartik 58696e4bd4 fix(ci): remove deprecated embedchain CI and fix required check reporting (#5210)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-05-22 12:41:55 +05:30
Harsh Vardhan Gupta 8b11e0787a fix(deps): address additional CVEs in langchain, starlette, mcp, cryptography, and lodash (#5219) 2026-05-22 01:18:41 +05:30
Harsh Vardhan Gupta 09dc74d61a fix(deps): bump vulnerable dependencies across Python and TypeScript. (#5217) 2026-05-21 23:28:35 +05:30
Kartik 606ede7c0a feat(mem0-plugin): onboarding, project scoping, identity banner (#5207) 2026-05-20 22:30:07 +05:30
Mragank Shekhar edd1b3e2f2 feat(cli): add mem0 whoami + mem0 agent-rush subcommands (#5199) 2026-05-20 19:05:12 +05:30
Prathamesh 74d043731b docs(llms.txt): lead with signup flow and CLI install (#5159)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:25:24 -07:00
Mragank Shekhar 843ab82905 chore: trigger Mintlify redeploy for #5152 docs (#5185) 2026-05-18 16:22:48 +05:30
youneshima 79793b0d2e docs: link platform migration guide from readme (#5171) 2026-05-16 19:13:47 -07:00
Mragank Shekhar 5f7ace2aef docs: promote "Sign up as an agent" + drop plugin-sync prose (#5152) 2026-05-16 21:52:34 +05:30
rudrajmehta-mem0 219b1a6f3d docs(faqs): add self-serve account deletion FAQ (#5158)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 03:16:42 +05:30
youneshima 57c8468ce6 docs: add hosted Qdrant migration script (#5156) 2026-05-16 01:46:20 +05:30
Chaithanya Kumar ddee5f8671 feat(ts): expose latestOnly on hosted memory reads (#5153) 2026-05-15 23:15:51 +05:30
Deshraj Yadav fbce5fab14 docs: remove unverified SOC2/GDPR compliance claims (#5150) 2026-05-14 23:22:37 -07:00
Mragank Shekhar 6a1597c6fb fix(plugin): drop API-key-derived user_id, restore $USER fallback (#5147) 2026-05-15 02:37:55 +05:30
Mragank Shekhar c9e8482a35 fix(docs): Mintlify <5s parse error + add Agent Mode to /platform/cli (#5145) 2026-05-14 21:49:08 +05:30
Mragank Shekhar e602923751 feat(cli): Agent Mode bootstrap + claim flow (Python + Node) (#5123) 2026-05-14 20:35:25 +05:30
1347 changed files with 61852 additions and 62406 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
"name": "mem0",
"source": {
"source": "local",
"path": "./mem0-plugin"
"path": "./integrations/mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
+2 -2
View File
@@ -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.1.2"
"version": "0.2.12"
}
]
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "mem0-plugins",
"interface": {
"displayName": "Mem0 Plugins"
},
"plugins": [
{
"name": "mem0",
"source": {
"source": "local",
"path": "./integrations/mem0-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+2 -2
View File
@@ -10,9 +10,9 @@
"plugins": [
{
"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.1.1"
"version": "0.2.12"
}
]
}
+18 -4
View File
@@ -1,18 +1,33 @@
name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI
# Dispatched by release.yml (Release Router) when a release tagged v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. v1.2.3)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
if: startsWith(github.event.release.tag_name, 'v')
# Pure SDK version tags only (v1.2.3) — excludes package-prefixed tags
# like vercel-ai-v* that also start with 'v'
if: startsWith(inputs.tag, 'v') && !contains(inputs.tag, '-v')
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- uses: actions/checkout@v2
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v2
@@ -39,7 +54,6 @@ jobs:
# packages_dir: dist/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags/v')
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages_dir: dist/
+171
View File
@@ -0,0 +1,171 @@
name: CI Gate
# Single required status check for all PRs.
#
# Path-filtered CI workflows can't be marked as required in branch
# protection: on a PR that doesn't touch their paths they never report, and
# the required check hangs at "Expected" forever. This gate solves that. It
# runs on every PR, detects which packages changed, calls only the relevant
# package CI workflows (as reusable workflows), and the final "CI Gate" job
# reports the aggregate result — success when every invoked pipeline passed
# (skipped pipelines are fine), failure when any failed.
#
# Branch protection should require exactly one status check: "CI Gate".
#
# Package CI workflows keep their own push-to-main and workflow_dispatch
# triggers; only their pull_request triggers moved here. To wire in a new
# package: add a filter under the `changes` job, a call job that `uses:` the
# package workflow, and list the call job in the gate's `needs`.
on:
pull_request:
concurrency:
group: ci-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
jobs:
changes:
name: Detect changed packages
runs-on: ubuntu-latest
outputs:
python_sdk: ${{ steps.filter.outputs.python_sdk }}
ts_sdk: ${{ steps.filter.outputs.ts_sdk }}
cli_python: ${{ steps.filter.outputs.cli_python }}
cli_node: ${{ steps.filter.outputs.cli_node }}
openclaw: ${{ steps.filter.outputs.openclaw }}
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
# Each filter mirrors the package workflow's old pull_request
# paths, plus the package workflow file itself and this gate file
# (changing either must re-exercise the pipeline).
filters: |
python_sdk:
- 'mem0/**'
- 'tests/**'
- 'pyproject.toml'
- '.github/workflows/ci.yml'
- '.github/workflows/ci-gate.yml'
ts_sdk:
- 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml'
- '.github/workflows/ci-gate.yml'
cli_python:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
- '.github/workflows/ci-gate.yml'
cli_node:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
- '.github/workflows/ci-gate.yml'
openclaw:
- 'integrations/openclaw/**'
- '.github/workflows/openclaw-checks.yml'
- '.github/workflows/ci-gate.yml'
opencode_plugin:
- 'integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
pi_agent_plugin:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
docs_llms_txt:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
- 'scripts/check-llms-txt-coverage.py'
- 'scripts/llms-txt-ignore.txt'
- '.github/workflows/docs-llms-txt-check.yml'
- '.github/workflows/ci-gate.yml'
python-sdk:
name: Python SDK
needs: changes
if: needs.changes.outputs.python_sdk == 'true'
uses: ./.github/workflows/ci.yml
secrets: inherit
ts-sdk:
name: TypeScript SDK
needs: changes
if: needs.changes.outputs.ts_sdk == 'true'
uses: ./.github/workflows/ts-sdk-ci.yml
secrets: inherit
cli-python:
name: Python CLI
needs: changes
if: needs.changes.outputs.cli_python == 'true'
uses: ./.github/workflows/cli-python-ci.yml
secrets: inherit
cli-node:
name: Node CLI
needs: changes
if: needs.changes.outputs.cli_node == 'true'
uses: ./.github/workflows/cli-node-ci.yml
secrets: inherit
openclaw:
name: OpenClaw
needs: changes
if: needs.changes.outputs.openclaw == 'true'
uses: ./.github/workflows/openclaw-checks.yml
secrets: inherit
opencode-plugin:
name: OpenCode Plugin
needs: changes
if: needs.changes.outputs.opencode_plugin == 'true'
uses: ./.github/workflows/opencode-plugin-checks.yml
secrets: inherit
pi-agent-plugin:
name: Pi Agent Plugin
needs: changes
if: needs.changes.outputs.pi_agent_plugin == 'true'
uses: ./.github/workflows/pi-agent-plugin-checks.yml
secrets: inherit
docs-llms-txt:
name: docs llms.txt
needs: changes
if: needs.changes.outputs.docs_llms_txt == 'true'
uses: ./.github/workflows/docs-llms-txt-check.yml
secrets: inherit
gate:
name: CI Gate
needs:
- changes
- python-sdk
- ts-sdk
- cli-python
- cli-node
- openclaw
- opencode-plugin
- pi-agent-plugin
- docs-llms-txt
if: always()
runs-on: ubuntu-latest
steps:
- name: Evaluate pipeline results
env:
NEEDS: ${{ toJSON(needs) }}
run: |
echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
failed=$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result == "failure" or .value.result == "cancelled") | .key] | join(", ")')
if [ -n "$failed" ]; then
echo "::error::Failing pipelines: $failed"
exit 1
fi
echo "All pipelines relevant to this change passed."
+18 -59
View File
@@ -1,20 +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]
paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- '.github/workflows/**'
- 'pyproject.toml'
pull_request:
paths:
- 'mem0/**'
- 'tests/**'
- 'embedchain/**'
- 'pyproject.toml'
workflow_call:
jobs:
changelog_check:
@@ -60,9 +51,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
mem0_changed: ${{ steps.filter.outputs.mem0 }}
embedchain_changed: ${{ steps.filter.outputs.embedchain }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v2
id: filter
with:
@@ -70,25 +60,28 @@ jobs:
mem0:
- 'mem0/**'
- 'tests/**'
- '.github/workflows/**'
- '.github/workflows/ci.yml'
- 'pyproject.toml'
embedchain:
- 'embedchain/**'
build_mem0:
needs: check_changes
if: needs.check_changes.outputs.mem0_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Skip — no relevant changes
if: needs.check_changes.outputs.mem0_changed != 'true'
run: echo "No changes in mem0/, tests/, pyproject.toml, or ci.yml — skipping"
- uses: actions/checkout@v4
if: needs.check_changes.outputs.mem0_changed == 'true'
- name: Set up Python ${{ matrix.python-version }}
if: needs.check_changes.outputs.mem0_changed == 'true'
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Clean up disk space
if: needs.check_changes.outputs.mem0_changed == 'true'
run: |
df -h
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
@@ -96,61 +89,27 @@ jobs:
sudo docker builder prune -a
df -h
- name: Install Hatch
if: needs.check_changes.outputs.mem0_changed == 'true'
run: pip install hatch
- name: Load cached venv
if: needs.check_changes.outputs.mem0_changed == 'true'
id: cached-hatch-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install GEOS Libraries
if: needs.check_changes.outputs.mem0_changed == 'true'
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
- name: Install dependencies
if: needs.check_changes.outputs.mem0_changed == 'true' && steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
run: |
pip install --upgrade pip
pip install -e ".[test,graph,vector_stores,llms,extras]"
pip install ruff
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Linting
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make lint
- name: Run tests and generate coverage report
if: needs.check_changes.outputs.mem0_changed == 'true'
run: make test
build_embedchain:
needs: check_changes
if: needs.check_changes.outputs.embedchain_changed == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install Hatch
run: pip install hatch
- name: Load cached venv
id: cached-hatch-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-embedchain-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install dependencies
run: cd embedchain && make install_all
if: steps.cached-hatch-dependencies.outputs.cache-hit != 'true'
- name: Run Formatting
run: |
mkdir -p embedchain/.ruff_cache && chmod -R 777 embedchain/.ruff_cache
cd embedchain && hatch run format
- name: Lint with ruff
run: cd embedchain && make lint
- name: Run tests and generate coverage report
run: cd embedchain && make coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
file: coverage.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+18 -4
View File
@@ -1,13 +1,25 @@
name: Publish @mem0/cli 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# cli-node-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/cli 📦 to npm
if: startsWith(github.event.release.tag_name, 'cli-node-v')
if: startsWith(inputs.tag, 'cli-node-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -16,6 +28,8 @@ jobs:
working-directory: cli/node
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -38,7 +52,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
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
+3 -4
View File
@@ -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:
+16 -3
View File
@@ -1,13 +1,24 @@
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged cli-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish mem0-cli 📦 to PyPI
if: startsWith(github.event.release.tag_name, 'cli-v')
if: startsWith(inputs.tag, 'cli-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -16,6 +27,8 @@ jobs:
working-directory: cli/python
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v5
+3 -4
View File
@@ -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:
+3 -6
View File
@@ -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:
+20 -6
View File
@@ -1,21 +1,35 @@
name: Publish @mem0/openclaw-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# openclaw-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/openclaw-mem0 📦 to npm
if: startsWith(github.event.release.tag_name, 'openclaw-v')
if: startsWith(inputs.tag, 'openclaw-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: openclaw
working-directory: integrations/openclaw
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -28,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
@@ -38,7 +52,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
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
+16 -17
View File
@@ -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)
+58
View File
@@ -0,0 +1,58 @@
name: Publish @mem0/opencode-plugin 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# opencode-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. opencode-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/opencode-plugin 📦 to npm
if: startsWith(inputs.tag, 'opencode-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build
run: bun run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -0,0 +1,39 @@
name: opencode-plugin checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/mem0-plugin/.opencode-plugin/**'
- '.github/workflows/opencode-plugin-checks.yml'
workflow_call:
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/mem0-plugin/.opencode-plugin
steps:
- uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Type check
run: bun run type-check
- name: Build
run: bun run build
- name: Verify dist output exists
run: |
test -f dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
+60
View File
@@ -0,0 +1,60 @@
name: Publish @mem0/pi-agent-plugin 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# pi-agent-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. pi-agent-v0.1.1)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/pi-agent-plugin 📦 to npm
if: startsWith(inputs.tag, 'pi-agent-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/pi-agent-plugin
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -0,0 +1,92 @@
name: pi-agent-plugin checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Type check
run: cd integrations/pi-agent-plugin && pnpm exec tsc --noEmit
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Run tests
run: cd integrations/pi-agent-plugin && pnpm exec vitest run
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/pi-agent-plugin/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/pi-agent-plugin && pnpm install --frozen-lockfile
- name: Build
run: cd integrations/pi-agent-plugin && pnpm build
- name: Verify dist output exists
run: |
test -f integrations/pi-agent-plugin/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
test -f integrations/pi-agent-plugin/dist/index.d.ts || (echo "Build output missing: dist/index.d.ts" && exit 1)
test -f integrations/pi-agent-plugin/dist/entry.js || (echo "Build output missing: dist/entry.js" && exit 1)
test -f integrations/pi-agent-plugin/dist/entry.d.ts || (echo "Build output missing: dist/entry.d.ts" && exit 1)
+68
View File
@@ -0,0 +1,68 @@
name: Release Router 🚦
# Single entry point for all release publishing.
#
# Package CD workflows no longer listen to release events themselves — this
# router inspects the release tag and dispatches only the matching pipeline,
# so each release produces one routed run instead of one real run plus seven
# skipped ones.
#
# Re-publishing a release (e.g. after fixing registry settings) does NOT
# require deleting and recreating it anymore — manually dispatch the
# package's CD workflow from the tag instead:
#
# gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>
#
# Note: dispatching runs the workflow file as it exists at the given ref, so
# this router can only dispatch tags created after the workflow_dispatch
# conversion landed on main. For older tags, dispatch manually from main.
on:
release:
types: [published]
permissions:
actions: write
jobs:
route:
name: Route ${{ github.event.release.tag_name }} to its CD pipeline
runs-on: ubuntu-latest
steps:
- name: Match tag prefix to CD workflow
id: match
env:
TAG: ${{ github.event.release.tag_name }}
run: |
# Specific package prefixes first; the bare v* (Python SDK) arm
# must stay last so prefixed tags that also start with 'v'
# (vercel-ai-v*) can never be routed to the Python pipeline.
case "$TAG" in
ts-v*) workflow="ts-sdk-cd.yml" ;;
cli-node-v*) workflow="cli-node-cd.yml" ;;
cli-v*) workflow="cli-python-cd.yml" ;;
vercel-ai-v*) workflow="vercel-ai-cd.yml" ;;
openclaw-v*) workflow="openclaw-cd.yml" ;;
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
v*) workflow="cd.yml" ;;
*)
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
exit 1
;;
esac
echo "workflow=$workflow" >> "$GITHUB_OUTPUT"
echo ":outbox_tray: Routed \`$TAG\` → \`$workflow\`" >> "$GITHUB_STEP_SUMMARY"
- name: Dispatch ${{ steps.match.outputs.workflow }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
run: |
# --ref points at the tag so the dispatched run builds (and signs
# provenance for) the exact tagged commit.
gh workflow run "${{ steps.match.outputs.workflow }}" \
--repo "$GITHUB_REPOSITORY" \
--ref "refs/tags/$TAG" \
-f tag="$TAG" \
-f prerelease="${{ github.event.release.prerelease }}"
+17 -4
View File
@@ -1,13 +1,24 @@
name: Publish mem0ai 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged ts-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish mem0ai 📦 to npm
if: startsWith(github.event.release.tag_name, 'ts-v')
if: startsWith(inputs.tag, 'ts-v')
runs-on: ubuntu-latest
permissions:
id-token: write
@@ -16,6 +27,8 @@ jobs:
working-directory: mem0-ts
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -38,7 +51,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
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
+3 -3
View File
@@ -1,14 +1,14 @@
name: TypeScript SDK CI
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main runs remain standalone.
on:
push:
branches: [main]
paths:
- 'mem0-ts/**'
- '.github/workflows/ts-sdk-ci.yml'
pull_request:
paths:
- 'mem0-ts/**'
workflow_call:
jobs:
check_changes:
+20 -6
View File
@@ -1,21 +1,35 @@
name: Publish @mem0/vercel-ai-provider 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# vercel-ai-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
if: startsWith(github.event.release.tag_name, 'vercel-ai-v')
if: startsWith(inputs.tag, 'vercel-ai-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: vercel-ai-sdk
working-directory: integrations/vercel-ai-sdk
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -28,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
@@ -38,7 +52,7 @@ jobs:
- name: Publish to npm
run: |
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
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
+2 -1
View File
@@ -170,7 +170,6 @@ cython_debug/
# Database
db
test-db
!embedchain/embedchain/core/db/
.vscode
.idea/
@@ -190,3 +189,5 @@ eval/
qdrant_storage/
.crossnote
testing.ipynb
.weave/
+4
View File
@@ -0,0 +1,4 @@
[submodule "evaluation"]
path = evaluation
url = https://github.com/mem0ai/memory-benchmarks
branch = main
+68 -42
View File
@@ -12,7 +12,7 @@ This file provides context for AI coding assistants (Claude Code, Cursor, GitHub
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, documentation, and evaluation tooling.
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
### Key Directories
@@ -22,18 +22,18 @@ 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 |
| `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 |
| `embedchain/` | Legacy Embedchain RAG framework (maintained separately, Poetry-based) |
| `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) |
@@ -50,8 +50,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
@@ -74,8 +74,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
@@ -163,10 +163,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
@@ -181,10 +181,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
@@ -246,18 +246,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
@@ -330,7 +331,7 @@ 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 `embedchain/` and `openmemory/` from root config.
- Ruff excludes `openmemory/` from root config.
### TypeScript Conventions
@@ -343,8 +344,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
@@ -382,14 +383,14 @@ Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
- **Plugin:** MCP tools in `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
@@ -403,32 +404,58 @@ 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 |
| Embedchain | `ci.yml` (shared) | PRs on `embedchain/` | Ruff + pytest + coverage on Python 3.9–3.12 |
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
| Workflow | File | Standalone Triggers | Tests |
|----------|------|---------------------|-------|
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
### CD Workflows (automated publishing)
Publishing is routed through a single entry point: **`release.yml` (Release Router)** is the only workflow that listens to `release: published` events. It matches the release tag prefix and dispatches the corresponding package workflow via `workflow_dispatch`, so each release produces exactly one routed run (no skipped runs from the other pipelines).
| Workflow | File | Tag Prefix | Target |
|----------|------|------------|--------|
| Release Router | `release.yml` | (all releases) | dispatches the matching workflow below |
| Python SDK | `cd.yml` | `v*` | PyPI (`mem0ai`) |
| TypeScript SDK | `ts-sdk-cd.yml` | `ts-v*` | npm (`mem0ai`) |
| Python CLI | `cli-python-cd.yml` | `cli-v*` | PyPI (`mem0-cli`) |
| Node CLI | `cli-node-cd.yml` | `cli-node-v*` | npm (`@mem0/cli`) |
| Vercel AI SDK | `vercel-ai-cd.yml` | `vercel-ai-v*` | npm (`@mem0/vercel-ai-provider`) |
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
### Utility Workflows
@@ -576,7 +603,6 @@ N/A
- Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials.
- Modify `embedchain/` unless specifically working on that package — it has its own build system (Poetry).
- Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
+134 -47
View File
@@ -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/`, `openmemory/`, 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!
+2 -1
View File
@@ -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
-221
View File
@@ -1,221 +0,0 @@
# Migration Guide: Upgrading to mem0 1.0.0
## TL;DR
**What changed?** We simplified the API by removing confusing version parameters. Now everything returns a consistent format: `{"results": [...]}`.
**What you need to do:**
1. Upgrade: `pip install mem0ai==1.0.0`
2. Remove `version` and `output_format` parameters from your code
3. Update response handling to use `result["results"]` instead of treating responses as lists
**Time needed:** ~5-10 minutes for most projects
---
## Quick Migration Guide
### 1. Install the Update
```bash
pip install mem0ai==1.0.0
```
### 2. Update Your Code
**If you're using the Memory API:**
```python
# Before
memory = Memory(config=MemoryConfig(version="v1.1"))
result = memory.add("I like pizza")
# After
memory = Memory() # That's it - version is automatic now
result = memory.add("I like pizza")
```
**If you're using the Client API:**
```python
# Before
client.add(messages, output_format="v1.1")
client.search(query, version="v2", output_format="v1.1")
# After
client.add(messages) # Just remove those extra parameters
client.search(query)
```
### 3. Update How You Handle Responses
All responses now use the same format: a dictionary with `"results"` key.
```python
# Before - you might have done this
result = memory.add("I like pizza")
for item in result: # Treating it as a list
print(item)
# After - do this instead
result = memory.add("I like pizza")
for item in result["results"]: # Access the results key
print(item)
# Graph relations (if you use them)
if "relations" in result:
for relation in result["relations"]:
print(relation)
```
---
## Enhanced Message Handling
The platform client (MemoryClient) now supports the same flexible message formats as the OSS version:
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-key")
# All three formats now work:
# 1. Single string (automatically converted to user message)
client.add("I like pizza", user_id="alice")
# 2. Single message dictionary
client.add({"role": "user", "content": "I like pizza"}, user_id="alice")
# 3. List of messages (conversation)
client.add([
{"role": "user", "content": "I like pizza"},
{"role": "assistant", "content": "I'll remember that!"}
], user_id="alice")
```
### Async Mode Configuration
The `async_mode` parameter now defaults to `True` but can be configured:
```python
# Default behavior (async_mode=True)
client.add(messages, user_id="alice")
# Explicitly set async mode
client.add(messages, user_id="alice", async_mode=True)
# Disable async mode if needed
client.add(messages, user_id="alice", async_mode=False)
```
**Note:** `async_mode=True` provides better performance for most use cases. Only set it to `False` if you have specific synchronous processing requirements.
---
## That's It!
For most users, that's all you need to know. The changes are:
- ✅ No more `version` or `output_format` parameters
- ✅ Consistent `{"results": [...]}` response format
- ✅ Cleaner, simpler API
---
## Common Issues
**Getting `KeyError: 'results'`?**
Your code is still treating the response as a list. Update it:
```python
# Change this:
for memory in response:
# To this:
for memory in response["results"]:
```
**Getting `TypeError: unexpected keyword argument`?**
You're still passing old parameters. Remove them:
```python
# Change this:
client.add(messages, output_format="v1.1")
# To this:
client.add(messages)
```
**Seeing deprecation warnings?**
Remove any explicit `version="v1.0"` from your config:
```python
# Change this:
memory = Memory(config=MemoryConfig(version="v1.0"))
# To this:
memory = Memory()
```
---
## What's New in 1.0.0
- **Better vector stores:** Fixed OpenSearch and improved reliability across all stores
- **Cleaner API:** One way to do things, no more confusing options
- **Enhanced GCP support:** Better Vertex AI configuration options
- **Flexible message input:** Platform client now accepts strings, dicts, and lists (aligned with OSS)
- **Configurable async_mode:** Now defaults to `True` but users can override if needed
---
## Need Help?
- Check [GitHub Issues](https://github.com/mem0ai/mem0/issues)
- Read the [documentation](https://docs.mem0.ai/)
- Open a new issue if you're stuck
---
## Advanced: Configuration Changes
**If you configured vector stores with version:**
```python
# Before
config = MemoryConfig(
version="v1.1",
vector_store=VectorStoreConfig(...)
)
# After
config = MemoryConfig(
vector_store=VectorStoreConfig(...)
)
```
---
## Testing Your Migration
Quick sanity check:
```python
from mem0 import Memory
memory = Memory()
# Add should return a dict with "results"
result = memory.add("I like pizza", user_id="test")
assert "results" in result
# Search should return a dict with "results"
search = memory.search("food", user_id="test")
assert "results" in search
# Get all should return a dict with "results"
all_memories = memory.get_all(user_id="test")
assert "results" in all_memories
print("✅ Migration successful!")
```
+23 -1
View File
@@ -86,6 +86,26 @@ See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgra
## 🚀 Quickstart Guide <a name="quickstart"></a>
### Sign up as an agent
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
```bash
# 1. Install
npm install -g @mem0/cli # or: pip install mem0-cli
# 2. Sign up as an agent (replace `claude-code` with your name)
mem0 init --agent --agent-caller claude-code
# 3. Add a memory
mem0 add "I am using mem0"
# 4. Search
mem0 search "am I using mem0"
```
The human owner can claim the account later with `mem0 init --email <their-email>` — same key, memories preserved. Full guide: [Sign up as an agent](https://docs.mem0.ai/platform/agent-signup).
| | Library | Self-Hosted Server | Cloud Platform |
|---|---------|-------------------|----------------|
| **Best for** | Testing, prototyping | Teams running on their own infrastructure | Zero-ops production use |
@@ -133,6 +153,7 @@ See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for config
1. Sign up on [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=readme)
2. Embed the memory layer via SDK or API keys
3. Using hosted Qdrant vectors? See the [Platform migration guide](https://docs.mem0.ai/migration/oss-to-platform) to import them into Mem0 Platform.
### CLI
@@ -165,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
View File
@@ -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, OpenMemory)
- 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.
+4 -2
View File
@@ -503,7 +503,7 @@
},
{
"name": "init",
"description": "Setup wizard for mem0 CLI. Supports email login (--email) or manual API key (--api-key).",
"description": "Setup wizard for mem0 CLI. Supports Agent Mode bootstrap (--agent), email login (--email), or manual API key (--api-key).",
"usage": "mem0 init [OPTIONS]",
"needsBackend": false,
"needsConfig": false,
@@ -516,7 +516,9 @@
{ "name": "user-id", "flags": ["-u", "--user-id"], "type": "string", "default": null, "help": "Default user ID (skip prompt)." },
{ "name": "email", "flags": ["--email"], "type": "string", "default": null, "help": "Login via email verification code." },
{ "name": "code", "flags": ["--code"], "type": "string", "default": null, "help": "Verification code (use with --email for non-interactive login)." },
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." }
{ "name": "force", "flags": ["--force"], "type": "boolean", "default": false, "help": "Overwrite existing config without confirmation." },
{ "name": "agent", "flags": ["--agent"], "type": "boolean", "default": false, "help": "Bootstrap an unattended Agent Mode account (no email required)." },
{ "name": "source", "flags": ["--source"], "type": "string", "default": null, "help": "Channel attribution for signup (e.g. github, hn, ph)." }
]
},
{
+13 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/cli",
"version": "0.2.4",
"version": "0.2.10",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
@@ -40,8 +40,19 @@
"typescript": "^5.4.0",
"tsup": "^8.0.0",
"tsx": "^4.7.0",
"vitest": "^1.5.0",
"vite": "^6.0.0",
"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",
"postcss@<8.5.10": ">=8.5.10",
"esbuild": ">=0.28.1"
}
}
}
+310 -723
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
packages:
- '.'
onlyBuiltDependencies:
- "@biomejs/biome"
- esbuild
overrides:
jws@4.0.0: 4.0.1
langsmith@<0.6.0: ^0.6.0
tar-fs@>=2.0.0 <2.1.4: ^2.1.4
picomatch@<2.3.2: ^2.3.2
"postcss@<8.5.10": ">=8.5.10"
"esbuild": ">=0.28.1"
+32
View File
@@ -0,0 +1,32 @@
/**
* Detect whether the CLI is being invoked from inside an AI-agent context.
*
* Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
* agent runtime env var is present. The return value is a context **trigger
* only** — the canonical agent identity is self-declared by the agent via
* `--agent-caller <name>` (Proof Editor-style) and never sniffed from env
* vars to fill the `agent_caller` field on the APIKey row.
*
* Returns a short name or null. Honest reporting depends on `--agent-caller`;
* this list is just enough to enable the zero-friction auto-bootstrap UX.
*/
const AGENT_CALLER_ENV: ReadonlyArray<readonly [string, readonly string[]]> = [
["claude-code", ["CLAUDECODE", "CLAUDE_CODE"]],
["cursor", ["CURSOR_AGENT", "CURSOR_SESSION_ID"]],
["codex", ["CODEX_CLI", "OPENAI_CODEX"]],
["cline", ["CLINE_AGENT", "CLINE"]],
["continue", ["CONTINUE_AGENT", "CONTINUE_SESSION"]],
["aider", ["AIDER_SESSION"]],
["goose", ["GOOSE_AGENT"]],
["windsurf", ["WINDSURF_AGENT"]],
] as const;
export function detectAgentCaller(): string | null {
for (const [name, envVars] of AGENT_CALLER_ENV) {
if (envVars.some((v) => process.env[v])) {
return name;
}
}
return null;
}
+40 -6
View File
@@ -3,7 +3,7 @@
*/
import type { PlatformConfig } from "../config.js";
import { isAgentMode } from "../state.js";
import { captureNotice, isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
APIError,
@@ -90,7 +90,39 @@ export class PlatformBackend implements Backend {
if (resp.status === 204) {
return {};
}
return resp.json();
const data = await resp.json();
// Pull the unclaimed-Agent-Mode notice out of the body (or the header
// fallback for endpoints returning non-dict / non-dict-leading payloads)
// and stash for end-of-command surfacing.
let notice: string | null = null;
if (
data &&
typeof data === "object" &&
!Array.isArray(data) &&
"mem0_notice" in data
) {
notice = (data as Record<string, unknown>).mem0_notice as string;
// biome-ignore lint/performance/noDelete: intentional strip so downstream consumers don't see duplicate notice
delete (data as Record<string, unknown>).mem0_notice;
} else if (
Array.isArray(data) &&
data.length > 0 &&
typeof data[0] === "object" &&
data[0] !== null &&
"mem0_notice" in data[0]
) {
notice = (data[0] as Record<string, unknown>).mem0_notice as string;
// biome-ignore lint/performance/noDelete: see above.
delete (data[0] as Record<string, unknown>).mem0_notice;
}
if (!notice) {
notice = resp.headers.get("X-Mem0-Notice-Message") ?? null;
}
captureNotice(notice);
return data;
}
async add(
@@ -284,16 +316,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}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
return result;
return results;
}
async ping(): Promise<Record<string, unknown>> {
+285
View File
@@ -0,0 +1,285 @@
/**
* Agent Mode commands — bootstrap (unattended signup) and OTP-based claim.
*/
import readline from "node:readline";
import { colors, printError, printInfo, printSuccess } from "../branding.js";
import { type Mem0Config, saveConfig } from "../config.js";
const { brand, dim } = colors;
const SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
} as const;
export interface BootstrapEnvelope {
api_key: string;
default_user_id: string;
org_id: string;
project_id: string;
mcp_url?: string;
smoke_test_url?: string;
claim_command?: string;
mem0_notice?: string;
}
function isValidEnvelope(v: unknown): v is BootstrapEnvelope {
return (
!!v &&
typeof v === "object" &&
typeof (v as BootstrapEnvelope).api_key === "string" &&
(v as BootstrapEnvelope).api_key.length > 0 &&
typeof (v as BootstrapEnvelope).default_user_id === "string" &&
(v as BootstrapEnvelope).default_user_id.length > 0
);
}
/**
* POST /api/v1/auth/agent_mode/ and mutate config in place.
*
* @param config - Mem0Config mutated in place with the new platform values.
* @param source - `--source` flag passthrough (analytics tag, free-form).
* @param agentCaller - Self-declared agent identity passed via `--agent-caller`
* (e.g. `claude-code`, `cursor`). May be null when the caller omitted the
* flag; the agent can backfill later via `mem0 identify <name>`. Sent to the
* backend in the request body and saved into `platform.agentCaller` for
* local introspection.
*/
export async function bootstrapViaBackend(
config: Mem0Config,
{
source,
agentCaller,
}: { source?: string | null; agentCaller?: string | null } = {},
): Promise<void> {
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
/\/+$/,
"",
);
const body: Record<string, unknown> = {};
if (source) body.source = source;
if (agentCaller) body.agent_caller = agentCaller;
let resp: Response;
try {
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/`, {
method: "POST",
headers: {
...SOURCE_HEADERS,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
printError(
`Network error contacting Mem0: ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
if (resp.status === 429) {
printError("Rate-limited. Try again in a few minutes.");
process.exit(1);
}
if (resp.status === 503) {
printError("Agent Mode is temporarily disabled. Try again later.");
process.exit(1);
}
if (!resp.ok) {
let detail: string = resp.statusText;
try {
const errBody = (await resp.json()) as {
error?: string;
detail?: string;
};
detail = errBody.error ?? errBody.detail ?? resp.statusText;
} catch {
/* leave detail as statusText */
}
// Backend's @ratelimit decorator raises PermissionDenied, which DRF
// translates to a generic 403 "You do not have permission to perform
// this action." That's opaque — surface it as the rate-limit message
// it actually is.
if (resp.status === 403 && /permission/i.test(detail)) {
printError(
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
);
process.exit(1);
}
printError(`Bootstrap failed: ${detail}`);
process.exit(1);
}
const envelope = (await resp.json()) as BootstrapEnvelope;
if (!isValidEnvelope(envelope)) {
// Defend against partial/malformed backend responses (e.g. {api_key: null}).
// Without this guard, the typed `string` field is silently set to
// undefined/null and persisted, producing confusing downstream errors.
printError(
"Bootstrap response missing required fields — please update the CLI.",
);
process.exit(1);
}
config.platform.apiKey = envelope.api_key;
config.platform.baseUrl = baseUrl;
config.platform.agentMode = true;
config.platform.createdVia = "agent_mode";
config.platform.agentCaller = agentCaller ?? "";
config.platform.claimedAt = "";
config.platform.defaultUserId = envelope.default_user_id;
// Adopt the slug-derived user_id as the default scope for memory ops.
config.defaults.userId = envelope.default_user_id;
saveConfig(config);
printSuccess(
`Agent Mode active. Default user_id: ${envelope.default_user_id}`,
);
if (envelope.mem0_notice) {
console.log(`\n\x1b[33m🔔 ${envelope.mem0_notice}\x1b[0m\n`);
} else {
// Fallback for older backends without the unified notice field.
const claimCmd = envelope.claim_command ?? "mem0 init --email <your-email>";
console.log(` ${dim(`To claim this account later: ${claimCmd}`)}`);
}
if (!agentCaller) {
console.log(
` ${dim("If you are an AI agent, identify yourself: `mem0 identify <your-name>` (e.g. claude-code, cursor).")}`,
);
}
}
/**
* Claim an existing Agent Mode account via OTP — no browser, no polling.
*
* Hits /api/v1/auth/email_code/ to send a verification code, prompts for it
* interactively (or accepts via `code`), then sends it to /verify/ alongside
* `agent_mode_api_key`. Backend's verify_email_code runs upgrade-in-place
* inline and returns the claim result.
*/
export async function claimViaOtp(
config: Mem0Config,
{ email, code }: { email: string; code?: string },
): Promise<void> {
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
/\/+$/,
"",
);
if (!config.platform.apiKey || !config.platform.agentMode) {
printError(
"This command requires an active Agent Mode config. Run `mem0 init` first.",
);
process.exit(1);
}
const rawKey = config.platform.apiKey;
// Step 1: request OTP (unless --code was supplied)
if (!code) {
const sendResp = await fetch(`${baseUrl}/api/v1/auth/email_code/`, {
method: "POST",
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (sendResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!sendResp.ok) {
let detail: string = sendResp.statusText;
try {
const errBody = (await sendResp.json()) as { error?: string };
if (errBody.error) detail = errBody.error;
} catch {
/* leave as statusText */
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess(`Verification code sent to ${email}. Check your inbox.`);
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
`Re-run: mem0 init --email ${email} --code <code>`,
);
process.exit(1);
}
console.log();
code = await promptLine(` ${brand("Verification Code")}`);
if (!code) {
printError("Code is required.");
process.exit(1);
}
}
// Step 2: verify + claim atomically
const verifyResp = await fetch(`${baseUrl}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: { ...SOURCE_HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({
email,
code: code.trim(),
agent_mode_api_key: rawKey,
}),
signal: AbortSignal.timeout(30_000),
});
if (!verifyResp.ok) {
let detail: string = verifyResp.statusText;
let errCode = "";
try {
const errBody = (await verifyResp.json()) as {
error?: string;
code?: string;
};
if (errBody.error) detail = errBody.error;
if (errBody.code) errCode = errBody.code;
} catch {
/* leave as statusText */
}
printError(`Claim failed: ${detail}`);
if (errCode === "email_already_claimed") {
console.log(
` ${dim("Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.")}`,
);
}
process.exit(1);
}
const claimBody = (await verifyResp.json()) as {
claimed?: boolean;
claimed_at?: string;
};
if (!claimBody.claimed) {
printError(`Unexpected verify response: ${JSON.stringify(claimBody)}`);
process.exit(1);
}
config.platform.agentMode = false;
config.platform.claimedAt = claimBody.claimed_at ?? new Date().toISOString();
config.platform.userEmail = email;
config.platform.createdVia = "email";
saveConfig(config);
printSuccess(`Agent claimed to ${email}. Your API key is unchanged.`);
}
function promptLine(label: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${label}: `, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
+147
View File
@@ -0,0 +1,147 @@
/**
* `mem0 agent-rush <add|search> "..."` — wraps the AGENTRUSH platform endpoints.
* Project routing is implicit (server-side); zero flags needed.
*/
import readline from "node:readline";
import { colors, printError, printSuccess } from "../branding.js";
import { loadConfig, saveConfig } from "../config.js";
import { CLI_VERSION } from "../version.js";
const PII_WARNING = [
"",
"⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.",
" Do not include real names, emails, secrets, work content, or PII.",
"",
].join("\n");
const ERROR_HINTS: Record<string, string> = {
agentrush_search_first:
"Run 3 'mem0 agent-rush search' commands before adding.",
agentrush_search_quota: "You've used your 3 lifetime searches.",
agentrush_add_quota: "You've used your 3 lifetime adds.",
agentrush_not_agent_mode:
"Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
agentrush_length: "Memory text must be 50-1000 characters.",
agentrush_no_urls: "URLs are not allowed.",
agentrush_blocklist: "Content contains a blocked term.",
agentrush_global_quota: "Event-wide cap reached. Try again later.",
agentrush_not_provisioned:
"AGENTRUSH is not provisioned in this environment.",
};
async function callEndpoint(
path: string,
body: Record<string, unknown>,
): Promise<unknown> {
const config = loadConfig();
const baseUrl = (config.platform?.baseUrl ?? "https://api.mem0.ai").replace(
/\/+$/,
"",
);
if (!config.platform?.apiKey) {
printError("Not initialized. Run `mem0 init --agent` first.");
process.exit(1);
}
const resp = await fetch(`${baseUrl}${path}`, {
method: "POST",
headers: {
Authorization: `Token ${config.platform.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
"X-Mem0-Mode": "agent-rush",
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
const code =
(json as { error?: { code?: string } }).error?.code ?? "unknown";
printError(`AGENTRUSH error: ${code}`);
if (ERROR_HINTS[code]) {
console.log(` ${colors.dim(ERROR_HINTS[code])}`);
}
process.exit(1);
}
return json;
}
function promptLine(question: string): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
/**
* Ensure the human has acknowledged that AGENTRUSH memories are PUBLIC.
*
* Interactive (TTY): show the prompt; on "y" persist `agentRush.acknowledgedAt`
* so we never ask the same machine twice. On anything else, abort.
*
* Non-interactive (agent invocation, no TTY): print the warning to stderr
* for the human reading the agent's transcript and proceed — agents can't
* answer y/N prompts.
*/
async function ensureWarningAcknowledged(): Promise<void> {
const config = loadConfig();
if (config.agentRush?.acknowledgedAt) return;
if (!process.stdin.isTTY || !process.stdout.isTTY) {
// Agent context: surface the warning to stderr, don't block.
console.error(PII_WARNING);
return;
}
console.log(PII_WARNING);
const answer = (await promptLine(" Continue? [y/N]: ")).toLowerCase();
if (answer !== "y" && answer !== "yes") {
printError("Aborted.");
process.exit(1);
}
config.agentRush.acknowledgedAt = new Date().toISOString();
saveConfig(config);
}
export async function cmdAgentRushAdd(content: string): Promise<void> {
await ensureWarningAcknowledged();
const result = await callEndpoint("/v1/agent-rush/memories/", { content });
printSuccess(
`Memory submitted (event_id: ${(result as { event_id?: string }).event_id ?? "?"})`,
);
}
export async function cmdAgentRushSearch(query: string): Promise<void> {
const result = (await callEndpoint("/v1/agent-rush/memories/search/", {
query,
})) as {
results?: Array<{ memory?: string }>;
memories?: Array<{ memory?: string }>;
};
const memories = result.results ?? result.memories ?? [];
if (memories.length === 0) {
console.log(colors.dim("(no results)"));
return;
}
memories.slice(0, 5).forEach((m, i) => {
console.log(` ${i + 1}. ${m.memory ?? JSON.stringify(m)}`);
});
}
+75
View File
@@ -0,0 +1,75 @@
/**
* mem0 identify — declare which agent owns the current agent-mode key.
*
* Used when `mem0 init --agent` ran without --agent-caller, so the backend
* saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
* its own row with its real identity. Idempotent.
*/
import { printError, printSuccess } from "../branding.js";
import { loadConfig, saveConfig } from "../config.js";
const SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
} as const;
export async function runIdentify(name: string): Promise<void> {
const config = loadConfig();
if (!config.platform.apiKey) {
printError("No API key configured. Run `mem0 init --agent` first.");
process.exit(1);
}
if (!config.platform.agentMode) {
printError("This command only works on unclaimed agent-mode keys.");
process.exit(1);
}
const clean = (name ?? "").trim();
if (!clean) {
printError("Agent name is required.");
process.exit(1);
}
const baseUrl = (config.platform.baseUrl || "https://api.mem0.ai").replace(
/\/+$/,
"",
);
let resp: Response;
try {
resp = await fetch(`${baseUrl}/api/v1/auth/agent_mode/caller/`, {
method: "PATCH",
headers: {
...SOURCE_HEADERS,
Authorization: `Token ${config.platform.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ agent_caller: clean }),
signal: AbortSignal.timeout(30_000),
});
} catch (err) {
printError(
`Network error: ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
if (!resp.ok) {
let detail: string = resp.statusText;
try {
const body = (await resp.json()) as { error?: string };
if (body.error) detail = body.error;
} catch {
/* leave as statusText */
}
printError(`Identify failed: ${detail}`);
process.exit(1);
}
const body = (await resp.json()) as { agent_caller?: string };
const canonical = body.agent_caller ?? clean;
config.platform.agentCaller = canonical;
saveConfig(config);
printSuccess(`Identified as ${canonical}.`);
}
+167 -1
View File
@@ -21,6 +21,8 @@ import {
redactKey,
saveConfig,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
import { isAgentMode } from "../state.js";
const { brand, dim } = colors;
@@ -33,6 +35,65 @@ function validateEmail(email: string): void {
}
}
/** @internal — exported for unit tests. */
export async function pingKey(
apiKey: string,
baseUrl: string,
timeoutMs = 5000,
): Promise<boolean> {
// Returns false ONLY on a definitive "invalid key" signal (HTTP 401/403).
// Network errors, timeouts, and 5xx responses return true so we prefer
// reusing an existing key over silently minting a new shadow on a transient
// blip (which would also clobber config + plugin-sync targets).
try {
const resp = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/ping/`, {
headers: { Authorization: `Token ${apiKey}` },
signal: AbortSignal.timeout(timeoutMs),
});
return resp.status !== 401 && resp.status !== 403;
} catch {
return true; // unknown — prefer reuse
}
}
async function maybeIdentify(
key: string,
baseUrl: string,
agentCaller: string | undefined,
): Promise<void> {
// Best-effort PATCH agent_caller when --agent-caller is supplied on a
// reused key. Silent no-op on any failure — reuse must not break.
if (!agentCaller) return;
try {
const resp = await fetch(
`${baseUrl.replace(/\/+$/, "")}/api/v1/auth/agent_mode/caller/`,
{
method: "PATCH",
headers: {
Authorization: `Token ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ agent_caller: agentCaller }),
signal: AbortSignal.timeout(10_000),
},
);
if (resp.ok) {
try {
const body = (await resp.json()) as { agent_caller?: string };
if (fs.existsSync(CONFIG_FILE)) {
const cfg = loadConfig();
cfg.platform.agentCaller = body.agent_caller ?? agentCaller;
saveConfig(cfg);
}
} catch {
/* swallow — best effort */
}
}
} catch {
/* swallow — best effort */
}
}
async function emailLogin(
email: string,
code: string | undefined,
@@ -196,6 +257,7 @@ async function setupPlatform(config: Mem0Config): Promise<void> {
process.exit(1);
}
config.platform.apiKey = apiKey;
config.platform.createdVia = "api_key";
}
async function setupDefaults(config: Mem0Config): Promise<void> {
@@ -249,14 +311,35 @@ export async function runInit(
email?: string;
code?: string;
force?: boolean;
agent?: boolean;
source?: string;
agentCaller?: string;
} = {},
): Promise<void> {
const { detectAgentCaller } = await import("../agent-detect.js");
const { bootstrapViaBackend, claimViaOtp } = await import("./agent-mode.js");
const { isAgentMode } = await import("../state.js");
const { captureEvent } = await import("../telemetry.js");
const fireInit = (
mode: "agent" | "email" | "api_key" | "existing_key",
claimed = false,
) => {
const props: Record<string, unknown> = { command: "init", mode };
// Self-declared via --agent-caller; not sniffed from env vars.
if (opts.agentCaller) props.agent_caller = opts.agentCaller;
if (opts.source) props.signup_source = opts.source;
if (claimed) props.claimed_agent_mode = true;
captureEvent("cli.init", props);
};
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
config.platform.baseUrl = baseUrl;
// Guards
if (opts.code && !opts.email) {
@@ -268,6 +351,84 @@ export async function runInit(
process.exit(1);
}
// ── Claim flow: --email against an existing agent-mode config ───────────
if (
opts.email &&
fs.existsSync(CONFIG_FILE) &&
savedConfig.platform.agentMode &&
savedConfig.platform.apiKey
) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printInfo(`Claiming Agent Mode account to ${email}...`);
await claimViaOtp(savedConfig, { email, code: opts.code });
fireInit("email", true);
return;
}
// ── Agent Mode path runs BEFORE the existing-config guard ──────────────
// Rule 1/2 will REUSE a valid existing key (not overwrite), so we must
// short-circuit before the guard prompts the user about overwriting.
// Rule 3 only mints when there's no valid key to reuse — in that case
// overwriting is what the user wants.
const agentCtx =
opts.agent === true || isAgentMode() || detectAgentCaller() !== null;
if (!opts.apiKey && !opts.email && agentCtx) {
const emitReuseEnvelope = (source: "env" | "config") => {
if (isAgentMode()) {
formatJsonEnvelope({
command: "init",
data: {
api_key_saved: false,
api_key_source: source,
agent_mode: false,
message:
"Existing Mem0 API key found and reused. No Agent Mode key was created.",
},
});
} else {
printSuccess(
source === "env"
? "Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
: "Existing API key in config is valid; reusing it. No new Agent Mode key was minted.",
);
}
};
// Rule 1: env MEM0_API_KEY valid → reuse, no new key.
const envKey = (process.env.MEM0_API_KEY || "").trim();
if (envKey && (await pingKey(envKey, baseUrl))) {
await maybeIdentify(envKey, baseUrl, opts.agentCaller);
emitReuseEnvelope("env");
fireInit("existing_key");
return;
}
// Rule 2: existing config api_key valid → reuse.
if (
savedConfig.platform.apiKey &&
(await pingKey(savedConfig.platform.apiKey, baseUrl))
) {
await maybeIdentify(
savedConfig.platform.apiKey,
baseUrl,
opts.agentCaller,
);
emitReuseEnvelope("config");
fireInit("existing_key");
return;
}
// Rule 3: mint a fresh shadow (no valid key to reuse).
// agent_caller is self-declared via --agent-caller (Proof Editor-style),
// not derived from env-var sniffing. detectAgentCaller() above is still
// used as a context trigger (does this look like an agent?) but never
// to fill identity.
await bootstrapViaBackend(config, {
source: opts.source ?? null,
agentCaller: opts.agentCaller ?? null,
});
fireInit("agent");
return;
}
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
@@ -324,6 +485,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.platform.createdVia = "email";
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
@@ -339,13 +501,15 @@ export async function runInit(
}
// ── API key flow ──────────────────────────────────────────────────────────
// (Agent Mode branch runs earlier — see above, before the existing-config
// guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
// Non-TTY: resolve defaults so partial flags work in pipelines / CI
if (!process.stdin.isTTY) {
if (!opts.apiKey) {
printError(
"Non-interactive terminal detected and --api-key is required.",
"Usage: mem0 init --api-key <key> [--user-id <id>]",
"Usage: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
);
process.exit(1);
}
@@ -356,6 +520,7 @@ export async function runInit(
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.platform.createdVia = "api_key";
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
@@ -403,6 +568,7 @@ export async function runInit(
config.platform.apiKey = apiKeyVal;
config.platform.baseUrl = baseUrl;
config.platform.userEmail = email;
config.platform.createdVia = "email";
config.defaults.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
+2 -2
View File
@@ -46,7 +46,7 @@ export async function cmdAdd(
file?: string;
metadata?: string;
immutable: boolean;
noInfer: boolean;
infer?: boolean;
expires?: string;
categories?: string;
output: string;
@@ -136,7 +136,7 @@ export async function cmdAdd(
runId: opts.runId,
metadata: meta,
immutable: opts.immutable,
infer: !opts.noInfer,
infer: opts.infer !== false,
expires: opts.expires,
categories: cats,
});
+18
View File
@@ -0,0 +1,18 @@
/**
* `mem0 whoami` — print the active agent's default_user_id (AGENTRUSH identifier).
* Reads from local config; no network call.
*/
import { colors, printError, printInfo } from "../branding.js";
import { loadConfig } from "../config.js";
export async function cmdWhoami(): Promise<void> {
const config = loadConfig();
const sessionId = config.platform?.defaultUserId;
if (!sessionId) {
printError("No default_user_id found. Run `mem0 init --agent` first.");
process.exit(1);
}
console.log(`Your AGENTRUSH identifier: ${colors.brand(sessionId)}`);
printInfo("Find your row at https://mem0.ai/agentrush");
}
+50
View File
@@ -21,6 +21,12 @@ export interface PlatformConfig {
apiKey: string;
baseUrl: string;
userEmail: string;
// Agent Mode (unclaimed-shadow signup)
agentMode: boolean; // true while the key is an unclaimed agent-mode key
createdVia: string; // "agent_mode" | "email" | "api_key" | "existing_key"
agentCaller: string; // canonical agent name when createdVia === "agent_mode" (e.g. "claude-code")
claimedAt: string; // ISO timestamp once the agent has been claimed
defaultUserId: string; // `user_<slug>` returned by bootstrap; auto-default scope
}
export interface DefaultsConfig {
@@ -34,11 +40,18 @@ export interface TelemetryConfig {
anonymousId: string;
}
export interface AgentRushConfig {
// ISO timestamp the human acknowledged the "memories are public" warning.
// Empty until first interactive `mem0 agent-rush add`.
acknowledgedAt: string;
}
export interface Mem0Config {
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
telemetry: TelemetryConfig;
agentRush: AgentRushConfig;
}
export function createDefaultConfig(): Mem0Config {
@@ -54,10 +67,18 @@ export function createDefaultConfig(): Mem0Config {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
agentMode: false,
createdVia: "",
agentCaller: "",
claimedAt: "",
defaultUserId: "",
},
telemetry: {
anonymousId: "",
},
agentRush: {
acknowledgedAt: "",
},
};
}
@@ -79,6 +100,11 @@ export function loadConfig(): Mem0Config {
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
config.platform.userEmail = plat.user_email ?? "";
config.platform.agentMode = Boolean(plat.agent_mode ?? false);
config.platform.createdVia = plat.created_via ?? "";
config.platform.agentCaller = plat.agent_caller ?? "";
config.platform.claimedAt = plat.claimed_at ?? "";
config.platform.defaultUserId = plat.default_user_id ?? "";
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
@@ -87,6 +113,8 @@ export function loadConfig(): Mem0Config {
config.defaults.runId = defaults.run_id ?? "";
const telemetry = data.telemetry ?? {};
config.telemetry.anonymousId = telemetry.anonymous_id ?? "";
const agentRush = data.agent_rush ?? {};
config.agentRush.acknowledgedAt = agentRush.acknowledged_at ?? "";
}
// Environment variable overrides
@@ -118,14 +146,36 @@ export function saveConfig(config: Mem0Config): void {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
user_email: config.platform.userEmail,
agent_mode: config.platform.agentMode,
created_via: config.platform.createdVia,
agent_caller: config.platform.agentCaller,
claimed_at: config.platform.claimedAt,
default_user_id: config.platform.defaultUserId,
},
telemetry: {
anonymous_id: config.telemetry.anonymousId,
},
agent_rush: {
acknowledged_at: config.agentRush.acknowledgedAt,
},
};
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2));
fs.chmodSync(CONFIG_FILE, 0o600);
// Propagate api_key to ecosystem touchpoints (Claude plugin env injection,
// shell rc exports). Idempotent — updates only EXISTING entries; never
// creates new ones. Best-effort: errors swallowed so config.json is
// always authoritative, never blocked by plugin-state issues.
if (config.platform.apiKey) {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { syncApiKey } = require("./plugin-sync.js");
syncApiKey(config.platform.apiKey);
} catch {
/* swallow */
}
}
}
export function redactKey(key: string): string {
+112 -4
View File
@@ -13,7 +13,12 @@ import { colors, printError, printWarning } from "./branding.js";
import type { Mem0Config } from "./config.js";
import { loadConfig, saveConfig } from "./config.js";
import { richFormatHelp } from "./help.js";
import { setAgentMode } from "./state.js";
import {
isAgentMode,
setAgentMode,
setCurrentCommand,
takeNotice,
} from "./state.js";
import { captureEvent } from "./telemetry.js";
import { CLI_VERSION } from "./version.js";
@@ -141,6 +146,11 @@ program
.description(
`◆ Mem0 CLI v${CLI_VERSION} · Node.js SDK\n\nThe Memory Layer for AI Agents`,
)
// Positional options: flags AFTER a subcommand name belong to that
// subcommand, not the global program. Without this, `mem0 init --agent`
// routes `--agent` to the program-level alias (for --json) and init's own
// `--agent` (Agent Mode bootstrap) silently never fires.
.enablePositionalOptions()
.option("--version", "Show version and exit.")
.on("option:version", () => {
console.log(` ${colors.brand("◆ Mem0")} CLI v${CLI_VERSION}`);
@@ -149,7 +159,7 @@ program
.option("--json", "Output as JSON for agent/programmatic use.")
.option(
"--agent",
"Output as JSON for agent/programmatic use. (alias: --json)",
"Output as JSON for agent/programmatic use. (alias: --json) Place BEFORE the subcommand: `mem0 --agent <cmd>`. On `init`, `mem0 init --agent` is the Agent Mode bootstrap flag instead.",
)
.usage("<command> [options]")
.helpOption("--help", "Show this message and exit.")
@@ -166,6 +176,14 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
parentName && parentName !== "mem0"
? `${parentName}.${commandName}`
: commandName;
// Stash the active command name in shared state so the JSON
// error envelope (printError) can report which command failed
// instead of an empty `"command": ""` field.
setCurrentCommand(fullCommand);
// init fires its own telemetry from runInit with full M1-M6 props
// (mode/agent_caller/signup_source/claimed_agent_mode); skip the
// auto-fire here so we don't double-count.
if (fullCommand === "init") return;
const isAgent = !!(program.opts().json || program.opts().agent);
captureEvent(
`cli.${fullCommand}`,
@@ -193,11 +211,32 @@ program
"Verification code (use with --email for non-interactive login).",
)
.option("--force", "Overwrite existing config without confirmation.", false)
.option(
"--agent",
"Bootstrap an unattended Agent Mode account (no email required).",
false,
)
.option(
"--source <channel>",
"Channel attribution for signup (e.g. github, hn, ph).",
)
.option(
"--agent-caller <name>",
"Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
)
// Accept `--json` at the init level too so the PRD-documented form
// `mem0 init --agent --json` works without requiring users to move it
// before the subcommand. Effect is identical to the global `--json`:
// flip agent-mode output state.
.option("--json", "Output as JSON (alias for global `--json`).", false)
.addHelpText(
"after",
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456",
"\nExamples:\n $ mem0 init\n $ mem0 init --api-key m0-xxx --user-id alice\n $ mem0 init --email you@example.com\n $ mem0 init --email you@example.com --code 123456\n $ mem0 init --agent # Bootstrap an Agent Mode account (unattended)\n $ mem0 init --email you@example.com # Claims an existing Agent Mode key when one is present",
)
.action(async (opts) => {
// `--json` at init level mirrors the global flag — flip agent_mode
// state so downstream formatters use JSON envelopes.
if (opts.json) setAgentMode(true);
const { runInit } = await import("./commands/init.js");
await runInit({
apiKey: opts.apiKey,
@@ -205,9 +244,66 @@ program
email: opts.email,
code: opts.code,
force: opts.force,
agent: opts.agent,
source: opts.source,
agentCaller: opts.agentCaller,
});
});
// ── Setup: identify (post-bootstrap agent self-tag) ──────────────────────
program
.command("identify <name>")
.description(
"Tag your active Agent Mode key with the AI agent that's using it (e.g. claude-code, cursor).",
)
.action(async (name: string) => {
const { runIdentify } = await import("./commands/identify.js");
await runIdentify(name);
});
// ── Setup: whoami (print active agent identifier) ────────────────────────
program
.command("whoami")
.description("Print the active agent's AGENTRUSH identifier.")
.action(async () => {
const { cmdWhoami } = await import("./commands/whoami.js");
await cmdWhoami();
});
// ── AGENTRUSH subcommand group ────────────────────────────────────────────
const agentRush = program
.command("agent-rush")
.description("AGENTRUSH game commands.")
.addHelpCommand(false)
.configureHelp({ formatHelp: richFormatHelp });
agentRush
.command("add <content...>")
.description("Submit a memory to AGENTRUSH.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 agent-rush add "I used mem0 to build a coding agent"\n $ mem0 agent-rush add "Agents that remember are better agents"',
)
.action(async (parts: string[]) => {
const { cmdAgentRushAdd } = await import("./commands/agent-rush.js");
await cmdAgentRushAdd(parts.join(" "));
});
agentRush
.command("search <query...>")
.description("Search AGENTRUSH memories.")
.addHelpText(
"after",
'\nExamples:\n $ mem0 agent-rush search "agents and memory and tools"\n $ mem0 agent-rush search "coding assistant"',
)
.action(async (parts: string[]) => {
const { cmdAgentRushSearch } = await import("./commands/agent-rush.js");
await cmdAgentRushSearch(parts.join(" "));
});
// ── Memory: add ───────────────────────────────────────────────────────────
program
@@ -769,4 +865,16 @@ program
// ── Entrypoint ────────────────────────────────────────────────────────────
program.parse();
// Surface any unclaimed Agent Mode notice once per command, after the primary
// output. In JSON/agent mode the notice is folded into the envelope by
// formatJsonEnvelope, so skip the stderr banner there to avoid duplication.
function surfaceNotice(): void {
const notice = takeNotice();
if (notice && !isAgentMode()) {
process.stderr.write(`\n\x1b[33m🔔 ${notice}\x1b[0m\n\n`);
}
}
program.parseAsync().finally(() => {
surfaceNotice();
});
+16
View File
@@ -5,6 +5,7 @@
import boxen from "boxen";
import Table from "cli-table3";
import { colors, sym } from "./branding.js";
import { takeNotice } from "./state.js";
const { brand, accent, success, error: errorColor, dim } = colors;
@@ -244,6 +245,15 @@ export function formatJsonEnvelope(opts: {
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
// If the platform flagged this as an unclaimed Agent Mode account, surface
// the notice inside the JSON envelope so an agent consuming the output
// sees it without needing to inspect HTTP headers.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { takeNotice } = require("./state.js");
const notice = takeNotice();
if (notice) envelope.mem0_notice = notice;
console.log(JSON.stringify(envelope, null, 2));
}
@@ -356,6 +366,12 @@ export function formatAgentEnvelope(opts: {
}
if (opts.count !== undefined) envelope.count = opts.count;
envelope.data = sanitizeAgentData(opts.command, opts.data);
// Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
// agent reading the JSON output sees it without inspecting HTTP headers.
const notice = takeNotice();
if (notice) envelope.mem0_notice = notice;
console.log(JSON.stringify(envelope, null, 2));
}
+120
View File
@@ -0,0 +1,120 @@
/**
* Sync the active Mem0 API key into other ecosystem touchpoints.
*
* Why: the CLI canonical state is ~/.mem0/config.json. MCP servers
* (Claude Code plugin, Codex plugin) read MEM0_API_KEY from env or
* their own config files. Without a sync, agent-mode bootstrap mints a
* new key into config.json but the plugin's MCP keeps using the old
* key from env — silent surprise.
*
* Design:
* - Update ONLY entries that already exist; never create new ones
* - Preserve surrounding content, formatting, other keys
* - Atomic writes (tmp + rename) so a crash mid-write doesn't corrupt
* - Idempotent — re-running with the same key is a no-op
*
* Targets:
* - ~/.claude/settings.json::env::MEM0_API_KEY (Claude Code env injection)
* - ~/.zshrc / ~/.bashrc `export MEM0_API_KEY="..."` lines
*
* Out of scope: Codex / Cursor MCP configs and the plugin's own
* <plugin-dir>/.api_key file (plugin-managed, different schema).
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const CLAUDE_SETTINGS = path.join(os.homedir(), ".claude", "settings.json");
const SHELL_RCS = [
path.join(os.homedir(), ".zshrc"),
path.join(os.homedir(), ".bashrc"),
path.join(os.homedir(), ".bash_profile"),
];
// Use [ \t]* (not \s*) so a trailing newline at end-of-file is preserved
// when the MEM0_API_KEY export is the last line of the rc file.
const RC_LINE_RE =
/^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["']?)([^"'\n]*)(["']?)[ \t]*$/m;
export function syncApiKey(apiKey: string): string[] {
if (!apiKey) return [];
const updated: string[] = [];
if (updateClaudeSettings(CLAUDE_SETTINGS, apiKey)) {
updated.push(CLAUDE_SETTINGS);
}
for (const rc of SHELL_RCS) {
if (updateShellRc(rc, apiKey)) updated.push(rc);
}
return updated;
}
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
export function updateClaudeSettings(
filePath: string,
apiKey: string,
): boolean {
if (!fs.existsSync(filePath)) return false;
let raw: string;
let data: Record<string, unknown>;
try {
raw = fs.readFileSync(filePath, "utf-8");
data = JSON.parse(raw);
} catch {
return false;
}
const env = data.env;
if (!env || typeof env !== "object" || !("MEM0_API_KEY" in env)) {
return false; // no existing entry — don't create one
}
const envObj = env as Record<string, string>;
if (envObj.MEM0_API_KEY === apiKey) return false; // already in sync
envObj.MEM0_API_KEY = apiKey;
atomicWriteText(filePath, `${JSON.stringify(data, null, 2)}\n`);
return true;
}
/** @internal — exported for unit tests; consumers should use {@link syncApiKey}. */
export function updateShellRc(filePath: string, apiKey: string): boolean {
if (!fs.existsSync(filePath)) return false;
let text: string;
try {
text = fs.readFileSync(filePath, "utf-8");
} catch {
return false;
}
const match = text.match(RC_LINE_RE);
if (!match) return false; // no existing line
if (match[3] === apiKey) return false;
const newText = text.replace(
RC_LINE_RE,
(_full, prefix) => `${prefix}"${apiKey}"`,
);
atomicWriteText(filePath, newText);
return true;
}
function atomicWriteText(filePath: string, content: string): void {
const dir = path.dirname(filePath);
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
try {
fs.writeFileSync(tmp, content, "utf-8");
// Preserve permissions if original existed.
if (fs.existsSync(filePath)) {
try {
const mode = fs.statSync(filePath).mode & 0o777;
fs.chmodSync(tmp, mode);
} catch {
/* best-effort */
}
}
fs.renameSync(tmp, filePath);
} catch (err) {
try {
fs.unlinkSync(tmp);
} catch {
/* ignore */
}
throw err;
}
}
+17
View File
@@ -5,6 +5,7 @@
let _agentMode = false;
let _currentCommand = "";
let _pendingNotice = "";
export function isAgentMode(): boolean {
return _agentMode;
@@ -21,3 +22,19 @@ export function getCurrentCommand(): string {
export function setCurrentCommand(name: string): void {
_currentCommand = name;
}
/**
* Stash a Mem0 backend notice (Agent Mode unclaimed reminder) for end-of-
* command surfacing. Called from the platform backend after each response so
* the notice prints once per command regardless of how many sub-requests
* fired. Last-write-wins is fine — the message text is identical.
*/
export function captureNotice(notice: string | null | undefined): void {
if (notice) _pendingNotice = notice;
}
export function takeNotice(): string {
const msg = _pendingNotice;
_pendingNotice = "";
return msg;
}
+9 -5
View File
@@ -115,6 +115,9 @@ export function captureEvent(
}
}
// M4: every cli.* event carries agent_mode based on the config flag
// (unclaimed Agent Mode key). This is the growth-doc property used to
// join init → add → search funnels in PostHog.
const payload = {
api_key: POSTHOG_API_KEY,
distinct_id: distinctId,
@@ -123,6 +126,7 @@ export function captureEvent(
source: "CLI",
language: "node",
cli_version: CLI_VERSION,
agent_mode: Boolean(config.platform.agentMode),
node_version: process.version,
os: process.platform,
...properties,
@@ -141,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 */
+28 -2
View File
@@ -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) {
+141
View File
@@ -0,0 +1,141 @@
/**
* Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
*
* Mirror of `cli/python/tests/test_agent_mode.py` — both files MUST stay
* in sync so that the Python and Node CLIs expose an identical surface
* for the Agent Mode entrypoint. If you add a flag here, add the same
* assertion on the Python side (and vice versa).
*
* Network-bound bootstrap is covered by the platform-side E2E suite
* (`backend/tests/e2e/test_05_agent_mode.py`); these tests only verify
* the CLI surface that ships in the binary.
*/
import { describe, it, expect } from "vitest";
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
function run(
args: string[],
opts: { home?: string; env?: Record<string, string> } = {},
): { stdout: string; stderr: string; exitCode: number } {
const env = { ...process.env };
for (const key of Object.keys(env)) {
if (key.startsWith("MEM0_")) delete env[key];
}
if (opts.home) env.HOME = opts.home;
if (opts.env) Object.assign(env, opts.env);
try {
const stdout = execSync(`npx tsx src/index.ts ${args.join(" ")}`, {
cwd: path.join(__dirname, ".."),
env,
encoding: "utf-8",
timeout: 15000,
});
return { stdout, stderr: "", exitCode: 0 };
} catch (e: any) {
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: e.status ?? 1,
};
}
}
function cleanHome(): string {
return fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
}
describe("init flag surface", () => {
it("init --help lists --agent", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--agent");
});
it("init --help describes Agent Mode", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
// Description must mention what --agent actually does so an agent
// reading the help can self-discover the bootstrap entrypoint.
expect(
result.stdout.includes("Agent Mode") ||
result.stdout.toLowerCase().includes("unattended"),
).toBe(true);
});
it("init --help lists --source", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--source");
});
it("init --help lists --email and --code", () => {
const result = run(["init", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--email");
expect(result.stdout).toContain("--code");
});
});
describe("argv preprocessing — --agent reaches init subcommand", () => {
// Regression for the bug where the global --agent JSON-alias swallowed
// the init-level --agent flag, making `mem0 init --agent` behave like
// the plain interactive wizard.
it("init --agent triggers bootstrap branch (not the wizard)", () => {
const home = cleanHome();
const result = run(["init", "--agent"], {
home,
env: {
MEM0_BASE_URL: "http://127.0.0.1:1", // blackhole
FORCE_COLOR: "0",
},
});
const combined = (result.stdout + result.stderr).toLowerCase();
// Either bootstrap-attempt error, or a connection/network error —
// both prove the --agent path executed (the wizard would prompt for
// input and succeed/hang, not surface a network error).
expect(
combined.includes("agent") ||
combined.includes("connect") ||
combined.includes("network") ||
combined.includes("fetch") ||
combined.includes("bootstrap"),
).toBe(true);
fs.rmSync(home, { recursive: true, force: true });
});
});
describe("JSON envelope on network failure", () => {
it("init --agent --json does not leak a stack trace when backend is unreachable", () => {
const home = cleanHome();
const result = run(["init", "--agent", "--json"], {
home,
env: {
MEM0_BASE_URL: "http://127.0.0.1:1",
FORCE_COLOR: "0",
},
});
const combined = result.stdout + result.stderr;
// No raw Node stack should escape the agent-mode handler.
expect(combined).not.toMatch(/at \w+\s*\(.+\.ts:\d+/);
expect(combined).not.toContain("UnhandledPromiseRejection");
expect(result.exitCode).not.toBe(0);
fs.rmSync(home, { recursive: true, force: true });
});
});
describe("top-level help lists init", () => {
// `mem0 --help` must list `init` so agents walking the top-level help
// can discover the Agent Mode entrypoint without prior knowledge.
it("--help lists init", () => {
const result = run(["--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("init");
});
});
+48 -16
View File
@@ -3,6 +3,7 @@
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Command } from "commander";
import { createMockBackend } from "./setup.js";
import type { Backend } from "../src/backend/base.js";
import { setAgentMode } from "../src/state.js";
@@ -41,8 +42,6 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "I prefer dark mode", {
userId: "alice",
immutable: false,
noInfer: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
@@ -54,8 +53,6 @@ describe("cmdAdd", () => {
userId: "alice",
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
immutable: false,
noInfer: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
@@ -66,8 +63,6 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
output: "json",
});
expect(output).toContain("results");
@@ -78,14 +73,59 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
output: "quiet",
});
expect(output).not.toContain("dark mode");
});
});
describe("cmdAdd forwards --no-infer (regression for #5261)", () => {
it("forwards infer: false when --no-infer is set", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
// `infer: false` is the shape Commander produces for `--no-infer`.
await cmdAdd(mockBackend, "store me verbatim", {
userId: "alice",
immutable: false,
infer: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledWith(
"store me verbatim",
undefined,
expect.objectContaining({ infer: false }),
);
});
it("forwards infer: true by default (flag absent)", async () => {
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "infer me", {
userId: "alice",
immutable: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledWith(
"infer me",
undefined,
expect.objectContaining({ infer: true }),
);
});
it("Commander stores --no-infer as opts.infer, not opts.noInfer", () => {
// Pins the assumption the fix relies on: Commander's `--no-X` option
// populates the positive camelCase key (`infer`), never `noInfer`.
const withFlag = new Command();
withFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
withFlag.parse(["--no-infer"], { from: "user" });
expect(withFlag.opts().infer).toBe(false);
expect(withFlag.opts().noInfer).toBeUndefined();
const withoutFlag = new Command();
withoutFlag.option("--no-infer", "Skip inference, store raw.").action(() => {});
withoutFlag.parse([], { from: "user" });
expect(withoutFlag.opts().infer).toBe(true);
});
});
describe("cmdAdd deduplicates PENDING", () => {
const DUPLICATE_PENDING = {
results: [
@@ -100,8 +140,6 @@ describe("cmdAdd deduplicates PENDING", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
output: "text",
});
expect(output.match(/Queued/g)?.length).toBe(1);
@@ -113,8 +151,6 @@ describe("cmdAdd deduplicates PENDING", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
output: "json",
});
const data = JSON.parse(output);
@@ -129,8 +165,6 @@ describe("cmdAdd deduplicates PENDING", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
output: "agent",
});
const data = JSON.parse(output);
@@ -315,8 +349,6 @@ describe("agent mode", () => {
await cmdAdd(mockBackend, "test preference", {
userId: "alice",
immutable: false,
noInfer: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
+168
View File
@@ -0,0 +1,168 @@
/**
* Unit tests for init internals — decision tree primitives + plugin sync.
*
* Mirror of `cli/python/tests/test_init_internals.py`. Both files MUST stay
* in sync — if you add a behavioral assertion here, mirror it on the Python
* side and vice versa.
*
* - `pingKey` must NOT treat network errors as "invalid key" (else a VPN
* flap silently mints a new shadow over a working key).
* - `plugin_sync` must only update entries that already exist, preserve
* trailing newlines, and never mangle other lines.
*/
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { pingKey } from "../src/commands/init.js";
import { updateClaudeSettings, updateShellRc } from "../src/plugin-sync.js";
// ── pingKey ──────────────────────────────────────────────────────────────
describe("pingKey — network vs auth distinction", () => {
const origFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = origFetch;
vi.restoreAllMocks();
});
it("returns true for 200", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 200 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns false for 401 (definitively invalid)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 401 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(false);
});
it("returns false for 403 (definitively invalid)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 403 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(false);
});
it("returns true for 5xx (transient upstream — prefer reuse)", async () => {
globalThis.fetch = vi.fn().mockResolvedValue({ status: 503 } as Response);
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns true on network error (prefer reuse over re-mint)", async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
it("returns true on timeout (prefer reuse)", async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error("aborted"));
await expect(pingKey("k", "http://x")).resolves.toBe(true);
});
});
// ── updateShellRc ────────────────────────────────────────────────────────
describe("updateShellRc — exists-only contract", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("updates existing export and preserves trailing newline", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, 'export MEM0_API_KEY="old"\n');
expect(updateShellRc(rc, "newkey")).toBe(true);
expect(fs.readFileSync(rc, "utf-8")).toBe('export MEM0_API_KEY="newkey"\n');
});
it("does NOT create a new export when none exists", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, "alias ll='ls -la'\n");
expect(updateShellRc(rc, "newkey")).toBe(false);
expect(fs.readFileSync(rc, "utf-8")).toBe("alias ll='ls -la'\n");
});
it("preserves surrounding content", () => {
const rc = path.join(tmpDir, ".zshrc");
const original =
"# my zshrc\n" +
"alias ll='ls -la'\n" +
"export MEM0_API_KEY='old'\n" +
"export OTHER=keepme\n";
fs.writeFileSync(rc, original);
updateShellRc(rc, "newkey");
const after = fs.readFileSync(rc, "utf-8");
expect(after).toContain("alias ll='ls -la'\n");
expect(after).toContain("export OTHER=keepme\n");
expect(after).toContain("# my zshrc\n");
expect(after).toContain('export MEM0_API_KEY="newkey"\n');
});
it("is idempotent when value already matches", () => {
const rc = path.join(tmpDir, ".zshrc");
fs.writeFileSync(rc, 'export MEM0_API_KEY="same"\n');
expect(updateShellRc(rc, "same")).toBe(false);
});
it("is a no-op for missing files", () => {
const rc = path.join(tmpDir, ".zshrc"); // does not exist
expect(updateShellRc(rc, "x")).toBe(false);
});
});
// ── updateClaudeSettings ─────────────────────────────────────────────────
describe("updateClaudeSettings — never creates entries", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("does not create env block when none exists", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, JSON.stringify({ otherKey: 1 }));
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
expect(JSON.parse(fs.readFileSync(settings, "utf-8"))).toEqual({
otherKey: 1,
});
});
it("does not create MEM0_API_KEY entry in existing env block", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, JSON.stringify({ env: { OTHER_KEY: "x" } }));
expect(updateClaudeSettings(settings, "newkey")).toBe(false);
});
it("updates existing entry and preserves siblings", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(
settings,
JSON.stringify({ env: { MEM0_API_KEY: "old", OTHER: "y" } }, null, 2),
);
expect(updateClaudeSettings(settings, "fresh")).toBe(true);
const data = JSON.parse(fs.readFileSync(settings, "utf-8"));
expect(data.env.MEM0_API_KEY).toBe("fresh");
expect(data.env.OTHER).toBe("y");
});
it("is idempotent when value already matches", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(
settings,
JSON.stringify({ env: { MEM0_API_KEY: "same" } }),
);
expect(updateClaudeSettings(settings, "same")).toBe(false);
});
it("is a no-op for malformed JSON", () => {
const settings = path.join(tmpDir, "settings.json");
fs.writeFileSync(settings, "{ this is not json");
expect(updateClaudeSettings(settings, "x")).toBe(false);
});
});
+52
View File
@@ -0,0 +1,52 @@
/**
* Tests for the Platform backend (mem0 Platform API client).
*/
import { describe, it, expect, vi } from "vitest";
import { PlatformBackend } from "../src/backend/platform.js";
import { createDefaultConfig } from "../src/config.js";
function makeBackend(): PlatformBackend {
// apiKey/baseUrl only build request headers; every test spies on _request,
// so no real network calls are made.
return new PlatformBackend(createDefaultConfig().platform);
}
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" });
// Regression: previously only the last entity's response survived.
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",
);
});
});
+59
View File
@@ -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);
});
});
+6
View File
@@ -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 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.2.4"
version = "0.2.9"
description = "The official CLI for mem0 — the memory layer for AI agents"
readme = "README.md"
license = "Apache-2.0"
+1 -1
View File
@@ -1,3 +1,3 @@
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
__version__ = "0.2.4"
__version__ = "0.2.9"
+36
View File
@@ -0,0 +1,36 @@
"""Detect whether the CLI is being invoked from inside an AI-agent context.
Used by `mem0 init` to auto-enter Agent Mode (Rule 3 bootstrap) when an
agent runtime env var is present. The return value is a context **trigger
only** — the canonical agent identity is self-declared by the agent via
``--agent-caller <name>`` (Proof Editor-style) and never sniffed from env
vars to fill the ``agent_caller`` field on the APIKey row.
Returns a short name or None. The list is curated, not exhaustive — env
vars we don't recognise fall through to None (caller treated as
non-agent). Honest reporting depends on ``--agent-caller``; this list is
just enough to enable the zero-friction auto-bootstrap UX.
"""
from __future__ import annotations
import os
_AGENT_CALLER_ENV: tuple[tuple[str, tuple[str, ...]], ...] = (
("claude-code", ("CLAUDECODE", "CLAUDE_CODE")),
("cursor", ("CURSOR_AGENT", "CURSOR_SESSION_ID")),
("codex", ("CODEX_CLI", "OPENAI_CODEX")),
("cline", ("CLINE_AGENT", "CLINE")),
("continue", ("CONTINUE_AGENT", "CONTINUE_SESSION")),
("aider", ("AIDER_SESSION",)),
("goose", ("GOOSE_AGENT",)),
("windsurf", ("WINDSURF_AGENT",)),
)
def detect_agent_caller() -> str | None:
"""Return a canonical agent name if any agent env var is set, else None."""
for name, env_vars in _AGENT_CALLER_ENV:
if any(os.environ.get(v) for v in env_vars):
return name
return None
+130 -5
View File
@@ -237,6 +237,14 @@ def main_callback(
cmd_version()
raise typer.Exit()
if ctx.invoked_subcommand:
# Stash the active subcommand name so the JSON error envelope
# (print_error in agent mode) can report which command failed
# instead of an empty `"command": ""` field.
from mem0_cli.state import set_current_command
set_current_command(ctx.invoked_subcommand)
if ctx.invoked_subcommand and ctx.invoked_subcommand != "init":
# init fires its own telemetry from init_cmd.run_init with full M1-M6 props.
_fire_telemetry(ctx.invoked_subcommand)
@@ -851,6 +859,19 @@ def init(
force: bool = typer.Option(
False, "--force", help="Overwrite existing config without confirmation."
),
agent_signal: bool = typer.Option(
False, "--agent", help="Bootstrap an unattended Agent Mode account (no email required)."
),
source: str | None = typer.Option(
None,
"--source",
help="Channel attribution for signup (e.g. github, hn, ph).",
),
agent_caller: str | None = typer.Option(
None,
"--agent-caller",
help="Self-declared agent identity (e.g. claude-code, cursor). Used with --agent to attribute Agent Mode signups.",
),
) -> None:
"""Interactive setup wizard for mem0 CLI.
@@ -859,10 +880,97 @@ def init(
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
mem0 init --agent --agent-caller claude-code # AI agent self-identifies on Agent Mode bootstrap
mem0 init --email alice@company.com # Claims an existing Agent Mode key when one is present
"""
from mem0_cli.commands.init_cmd import run_init
run_init(api_key=api_key, user_id=user_id, email=email, code=code, force=force)
run_init(
api_key=api_key,
user_id=user_id,
email=email,
code=code,
force=force,
source=source,
agent=agent_signal,
agent_caller=agent_caller,
)
@app.command(rich_help_panel="Setup")
def identify(
name: str = typer.Argument(..., help="Agent identity (e.g. claude-code, cursor, my-bot)."),
) -> None:
"""Tag your active Agent Mode key with the AI agent that's using it.
Run this once after `mem0 init --agent` if you didn't pass --agent-caller.
Idempotent — re-running just overwrites the value.
Example:
mem0 identify claude-code
"""
from mem0_cli.commands.identify_cmd import run_identify
run_identify(name)
@app.command(name="whoami", rich_help_panel="Setup")
def whoami_cmd() -> None:
"""Print your AGENTRUSH identifier (default_user_id).
Example:
mem0 whoami
"""
from mem0_cli.commands.whoami_cmd import run_whoami
run_whoami()
# ── AGENTRUSH sub-app ─────────────────────────────────────────────────────
agent_rush_app = typer.Typer(
name="agent-rush",
help="AGENTRUSH game commands",
no_args_is_help=True,
rich_markup_mode="rich",
)
@agent_rush_app.callback(invoke_without_command=True)
def _agent_rush_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"agent-rush.{ctx.invoked_subcommand}")
@agent_rush_app.command(name="add")
def agent_rush_add(
content: str = typer.Argument(..., help="Memory content (50-1000 characters, no URLs)."),
) -> None:
"""Submit a memory to AGENTRUSH.
Example:
mem0 agent-rush add "I enjoy solving constraint-satisfaction problems."
"""
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_add
run_agent_rush_add(content)
@agent_rush_app.command(name="search")
def agent_rush_search(
query: str = typer.Argument(..., help="Search query."),
) -> None:
"""Search AGENTRUSH memories.
Example:
mem0 agent-rush search "constraint satisfaction"
"""
from mem0_cli.commands.agent_rush_cmd import run_agent_rush_search
run_agent_rush_search(query)
app.add_typer(agent_rush_app, name="agent-rush", rich_help_panel="Setup")
# (entity_app registered at module level, below sub-group definitions)
@@ -1198,11 +1306,28 @@ def main() -> None:
import sys
# Allow --json/--agent anywhere in the command line (not just before subcommand).
_json_flags = {"--json", "--agent"}
if any(a in _json_flags for a in sys.argv[1:]):
# Special case: `mem0 init --agent` is a subcommand flag (Agent Mode bootstrap)
# consumed by init_cmd, not a global JSON-output toggle — leave it in argv.
argv_rest = sys.argv[1:]
is_init = "init" in argv_rest
_global_flags = {"--json"} if is_init else {"--json", "--agent"}
if any(a in _global_flags for a in argv_rest):
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
sys.argv = [sys.argv[0]] + [a for a in sys.argv[1:] if a not in _json_flags]
sys.argv = [sys.argv[0]] + [a for a in argv_rest if a not in _global_flags]
app()
try:
app()
finally:
# Surface any unclaimed Agent Mode notice once per command, after the
# primary output. In JSON/agent mode the notice is folded into the
# envelope by format_json_envelope, so skip the stderr banner there
# to avoid duplicate output.
from mem0_cli.state import is_agent_mode, take_notice
notice = take_notice()
if notice and not is_agent_mode():
from rich.console import Console
Console(stderr=True).print(f"\n[yellow]🔔 {notice}[/yellow]\n")
+27 -6
View File
@@ -30,7 +30,7 @@ class PlatformBackend(Backend):
)
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
from mem0_cli.state import is_agent_mode
from mem0_cli.state import capture_notice, is_agent_mode
self._client.headers["X-Mem0-Caller-Type"] = "agent" if is_agent_mode() else "user"
resp = self._client.request(method, path, **kwargs)
@@ -48,7 +48,26 @@ class PlatformBackend(Backend):
resp.raise_for_status()
if resp.status_code == 204:
return {}
return resp.json()
data = resp.json()
# Pull the unclaimed-Agent-Mode notice out of the body (or the header
# fallback for endpoints that return non-dict / non-dict-leading
# payloads) and stash it for end-of-command surfacing.
notice = None
if isinstance(data, dict) and "mem0_notice" in data:
notice = data.pop("mem0_notice")
elif (
isinstance(data, list)
and data
and isinstance(data[0], dict)
and "mem0_notice" in data[0]
):
notice = data[0].pop("mem0_notice")
if notice is None:
notice = resp.headers.get("X-Mem0-Notice-Message") or None
capture_notice(notice)
return data
def add(
self,
@@ -277,13 +296,15 @@ 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(
results[entity_type] = self._request(
"DELETE", f"/v2/entities/{entity_type}/{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.
+4 -2
View File
@@ -87,10 +87,12 @@ def print_error(console: Console, message: str, hint: str | None = None) -> None
}
print(_json.dumps(envelope))
return
from rich.markup import escape
sym = _sym("✗", "[error]")
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {message}")
console.print(f"[{ERROR_COLOR}]{sym} Error:[/] {escape(str(message))}")
if hint:
console.print(f" [{DIM_COLOR}]{hint}[/]")
console.print(f" [{DIM_COLOR}]{escape(str(hint))}[/]")
def print_warning(console: Console, message: str) -> None:
@@ -0,0 +1,239 @@
"""Agent Mode commands — bootstrap (unattended signup) and claim (OTP-based human upgrade)."""
from __future__ import annotations
import json
import sys
from datetime import datetime, timezone
from typing import Any
import httpx
import typer
from rich.console import Console
from rich.prompt import Prompt
from mem0_cli.branding import (
BRAND_COLOR,
DIM_COLOR,
print_error,
print_success,
)
from mem0_cli.config import Mem0Config, save_config
console = Console()
err_console = Console(stderr=True)
_SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
}
def _validate_envelope(envelope: Any) -> None:
"""Defend against partial/malformed backend responses.
A backend regression that returns ``{"api_key": null}`` would otherwise be
silently persisted, producing confusing downstream errors far from the
source. Fail fast with a clear message if the required fields are missing.
"""
if not isinstance(envelope, dict):
print_error(err_console, "Bootstrap response was not a JSON object.")
raise typer.Exit(1)
for field in ("api_key", "default_user_id"):
value = envelope.get(field)
if not isinstance(value, str) or not value:
print_error(
err_console,
f"Bootstrap response missing required field {field!r} — please update the CLI.",
)
raise typer.Exit(1)
def bootstrap_via_backend(
config: Mem0Config,
*,
source: str | None = None,
agent_caller: str | None = None,
) -> None:
"""POST /api/v1/auth/agent_mode/ and mutate config in place.
Args:
config: Mem0Config mutated in place with the new platform values.
source: ``--source`` flag passthrough (analytics tag, free-form).
agent_caller: Self-declared agent identity passed via ``--agent-caller``
(e.g. ``claude-code``, ``cursor``). May be None when the caller
omitted the flag; the agent can backfill later via
``mem0 identify <name>``. Sent to the backend in the request body
and saved into ``platform.agent_caller`` for local introspection.
Raises typer.Exit(1) on failure.
"""
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
body: dict[str, Any] = {}
if source:
body["source"] = source
if agent_caller:
body["agent_caller"] = agent_caller
try:
with httpx.Client(timeout=30.0) as client:
resp = client.post(
f"{base_url}/api/v1/auth/agent_mode/",
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
json=body,
)
except httpx.HTTPError as exc:
print_error(err_console, f"Network error contacting Mem0: {exc}")
raise typer.Exit(1) from exc
if resp.status_code == 429:
print_error(err_console, "Rate-limited. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code == 503:
print_error(err_console, "Agent Mode is temporarily disabled. Try again later.")
raise typer.Exit(1)
if resp.status_code != 200:
detail = resp.text
try:
err_body = resp.json()
detail = err_body.get("error") or err_body.get("detail") or resp.text
except (json.JSONDecodeError, ValueError, AttributeError):
pass
# Backend's @ratelimit decorator raises PermissionDenied, which DRF
# translates to a generic 403 "You do not have permission to perform
# this action." That's opaque — surface as the rate-limit it actually is.
if resp.status_code == 403 and "permission" in str(detail).lower():
print_error(
err_console,
"Daily Agent Mode signup limit reached for this network (5/day). Try again from a different IP or after midnight UTC.",
)
raise typer.Exit(1)
print_error(err_console, f"Bootstrap failed: {detail}")
raise typer.Exit(1)
envelope = resp.json()
_validate_envelope(envelope)
config.platform.api_key = envelope["api_key"]
config.platform.base_url = base_url
config.platform.agent_mode = True
config.platform.created_via = "agent_mode"
config.platform.agent_caller = agent_caller or ""
config.platform.claimed_at = ""
config.platform.default_user_id = envelope["default_user_id"]
# Adopt the slug-derived user_id as the default scope for memory ops.
config.defaults.user_id = envelope["default_user_id"]
save_config(config)
print_success(console, f"Agent Mode active. Default user_id: {envelope['default_user_id']}")
notice = envelope.get("mem0_notice")
if notice:
console.print(f"\n[yellow]🔔 {notice}[/yellow]\n")
else:
# Fallback if the backend hasn't deployed the unified notice yet.
claim_cmd = envelope.get("claim_command", "mem0 init --email <your-email>")
console.print(f" [{DIM_COLOR}]To claim this account later: {claim_cmd}[/]")
if not agent_caller:
console.print(
f" [{DIM_COLOR}]If you are an AI agent, identify yourself: "
f"`mem0 identify <your-name>` (e.g. claude-code, cursor).[/]"
)
def claim_via_otp(config: Mem0Config, *, email: str, code: str | None = None) -> None:
"""Claim an existing Agent Mode account via OTP — no browser, no polling.
Reuses the standard email-code flow (`/api/v1/auth/email_code/` then
`/.../verify/`) and adds the local agent-mode API key in the verify body
as `agent_mode_api_key`. Backend's `verify_email_code` runs the
upgrade-in-place transaction inline and returns claim result.
On success: flips `platform.agent_mode=false`, sets `claimed_at`, stamps
`user_email`. The api_key value itself never changes.
"""
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
if not config.platform.api_key or not config.platform.agent_mode:
print_error(
err_console,
"This command requires an active Agent Mode config. Run `mem0 init` first.",
)
raise typer.Exit(1)
raw_key = config.platform.api_key
with httpx.Client(timeout=30.0) as client:
# Step 1: request OTP (unless --code provided)
if not code:
send = client.post(
f"{base_url}/api/v1/auth/email_code/",
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
json={"email": email},
)
if send.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if send.status_code != 200:
try:
detail = send.json().get("error", send.text)
except Exception:
detail = send.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, f"Verification code sent to {email}. Check your inbox.")
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint=f"Re-run: mem0 init --email {email} --code <code>",
)
raise typer.Exit(1)
console.print()
code = Prompt.ask(f" [{BRAND_COLOR}]Verification Code[/]")
if not code:
print_error(err_console, "Code is required.")
raise typer.Exit(1)
# Step 2: verify + claim in one shot
verify = client.post(
f"{base_url}/api/v1/auth/email_code/verify/",
headers={**_SOURCE_HEADERS, "Content-Type": "application/json"},
json={
"email": email,
"code": code.strip(),
"agent_mode_api_key": raw_key,
},
)
if verify.status_code != 200:
try:
err_body = verify.json()
detail = err_body.get("error", verify.text)
code_str = err_body.get("code", "")
except (json.JSONDecodeError, ValueError, AttributeError):
detail = verify.text
code_str = ""
print_error(err_console, f"Claim failed: {detail}")
if code_str == "email_already_claimed":
console.print(
f" [{DIM_COLOR}]Tip: this email already has a Mem0 account. Sign in at app.mem0.ai with your existing credentials.[/]"
)
raise typer.Exit(1)
claim_body = verify.json()
if not claim_body.get("claimed"):
print_error(err_console, f"Unexpected verify response: {claim_body}")
raise typer.Exit(1)
config.platform.agent_mode = False
config.platform.claimed_at = claim_body.get("claimed_at") or _utcnow_iso()
config.platform.user_email = email
config.platform.created_via = "email"
save_config(config)
print_success(console, f"Agent claimed to {email}. Your API key is unchanged.")
def _utcnow_iso() -> str:
return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,132 @@
"""mem0 agent-rush — AGENTRUSH game commands.
Wraps the platform's /v1/agent-rush/{memories/, memories/search/} endpoints.
Hardcoded routing; no flags needed.
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone
import httpx
import typer
from rich.console import Console
from mem0_cli.branding import print_error, print_success
from mem0_cli.config import load_config, save_config
console = Console()
err_console = Console(stderr=True)
_PII_WARNING_LINES = (
"",
"[yellow]⚠️ AGENTRUSH memories are PUBLIC — visible to any other player.[/yellow]",
"[yellow] Do not include real names, emails, secrets, work content, or PII.[/yellow]",
"",
)
_SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Mode": "agent-rush",
}
_ERROR_HINTS = {
"agentrush_search_first": "Run 3 'mem0 agent-rush search' commands before adding.",
"agentrush_search_quota": "You've used your 3 lifetime searches.",
"agentrush_add_quota": "You've used your 3 lifetime adds.",
"agentrush_not_agent_mode": "Re-run 'mem0 init --agent' to bootstrap an agent-mode key.",
"agentrush_length": "Memory text must be 50-1000 characters.",
"agentrush_no_urls": "URLs are not allowed.",
"agentrush_blocklist": "Content contains a blocked term.",
"agentrush_global_quota": "Event-wide cap reached. Try again later.",
"agentrush_not_provisioned": "AGENTRUSH is not provisioned in this environment.",
}
def _call(path: str, body: dict) -> dict:
config = load_config()
if not config.platform.api_key:
print_error(err_console, "Not initialized. Run `mem0 init --agent` first.")
raise typer.Exit(1)
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
try:
with httpx.Client(timeout=30.0) as client:
resp = client.post(
f"{base_url}{path}",
headers={
**_SOURCE_HEADERS,
"Authorization": f"Token {config.platform.api_key}",
"Content-Type": "application/json",
},
json=body,
)
except httpx.HTTPError as exc:
print_error(err_console, f"Network error: {exc}")
raise typer.Exit(1) from exc
try:
data = resp.json()
except Exception:
data = {}
if resp.status_code >= 400:
code = (
(data.get("error") or {}).get("code", "unknown")
if isinstance(data, dict)
else "unknown"
)
print_error(err_console, f"AGENTRUSH error: {code}")
hint = _ERROR_HINTS.get(code)
if hint:
console.print(f" [dim]{hint}[/dim]")
raise typer.Exit(1)
return data
def _ensure_warning_acknowledged() -> None:
"""Block the first interactive add on the PII warning; pass-through for agents.
Interactive (TTY): show prompt, require explicit 'y', persist
`agent_rush.acknowledged_at` so we never ask the same machine twice.
Non-interactive (no TTY — typical when an agent runs the CLI): surface
the warning to stderr for the human reading the agent transcript and
proceed without prompting (agents can't answer y/N).
"""
config = load_config()
if config.agent_rush.acknowledged_at:
return
is_tty = sys.stdin.isatty() and sys.stdout.isatty()
if not is_tty:
for line in _PII_WARNING_LINES:
err_console.print(line)
return
for line in _PII_WARNING_LINES:
console.print(line)
answer = typer.prompt(" Continue? [y/N]", default="N", show_default=False).strip().lower()
if answer not in ("y", "yes"):
print_error(err_console, "Aborted.")
raise typer.Exit(1)
config.agent_rush.acknowledged_at = datetime.now(timezone.utc).isoformat()
save_config(config)
def run_agent_rush_add(content: str) -> None:
_ensure_warning_acknowledged()
result = _call("/v1/agent-rush/memories/", {"content": content})
event_id = result.get("event_id", "?")
print_success(console, f"Memory submitted (event_id: {event_id})")
def run_agent_rush_search(query: str) -> None:
result = _call("/v1/agent-rush/memories/search/", {"query": query})
memories = result.get("results") or result.get("memories") or []
if not memories:
console.print("[dim](no results)[/dim]")
return
for i, m in enumerate(memories[:5], start=1):
text = m.get("memory") if isinstance(m, dict) else str(m)
console.print(f" {i}. {text}")
@@ -0,0 +1,75 @@
"""mem0 identify — declare which agent owns the current agent-mode key.
Used when `mem0 init --agent` ran without --agent-caller, so the backend
saved agent_caller=NULL. The agent re-runs `mem0 identify <name>` to PATCH
its own row with its real identity. Idempotent — running it again just
overwrites.
"""
from __future__ import annotations
import httpx
import typer
from rich.console import Console
from mem0_cli.branding import print_error, print_success
from mem0_cli.config import load_config, save_config
console = Console()
err_console = Console(stderr=True)
_SOURCE_HEADERS = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
}
def run_identify(name: str) -> None:
"""PATCH the active agent-mode key's agent_caller field."""
config = load_config()
if not config.platform.api_key:
print_error(
err_console,
"No API key configured. Run `mem0 init --agent` first.",
)
raise typer.Exit(1)
if not config.platform.agent_mode:
print_error(
err_console,
"This command only works on unclaimed agent-mode keys.",
)
raise typer.Exit(1)
name = (name or "").strip()
if not name:
print_error(err_console, "Agent name is required.")
raise typer.Exit(1)
base_url = (config.platform.base_url or "https://api.mem0.ai").rstrip("/")
try:
with httpx.Client(timeout=30.0) as client:
resp = client.patch(
f"{base_url}/api/v1/auth/agent_mode/caller/",
headers={
**_SOURCE_HEADERS,
"Authorization": f"Token {config.platform.api_key}",
"Content-Type": "application/json",
},
json={"agent_caller": name},
)
except httpx.HTTPError as exc:
print_error(err_console, f"Network error: {exc}")
raise typer.Exit(1) from exc
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Identify failed: {detail}")
raise typer.Exit(1)
canonical = resp.json().get("agent_caller", name)
config.platform.agent_caller = canonical
save_config(config)
print_success(console, f"Identified as {canonical}.")
+149 -1
View File
@@ -103,6 +103,25 @@ def _validate_email(email: str) -> None:
raise typer.Exit(1)
def _ping_key(api_key: str, base_url: str, timeout: float = 5.0) -> bool:
"""Validate api_key against /v1/ping/.
Returns False ONLY on a definitive "invalid key" signal (HTTP 401 / 403).
Network errors, timeouts, and 5xx responses return True so we prefer
reusing an existing key over silently minting a new shadow on a transient
blip (which would also clobber config + plugin-sync targets).
"""
try:
resp = httpx.get(
f"{base_url.rstrip('/')}/v1/ping/",
headers={"Authorization": f"Token {api_key}"},
timeout=timeout,
)
except httpx.HTTPError:
return True # unknown — prefer reuse
return resp.status_code not in (401, 403)
def _email_login(
email: str,
code: str | None,
@@ -182,21 +201,143 @@ def run_init(
email: str | None = None,
code: str | None = None,
force: bool = False,
source: str | None = None,
agent: bool = False,
agent_caller: str | None = None,
) -> None:
"""Interactive setup wizard for mem0 CLI.
When both *api_key* and *user_id* are supplied, all prompts are skipped
(non-interactive mode). When running in a non-TTY without the required
flags, an error message is printed.
Agent Mode dispatch (no email/api-key flags):
- If existing config has an active API key → reuse (existing_key path).
- Else if any positive agent signal (--agent, --json global, agent env
var, or `agent` flag) → POST /api/v1/auth/agent_mode/ and write config.
- Else fall through to the interactive wizard.
Claim dispatch:
- If `--email` is set AND existing config has `agent_mode=true`, run the
claim device-flow against the existing key instead of minting a new
email-based key.
"""
from mem0_cli.agent_detect import detect_agent_caller
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend, claim_via_otp
from mem0_cli.state import is_agent_mode as _global_agent_mode
from mem0_cli.telemetry import capture_event
def _fire_init(mode: str, *, claimed: bool = False) -> None:
"""Fire cli.init telemetry with M1-M6 properties."""
props: dict = {"command": "init", "mode": mode}
if agent_caller:
# Self-declared via --agent-caller; not sniffed from env vars.
props["agent_caller"] = agent_caller
if source:
props["signup_source"] = source
if claimed:
props["claimed_agent_mode"] = True
capture_event("cli.init", props)
config = Mem0Config()
base_url = os.environ.get("MEM0_BASE_URL", config.platform.base_url or DEFAULT_BASE_URL)
config.platform.base_url = base_url
if code and not email:
print_error(err_console, "--code requires --email.")
raise typer.Exit(1)
# ── Email + existing agent-mode config → claim flow ─────────────────
if email and CONFIG_FILE.exists():
existing = load_config()
if existing.platform.agent_mode and existing.platform.api_key:
email = email.strip().lower()
_validate_email(email)
print_info(console, f"Claiming Agent Mode account to {email}...")
claim_via_otp(existing, email=email, code=code)
_fire_init("email", claimed=True)
return
# ── Agent Mode path runs BEFORE the existing-config guard ──────────
# Rules 1/2 REUSE a valid existing key (not overwrite), so we must
# short-circuit before the guard prompts. Rule 3 mints only when there
# is no valid key to reuse — in that case overwriting is correct.
_agent_ctx = agent or _global_agent_mode() or (detect_agent_caller() is not None)
if not api_key and not email and _agent_ctx:
from mem0_cli.output import format_json_envelope
from mem0_cli.state import is_agent_mode as _is_json_mode
def _emit_reuse(source: str) -> None:
if _is_json_mode():
format_json_envelope(
console,
command="init",
data={
"api_key_saved": False,
"api_key_source": source,
"agent_mode": False,
"message": "Existing Mem0 API key found and reused. No Agent Mode key was created.",
},
)
else:
msg = (
"Existing MEM0_API_KEY is valid; reusing it. No new Agent Mode key was minted."
if source == "env"
else "Existing API key in config is valid; reusing it. No new Agent Mode key was minted."
)
print_success(console, msg)
def _maybe_identify(key: str) -> None:
"""Best-effort PATCH agent_caller when --agent-caller is supplied on a
reused key. Silent no-op on any failure — reuse must not break.
"""
if not agent_caller:
return
try:
resp = httpx.patch(
f"{base_url.rstrip('/')}/api/v1/auth/agent_mode/caller/",
headers={
"Authorization": f"Token {key}",
"Content-Type": "application/json",
},
json={"agent_caller": agent_caller},
timeout=10.0,
)
# Also reflect in local config so introspection matches backend.
if resp.status_code == 200 and CONFIG_FILE.exists():
try:
cfg = load_config()
cfg.platform.agent_caller = resp.json().get("agent_caller", agent_caller)
save_config(cfg)
except Exception:
pass
except httpx.HTTPError:
pass
# Rule 1: env MEM0_API_KEY valid → reuse, no new key.
_env_key = (os.environ.get("MEM0_API_KEY") or "").strip()
if _env_key and _ping_key(_env_key, base_url):
_maybe_identify(_env_key)
_emit_reuse("env")
_fire_init("existing_key")
return
# Rule 2: existing config api_key valid → reuse.
if CONFIG_FILE.exists():
_existing = load_config()
if _existing.platform.api_key and _ping_key(_existing.platform.api_key, base_url):
_maybe_identify(_existing.platform.api_key)
_emit_reuse("config")
_fire_init("existing_key")
return
# Rule 3: mint a fresh shadow (no valid key to reuse).
# agent_caller is the agent's self-declared identity from --agent-caller
# (Proof Editor-style). Env-var auto-detect is still used above to
# decide we're in an agent context, but never to fill identity.
bootstrap_via_backend(config, source=source, agent_caller=agent_caller)
_fire_init("agent")
return
# Warn if an existing config with an API key would be overwritten
if not force and CONFIG_FILE.exists():
existing = load_config()
@@ -242,6 +383,7 @@ def run_init(
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.platform.user_email = email
config.platform.created_via = "email"
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
@@ -258,6 +400,8 @@ def run_init(
return
# ── API key flow (existing) ───────────────────────────────────────
# (Agent Mode branch runs earlier — see above, before the existing-config
# guard, so Rules 1/2 can REUSE a valid key without prompting overwrite.)
# Non-TTY: resolve defaults so partial flags work in pipelines / CI
if not sys.stdin.isatty():
@@ -265,7 +409,7 @@ def run_init(
print_error(
err_console,
"Non-interactive terminal detected and --api-key is required.",
hint="Run: mem0 init --api-key <key> [--user-id <id>]",
hint="Run: mem0 init --api-key <key>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
)
raise typer.Exit(1)
user_id = user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
@@ -273,6 +417,7 @@ def run_init(
# Fully non-interactive when both flags provided
if api_key and user_id:
config.platform.api_key = api_key
config.platform.created_via = "api_key"
config.defaults.user_id = user_id
_validate_platform(config)
save_config(config)
@@ -313,6 +458,7 @@ def run_init(
config.platform.api_key = api_key_val
config.platform.base_url = base_url
config.platform.user_email = email_addr
config.platform.created_via = "email"
config.defaults.user_id = (
user_id or os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
)
@@ -331,6 +477,7 @@ def run_init(
# API key flow
if api_key:
config.platform.api_key = api_key
config.platform.created_via = "api_key"
else:
_setup_platform(config)
@@ -370,6 +517,7 @@ def _setup_platform(config: Mem0Config) -> None:
raise typer.Exit(1)
config.platform.api_key = api_key
config.platform.created_via = "api_key"
def _setup_defaults(config: Mem0Config) -> None:
@@ -0,0 +1,25 @@
"""mem0 whoami — print the active agent's default_user_id (AGENTRUSH identifier)."""
from __future__ import annotations
import typer
from rich.console import Console
from mem0_cli.branding import BRAND_COLOR, print_error, print_info
from mem0_cli.config import load_config
console = Console()
err_console = Console(stderr=True)
def run_whoami() -> None:
config = load_config()
session_id = config.platform.default_user_id if config.platform else None
if not session_id:
print_error(
err_console,
"No default_user_id found. Run `mem0 init --agent` first.",
)
raise typer.Exit(1)
console.print(f"Your AGENTRUSH identifier: [{BRAND_COLOR}]{session_id}[/{BRAND_COLOR}]")
print_info(console, "Find your row at https://mem0.ai/agentrush")
+49 -1
View File
@@ -28,6 +28,14 @@ class PlatformConfig:
api_key: str = ""
base_url: str = DEFAULT_BASE_URL
user_email: str = ""
# Agent Mode (unclaimed-shadow signup)
agent_mode: bool = False # True while the key is an unclaimed agent-mode key
created_via: str = "" # "agent_mode" | "email" | "api_key" | "existing_key"
agent_caller: str = (
"" # canonical agent name when created_via == "agent_mode" (e.g. "claude-code")
)
claimed_at: str = "" # ISO timestamp once the agent has been claimed by a human
default_user_id: str = "" # `user_<slug>` returned by bootstrap; used as auto-default
@dataclass
@@ -43,12 +51,20 @@ class TelemetryConfig:
anonymous_id: str = ""
@dataclass
class AgentRushConfig:
# ISO timestamp the human acknowledged the "memories are public" warning.
# Empty until first interactive `mem0 agent-rush add`.
acknowledged_at: str = ""
@dataclass
class Mem0Config:
version: int = CONFIG_VERSION
defaults: DefaultsConfig = field(default_factory=DefaultsConfig)
platform: PlatformConfig = field(default_factory=PlatformConfig)
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
agent_rush: AgentRushConfig = field(default_factory=AgentRushConfig)
SHORT_KEY_ALIASES: dict[str, str] = {
@@ -83,6 +99,11 @@ def load_config() -> Mem0Config:
config.platform.api_key = plat.get("api_key", "")
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
config.platform.user_email = plat.get("user_email", "")
config.platform.agent_mode = bool(plat.get("agent_mode", False))
config.platform.created_via = plat.get("created_via", "")
config.platform.agent_caller = plat.get("agent_caller", "")
config.platform.claimed_at = plat.get("claimed_at", "")
config.platform.default_user_id = plat.get("default_user_id", "")
defaults = data.get("defaults", {})
config.defaults.user_id = defaults.get("user_id", "")
@@ -92,6 +113,9 @@ def load_config() -> Mem0Config:
telemetry = data.get("telemetry", {})
config.telemetry.anonymous_id = telemetry.get("anonymous_id", "")
agent_rush = data.get("agent_rush", {})
config.agent_rush.acknowledged_at = agent_rush.get("acknowledged_at", "")
# Environment variable overrides
env_key = os.environ.get("MEM0_API_KEY")
if env_key:
@@ -136,10 +160,18 @@ def save_config(config: Mem0Config) -> None:
"api_key": config.platform.api_key,
"base_url": config.platform.base_url,
"user_email": config.platform.user_email,
"agent_mode": config.platform.agent_mode,
"created_via": config.platform.created_via,
"agent_caller": config.platform.agent_caller,
"claimed_at": config.platform.claimed_at,
"default_user_id": config.platform.default_user_id,
},
"telemetry": {
"anonymous_id": config.telemetry.anonymous_id,
},
"agent_rush": {
"acknowledged_at": config.agent_rush.acknowledged_at,
},
}
with open(CONFIG_FILE, "w") as f:
@@ -147,6 +179,19 @@ def save_config(config: Mem0Config) -> None:
os.chmod(CONFIG_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
# Propagate the active api_key to ecosystem touchpoints (Claude Code
# plugin env injection, shell rc exports). Idempotent — only updates
# EXISTING entries; never creates new ones. Best-effort: any IOError
# in the sync is swallowed so config.json is always the authoritative
# write, never blocked by plugin-state issues.
if config.platform.api_key:
try:
from mem0_cli.plugin_sync import sync_api_key
sync_api_key(config.platform.api_key)
except Exception:
pass
def redact_key(key: str) -> str:
"""Redact an API key for display: m0-xxx...xxx"""
@@ -190,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
+25 -6
View File
@@ -20,8 +20,8 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print(f"\n[{BRAND_COLOR}]Found {count} {title}:[/]\n")
for i, mem in enumerate(memories, 1):
memory_text = mem.get("memory", 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}[/]")
@@ -229,6 +229,16 @@ def format_json_envelope(
if error:
envelope["error"] = error
envelope["data"] = data
# If the platform flagged this as an unclaimed Agent Mode account, surface
# the notice inside the JSON envelope so an agent consuming the output
# sees it without needing to inspect HTTP headers.
from mem0_cli.state import take_notice
notice = take_notice()
if notice:
envelope["mem0_notice"] = notice
console.print_json(json.dumps(envelope, default=str))
@@ -323,6 +333,15 @@ def format_agent_envelope(
if count is not None:
envelope["count"] = count
envelope["data"] = sanitize_agent_data(command, data)
# Surface the unclaimed-Agent-Mode notice (if any) in the envelope so an
# agent reading the JSON output sees it without inspecting HTTP headers.
from mem0_cli.state import take_notice
notice = take_notice()
if notice:
envelope["mem0_notice"] = notice
console.print_json(json.dumps(envelope, default=str))
+119
View File
@@ -0,0 +1,119 @@
"""Sync the active Mem0 API key into other ecosystem touchpoints.
Why this exists:
The CLI canonical state lives in ``~/.mem0/config.json``. But MCP servers
(Claude Code plugin, Codex plugin, etc.) read ``MEM0_API_KEY`` from env
vars or their own config files. Without a sync, an agent-mode bootstrap
mints a new key into config.json but the plugin's MCP keeps using the
old key from env — silent surprise.
Design:
- Update ONLY entries that already exist (never create new ones)
- Preserve all surrounding content / formatting / other keys
- Atomic writes (tmpfile + rename) so a crash mid-write doesn't corrupt
- Idempotent — re-running with the same key is a no-op
- Skip on dry_run
Targets currently handled:
- ``~/.claude/settings.json::env::MEM0_API_KEY`` (Claude Code env injection)
- ``~/.zshrc`` / ``~/.bashrc`` ``export MEM0_API_KEY="..."`` lines
Out of scope (deliberately not touched):
- Codex / Cursor MCP configs — would require schema-aware edits and
those tools don't have mem0 entries by default
- Plugin's own ``<plugin-dir>/.api_key`` file — plugin-managed
"""
from __future__ import annotations
import contextlib
import json
import os
import re
import tempfile
from pathlib import Path
# Files we know how to update safely.
_CLAUDE_SETTINGS = Path.home() / ".claude" / "settings.json"
_SHELL_RCS = [Path.home() / ".zshrc", Path.home() / ".bashrc", Path.home() / ".bash_profile"]
def sync_api_key(api_key: str) -> list[str]:
"""Propagate ``api_key`` into known ecosystem touchpoints.
Returns the list of paths actually updated. Empty list means nothing
needed updating (either targets didn't exist or already had this value).
"""
if not api_key:
return []
updated: list[str] = []
if _update_claude_settings(_CLAUDE_SETTINGS, api_key):
updated.append(str(_CLAUDE_SETTINGS))
for rc in _SHELL_RCS:
if _update_shell_rc(rc, api_key):
updated.append(str(rc))
return updated
def _update_claude_settings(path: Path, api_key: str) -> bool:
"""Update ``env.MEM0_API_KEY`` in path. Returns True if file was changed."""
if not path.is_file():
return False
try:
with path.open("r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return False
env = data.get("env")
if not isinstance(env, dict) or "MEM0_API_KEY" not in env:
# No existing entry — don't create one.
return False
if env["MEM0_API_KEY"] == api_key:
return False # already in sync
env["MEM0_API_KEY"] = api_key
_atomic_write_text(path, json.dumps(data, indent=2, ensure_ascii=False) + "\n")
return True
# Match `export MEM0_API_KEY="..."` (or single quotes, or no quotes).
# Use [ \t]* (not \s*) for trailing whitespace so a trailing newline at
# end-of-file is preserved when MEM0_API_KEY is the last line.
_RC_LINE = re.compile(
r'^([ \t]*export[ \t]+MEM0_API_KEY[ \t]*=[ \t]*)(["\']?)([^"\'\n]*)(["\']?)[ \t]*$',
re.MULTILINE,
)
def _update_shell_rc(path: Path, api_key: str) -> bool:
"""Update an existing ``export MEM0_API_KEY=...`` line in path."""
if not path.is_file():
return False
try:
text = path.read_text(encoding="utf-8")
except OSError:
return False
match = _RC_LINE.search(text)
if not match:
return False # no existing line
if match.group(3) == api_key:
return False
new_text = _RC_LINE.sub(lambda m: f'{m.group(1)}"{api_key}"', text, count=1)
_atomic_write_text(path, new_text)
return True
def _atomic_write_text(path: Path, content: str) -> None:
"""Write content to path atomically (temp + rename)."""
dirname = path.parent
fd, tmp_path = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=dirname)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
# Preserve mode if the original existed.
if path.exists():
os.chmod(tmp_path, path.stat().st_mode & 0o777)
os.replace(tmp_path, path)
except Exception:
with contextlib.suppress(OSError):
os.unlink(tmp_path)
raise
+21
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
_agent_mode: bool = False
_current_command: str = ""
_pending_notice: str = ""
def is_agent_mode() -> bool:
@@ -22,3 +23,23 @@ def get_current_command() -> str:
def set_current_command(name: str) -> None:
global _current_command
_current_command = name
def capture_notice(notice: str | None) -> None:
"""Stash a Mem0 backend notice for end-of-command surfacing.
Called from the platform backend after each response so the notice can
be printed once per command (regardless of how many sub-requests fired).
Last-write-wins is fine — the message text is identical across requests.
"""
global _pending_notice
if notice:
_pending_notice = notice
def take_notice() -> str:
"""Return and clear the pending notice."""
global _pending_notice
msg = _pending_notice
_pending_notice = ""
return msg
+13 -4
View File
@@ -87,7 +87,6 @@ def capture_event(
try:
from mem0_cli import __version__
from mem0_cli.config import CONFIG_FILE, load_config, save_config
from mem0_cli.state import is_agent_mode
config = load_config()
distinct_id = pre_resolved_email or _get_distinct_id()
@@ -107,6 +106,9 @@ def capture_event(
with contextlib.suppress(Exception):
save_config(config)
# M4: every cli.* event carries agent_mode based on the config flag
# (unclaimed Agent Mode key). This is the growth-doc property used to
# join init → add → search funnels in PostHog.
payload = {
"api_key": POSTHOG_API_KEY,
"distinct_id": distinct_id,
@@ -115,7 +117,7 @@ def capture_event(
"source": "CLI",
"language": "python",
"cli_version": __version__,
"agent_mode": is_agent_mode(),
"agent_mode": bool(config.platform.agent_mode),
"python_version": sys.version,
"os": sys.platform,
"os_version": platform.version(),
@@ -135,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
+13 -2
View File
@@ -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"):
+160
View File
@@ -0,0 +1,160 @@
"""Parity tests for `mem0 init --agent` (Agent Mode bootstrap).
Mirror of ``cli/node/tests/agent-mode.test.ts`` — both files MUST stay in
sync so that the Python and Node CLIs expose an identical surface for the
Agent Mode entrypoint. If you add a flag here, add the same assertion on
the Node side (and vice versa).
Network-bound bootstrap is covered by the platform-side E2E suite
(``backend/tests/e2e/test_05_agent_mode.py``); these tests only verify
the CLI surface that ships in the binary.
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
import pytest
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[mKJHABCDfsu]")
def _strip_ansi(text: str) -> str:
return _ANSI_RE.sub("", text)
def _run(args: list[str], home_dir: str | None = None) -> subprocess.CompletedProcess:
env = os.environ.copy()
for key in list(env.keys()):
if key.startswith("MEM0_"):
del env[key]
env.pop("FORCE_COLOR", None)
env["PYTHONIOENCODING"] = "utf-8"
if home_dir:
env["HOME"] = home_dir
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", *args],
capture_output=True,
encoding="utf-8",
env=env,
timeout=15,
)
return subprocess.CompletedProcess(
args=result.args,
returncode=result.returncode,
stdout=_strip_ansi(result.stdout),
stderr=_strip_ansi(result.stderr),
)
@pytest.fixture
def clean_home(tmp_path):
return str(tmp_path)
class TestInitFlagSurface:
"""`mem0 init --help` must expose the Agent Mode flags."""
def test_init_help_lists_agent_flag(self):
result = _run(["init", "--help"])
assert result.returncode == 0
assert "--agent" in result.stdout
def test_init_help_describes_agent_mode(self):
result = _run(["init", "--help"])
assert result.returncode == 0
# Description must mention what --agent actually does so an agent
# reading the help can self-discover the bootstrap entrypoint.
assert "Agent Mode" in result.stdout or "unattended" in result.stdout.lower()
def test_init_help_lists_source_flag(self):
result = _run(["init", "--help"])
assert result.returncode == 0
assert "--source" in result.stdout
def test_init_help_lists_email_and_code(self):
# Claim flow flags must remain present alongside Agent Mode flags.
result = _run(["init", "--help"])
assert result.returncode == 0
assert "--email" in result.stdout
assert "--code" in result.stdout
class TestArgvPreprocessing:
"""`--agent` on `init` must reach init_cmd, not be eaten by the global preprocessor.
Regression for the bug where the top-level `--agent` JSON-alias was
stripped from ``sys.argv`` before Typer could bind it to the init
subcommand, making ``mem0 init --agent`` indistinguishable from a
plain ``mem0 init`` (interactive wizard).
"""
def test_init_with_agent_reaches_subcommand(self, clean_home):
# We can't hit a real backend in unit tests, so we point the CLI at
# a guaranteed-dead URL and assert the failure is the bootstrap
# request failing — proving the --agent flag was honored and the
# bootstrap branch ran, not the interactive wizard.
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent"],
capture_output=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,
)
combined = _strip_ansi(result.stdout + result.stderr).lower()
# Either we got a connection/network error from the bootstrap POST,
# or the CLI surfaced an Agent Mode-specific failure message.
assert (
"agent" in combined
or "connect" in combined
or "network" in combined
or "fetch" in combined
or "bootstrap" in combined
), f"Expected bootstrap attempt, got: {combined!r}"
class TestJsonEnvelopeParity:
"""`mem0 init --agent --json` should produce a JSON envelope on success.
Without a live backend we can only assert the failure shape: when the
backend is unreachable, the CLI must still exit non-zero AND not crash
on a Python traceback (which would mean we leaked an exception past
the agent-mode handler).
"""
def test_init_agent_json_no_traceback_on_network_failure(self, clean_home):
result = subprocess.run(
[sys.executable, "-m", "mem0_cli", "init", "--agent", "--json"],
capture_output=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,
)
combined = _strip_ansi(result.stdout + result.stderr)
assert "Traceback (most recent call last)" not in combined
assert result.returncode != 0
class TestInitInCommandList:
"""`mem0 --help` must list `init` so agents walking the top-level help
can discover the Agent Mode entrypoint without prior knowledge."""
def test_top_level_help_lists_init(self):
result = _run(["--help"])
assert result.returncode == 0
assert "init" in result.stdout
+2 -1
View File
@@ -49,6 +49,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 +57,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(
+7 -7
View File
@@ -8,8 +8,8 @@ from io import StringIO
from unittest.mock import patch
import pytest
from click.exceptions import Exit as ClickExit
from rich.console import Console
from typer import Exit as TyperExit
from mem0_cli.commands.config_cmd import (
cmd_config_get,
@@ -181,7 +181,7 @@ class TestAddCommand:
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("mem0_cli.commands.memory._stdin_is_piped", return_value=False),
pytest.raises((SystemExit, 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,
@@ -764,7 +764,7 @@ class TestImportCommand:
with (
patch("mem0_cli.commands.utils.console", console),
patch("mem0_cli.commands.utils.err_console", err_console),
pytest.raises((SystemExit, ClickExit)),
pytest.raises((SystemExit, TyperExit)),
):
cmd_import(mock_backend, "/nonexistent/file.json", user_id=None, agent_id=None)
@@ -801,7 +801,7 @@ class TestEntitiesListCommand:
with (
patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, ClickExit)),
pytest.raises((SystemExit, TyperExit)),
):
cmd_entities_list(mock_backend, "invalid", output="table")
@@ -944,7 +944,7 @@ class TestEntitiesDeleteCommand:
with (
patch("mem0_cli.commands.entities.console", console),
patch("mem0_cli.commands.entities.err_console", err_console),
pytest.raises((SystemExit, ClickExit)),
pytest.raises((SystemExit, TyperExit)),
):
cmd_entities_delete(
mock_backend,
@@ -1308,7 +1308,7 @@ class TestAgentMode:
patch("mem0_cli.commands.memory.console", console),
patch("mem0_cli.commands.memory.err_console", err_console),
patch("sys.stdout", captured_stdout),
pytest.raises((SystemExit, ClickExit)),
pytest.raises((SystemExit, TyperExit)),
):
cmd_get(mock_backend, "bad-id", output="text")
+7 -1
View File
@@ -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
+206
View File
@@ -0,0 +1,206 @@
"""Unit tests for init internals — decision tree primitives + plugin sync.
These tests exercise the units that the high-level subprocess parity tests in
``test_agent_mode.py`` deliberately can't reach:
- ``_ping_key`` must NOT treat network errors as "invalid key" (else a VPN
flap silently mints a new shadow over a working key).
- ``plugin_sync`` must only update entries that already exist, preserve
trailing newlines, and never mangle other lines.
- The 403→ratelimit translation in ``bootstrap_via_backend`` surfaces the
real cause instead of DRF's opaque "You do not have permission" string.
Mirror surface lives in ``cli/node/tests/agent-mode.test.ts``; if you add a
behavioral assertion here, mirror it on the Node side and vice versa.
"""
from __future__ import annotations
from unittest.mock import MagicMock
import httpx
import pytest
from mem0_cli.commands.init_cmd import _ping_key
from mem0_cli.plugin_sync import _update_claude_settings, _update_shell_rc
# ── _ping_key ──────────────────────────────────────────────────────────────
class _Resp:
def __init__(self, status_code: int) -> None:
self.status_code = status_code
def test_ping_key_200_is_valid(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(200))
assert _ping_key("k", "http://x") is True
def test_ping_key_401_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(401))
assert _ping_key("k", "http://x") is False
def test_ping_key_403_is_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(403))
assert _ping_key("k", "http://x") is False
def test_ping_key_5xx_is_not_definitively_invalid(monkeypatch: pytest.MonkeyPatch) -> None:
# Transient upstream failure must NOT cause a shadow to be minted.
monkeypatch.setattr(httpx, "get", lambda *a, **kw: _Resp(503))
assert _ping_key("k", "http://x") is True
def test_ping_key_connect_error_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
# Network blip (DNS, captive portal, etc.) — must NOT trigger a re-mint.
def boom(*a, **kw):
raise httpx.ConnectError("nope")
monkeypatch.setattr(httpx, "get", boom)
assert _ping_key("k", "http://x") is True
def test_ping_key_timeout_prefers_reuse(monkeypatch: pytest.MonkeyPatch) -> None:
def boom(*a, **kw):
raise httpx.ReadTimeout("slow")
monkeypatch.setattr(httpx, "get", boom)
assert _ping_key("k", "http://x") is True
# ── plugin_sync._update_shell_rc ──────────────────────────────────────────
def test_shell_rc_updates_existing_export_preserves_trailing_newline(tmp_path) -> None:
rc = tmp_path / ".zshrc"
rc.write_text('export MEM0_API_KEY="old"\n', encoding="utf-8")
changed = _update_shell_rc(rc, "newkey")
assert changed is True
assert rc.read_text(encoding="utf-8") == 'export MEM0_API_KEY="newkey"\n'
def test_shell_rc_does_not_create_new_export(tmp_path) -> None:
rc = tmp_path / ".zshrc"
rc.write_text("alias ll='ls -la'\n", encoding="utf-8")
changed = _update_shell_rc(rc, "newkey")
assert changed is False
assert rc.read_text(encoding="utf-8") == "alias ll='ls -la'\n"
def test_shell_rc_preserves_surrounding_content(tmp_path) -> None:
rc = tmp_path / ".zshrc"
original = "# my zshrc\nalias ll='ls -la'\nexport MEM0_API_KEY='old'\nexport OTHER=keepme\n"
rc.write_text(original, encoding="utf-8")
_update_shell_rc(rc, "newkey")
after = rc.read_text(encoding="utf-8")
assert "alias ll='ls -la'\n" in after
assert "export OTHER=keepme\n" in after
assert "# my zshrc\n" in after
assert 'export MEM0_API_KEY="newkey"\n' in after
def test_shell_rc_idempotent_when_already_matching(tmp_path) -> None:
rc = tmp_path / ".zshrc"
rc.write_text('export MEM0_API_KEY="same"\n', encoding="utf-8")
assert _update_shell_rc(rc, "same") is False
def test_shell_rc_missing_file_is_noop(tmp_path) -> None:
rc = tmp_path / ".zshrc" # does not exist
assert _update_shell_rc(rc, "x") is False
# ── plugin_sync._update_claude_settings ────────────────────────────────────
def test_claude_settings_does_not_create_env_block(tmp_path) -> None:
import json
settings = tmp_path / "settings.json"
settings.write_text(json.dumps({"otherKey": 1}), encoding="utf-8")
changed = _update_claude_settings(settings, "newkey")
assert changed is False
# Original content unchanged.
assert json.loads(settings.read_text(encoding="utf-8")) == {"otherKey": 1}
def test_claude_settings_does_not_create_mem0_entry_in_existing_env(tmp_path) -> None:
import json
settings = tmp_path / "settings.json"
settings.write_text(json.dumps({"env": {"OTHER_KEY": "x"}}), encoding="utf-8")
changed = _update_claude_settings(settings, "newkey")
assert changed is False
def test_claude_settings_updates_existing_entry(tmp_path) -> None:
import json
settings = tmp_path / "settings.json"
settings.write_text(
json.dumps({"env": {"MEM0_API_KEY": "old", "OTHER": "y"}}, indent=2),
encoding="utf-8",
)
changed = _update_claude_settings(settings, "fresh")
assert changed is True
data = json.loads(settings.read_text(encoding="utf-8"))
assert data["env"]["MEM0_API_KEY"] == "fresh"
assert data["env"]["OTHER"] == "y" # other keys preserved
def test_claude_settings_idempotent(tmp_path) -> None:
import json
settings = tmp_path / "settings.json"
settings.write_text(json.dumps({"env": {"MEM0_API_KEY": "same"}}), encoding="utf-8")
assert _update_claude_settings(settings, "same") is False
def test_claude_settings_malformed_json_is_noop(tmp_path) -> None:
settings = tmp_path / "settings.json"
settings.write_text("{ this is not json", encoding="utf-8")
assert _update_claude_settings(settings, "x") is False
# ── bootstrap rate-limit translation ──────────────────────────────────────
def test_bootstrap_403_permission_surfaces_ratelimit(monkeypatch, capsys) -> None:
"""DRF 403 'You do not have permission' must be translated to the daily limit message."""
from mem0_cli.commands.agent_mode_cmd import bootstrap_via_backend
from mem0_cli.config import Mem0Config
fake_resp = MagicMock()
fake_resp.status_code = 403
fake_resp.text = '{"detail": "You do not have permission to perform this action."}'
fake_resp.json = MagicMock(
return_value={"detail": "You do not have permission to perform this action."}
)
class _Client:
def __init__(self, *a, **kw):
pass
def __enter__(self):
return self
def __exit__(self, *a):
return False
def post(self, *a, **kw):
return fake_resp
monkeypatch.setattr(httpx, "Client", _Client)
cfg = Mem0Config()
cfg.platform.base_url = "https://api.mem0.ai"
import typer
with pytest.raises(typer.Exit):
bootstrap_via_backend(cfg)
captured = capsys.readouterr()
combined = captured.out + captured.err
assert "Daily Agent Mode signup limit reached" in combined
assert "permission to perform this action" not in combined
+23
View File
@@ -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):
+46
View File
@@ -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()
+80
View File
@@ -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"
+10 -1
View File
@@ -4,7 +4,7 @@ description: "Add facts, messages, or metadata to a user memory store with async
openapi: post /v3/memories/add/
---
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction: one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
## Endpoint
@@ -50,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.
@@ -83,3 +84,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.
+8 -1
View File
@@ -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.
+13 -7
View File
@@ -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"]
+11 -2
View File
@@ -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/"
---
+42 -3
View File
@@ -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, graph settings, and language preferences:
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
```python
# Update project with custom categories
@@ -98,6 +98,17 @@ client.project.update(
# Use the input language for memory storage and retrieval
client.project.update(multilingual=True)
# Set retrieval criteria to control which memories are surfaced in search
client.project.update(
retrieval_criteria=[
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
]
)
# 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="...",
@@ -109,9 +120,37 @@ client.project.update(
)
```
#### Set Retrieval Criteria
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
```python
client.project.update(
retrieval_criteria=[
{
"name": "joy",
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
"weight": 3
},
{
"name": "curiosity",
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
"weight": 2
},
{
"name": "access_frequency",
"description": "How often this memory has been accessed or surfaced recently.",
"weight": 1
}
]
)
```
Pass an empty list to clear all criteria and restore default retrieval behaviour.
#### 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/"
---
@@ -0,0 +1,5 @@
---
title: "Update Project"
description: "Update a project's settings, including name, custom instructions, and other configuration options."
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/"
---
+140 -63
View File
@@ -4,16 +4,121 @@ description: "Major product launches, headline features, and milestones for Mem0
mode: "wide"
---
<Update label="2026-06-27" description="SDK memory expiration">
**SDK Memory Expiration: Expiring Memories Across Python and TypeScript**
The latest SDK releases add first-class expiration controls to memory writes, updates, and reads, plus new TypeScript provider coverage for production deployments.
- **Python client updates:** `MemoryClient.update()` and `AsyncMemoryClient.update()` now accept `expiration_date`, including `None` to clear an existing expiration.
- **TypeScript client updates:** `AddMemoryOptions`, `update()`, and `Memory` now support `expirationDate`; `search()` and `getAll()` can include expired memories with `showExpired`.
- **New TypeScript LLM providers:** `MiniMaxLLM` and `LiteLLM` are now available for OpenAI-compatible MiniMax and LiteLLM proxy deployments.
- **PGVector deployment flexibility:** TypeScript PGVector config now supports `connectionString` and `ssl`, so apps can use a managed Postgres URI instead of separate connection fields.
See [SDK & Tools](/changelog/sdk) for version details and PR links.
</Update>
<Update label="2026-06-25" description="Mem0 Plugin v0.2.11">
**Mem0 Plugin: Shared Memory for Claude Code, Cursor, Codex, and Antigravity**
The shared Mem0 editor plugin is current through v0.2.11:
- **Automatic context injection:** File reads, bash errors, session resume prompts, and startup timelines can retrieve relevant memories automatically.
- **Project and global scopes:** Project-scoped memories remain the default, while `global_search` supports team-wide recall across users and app scopes.
- **Coding categories:** A 17-category coding taxonomy installs in the background and is cached per Mem0 account.
- **Reliable capture:** Auto-capture, compaction summaries, session summaries, and metadata defaults keep memories scoped and readable.
- **Editor correctness:** Telemetry now reports Claude Code, Cursor, Codex, and Antigravity separately with each editor's real plugin version.
</Update>
<Update label="2026-06-25" description="Antigravity Plugin v0.1.3">
**Antigravity Plugin: Mem0 for Google Antigravity**
Antigravity support in the shared Mem0 editor plugin family is current through v0.1.3:
- **Self-contained plugin:** Includes its own plugin manifest, MCP config, hooks, shared scripts, and skills.
- **AGENTS.md convention:** Uses Antigravity's `contextFileName: "AGENTS.md"` convention.
- **Lifecycle hooks:** Wires session start, prompt recall, file-read context, memory-tool metadata enforcement, bash-error lookup, post-tool tracking, and stop summaries.
- **Shared memory layer:** Reuses the Mem0 Platform MCP tools and the shared 16-skill command bundle.
- **Better automatic recall:** v0.1.3 adds reranked injected context and clean `files_touched` metadata in session summaries.
- **Telemetry correctness:** v0.1.2 reports Antigravity as `antigravity` with the plugin's own version.
</Update>
<Update label="2026-06-22" description="OpenCode Plugin v0.2.0">
**OpenCode Plugin: Native SDK Memory Tools for OpenCode**
`@mem0/opencode-plugin` adds memory to OpenCode and is current through v0.2.0:
- **Native SDK tools:** Memory tools register through `@opencode-ai/plugin` and call the `mem0ai` SDK directly, so the plugin no longer depends on `mcp.mem0.ai`.
- **Memory scopes:** Operations support `project`, `session`, and `global` scope, with `/mem0-scope` to change the default.
- **Automatic context:** File-context injection, structured compaction summaries, and a recent-activity timeline surface relevant memories without manual recall.
- **Auto-dream consolidation:** Gated consolidation merges duplicates, drops stale or sensitive entries, and rewrites vague memories.
- **Skills load in place:** The `config` hook adds bundled skills to `skills.paths` instead of copying them into user config directories.
- **Safety controls:** Blocks `MEMORY.md` writes and redacts secrets before storing memories.
</Update>
<Update label="2026-06-12" description="Pi Agent Plugin v0.1.2">
**Pi Agent Plugin: Persistent Memory for Pi Agent**
`@mem0/pi-agent-plugin` adds semantic memory to Pi Agent and has been updated through v0.1.2:
- **Agent memory tool:** Registers `mem0_memory` for scoped search, add, get, delete, and delete-all operations.
- **Slash commands:** Adds `/mem0-remember`, `/mem0-search`, `/mem0-forget`, `/mem0-tour`, `/mem0-dream`, `/mem0-pin`, `/mem0-scope`, and `/mem0-status`.
- **Auto-capture:** Stores user and assistant memories after agent turns.
- **Dream consolidation:** Merges duplicates, resolves contradictions, and prunes stale memories behind session, time, and memory-count gates.
- **Project scoping:** Uses git-root detection for stable `app_id` values across monorepos.
- **Relevant command results:** v0.1.2 adds visible command feedback and thresholded, reranked search for search, forget, and pin commands.
</Update>
<Update label="2026-06-12" description="OpenClaw v1.0.13">
**OpenClaw Plugin: Current Memory Backend for OpenClaw**
`@mem0/openclaw-mem0` is current through v1.0.13, with the original production-ready memory backend plus newer setup, security, and runtime work:
- **Skills-based memory architecture:** Triage, recall, and dream skills handle extraction, recall, consolidation, and tool guidance.
- **Chat and CLI setup:** Supports chat-based platform setup, `openclaw mem0 init`, direct API keys, email OTP, autonomous agent setup, and OSS onboarding.
- **Platform and OSS modes:** Works with Mem0 Platform or self-hosted OSS providers including OpenAI, Anthropic, Ollama, Qdrant, and PGVector.
- **Agent-friendly CLI:** All 16 CLI commands support `--json` for machine-driven setup and diagnostics.
- **Runtime integration:** Exposes OpenClaw memory capability APIs for search manager and backend config status.
- **Security and compliance:** Added path containment checks, sensitive config metadata, dependency overrides, telemetry hashing, and metadata-only registration safety.
</Update>
<Update label="2026-06-10" description="Vercel AI SDK Provider v3.0.0">
**Vercel AI SDK Provider: Memory-Augmented Generation for AI SDK v6**
The Vercel AI SDK provider moved to the v6 provider contract and Mem0 v3 APIs:
- **AI SDK v6 support:** Migrated to `LanguageModelV3` / `ProviderV3`, including v3 stream lifecycle events and content arrays.
- **Mem0 v3 API support:** Memory writes and searches now use `/v3/memories/add/` and `/v3/memories/search/`.
- **Mem0 sources in responses:** `generateText` and `streamText` responses include memories as sources with `providerMetadata.mem0.memories`.
- **Safer prompt handling:** Prompts are cloned before memory injection, avoiding caller-side mutation.
- **Async storage fix:** `addMemories` is awaited so generated memories are not silently dropped.
- **Raw memory utilities:** Exports `searchMemories`, `retrieveMemories`, `getMemories`, and `addMemories` for apps that need direct memory control.
- **Deployment controls:** Per-request `mem0ApiKey` and `host` support Mem0 Platform, custom API keys, and self-hosted API endpoints.
</Update>
<Update label="2026-05-13" description="Temporal Reasoning for Mem0 Platform v3">
**Temporal Reasoning — Time-Aware Retrieval for Platform v3**
**Temporal Reasoning: Time-Aware Retrieval for Platform v3**
Mem0 Platform v3 can now interpret time-aware memories and queries so assistants retrieve the right information for questions about the past, upcoming plans, and current state.
- **Time-aware search intent** — Queries like `last week`, `upcoming`, `right now`, and `as of March 2025` return contextually appropriate results automatically
- **Enabled by default** — No per-request toggle required for v3 writes or searches
- **Anchored relative queries** — `reference_date` anchors relative search phrases for tests, backfills, and reproducible demos
- **Normal response shape** — Temporal reasoning affects ranking while preserving existing client response patterns
- **Search intent parsing:** Queries like `last week`, `upcoming`, `right now`, and `as of March 2025` now resolve against memory timestamps automatically.
- **Default v3 behavior:** No per-request toggle is needed for v3 writes or searches.
- **Deterministic testing:** Pass `reference_date` to anchor relative phrases in tests, backfills, and demos.
- **Stable API shape:** Temporal reasoning changes ranking, not the client response contract.
See [Temporal Reasoning](/platform/features/temporal-reasoning) for usage details.
@@ -21,11 +126,11 @@ See [Temporal Reasoning](/platform/features/temporal-reasoning) for usage detail
<Update label="2026-05-08" description="Memory Decay">
**Memory Decay — Recently-Used Memories Surface Higher, Automatically**
**Memory Decay: Recently-Used Memories Surface Higher**
Per-project search-time ranking bias that boosts recently-touched memories and gently dampens stale ones. Off by default; opt in per project via the `decay` field on the project endpoint, or via `client.project.update(decay=True)` in the SDKs (Python `v2.0.2` / TypeScript `v3.0.3`).
- **Soft bias, never a filter.** The scaling factor stays in `0.3×–1.5×`. Decay can reorder candidates but never zeros them out — anything that surfaced before decay can still surface after.
- **Soft bias, never a filter.** The scaling factor stays in `0.3×–1.5×`. Decay can reorder candidates but never removes them; anything that surfaced before decay can still surface after.
- **Reinforcement loop.** Every memory returned in a search has its access history updated, so frequently-used facts naturally float to the top over time.
- **Public score still clamped to `[0, 1]`.** Existing API contract preserved; no client-side changes needed.
- **v3 search only**, fully reversible. See [Memory Decay docs](/platform/features/memory-decay).
@@ -34,76 +139,48 @@ Per-project search-time ranking bias that boosts recently-touched memories and g
<Update label="2026-04-14" description="Mem0 SDK v2.0.0 / v3.0.0">
**New Memory Algorithm — State-of-the-Art Accuracy at ~3-4x Lower Cost**
**New Memory Algorithm: State-of-the-Art Accuracy at ~3-4x Lower Cost**
Ground-up rewrite of the memory pipeline with 20+ point benchmark improvements:
- **LoCoMo:** 71.4 → **91.6** (+20) — multi-turn conversation recall
- **LongMemEval:** 67.8 → **93.4** (+26) — long-term memory across sessions
- **BEAM (1M tokens):** **64.1** — production-scale memory evaluation
- **Agent memories are first-class** — Previous algorithm: 46% on assistant recall. New: **100%**
- **Temporal reasoning works** — "Where did I live before SF?" Previous: 51%. New: **93%**
- **~3-4x fewer tokens** — Under 7K tokens per retrieval vs 25K+ for full-context approaches
- **ADD-only extraction** — Memories accumulate; nothing is overwritten or deleted
- **Hybrid retrieval** — Semantic + BM25 keyword + entity boost, scored in parallel
- **Entity linking** — Entities extracted, embedded, and linked across memories
- **LoCoMo:** 71.4 → **91.6** (+20) for multi-turn conversation recall.
- **LongMemEval:** 67.8 → **93.4** (+26) for long-term memory across sessions.
- **BEAM (1M tokens):** **64.1** on production-scale memory evaluation.
- **Agent memories:** Assistant recall moves from 46% to **100%**.
- **Temporal reasoning:** "Where did I live before SF?" improves from 51% to **93%**.
- **Lower token use:** Retrieval stays under 7K tokens versus 25K+ for full-context approaches.
- **ADD-only extraction:** Memories accumulate; nothing is overwritten or deleted.
- **Hybrid retrieval:** Semantic search, BM25 keyword search, and entity boost are scored in parallel.
- **Graph memory (built-in)**: entities extracted, embedded, and linked across memories, with no external graph store required
Breaking changes: Graph memory removed from OSS, `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
Breaking changes: external graph stores removed from OSS (replaced by built-in graph memory), `search()` defaults changed, deprecated params removed. See [migration guide](/migration/oss-v2-to-v3).
</Update>
<Update label="2026-04-06" description="Mem0 Skill Graph">
**Mem0 Skill Graph — In-Context Documentation for AI Agents**
**Mem0 Skill Graph: In-Context Documentation for AI Agents**
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow — no doc searching required. Three interconnected skills launched:
AI coding agents in Claude Code, Cursor, and Codex can now access Mem0 knowledge directly in their workflow without leaving the editor. Three interconnected skills launched:
- **mem0 Core Skill** — Complete Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more
- **mem0-cli Skill** — Terminal command reference, configuration walkthroughs, and CI/CD recipes
- **mem0-vercel-ai-sdk Skill** — Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup
- **mem0 Core Skill:** Python and TypeScript SDK reference, REST API patterns, and integration guides for LangChain, CrewAI, Autogen, and more.
- **mem0-cli Skill:** Terminal command reference, configuration walkthroughs, and CI/CD recipes.
- **mem0-vercel-ai-sdk Skill:** Vercel AI SDK provider API, memory-augmented generation patterns, and multi-provider setup.
</Update>
<Update label="2026-04-06" description="Mem0 CLI v0.2.2">
**Official Mem0 CLI — Now on PyPI and npm**
**Official Mem0 CLI: Now on PyPI and npm**
A full-featured command-line interface for Mem0, available in both Python and Node.js:
- **Install:** `pip install mem0-cli` or `npm install -g @mem0/cli`
- **Full command suite** — `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`
- **Interactive setup** — `mem0 init` with email verification or direct API key entry
- **Works everywhere** — Platform (Mem0 Cloud) and self-hosted OSS modes
- **Scriptable** — `--json` flag for CI/CD pipelines and automation
- **Dual SDK** — Same commands, same experience across Python and Node.js
</Update>
<Update label="2026-04-06" description="OpenClaw v1.0.4">
**OpenClaw Plugin — Production-Ready**
The OpenClaw Mem0 plugin went from initial release to production-ready in one week (v1.0.0 → v1.0.4):
- **Skills-based memory architecture** — New extraction pipeline with skill-loader, batched extraction, and domain-aware memory triage
- **Dream gate** — Automatic memory consolidation during idle periods for higher-quality long-term recall
- **Interactive CLI** — `openclaw mem0 init`, `status`, `config`, `import`, and `event` commands
- **Unified tool naming** — `memory_add` and `memory_delete` replace 4 legacy tools, matching the platform API
- **Security hardened** — Path traversal protection, pinned dependencies, 329 tests across 10 files
</Update>
<Update label="2026-04-02" description="Mem0 Plugin for AI Editors">
**Mem0 Plugin for Claude Code, Cursor, and Codex**
Launched a unified Mem0 plugin across three major AI development environments — Claude Code and Cursor first (March 25), then Codex (April 2):
- **9 MCP memory tools** — add, search, get, update, delete, bulk delete, entity management via `mcp.mem0.ai`
- **Lifecycle hooks** — Automatic memory capture at session start, context compaction, task completion, and session end
- **Cloud MCP server** — Managed endpoint replaces local MCP and Smithery setup
- **Streamable HTTP transport** — New MCP transport protocol for real-time streaming
- **Codex-specific skill** — Dedicated skill in `mem0-plugin/skills/mem0-codex` for Codex workflows
- **Full command suite:** `add`, `search`, `list`, `get`, `update`, `delete`, `import`, `config`, `init`, `status`, `entity`, `event`.
- **Interactive setup:** `mem0 init` supports email verification and direct API key entry.
- **Runtime coverage:** Works with Mem0 Platform and self-hosted OSS modes.
- **Automation support:** Use `--json` for CI/CD pipelines and agent workflows.
- **Dual implementation:** Same commands and behavior across Python and Node.js.
</Update>
@@ -113,11 +190,11 @@ Launched a unified Mem0 plugin across three major AI development environments
Major expansion of the provider ecosystem:
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
- **Turbopuffer** — New vector database provider for Python SDK
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
- **Reasoning models** — `reasoning_effort` parameter for OpenAI o1/o3-style models
- **Apache AGE:** New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE). **Note:** All external graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE) were subsequently removed in v2.0.0 (2026-04-14). Graph memory is now built-in entity linking with no external graph store required; see the [v2.0.0 entry above](#mem0-sdk-v2-0-0-v3-0-0).
- **Turbopuffer:** New vector database provider for Python SDK.
- **MiniMax:** New LLM provider with dedicated AWS Bedrock support.
- **pgvector for Node.js:** PostgreSQL vector support added to the TypeScript OSS SDK.
- **Reasoning models:** `reasoning_effort` parameter for OpenAI o1/o3-style models.
</Update>
@@ -125,6 +202,6 @@ Major expansion of the provider ecosystem:
**Mem0 Platform Skill on skills.sh**
First skill launch — a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
First skill launch: a dedicated Mem0 skill providing platform API reference, quickstart patterns, and integration examples directly inside agent sessions. Available on [skills.sh](https://skills.sh) for any compatible AI coding agent.
</Update>
-303
View File
@@ -1,303 +0,0 @@
---
title: "OpenClaw"
description: "Release notes for the OpenClaw plugin and agent harness."
mode: "wide"
---
<Update label="2026-04-29" description="v1.0.11">
**New Features:**
- **Skills-mode auto-setup:** `enableSkillsConfig()` now runs automatically after onboarding — enables triage, recall (with reranking + keyword search), and dream consolidation with `tools.profile = "full"` and disables the built-in session-memory hook to avoid conflicts
- **Memory runtime capability:** Plugin now exposes `runtime.getMemorySearchManager()` and `resolveMemoryBackendConfig()` on the registered memory capability, enabling OpenClaw gateway to query memory status and backend config directly
- **Dimension-aware collections:** OSS wizard detects embedder dimension changes and creates a new collection (`mem0_<dims>d`) automatically, with a warning about old memories being inaccessible under the new embedder
- **Tool documentation in skills:** Both `memory-triage` and `memory-dream` SKILL.md files now include full tool reference sections listing all available tools with parameters
**Improvements:**
- **Auto-capture and auto-recall default to enabled:** `autoCapture` and `autoRecall` now default to `true` (was `false`). Manifest descriptions updated accordingly. Ignored in skills mode
- **`memory_update` over delete+add:** Skills now prefer `memory_update` for in-place edits — atomic and preserves edit history. Consolidation pattern updated: update best memory, delete redundant ones
- **Search threshold lowered:** Default `searchThreshold` reduced from `0.5` to `0.1` for broader recall. Removed hardcoded `0.6` recall-specific override — all searches now use the configured threshold
- **Embedder dimension propagation:** Vector store config auto-resolves dimensions from embedder config when not explicitly set. Syncs `dimension` and `embeddingModelDims` fields for Qdrant/PGVector compatibility
- **Config file write safety:** `writeFullConfig()` now re-reads and deep-merges the `plugins` section before writing, preserving `installs` and `slots` written by the OpenClaw gateway
- **Additional embedder models:** Added `mxbai-embed-large` (1024), `all-minilm` (384), and `snowflake-arctic-embed` (1024) to known embedder dimensions
**Security:**
- Bumped `protobufjs` to `>=7.5.5` via pnpm overrides (GHSA-xq3m-2v4x-88gg) ([#5012](https://github.com/mem0ai/mem0/pull/5012))
**Fixes:**
- Moved `bootstrapTelemetryFlag()` and removed `ensureInstallRecord()` from module-level side effects — both now run inside `register()` to avoid crashes when loaded outside OpenClaw gateway
- Fixed OSS history DB path resolution: absolute paths no longer passed through `resolvePath()`, preventing double-prefix bugs
- Manifest `providerAuthEnvVars` replaced with spec-compliant `setup.providers` format using `id` + `envVars`
**Dependencies:**
- Bumped `mem0ai` from `3.0.1` to `3.0.2`
- Bumped `pluginApi` and `minGatewayVersion` compat to `>=2026.4.24`
</Update>
<Update label="2026-04-23" description="v1.0.10">
**Security:**
- Telemetry `distinct_id` now uses SHA-256 instead of MD5 — prevents rainbow-table reversal of API key hashes
- User email is now SHA-256 hashed before sending as `distinct_id` — no PII in telemetry payloads
- Declared PostHog telemetry endpoint (`us.i.posthog.com`) in `providerEndpoints`
**Fixes:**
- Fixed version-pinned install records preventing plugin updates. `ensureInstallRecord()` now detects semver-pinned specs (e.g. `@mem0/openclaw-mem0@1.0.7`) and rewrites them to `@latest` or `clawhub:` prefix so `openclaw plugins update` resolves to the newest release
- Fixed `searchThreshold` default inconsistency: standardized to `0.3` across docs, README, and manifest
- `PLUGIN_VERSION` now injected at build time via tsup `define` from `package.json` — no more hardcoded version strings
**Manifest Compliance:**
- Removed non-spec fields: `requiredEnvVars`, `dataLocations`, `privacy`, `setup` (with `externalEndpoints`, `providers`, `requiresRuntime`, `postInstallHint`)
- Replaced `setup.externalEndpoints` with spec-compliant `providerEndpoints` using `endpointClass` + `hosts` format
- Env var declarations now rely solely on `providerAuthEnvVars` (already spec-compliant)
**Docs:**
- Fixed `openclaw plugins update` command: uses plugin ID (`openclaw-mem0`), not npm package name (`@mem0/openclaw-mem0`)
- Added update section to README
- Removed redundant "Key Features" and "Conclusion" sections from integration docs
</Update>
<Update label="2026-04-22" description="v1.0.9">
**Security & Compliance:**
- Added top-level `requiredEnvVars` to plugin manifest, declaring env vars per mode (platform, OSS OpenAI, OSS Anthropic, OSS Ollama). Fixes ClaHub scanner "required env vars: none" mismatch
- Added `sensitive: true` and descriptions to `apiKey` and `userEmail` in `configSchema` — previously only declared in `uiHints`
- Added `default: false` with descriptions to `autoCapture` and `autoRecall` in `configSchema` so scanner can confirm opt-in defaults
- Added `dataLocations` field to manifest declaring all persistence paths (config, vectorStore, historyDb, dreamState)
- Added `privacy` field to manifest documenting data flow for platform vs open-source mode and credential storage guidance
- Added `externalEndpoints` to `setup` section declaring api.mem0.ai and app.mem0.ai with purpose and requirement context
**Tests:**
- Replaced direct `process.env` access in `tests/cli-commands.test.ts` and `tests/fs-safe.test.ts` with `vi.stubEnv`/`vi.unstubAllEnvs`. Fixes ClaHub static analysis flag for "environment variable access combined with network send"
- 421 tests across 15 test files
</Update>
<Update label="2026-04-21" description="v1.0.8">
**New Features:**
- **OSS Onboarding Wizard:** New guided 4-step interactive setup for open-source mode — walks through LLM provider, embedding provider, vector store, and user ID selection with prefilled defaults
- **Agent-Friendly CLI:** Added `--json` flag to all 16 CLI commands for machine-readable output. Agents can call `openclaw mem0 help --json` to discover every command and flag
- **Non-Interactive OSS Setup:** Added `--mode open-source` with `--oss-llm`, `--oss-embedder`, `--oss-vector` flags for fully automated OSS configuration without prompts
- **JSON Helpers Module:** New `cli/json-helpers.ts` with `jsonOut`, `jsonErr`, and `redactSecrets` utilities for consistent structured output
**Improvements:**
- **Init Flow Redesigned:** Replaced 3-option flat menu with 2-level structure: Platform (email login or API key) and Open Source (guided wizard)
- **Provider Selection:** LLM providers: OpenAI, Ollama, Anthropic. Embedding providers: OpenAI, Ollama. Vector stores: Qdrant, PGVector
- **Input Prefill:** All prompts with defaults (base URL, user ID) now prefill the input field instead of showing defaults in brackets
- **Smart Reuse:** When LLM and embedder use the same provider, API key and base URL are automatically reused from the LLM step
- **Default Model:** Updated default LLM model to `gpt-5-mini`
- **Manifest Compliance:** Removed undocumented fields, aligned env var declarations between SKILL.md and manifest, fixed `configSchema.required` for clean installs
**Tests:**
- 404 tests across 15 test files (+3 new: `json-helpers.test.ts`, `oss-wizard.test.ts`, `cli-commands.test.ts`)
</Update>
<Update label="2026-04-20" description="v1.0.7">
**New Features:**
- **Chat-Based Setup:** Added chat-based Platform setup flow — users can now configure the plugin conversationally instead of editing config files manually
- **Installation Docs Rewrite:** Rewrote README and integration docs with chat-first setup, numbered manual steps.
**Improvements:**
- **SDK Upgrade:** Bumped `mem0ai` dependency to 3.0.1 for V3 API compatibility
- **Config Cleanup:** Dropped deprecated `orgId`, `projectId`, `enableGraph` config options; updated CLI prompts ([#4734](https://github.com/mem0ai/mem0/pull/4734), [#4764](https://github.com/mem0ai/mem0/pull/4764))
- **Noise Filtering:** Expanded noise patterns in memory add tool; handle leading text in JSON extraction
</Update>
<Update label="2026-04-11" description="v1.0.6">
**Bug Fixes:**
- **Telemetry:** Replaced shared `"anonymous-openclaw"` fallback with a persistent per-machine random hash (`openclaw-anon-<uuid>`), so anonymous plugin users are counted individually in PostHog ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added PostHog `$identify` event on first authenticated run to stitch anonymous history onto the authenticated profile ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Fixed event loss on short-lived CLI invocations — added `beforeExit` handler to flush queued events before the process exits ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Added lazy `/v1/ping/` email resolution so users who configure API key outside `mem0 init` show as their email in PostHog, not an md5 hash ([#4790](https://github.com/mem0ai/mem0/pull/4790))
- **Telemetry:** Unified CLI event prefix from `openclaw.<cmd>` to `openclaw.cli.<cmd>` on the needsSetup branch to match the authenticated branch ([#4790](https://github.com/mem0ai/mem0/pull/4790))
**Improvements:**
- **API:** Added `source: "OPENCLAW"` to all provider calls (`add`, `search`, `getAll`) across tools, CLI commands, recall, and the OSS backend adapter ([#4790](https://github.com/mem0ai/mem0/pull/4790))
</Update>
<Update label="2026-04-07" description="v1.0.5">
**Bug Fixes:**
- **Init interactive choice bug**: Fixed number selection in `openclaw mem0 init` — entering 1/2/3 now correctly selects the corresponding option (was broken by readline prefill concatenating with user input)
- **OSS pgvector crash** ([#4727](https://github.com/mem0ai/mem0/issues/4727)): Fixed "Client has already been connected" cascade when using pgvector in OSS mode. The warmup call swallowed errors leaving a half-initialized pg client; concurrent recall/capture then all hit `client.connect()` on the same client. Fix: let warmup errors propagate (so `initPromise` resets and retries with a fresh Memory + fresh pg client) and build fresh config objects per attempt instead of mutating shared state.
**Removed:**
- **`orgId` / `projectId` config parameters**: Removed from config schema, CLI (`config show/get/set`), init display, and providers. The API key is project-scoped, so separate org/project IDs are unnecessary and could cause access errors if mismatched.
- **`enableGraph` config parameter**: Removed from all config surfaces, providers, backend, and tools. Graph memory is being deprecated — removing the flag avoids unnecessary exposure.
</Update>
<Update label="2026-04-04" description="v1.0.4">
**New Features:**
- **Interactive init flow**: `openclaw mem0 init` with interactive menu (email verification or direct API key). Non-interactive modes: `--api-key`, `--email`, `--email --code`
- **`memory_add` tool**: Replaces `memory_store` — name now matches `mem0` CLI and platform API
- **`memory_delete` tool**: Unified delete — single ID, search-then-delete, bulk, entity cascade. Replaces `memory_forget` and `memory_delete_all`
- **CLI subcommands**: `openclaw mem0 init`, `openclaw mem0 status`, `openclaw mem0 config show`, `openclaw mem0 config set`
- **`import` CLI command**: Bulk-import memories from a JSON file with `--user-id` and `--agent-id` overrides
- **`event list` / `event status` CLI commands**: Monitor background processing events
- **`fs-safe.ts` module**: Isolated filesystem wrappers in a separate entry point
- **`backend/` module**: `PlatformBackend` with direct HTTP API access for CLI commands
- **Plugin manifest**: Added `contracts.tools`, `configSchema`, and `uiHints` to `openclaw.plugin.json`
- **Test suite**: 329 tests across 10 test files
**Changes:**
- **Modular architecture**: Extracted tools into `tools/` directory (6 files) and CLI into `cli/commands.ts`
- **Code splitting**: tsup builds with `splitting: true` and two entry points
- **Skills updated**: All SKILL.md files reference new tool names (`memory_add`, `memory_delete`)
- **Auto-recall timeout**: Recall wrapped in 8-second `Promise.race`
- **Auto-capture fire-and-forget**: `provider.add()` runs in background via `.then()/.catch()`
- **Auto-capture minimum content gate**: Skips extraction when total user content is fewer than 50 chars
**Removed:**
- `memory_store` tool — replaced by `memory_add`
- `memory_forget` tool — replaced by `memory_delete`
- `memory_delete_all` tool — merged into `memory_delete`
- `memory_history` tool and `history` CLI command — deprecated
</Update>
<Update label="2026-04-03" description="v1.0.3">
**Bug Fixes:**
- **Security**: Added `safePath()` containment helper to `readSkillFile` and `readDomainOverlay` in `skill-loader.ts` — prevents directory traversal
- **Noise filter**: Reverted incorrect `After-Compaction` regex rename back to `Post-Compaction`
**Changes:**
- **Supply-chain hardening**: Pinned `mem0ai` dependency to exact `2.3.0` (was `^2.3.0`)
**Tests:**
- 12 new tests covering `safePath`, `readSkillFile`, `readDomainOverlay`, and `loadSkill` with traversal inputs
</Update>
<Update label="2026-04-02" description="v1.0.2">
**Bug Fixes:**
- **Security**: Removed `resolveEnvVars()` and `resolveEnvVarsDeep()` from `config.ts` — plugin-side env resolution was redundant and triggered static analysis warnings ([#4676](https://github.com/mem0ai/mem0/pull/4676))
</Update>
<Update label="2026-04-02" description="v1.0.1">
**New Features:**
- **CD workflow**: Added continuous deployment workflow with OIDC trusted publishing ([#4672](https://github.com/mem0ai/mem0/pull/4672))
- **Plugin configuration manifest**: Added `compat` and `build` metadata to `package.json` ([#4667](https://github.com/mem0ai/mem0/pull/4667))
- **LICENSE**: Added Apache-2.0 license file ([#4667](https://github.com/mem0ai/mem0/pull/4667))
**Bug Fixes:**
- **Dream gate**: Fixed cheap-first ordering, session isolation, and verified completion ([#4666](https://github.com/mem0ai/mem0/pull/4666))
- **Graceful startup**: Plugin now starts gracefully when no API key is configured ([#4669](https://github.com/mem0ai/mem0/pull/4669))
</Update>
<Update label="2026-04-01" description="v1.0.0">
**New Features:**
- **Skills-based memory architecture**: New skill-loader and skill-based extraction pipeline with batched extraction ([#4624](https://github.com/mem0ai/mem0/pull/4624))
- **Dream gate**: Memory consolidation and dream-cycle processing during idle periods
- **Enhanced recall**: New `recall.ts` module with improved recall logic and skill-aware retrieval
- **Memory triage skill**: Domain-aware memory triage with companion domain support and recall protocol
- **Memory dream skill**: Skill for memory consolidation during idle periods
- **Plugin configuration**: Added `openclaw.plugin.json` manifest and `scripts/configure.py` setup helper
**Changes:**
- Extraction pipeline refactored to use skills-based architecture for more contextual and higher quality memory capture
</Update>
<Update label="2026-03-26" description="v0.4.1">
**New Features:**
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions
**Bug Fixes:**
- **Credential detection**: Improved detection of credentials, API keys, and secrets in extraction instructions (#4552)
- **Standalone timestamps**: Prevented extraction of standalone timestamps as memories (#4550)
</Update>
<Update label="2026-03-16" description="v0.4.0">
**New Features:**
- **Non-interactive trigger filtering**: Skips recall and capture for `cron`, `heartbeat`, `automation`, and `schedule` triggers
- **Subagent hallucination prevention**: Detects ephemeral subagent sessions and routes recall to parent namespace
- **Dynamic recall thresholding**: Memories scoring less than 50% of top result are dropped
- **SQLite resilience**: Init error recovery with automatic retry for OSS mode
- **`disableHistory` config option**: New `oss.disableHistory` flag
- 78 unit tests covering filtering, isolation, trigger filtering, subagent detection, and SQLite resilience
**Changes:**
- Auto-recall threshold raised from 0.5 to 0.6 for stricter precision
- Recall candidate pool increased to `topK * 2` for better filtering headroom
- Relaxed extraction instructions: related facts kept together to preserve context
**Bug Fixes:**
- **Concurrent session race condition**: Lifecycle hooks now use `ctx.sessionKey` directly instead of a shared mutable variable
</Update>
<Update label="2026-03-12" description="v0.3.1">
**New Features:**
- **Message filtering pipeline**: Multi-stage noise removal before extraction
- **Broad recall for new sessions**: Short or new-session prompts trigger secondary broad search
- **Client-side threshold filtering**: Safety net that drops low-relevance results
- **Temporal anchoring**: Extraction instructions now include current date
- 55 unit tests covering filtering and isolation helpers
**Changes:**
- Extraction window expanded from last 10 to last 20 messages
- Rewritten custom extraction instructions for conciseness and deduplication
- Refactored monolithic `index.ts` (1772 lines) into 6 focused modules
</Update>
<Update label="2026-03-10" description="v0.3.0">
**Bug Fixes:**
- Updated `mem0ai` dependency with sqlite3 to better-sqlite3 migration (#4270)
</Update>
<Update label="2026-03-09" description="v0.2.0">
**New Features:**
- Per-agent memory isolation for multi-agent setups via `agentId`
- "Understanding userId" section in docs
**Changes:**
- Updated config examples to use concrete `userId` values instead of placeholders
**Bug Fixes:**
- Migrated platform search to Mem0 v2 API
</Update>
<Update label="2026-02-19" description="v0.1.2">
**New Features:**
- Source field for openclaw memory entries
**Bug Fixes:**
- Auto-recall injection and auto-capture message drop
</Update>
<Update label="2026-02-02" description="v0.1.0">
**New Features:**
- Initial release of the OpenClaw Mem0 plugin
- Platform mode (Mem0 Cloud) and open-source mode support
- Auto-recall: inject relevant memories before each turn
- Auto-capture: store facts after each turn
- Configurable `topK`, `threshold`, and `apiVersion` options
</Update>
+2 -2
View File
@@ -1,6 +1,6 @@
---
title: "Platform"
description: "Release notes for the Mem0 hosted platform — backend, dashboard, billing, and infrastructure changes."
description: "Release notes for the Mem0 hosted platform: backend, dashboard, billing, and infrastructure changes."
mode: "wide"
---
@@ -25,7 +25,7 @@ mode: "wide"
<Update label="2026-04-16" description="">
**Improvements:**
- **UI:** Removed Graph Memory tab, page, and all references from dashboard, sidebar, project settings, playground, and billing
- **UI:** Removed the legacy external-graph-store visualization tab, page, and its references from dashboard, sidebar, project settings, playground, and billing
</Update>
+1087 -41
View File
File diff suppressed because it is too large Load Diff
@@ -59,5 +59,9 @@ Here are the parameters available for configuring AWS Bedrock embedder:
| Parameter | Description | Default Value |
| --- | --- | --- |
| `model` | The name of the embedding model to use | `amazon.titan-embed-text-v1` |
| `aws_region` | AWS region for the Bedrock client | `us-west-2` |
| `aws_access_key_id` | AWS access key ID for authentication | `None` |
| `aws_secret_access_key` | AWS secret access key for authentication | `None` |
| `aws_session_token` | AWS session token for temporary credentials | `None` |
</Tab>
</Tabs>
@@ -0,0 +1,50 @@
---
title: "FastEmbed"
description: "Configure FastEmbed as an embedding provider in Mem0 to generate embeddings locally using ONNX-based models without a GPU."
---
You can use FastEmbed to run embedding models locally in Mem0. FastEmbed is an ONNX-based embedding library that runs efficiently on CPU without requiring a GPU or an external API key.
### Installation
```bash
pip install fastembed
```
### Usage
<CodeGroup>
```python Python
import os
from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "your_api_key" # For LLM
config = {
"embedder": {
"provider": "fastembed",
"config": {
"model": "thenlper/gte-large"
}
}
}
m = Memory.from_config(config)
messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
m.add(messages, user_id="john")
```
</CodeGroup>
### Config
Here are the parameters available for configuring FastEmbed embedder:
| Parameter | Description | Default Value |
| --- | --- | --- |
| `model` | The name of the FastEmbed model to use | `thenlper/gte-large` |
| `embedding_dims` | Dimensions of the embedding model (auto-derived from the model if not set) | `None` |
@@ -67,14 +67,15 @@ Here are the parameters available for configuring Gemini embedder:
| Parameter | Description | Default Value |
| ---------------- | ------------------------------------ | ----------------------- |
| `model` | The name of the embedding model to use| `models/gemini-embedding-001` |
| `embedding_dims` | Dimensions of the embedding model | `1536` |
| `embedding_dims` | Dimensions of the embedding model | `768` |
| `api_key` | The Google API key | `None` |
| `output_dimensionality` | Output dimensionality for the embedding model (Gemini-specific; used when `embedding_dims` is not set) | `None` |
</Tab>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
| ----------------- | --------------------------------------------- | -------------------------- |
| `model` | The name of the embedding model to use | `gemini-embedding-001` |
| `embeddingDims` | Dimensions of the embedding model | `1536` |
| `embeddingDims` | Dimensions of the embedding model. When not set, uses the model's native output dimensionality (3072 for `gemini-embedding-001`; MRL truncation to 768, 1536, or 3072 is supported) | `None` |
| `apiKey` | Google API key | `None` |
</Tab>
</Tabs>
@@ -16,7 +16,7 @@ config = {
"embedder": {
"provider": "lmstudio",
"config": {
"model": "nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf"
"model": "nomic-ai/nomic-embed-text-v1.5-GGUF"
}
}
}
@@ -37,6 +37,6 @@ Here are the parameters available for configuring LM Studio embedder:
| Parameter | Description | Default Value |
| --- | --- | --- |
| `model` | The name of the LM Studio model to use | `nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.f16.gguf` |
| `model` | The name of the LM Studio model to use | `nomic-ai/nomic-embed-text-v1.5-GGUF` |
| `embedding_dims` | Dimensions of the embedding model | `1536` |
| `lmstudio_base_url` | Base URL for LM Studio connection | `http://localhost:1234/v1` |

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