Compare commits

...

448 Commits

Author SHA1 Message Date
Kartik 42cf18c4e6 chore: update changelog, bump SDK versions to Python 2.0.12 and TypeScript 3.1.0 (#6281) 2026-07-13 22:11:51 +05:30
Kartik d89793b666 refactor(ts-sdk): lazy-load optional provider SDKs so importing mem0ai/oss never requires them (#6280) 2026-07-13 15:23:12 +05:30
Rod Boev 17836748d7 feat(vector-stores): add Databricks provider to TypeScript OSS SDK (#5824)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-11 16:35:47 +05:30
HJ Q. c9af55986e fix(anthropic): Remove sampling parameters when using new model. (#6211)
Co-authored-by: HJ Qu <35272962+ChristopherQu@users.noreply.github.com>
2026-07-10 21:39:04 +05:30
Aditya Jethani f69f8dcc7b fix(weaviate): don't write OutputData model fields as properties on update (#6149)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-07-10 20:47:59 +05:30
Shaurya 28e4d819f8 fix(ts-sdk): re-raise LLM extraction transport failures instead of returning [] (#6102)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-07-10 20:46:08 +05:30
Qun 1d383c4ee2 fix(vector-stores): improve milvus wildcard search (#6187) 2026-07-10 20:36:22 +05:30
Kartik 8488abe603 docs: correct custom categories, per-call custom_categories is supported (#6218) 2026-07-10 20:08:32 +05:30
Kartik 49863e9a7a docs: add a dedicated Memory Expiration page (#6194) 2026-07-10 20:08:24 +05:30
Abhay Singh 770ce97bd9 fix(core): coerce non-string entity ids instead of crashing on .strip() (#6206) 2026-07-10 20:00:33 +05:30
Abhay Singh 44bcfbe1f3 fix(core): don't require langchain-core for default async procedural memory (#6209) 2026-07-10 19:59:21 +05:30
Rod Boev 33a0ed7559 feat(ts-sdk): add AWS Neptune Analytics vector store (#5797)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-10 19:51:05 +05:30
Bartok ba9054e0c8 feat(ts-sdk): add AWS Bedrock LLM provider (closes #5765) (#5890)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-10 19:14:15 +05:30
Granis87 df9d5cc4b1 Fix stale Node SDK delete note and add TypeScript examples (#6159) 2026-07-10 01:45:45 +05:30
Fahmid Arman 3b2357bfe0 feat(ts-sdk): add Vertex AI embedding provider (#5882)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-09 23:56:46 +05:30
Aditya 573b20cec8 feat(mem0-ts): add Baidu vector store provider (#5790)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-09 23:52:53 +05:30
Platon Sterkhov a781800d3f docs(ts-sdk): use dimension config option instead of embeddingModelDims in qdrant (#6192) 2026-07-09 23:33:08 +05:30
Kartik 4470803fe5 docs: replace em-dashes in reranker docs with sentence-appropriate punctuation (#6193) 2026-07-09 23:11:23 +05:30
Kartik 6a801bfe2f feat(oss): add reranker + per-search rerank to the TypeScript OSS SDK (#6055) 2026-07-09 22:18:38 +05:30
Kartik 2a4aa232b2 docs: replace em-dashes with sentence-appropriate punctuation (#6188) 2026-07-09 21:46:51 +05:30
Kartik 99206f0c64 feat(oss): accept text in Memory.update(), deprecate data (#6044) 2026-07-09 19:32:51 +05:30
Kartik 5dbf071356 docs: update README benchmarks to current temporal-reasoning results (#6182) 2026-07-09 15:57:05 +05:30
Rod Boev b26469e006 feat(vector-stores): add Weaviate adapter to TypeScript OSS SDK (#5800)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-08 21:12:01 +05:30
Bartok e72ae96ad4 feat(ts-sdk): add Milvus vector store provider (closes #5774) (#5889)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-08 20:25:06 +05:30
Bartok fbdbab805d feat(ts-sdk): add HuggingFace embedding provider (#6027)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-08 19:43:53 +05:30
sahithreddy05 22f70d50e1 feat(ts-sdk): add Chroma vector store provider (#6145)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-08 19:14:05 +05:30
Saumya Kathuria f89edb45dc feat(ts-sdk): add Sarvam LLM provider to OSS SDK (#6130)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-08 19:03:38 +05:30
Harsh Vardhan Gupta 846f25bd39 fix(mem0-ts): patch fast-xml-parser and tar transitive CVEs (#6160) 2026-07-08 18:25:59 +05:30
张思孝 9b04509433 feat(ts-sdk): add Together LLM provider (#6049)
Co-authored-by: zhangsixiao <zhangsixiao@bytedance.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 21:08:26 +05:30
Diveyam Mishra 803ff13bb8 feat(mem0-ts): add MongoDB vector store provider to OSS TypeScript SDK (#5793)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 17:13:51 +05:30
404 Ameyy 94e46526bc feat(ts-sdk): add Elasticsearch vector store provider (#5866)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 17:10:25 +05:30
Parteeksachdeva d122479687 security: fix SQL and Cypher injection vulnerabilities in PGVector, Azure MySQL, and Neptune (#4878)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 16:16:36 +05:30
VectorPeak cc52f0e367 fix: encode dynamic URL path segments (#5963)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 16:11:54 +05:30
AxelRay 87276ef968 feat(ts-sdk): add OpenSearch vector store (#5810)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-07 15:40:20 +05:30
Kartik 002fe46ab3 feat(ts-sdk): add xAI (Grok) LLM provider to OSS SDK (#6115) 2026-07-07 11:29:56 +05:30
AxelRay 9d36b2c94d feat(ts-sdk): add Upstash Vector vector store (#5811)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 23:41:05 +05:30
Rod Boev c944bed460 feat(ts-sdk): add Azure MySQL vector store (#5827)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 23:17:43 +05:30
Rod Boev 2cc060fd76 feat(vector-stores): add Turbopuffer provider to TypeScript OSS SDK (#5801)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 22:57:00 +05:30
Div 2bc2f763d9 feat: add Google Vertex AI Vector Search support to vector store factory (#5791)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
Co-authored-by: divyansh-1009 <divyansh-1009@users.noreply.github.com>
2026-07-06 22:54:24 +05:30
Rod Boev 7fb3feb5cd feat(vector-stores): add Cassandra provider to TypeScript OSS SDK (#5823)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 21:57:32 +05:30
Rod Boev fec7cdf118 feat(ts-sdk): add FastEmbed embedding provider (#5862)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 21:56:11 +05:30
Rod Boev 03b41ab00f feat(vector-stores): add Pinecone provider to TypeScript SDK (#5802)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 21:39:10 +05:30
Jaco-Ren 4c974c8fa8 Add Together embedder to TS SDK (#5989)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 20:24:14 +05:30
Barry b0bee551cb feat(ts-sdk): add vLLM provider (#5805)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 20:23:43 +05:30
Kartik b8141aaea8 docs: remove duplicate multimodal page and normalize em/en-dashes (#6112) 2026-07-06 20:22:20 +05:30
Yash a7ecf781cd Feat/valkey vector store (#5826)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 20:20:59 +05:30
Rod Boev 6dc4606dcf feat(vector-stores): add S3 Vectors provider to TypeScript OSS SDK (#5822)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-07-06 20:08:13 +05:30
Harsh Vardhan Gupta 580d390e4d fix(transformers): upgrade to >=5.3.0 (GHSA-29pf-2h5f-8g72 / CVE-2026-4372) (#6110) 2026-07-06 17:56:39 +05:30
Kartik 2bd3ff1eff fix(vector-stores): prevent unhandled promise rejection in Supabase & Redis constructors (#6111) 2026-07-06 14:57:30 +05:30
Agam Pandey cd79fa8914 docs: show current benchmark numbers on memory evaluation page (#6056)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:38:33 +05:30
Hrushikesh Yadav 59484f066f fix(elasticsearch): validate filter keys and values to prevent term injection (#5980)
Signed-off-by: Hrushikesh Yadav <yadavhrushikesh65@gmail.com>
2026-07-02 18:35:02 +05:30
Kushagra Gupta 8a5c0729e5 fix(server): do not forward empty-string entity ids as filters (#5992)
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-07-02 18:33:16 +05:30
Trupti Agrawal 3b9aed866a docs: fix typos and grammar errors across docs (#6033) 2026-07-01 23:25:12 +05:30
Kartik fb2593e10d docs: add subtle GitHub star nudges at OSS win-moments (#5928) 2026-07-01 23:13:51 +05:30
Kartik 207f65deda docs: navigation (#5900) 2026-07-01 22:48:23 +05:30
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
Agam Pandey 70bc9e51d5 docs(readme): update LongMemEval benchmark to 94.8 and add Temporal Reasoning (#5131) 2026-05-13 14:31:15 +05:30
Agam Pandey 0107fd53b8 feat: add temporal reasoning cookbook and docs (#5061) 2026-05-13 01:59:38 +05:30
Mragank Shekhar 54a03cc721 chore(plugin): bump mem0 plugin to v0.1.2 (#5094) 2026-05-09 20:56:34 +05:30
Mragank Shekhar e95de4ca50 fix(plugin): hook cleanup + identity + compact-summary flow (#5076) 2026-05-09 19:19:30 +05:30
youneshima a623cfaf76 Oss qdrant hosted memories to platform migration (#5080) 2026-05-08 08:04:09 +05:30
Chaithanya Kumar 92491c00c2 docs(memory-decay): use SDK calls in code samples (#5079)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:53:55 +05:30
Mragank Shekhar 9043fbf61e chore(release): bump mem0ai to 2.0.2 (py) and 3.0.3 (ts) (#5078) 2026-05-08 01:27:23 +05:30
Chaithanya Kumar c90cbc75a2 docs: memory decay v0.5 — platform feature page + API reference (#5056)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:21:33 +05:30
Gabriel Stein 58304fc939 refactor(plugin): hand mem0 search decisions to the agent (#4992)
Co-authored-by: Mgeeeek <ms8939@bennett.edu.in>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:41:39 +05:30
Chaithanya Kumar 397f3414ee feat(sdk): expose decay on project.update (Python + TypeScript) (#5062)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:33:32 +05:30
Gabriel Stein a734e057cf fix (telemetry): stitch oss and platform telemetry identities for python and typescript sdk
Co-authored-by: Younes Slaoui <younes.slaoui@mem0.ai>
2026-05-05 13:48:21 -07:00
Saket Aryan 0fdaa29b4a feat(skills): add mem0-integrate + mem0-test-integration pipeline skills (#4961) 2026-05-05 18:52:22 +05:30
Kartik 6d3486ca56 docs: update changelog for v1.0.11 with new features, improvements, fixes, and dependency updates (#5022) 2026-04-29 22:45:26 +05:30
Kartik ebb9bb2b15 fix: adding skills config and updating the plugin the config (#4958) 2026-04-29 22:19:40 +05:30
Kabir Kohli 594b4e65d6 fix(openclaw): bump protobufjs to >=7.5.5 (GHSA-xq3m-2v4x-88gg) (#5012) 2026-04-29 10:43:19 +05:30
Harsh Vardhan Gupta 1b95c99db4 fix: sql injection, prompt injection (#4997)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-29 00:51:16 +05:30
Gabriel Stein b66cf0f272 docs(mcp): document list_events and get_event_status tools (#4989) 2026-04-29 00:32:48 +05:30
Gabriel Stein 72dca1cdf5 docs(codex): fix broken install instructions, lead with direct MCP (#4951) 2026-04-29 00:32:27 +05:30
Zeger Hoogeboom ece7ff6b84 (TS) Fix PGVector implementation, where vector distance was inverted. (#4944) 2026-04-28 00:39:48 +05:30
Gabriel Stein 30ce028a71 feat(mem0-plugin): add Codex lifecycle hooks via opt-in installer (#4917) 2026-04-27 22:59:35 +05:30
Kartik bd9d27ff50 docs: changelog updates, version bump in mem0-ts and pyproject (#4976) 2026-04-25 23:06:57 +05:30
Prathamesh 08b746c9be chore(readme): update cover banner image (#4966) 2026-04-25 19:17:02 +05:30
Pratik Rai 693e709389 fix(api): map entity params to filters in GET /memories (#4955) (#4960) 2026-04-24 23:52:14 +05:30
Kartik 553e275112 fix(docs): updating endpoints to v3 in the api reference (#4953) 2026-04-24 17:34:11 +05:30
Varun Chawla 43dde3b186 fix: add ca_certs config option for Elasticsearch vector store (#3993) 2026-04-24 02:46:32 +05:30
Andrew Halpern cca7551192 fix(memory): honor prompt param in vector store extraction (#4914) 2026-04-23 22:36:54 +05:30
cid 5be2630f5b fix: add missing text_lemmatized in AsyncMemory._create_memory (#4886) 2026-04-23 20:04:43 +05:30
Kartik 2549a84e5c fix: update command on docs and logic (#4946) 2026-04-23 19:42:28 +05:30
Jean Ibarz 34ed122ef3 fix(ts): forward timeout config to OpenAI client in JS OSS LLM providers (#4770)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-04-23 19:29:27 +05:30
Gabriel Stein db8ac61713 Self-hosted dashboard and admin auth (#4837)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-23 19:06:36 +05:30
Rudrasinh Nimeshkumar Ravalji 15feaa8ac4 fix(llms): narrow _is_reasoning_model to not match gpt-5.x variants (#4746)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 18:58:12 +05:30
Kartik 282feaebf2 fix: remove the process env from the tests and fix the plugin manifest (#4927) 2026-04-22 22:57:44 +05:30
Kartik f5dc825d47 refactor: update memory skill loader, plugin config, and add privacy docs (#4905) 2026-04-22 17:15:19 +05:30
Saket Aryan 32b74e18b7 feat(cli): migrate Python and Node CLIs to v3 API routes (#4916) 2026-04-22 15:20:38 +05:30
Gabriel Stein daa4495583 docs(claude-code): split marketplace install into two separate steps (#4915) 2026-04-22 03:32:31 +05:30
Kabir Kohli cfb5f1776e chore(security): bump vulnerable dependencies to patched versions (#4835) 2026-04-21 01:27:13 +05:30
jessai2099 573e5212a4 fix(vector-stores): add agent_id and run_id to Elasticsearch/OpenSearch default mappings (#4906)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 23:10:59 +05:30
Yarizakura 8ba225cec8 fix: merge same-key operator dicts in AND metadata filters (#4853) 2026-04-20 21:54:02 +05:30
mintlify[bot] 4b09943092 Fix broken link in delete memory docs (#4894)
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
2026-04-20 21:19:30 +05:30
Kartik 4e611e8dba docs: update memory tool list, CLI usage, and config file reading logic (#4861)
Co-authored-by: Livia Ellen <liviaellen@msn.com>
2026-04-20 20:09:45 +05:30
Kartik 5520226b5b fix: updating docs with v3 integrations updates (#4898) 2026-04-20 18:54:21 +05:30
Saket Aryan 00695e3113 ci(sdk): require changelog entry on version bump + harden TS telemetry (#4900) 2026-04-20 18:09:03 +05:30
Saket Aryan 7b6790bafb fix(ts-sdk): inject SDK version into telemetry at build time (#4897) 2026-04-20 17:29:29 +05:30
Kartik 93da5ef8f7 fix: update skills and docs (#4868) 2026-04-18 11:42:37 +05:30
Saket Aryan c1c5bd62f6 docs(llms-txt): platform-first override with scope tags + CI check (#4880) 2026-04-17 22:31:50 +05:30
Prithvi Monangi 2ec3c4ab20 fix(embeddings): set FastEmbed embedding_dims from model metadata at init (#4711) 2026-04-17 18:17:21 +05:30
Kartik 3fbc1c9aef fix(docs): updating the changelog, and removing cookbook page referencing graph memory (#4867) 2026-04-16 21:23:29 +05:30
Kartik 0b14f75c05 fix(docs): update the cookbooks and remove and update teh depcreataed param (#4814)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-16 17:39:55 +05:30
Saket Aryan fb224083e4 chore(release): promote Python SDK to 2.0.0 and TS SDK to 3.0.0 (#4860) 2026-04-16 17:13:50 +05:30
Chaithanya Kumar 30469aec17 docs: new algorithm migration guides + memory evaluation (#4811)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-16 17:13:13 +05:30
Saket Aryan 50db9e428d chore(release): bump SDK versions to next beta (#4859) 2026-04-16 16:23:50 +05:30
soumil-rathi fb87349664 fix(oss): v3 entity cleanup, filter fixes, and QA hardening (TS + Python) (#4858)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 12:17:13 +05:30
Saket Aryan 8827553576 fix: adopt new v3 memory endpoints in Python + TS clients (#4856) 2026-04-16 05:28:49 +05:30
Kabir Kohli c8e20a9bb5 fix(docs): resolve duplicate operationIds and expiration_date type in openapi spec (#4854) 2026-04-16 04:09:51 +05:30
Kartik 93a51f4763 test: update integration tests for v1.1 output_format (#4847) 2026-04-16 01:37:51 +05:30
Saket Aryan 86fe275f53 fix(ts): entity store isolation, backward compat, pgvector + redis init fixes (#4841) 2026-04-15 21:01:01 +05:30
Kartik e6d6276bb9 refactor: add entity ID and search param validation, rename textLemmatized field, update tests (#4843) 2026-04-15 20:57:09 +05:30
Chaithanya Kumar 9692726db4 fix(ts-oss): isolate entity store from memory store by default (#4829)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-15 14:47:43 +05:30
soumil-rathi d8d776636f fix(v3): migration crashes + entity linking on OSS (#4836)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:46:28 +05:30
Kartik a5a688295e fix: prevent arbitrary code execution via pickle in FAISS vector store (#4833) 2026-04-15 00:02:07 +05:30
Saket Aryan 5d40592e42 chore: version bump to beta1 (#4827) 2026-04-14 18:05:22 +05:30
soumil-rathi a488e19044 feat(oss): port v3 pipeline with hybrid search, entity extraction, and additive scoring (#4805)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
Co-authored-by: chaithanyak42 <chaithanya.kumar42a@gmail.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-14 18:00:58 +05:30
Parteeksachdeva 57f944e18a fix: allow anonymousTelemetryId in openclaw.json config (#4826)
Co-authored-by: parteeksachdeva-123 <parteek.sachdeva@aerchain.io>
2026-04-14 17:31:28 +05:30
Gabriel Stein fe3f7ae618 fix(plugin): remove invalid keys from Claude plugin config (#4821) 2026-04-14 01:13:03 +05:30
shafdev 4a7e166f9a fix(tests): use top_k instead of limit in test_server_params (#4820) 2026-04-14 01:11:31 +05:30
Kartik 85768e78e7 fix(docs): remove chrome extension cookbooks (#4813) 2026-04-13 22:14:00 +05:30
Yunsu 7b395f3bf7 fix(openai): make store opt-in so it stops leaking to non-OpenAI backends (#4757) 2026-04-13 21:53:22 +05:30
HUANG XIAO 4180409b09 fix(s3vectors): handle vector=None in update() to prevent boto3 validation error (#4594) 2026-04-13 21:12:49 +05:30
Joe Wu 649e719ce6 fix: LLM config manager falls back to userConf.url for baseURL (#4715) (#4761) 2026-04-13 20:46:18 +05:30
Kartik 1a53852d93 test: update valkey cluster search test to use top_k parameter (#4815) 2026-04-13 20:44:18 +05:30
Chinnu Abey ac9cdd4840 Fix incorrect use of SentenceTransformer for cross-encoder reranker models (#4806) 2026-04-13 20:14:22 +05:30
Swarnaprakash Udayakumar cf530c4bec feat(valkey): add cluster mode enabled (CME) support (#4759) 2026-04-13 20:07:11 +05:30
Saket Aryan 92b958c1cc chore: bump Python SDK to v2.0.0b0 and Node SDK to v3.0.0-beta.0 (#4810) 2026-04-13 15:51:33 +05:30
Asish Kumar c239d8a483 fix(client): prevent feedback telemetry TypeError (#4795) 2026-04-12 03:06:05 +05:30
Kartik 9d6b79a14e fix(sdk): removing the enable graph flag and switching from snake case to camel case for client ts sdk (#4776)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 02:40:54 +05:30
Kartik e44b46ef2e fix(sdk): removing deprecating param from our sdk and docs changes with it (#4740) 2026-04-12 00:34:58 +05:30
Saket Aryan 3882af7450 fix(cli): persistent anonymous telemetry ID + pass source=CLI in all API calls (#4789) 2026-04-11 21:00:05 +05:30
Saket Aryan d39ebad09f fix(openclaw): persistent anonymous telemetry ID, flush fix, and email resolution (#4790)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:57:14 +05:30
Gabriel Stein 9d82e2329d refactor(telemetry): sample OSS hot-path events at 10% to reduce PostHog volume (#4771) 2026-04-11 16:25:38 +05:30
szinvas 789cc9d607 docs - replace session_id with run_id (#4742) 2026-04-10 20:06:59 +05:30
Jared Diaz e59e3d5f0c Add deepseek.ts to src/llms with corresponding unit tests. Updates fa… (#4613)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-10 20:06:22 +05:30
Kartik d926f3697c fix(docs): eliminate ~222 SEO redirect chains on docs.mem0.ai (#4768) 2026-04-10 19:33:45 +05:30
Kartik c996b0e7fa docs: removing the changelog.mdx file adn replacing it with new changelog system (#4750) 2026-04-10 19:07:43 +05:30
Kartik 78ca85a260 refactor: update OpenClaw plugin config, hook logic, and documentation (#4764) 2026-04-09 17:36:05 +05:30
Kartik 88f696a60a refactor: drop orgId, projectId, enableGraph config options, update CLI prompts, and clean up related code (#4734) 2026-04-09 14:57:33 +05:30
Ignazio De Santis 081eca6d8f fix: guard temp_uuid_mapping lookups against LLM-hallucinated IDs (fixes #3931) (#4674)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-08 21:55:52 +05:30
Kartik 2434b9d550 docs: add ChatDev integration guide and update integrations list (#4751) 2026-04-08 20:16:04 +05:30
Rakhee Singh 3ffea554bc fix(azure_openai): forward response_format to Azure OpenAI API (#4689) 2026-04-08 19:22:02 +05:30
Rakhee Singh 1ad8a59b0c fix(deepseek): forward response_format to OpenAI-compatible API (#4688) 2026-04-08 19:21:11 +05:30
Saket Aryan a670333d67 feat: add AGENTS.md for AI coding agent instructions (#4726) 2026-04-06 21:32:43 +05:30
Saket Aryan 4c2db3e68b feat(skills): introduce Mem0 skill graph with dedicated CLI and Vercel AI SDK skills (#4725) 2026-04-06 20:41:29 +05:30
Saket Aryan 07f0d4f1e0 fix: use npx npm@latest for OIDC trusted publishing (#4724)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:22:00 +05:30
Saket Aryan 3565404eef fix: remove npm self-upgrade from CD workflows (#4723) 2026-04-06 17:11:28 +05:30
Kartik 144627c4ce fix(docs): change the position of the openclaw to agnet plugin and fix the integrations overview and sidebar list view (#4722) 2026-04-06 16:57:24 +05:30
Kartik 6984958138 chore: sat release (#4702) 2026-04-06 16:55:08 +05:30
Kartik b13748c446 feat: add import and event commands, refactor CLI, remove baseUrl config, update docs (#4704) 2026-04-06 16:54:27 +05:30
Saket Aryan 4642a1d6e3 feat(cli): validate API key upfront via ping and unify telemetry identity resolution (#4701) 2026-04-04 23:03:27 +05:30
Kartik 686d5e987d fix: openclaw plugin and fix the login section there (#4696)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-04 22:21:46 +05:30
DEVAN CHAUHAN c55447c1e4 [fix] groq model (#4700) 2026-04-04 21:36:17 +05:30
Saket Aryan ee67602c58 feat(cli): add PostHog telemetry and source tracking to Python & Node CLIs (#4699) 2026-04-04 20:47:38 +05:30
Saket Aryan 0daa5d7d03 fix(ci): handle npm prerelease publish across Node.js CD workflows (#4690)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 21:17:11 +05:30
Kartik cfb3f58e4a fix: adding login and fixing the plugin to follow the openclaw plugin standards (#4686) 2026-04-03 20:59:23 +05:30
Kartik 66230b3f1f docs: update integration docs with new SVG icons and links (#4684) 2026-04-03 20:54:46 +05:30
BillionToken 1941cae031 fix(server): add missing psycopg-pool dependency (#4374)
Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
2026-04-03 20:04:05 +05:30
Utkarsh fcbb70ab3b fix: prevent thread and memory leaks from PostHog telemetry (#4535)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-03 20:01:04 +05:30
Chaithanya Kumar 33d2bc495d fix(openclaw): clear security scanner exfiltration warning (#4678)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-04-03 00:34:49 +05:30
Gabriel Stein c0cae68646 feat(plugin): add Codex plugin support and integration docs (#4665)
Co-authored-by: Gabriel Stein <gabrielstein416@gmail.com>
2026-04-03 00:18:02 +05:30
Saket Aryan 3b2f01796e feat(cli): comprehensive docs, version bump, and purple branding (#4680)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:52:00 +05:30
Chaithanya Kumar 9cd3d2cca8 fix(openclaw): remove process.env access to clear security scanner warning (#4676)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 21:19:12 +05:30
Saket Aryan c53f1f126d docs(openclaw): add v1.0.1 changelog and release notes (#4675) 2026-04-02 20:25:49 +05:30
Patel Tirth 0b7615fa87 docs: add api_key parameter to Google AI LLM provider config examples (#4626) 2026-04-02 19:37:23 +05:30
Shaik Faizan Roshan Ali 66d34fab3c fix: update_memory endpoint passing dict instead of str (#3933) (#4595) 2026-04-02 19:35:09 +05:30
Krishna Chaitanya 868b63af63 fix: use DatetimeRange for datetime string values in Qdrant range filters (#4659)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-02 18:43:58 +05:30
soumil-rathi 6cc1c15320 feat(sdk): add multilingual param to project update (#4314)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 18:38:26 +05:30
Prithvi Monangi 7a20da59ee fix(configs): add missing ConfigDict to vector store configs (#4656)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-02 18:05:54 +05:30
Saket Aryan f89f7c7c81 ci: add CD workflow for @mem0/openclaw-mem0 with OIDC trusted publishing (#4672) 2026-04-02 16:32:31 +05:30
Saket Aryan 5723136bed fix: add repository field to Node packages for npm provenance (#4671) 2026-04-02 16:20:38 +05:30
Saket Aryan b5345f8498 ci: add CD workflows for Node SDK packages with OIDC trusted publishing (#4670) 2026-04-02 16:11:06 +05:30
Chaithanya Kumar 6577ae7616 fix(openclaw): graceful startup without API key (#4669)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:10:49 +05:30
Saket Aryan 5e00d5c452 chore(cli): bump version for Python CLI to 0.2.0 and Node CLI to 0.1.1 (#4668) 2026-04-02 13:36:21 +05:30
Kartik 3b152a3e85 fix(openclaw): updating the config of the openclaw plugin here (#4667) 2026-04-02 13:30:03 +05:30
Chaithanya Kumar beca7cc873 fix(openclaw): dream gate correctness — cheap-first ordering, session isolation, verified completion (#4666)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:29:49 +05:30
Saket Aryan 30f242dc4c feat(cli): update brand color palette from purple to golden (#4664) 2026-04-02 04:03:28 +05:30
Kartik 1bfaaf8750 chore: release (#4657) 2026-04-01 23:44:09 +05:30
Chaithanya Kumar c250ccfb5c feat(openclaw): skills-based memory architecture with batched extraction (#4624)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 23:29:15 +05:30
Saket Aryan e2b439c42a fix(cd): restrict PyPI publish to main SDK tags only (#4654) 2026-04-01 21:42:50 +05:30
Saket Aryan c788d771d3 feat(cli): add CD workflow and bump version to 0.2.0b1 (#4653) 2026-04-01 21:29:10 +05:30
Saket Aryan 2acf9571b3 feat(cli): add event commands, --json/--agent flag, agent output sanitization, and edge-case hardening for CLI SDKs (#4649)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 21:04:59 +05:30
Saharsh Patel 713dba5d0a fix: replace .single() with .maybeSingle() in SupabaseDB.get() to handle missing rows (#4599) 2026-04-01 18:41:24 +05:30
wobushixiaoj 8ae7a06220 fix: pass dimensions parameter to OpenAI embeddings API (#4632)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-04-01 18:40:17 +05:30
Prithvi Monangi f94ea06588 fix(configs): migrate CassandraConfig and AzureMySQLConfig to pydantic v2 ConfigDict (#4646) 2026-04-01 15:02:22 +05:30
Noah Stapp 215d8b5a71 fix: only list authorized collections when listing MongoDB collections (#3888) 2026-03-31 23:09:36 +05:30
Genaro Sanchez 82525dbf0f docs: update Twitter references to X (formerly Twitter) (#4432) 2026-03-31 22:44:04 +05:30
Saket Aryan 32c1ccba5b ci(cli): add CI pipelines for Node and Python CLI SDKs (#4640)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:30:31 +05:30
Saket Aryan 9bebcf45f7 fix(cli): fix critical crashes, improve error messages, UX and validation in Node.js and Python SDKs (#4636) 2026-03-31 20:28:48 +05:30
Rakhee Singh 93bd4e248c fix(deepseek): forward response_format to OpenAI-compatible API (#4635) 2026-03-31 18:06:53 +05:30
Kabir Kohli af19495f66 feat(cli): add email verification code login to mem0 init (#4623)
Co-authored-by: Saket Aryan <saketaryan2002@gmail.com>
2026-03-31 16:57:02 +05:30
Rakhee Singh dbac83218f fix(vllm): forward response_format to OpenAI-compatible API (#4608)
Co-authored-by: rasingh5 <rasingh@demandbase.com>
2026-03-31 13:25:45 +05:30
Prathamesh 3618aeff22 Link fix (#4631) 2026-03-30 21:36:00 -07:00
Prathamesh 25e25aaa2f redesign docs introduction page with compact 3x2 grid layout (#4598) 2026-03-30 16:14:02 -07:00
HUANG XIAO 5d30af9560 feat(bedrock): add MiniMax provider support for AWS Bedrock (#4609) 2026-03-30 20:40:24 +05:30
Krishna Chaitanya 47969aaa6b fix(ts): extract JSON from chatty LLM responses in fact retrieval (#4533)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-03-30 20:33:11 +05:30
Br1an 7213e1b1a6 fix: reset graph database in Memory.reset() (#4185)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-03-30 18:45:38 +05:30
Br1an 97291aa119 fix: make AsyncMemory.from_config a regular classmethod (#4183) 2026-03-30 16:19:37 +05:30
1975 changed files with 170215 additions and 104055 deletions
+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 to Claude workflows.",
"version": "0.1.0"
"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.0"
"version": "0.2.12"
}
]
}
+18 -3
View File
@@ -1,17 +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
# 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
@@ -38,7 +54,6 @@ jobs:
# packages_dir: dist/
- name: Publish distribution 📦 to PyPI
if: startsWith(github.ref, 'refs/tags')
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."
+57 -58
View File
@@ -1,28 +1,58 @@
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/**'
workflow_call:
jobs:
changelog_check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require CHANGELOG entry when Python SDK version changes
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
extract_version() {
python3 -c "import sys, re; m = re.search(r'^\s*version\s*=\s*\"([^\"]+)\"', sys.stdin.read(), re.M); print(m.group(1) if m else '')"
}
base_version=$(git show "$BASE_SHA:pyproject.toml" 2>/dev/null | extract_version || echo "")
head_version=$(extract_version < pyproject.toml)
echo "Base version: ${base_version:-<unknown>}"
echo "Head version: $head_version"
if [ -z "$base_version" ] || [ "$base_version" = "$head_version" ]; then
echo "pyproject.toml version unchanged — no CHANGELOG entry required."
exit 0
fi
echo "Detected version bump ${base_version} -> ${head_version}. Checking docs/changelog/sdk.mdx…"
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- docs/changelog/sdk.mdx | grep -q .; then
echo "Changelog update present in docs/changelog/sdk.mdx ✅"
else
echo "::error file=pyproject.toml::pyproject.toml version changed from ${base_version} to ${head_version} but docs/changelog/sdk.mdx was not updated in this PR. Add a new <Update> entry under the Python tab for v${head_version}."
exit 1
fi
check_changes:
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:
@@ -30,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
@@ -56,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 }}
+60
View File
@@ -0,0 +1,60 @@
name: Publish @mem0/cli 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# cli-node-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-node-v0.2.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/cli 📦 to npm
if: startsWith(inputs.tag, 'cli-node-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: cli/node
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- 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: cli/node/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+99
View File
@@ -0,0 +1,99 @@
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:
branches: [main]
paths:
- 'cli/node/**'
- '.github/workflows/cli-node-ci.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Lint
working-directory: cli/node
run: pnpm run lint
- name: Type check
working-directory: cli/node
run: pnpm run typecheck
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: 10
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Run tests
working-directory: cli/node
run: pnpm run test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: cli/node/pnpm-lock.yaml
- name: Install dependencies
working-directory: cli/node
run: pnpm install --frozen-lockfile
- name: Build
working-directory: cli/node
run: pnpm run build
- name: Verify dist output
run: |
test -f cli/node/dist/index.js || (echo "Build output missing: dist/index.js" && exit 1)
+47
View File
@@ -0,0 +1,47 @@
name: Publish mem0-cli 🐍 distributions 📦 to PyPI
# Dispatched by release.yml (Release Router) when a release tagged cli-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. cli-v0.2.0)'
required: true
type: string
prerelease:
description: 'Unused for PyPI (pre-releases are expressed in the version itself); accepted for router uniformity'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish mem0-cli 📦 to PyPI
if: startsWith(inputs.tag, 'cli-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: cli/python
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Hatch
run: pip install hatch
- name: Build a binary wheel and a source tarball
run: hatch build --clean
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: cli/python/dist/
+78
View File
@@ -0,0 +1,78 @@
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:
branches: [main]
paths:
- 'cli/python/**'
- '.github/workflows/cli-python-ci.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Lint with ruff
working-directory: cli/python
run: ruff check .
- name: Check formatting
working-directory: cli/python
run: ruff format --check .
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dev dependencies
working-directory: cli/python
run: pip install -e ".[dev]"
- name: Run tests
working-directory: cli/python
run: pytest
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Hatch
run: pip install hatch
- name: Build
working-directory: cli/python
run: hatch build --clean
- name: Verify dist output
run: |
ls cli/python/dist/*.whl || (echo "Wheel file missing" && exit 1)
ls cli/python/dist/*.tar.gz || (echo "Source dist missing" && exit 1)
+42
View File
@@ -0,0 +1,42 @@
name: docs - llms.txt check
# Blocks PRs that introduce new .mdx pages without a matching entry in
# docs/llms.txt, or that link to pages that no longer exist. Contributors
# must update docs/llms.txt in the same PR. Run locally with:
# python scripts/check-llms-txt-coverage.py # read-only
# python scripts/check-llms-txt-coverage.py --write # scaffold placeholders
# On PRs this is invoked by ci-gate.yml (the single required check);
# manual runs remain standalone.
on:
workflow_call:
workflow_dispatch: {}
permissions:
contents: read
jobs:
check-llms-txt:
runs-on: ubuntu-24.04-arm
timeout-minutes: 2
steps:
- uses: actions/checkout@v4
- name: Verify docs/llms.txt coverage
run: |
if ! python3 scripts/check-llms-txt-coverage.py; then
echo ""
echo "::error title=llms.txt out of sync::docs/llms.txt does not match docs/**/*.mdx."
echo ""
echo "To fix:"
echo " 1. Run locally: python scripts/check-llms-txt-coverage.py --write"
echo " This appends placeholder entries under '## Unclassified - needs triage'."
echo " 2. For each placeholder:"
echo " - replace [TODO: Platform|OSS|Both] with the correct scope tag"
echo " - rewrite the description as 'Use when ...'"
echo " - move the entry into the appropriate section"
echo " - delete the '## Unclassified - needs triage' heading once empty"
echo " 3. Resolve any stale URLs listed above by updating or removing the link."
echo " 4. Commit the updated docs/llms.txt to this PR."
exit 1
fi
+60
View File
@@ -0,0 +1,60 @@
name: Publish @mem0/openclaw-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# openclaw-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. openclaw-v0.5.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/openclaw-mem0 📦 to npm
if: startsWith(inputs.tag, 'openclaw-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/openclaw
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/openclaw/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
+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 }}"
+59
View File
@@ -0,0 +1,59 @@
name: Publish mem0ai 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged ts-v* is
# published. Can also be dispatched manually to re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. ts-v2.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish mem0ai 📦 to npm
if: startsWith(inputs.tag, 'ts-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: mem0-ts
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- 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: mem0-ts/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+39 -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:
@@ -24,6 +24,42 @@ jobs:
ts_sdk:
- 'mem0-ts/**'
changelog_check:
needs: check_changes
if: github.event_name == 'pull_request' && needs.check_changes.outputs.ts_sdk_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require CHANGELOG entry when SDK version changes
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
base_version=$(git show "$BASE_SHA:mem0-ts/package.json" 2>/dev/null | jq -r .version || echo "")
head_version=$(jq -r .version mem0-ts/package.json)
echo "Base version: ${base_version:-<unknown>}"
echo "Head version: $head_version"
if [ -z "$base_version" ] || [ "$base_version" = "$head_version" ]; then
echo "mem0-ts/package.json version unchanged — no CHANGELOG entry required."
exit 0
fi
echo "Detected version bump ${base_version} -> ${head_version}. Checking docs/changelog/sdk.mdx…"
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- docs/changelog/sdk.mdx | grep -q .; then
echo "Changelog update present in docs/changelog/sdk.mdx ✅"
else
echo "::error file=mem0-ts/package.json::mem0-ts/package.json version changed from ${base_version} to ${head_version} but docs/changelog/sdk.mdx was not updated in this PR. Add a new <Update> entry under the TypeScript tab for v${head_version}."
exit 1
fi
build_ts_sdk:
needs: check_changes
if: needs.check_changes.outputs.ts_sdk_changed == 'true'
+60
View File
@@ -0,0 +1,60 @@
name: Publish @mem0/vercel-ai-provider 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# vercel-ai-v* is published. Can also be dispatched manually to re-publish
# a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. vercel-ai-v2.0.7)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish @mem0/vercel-ai-provider 📦 to npm
if: startsWith(inputs.tag, 'vercel-ai-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/vercel-ai-sdk
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- 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/vercel-ai-sdk/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
+8 -3
View File
@@ -4,6 +4,10 @@ __pycache__/
*$py.class
**/node_modules/
# Self-hosted server local runtime state
server/history/
server/.env
# C extensions
*.so
@@ -15,8 +19,8 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/
@@ -166,7 +170,6 @@ cython_debug/
# Database
db
test-db
!embedchain/embedchain/core/db/
.vscode
.idea/
@@ -186,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
+611
View File
@@ -0,0 +1,611 @@
# AGENTS.md
This file provides context for AI coding assistants (Claude Code, Cursor, GitHub Copilot, Codex, etc.) working with the Mem0 repository.
## Project Overview
**Mem0** ("mem-zero") is an intelligent memory layer for AI agents and assistants. It provides persistent, personalized memory via both a hosted platform API and self-hosted open-source SDKs.
- **Repository**: https://github.com/mem0ai/mem0
- **Documentation**: https://docs.mem0.ai
- **License**: Apache-2.0
## Repository Structure
This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs, servers, plugins, and documentation.
### Key Directories
| Directory | Description |
|-----------|-------------|
| `mem0/` | Core Python SDK (`mem0ai` on PyPI) — memory, LLMs, embeddings, vector stores, graphs, rerankers |
| `mem0-ts/` | TypeScript SDK (`mem0ai` on npm) — client + OSS memory |
| `cli/python/` | Python CLI (`mem0-cli` on PyPI) — Typer-based, entry point `mem0` |
| `cli/node/` | Node CLI (`@mem0/cli` on npm) — Commander-based, entry point `mem0` |
| `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) |
| `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/` | 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) |
### Core Package Dependencies
```
mem0 (Python SDK) mem0-ts (TypeScript SDK)
├── mem0/memory/ ├── src/client/ (MemoryClient — hosted)
├── mem0/llms/ └── src/oss/ (Memory — self-hosted)
├── mem0/embeddings/ ├── src/llms/
├── mem0/vector_stores/ ├── src/embeddings/
├── mem0/graphs/ ├── src/vector_stores/
└── mem0/reranker/ └── src/graphs/
cli/python/ ──▶ mem0ai (optional, for OSS mode)
cli/node/ ──▶ mem0ai (npm, for API calls)
integrations/vercel-ai-sdk/ ──▶ ai, @ai-sdk/* providers
integrations/openclaw/ ──▶ mem0ai (npm)
```
## Development Setup
### Requirements
- **Python**: 3.9+ (3.10+ for CLI)
- **Node.js**: v18+ (v20 or v22 recommended)
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
- **Hatch**: Python build/environment tool (`pip install hatch`)
- **Docker**: Required for `server/` and `openmemory/` development
### Initial Setup
```bash
# Python SDK
hatch shell dev_py_3_11 # creates environment with all deps
pre-commit install # install git hooks
# TypeScript packages
cd mem0-ts && pnpm install # TS SDK
cd cli/node && pnpm install # Node CLI
cd integrations/vercel-ai-sdk && pnpm install # Vercel AI provider
cd integrations/openclaw && pnpm install # OpenClaw plugin
```
## Build, Lint, and Test Commands
### Python SDK (`mem0/`)
```bash
# Environment setup (uses Hatch)
hatch shell dev_py_3_11 # or dev_py_3_9, dev_py_3_10, dev_py_3_12
# Linting and formatting
make lint # ruff check
make format # ruff format
make sort # isort mem0/
# Tests
make test # pytest tests/
make test-py-3.9 # test specific Python version (3.9–3.12)
# Build and publish
make build # hatch build
make publish # hatch publish
```
- **Python:** 3.9, 3.10, 3.11, 3.12
- **Linter/formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Test framework:** pytest (with pytest-mock, pytest-asyncio)
- **Pre-commit hooks:** ruff + isort — run `pre-commit install` before committing
### TypeScript SDK (`mem0-ts/`)
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup
pnpm run test # jest (all tests)
pnpm run test:unit # jest --coverage (unit tests only)
pnpm run test:integration # jest (integration tests, needs MEM0_API_KEY)
pnpm run test:ci # jest --coverage --ci (CI mode)
pnpm run test:watch # jest watch mode
```
- **Node:** 20, 22 (CI-tested)
- **Build:** tsup (CJS + ESM)
- **Test:** jest
- **Formatter:** prettier
### Python CLI (`cli/python/`)
```bash
cd cli/python
pip install -e ".[dev]" # dev install with ruff + pytest
ruff check . # lint
ruff format . # format
pytest # test
hatch build # build
```
- **Python:** 3.10+ (not 3.9)
- **Linter/formatter:** Ruff (line length **100** — different from root SDK)
- **Ruff rules:** E, F, I, W, UP, B, SIM, RUF (ignores E501, B008 for Typer patterns, SIM108)
- **Framework:** Typer + Rich + httpx
- **Entry point:** `mem0 = "mem0_cli.app:main"`
- **Source layout:** `src/mem0_cli/`
- **Optional dependency:** `mem0ai` (for OSS mode, via `[oss]` extra)
### Node CLI (`cli/node/`)
```bash
cd cli/node
pnpm install
pnpm run build # tsup
pnpm run lint # biome check src/
pnpm run lint:fix # biome check --write src/
pnpm run typecheck # tsc --noEmit
pnpm run test # vitest run
pnpm run test:watch # vitest (watch mode)
pnpm run dev # tsx src/index.ts (development)
```
- **Node:** 18+ required
- **Build:** tsup (ESM)
- **Linter:** Biome (not ESLint, not Ruff)
- **Test:** vitest (not jest)
- **Framework:** Commander + Chalk + ora + cli-table3
### Vercel AI SDK Provider (`integrations/vercel-ai-sdk/`)
```bash
cd integrations/vercel-ai-sdk
pnpm install
pnpm run build # tsup
pnpm run lint # eslint
pnpm run type-check # tsc --noEmit
pnpm run prettier-check # prettier --check
pnpm run test # jest
pnpm run test:edge # vitest (edge runtime)
pnpm run test:node # vitest (node runtime)
```
- **Build:** tsup (CJS + ESM)
- **Lint:** ESLint + Prettier
- **Test:** jest + vitest (edge/node configs)
### OpenClaw Plugin (`integrations/openclaw/`)
```bash
cd integrations/openclaw
pnpm install
pnpm run build # tsup
pnpm run test # vitest run
```
- **Build:** tsup (ESM)
- **Test:** vitest (with Codecov in CI)
- **Plugin manifest:** `openclaw.plugin.json`
### Server (`server/`)
```bash
# Docker production build
cd server
make build # docker build -t mem0-api-server .
make run_local # docker run -p 8000:8000 with .env
# Docker Compose development (FastAPI + PostgreSQL/pgvector + Neo4j)
cd server
docker-compose up # starts all 3 services
# mem0 API: localhost:8888
# PostgreSQL: localhost:8432
# Neo4j HTTP: localhost:8474, Bolt: localhost:8687
```
- **Framework:** FastAPI with uvicorn (auto-reload in dev)
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
### OpenMemory (`openmemory/`)
```bash
# Full stack via Docker Compose
cd openmemory
docker-compose up
# Qdrant: localhost:6333
# API (MCP): localhost:8765
# UI: localhost:3000
# Individual development
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
cd openmemory/ui && npm run dev # Next.js frontend
# Tests
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
```
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
- **Vector store:** Qdrant
### Documentation (`docs/`)
```bash
make docs # or: cd docs && mintlify dev
```
- **Framework:** Mintlify
- **API spec:** `docs/openapi.json`
- **Structure:** `api-reference/`, `open-source/`, `platform/`, `integrations/`, `cookbooks/`, `core-concepts/`
### Evaluation / 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
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
### Python
| Function / Class | Purpose | Import |
|-----------------|---------|--------|
| `Memory` | Self-hosted memory (sync) | `from mem0 import Memory` |
| `AsyncMemory` | Self-hosted memory (async) | `from mem0 import AsyncMemory` |
| `MemoryClient` | Hosted platform client (sync) | `from mem0 import MemoryClient` |
| `AsyncMemoryClient` | Hosted platform client (async) | `from mem0 import AsyncMemoryClient` |
**Key `Memory` / `MemoryClient` methods:**
| Method | Purpose |
|--------|---------|
| `add(messages, *, user_id, agent_id, run_id, metadata)` | Store a new memory |
| `search(query, *, user_id, agent_id, run_id, limit, filters)` | Search memories |
| `get(memory_id)` | Retrieve a single memory by ID |
| `get_all(*, user_id, agent_id, run_id, limit)` | List all memories |
| `update(memory_id, data)` | Update a memory |
| `delete(memory_id)` | Delete a memory |
| `delete_all(*, user_id, agent_id, run_id)` | Delete all memories |
| `history(memory_id)` | Get change history for a memory |
### TypeScript
| Export | Purpose | Import |
|--------|---------|--------|
| `MemoryClient` | Hosted platform client | `import { MemoryClient } from 'mem0ai'` |
| `Memory` | Self-hosted OSS memory | `import { Memory } from 'mem0ai/oss'` |
## Import Patterns
### Python
| What | Import |
|------|--------|
| Core memory classes | `from mem0 import Memory, AsyncMemory` |
| Platform client | `from mem0 import MemoryClient, AsyncMemoryClient` |
| Configuration | `from mem0.configs.base import MemoryConfig` |
| LLM providers | `from mem0.llms.<provider> import <ProviderLLM>` |
| Embedding providers | `from mem0.embeddings.<provider> import <ProviderEmbedding>` |
| Vector store providers | `from mem0.vector_stores.<provider> import <ProviderVectorStore>` |
### TypeScript
| What | Import |
|------|--------|
| Hosted client | `import { MemoryClient } from 'mem0ai'` |
| OSS memory | `import { Memory } from 'mem0ai/oss'` |
| Specific providers (OSS) | `import { OpenAIEmbedding } from 'mem0ai/oss'` |
## Coding Standards
### File Naming Conventions
- **Python source files:** `snake_case.py` (e.g., `azure_openai.py`, `cohere_reranker.py`)
- **Python test files:** `test_<module>.py` (e.g., `test_memory.py`, `test_main.py`)
- **TypeScript source files:** `snake_case.ts` (e.g., `azure_ai_search.ts`)
- **TypeScript test files:** `<module>.test.ts` (e.g., `memory.test.ts`)
- **Config/manifest files:** `kebab-case` (e.g., `openclaw.plugin.json`, `jest.config.js`)
### Python Conventions
- **Provider pattern:** All providers (LLMs, embeddings, vector stores, graphs, rerankers) inherit from a `base.py` abstract class in their directory. Config classes live in `configs.py`.
- **Pydantic v2** for all data models and configuration.
- **Ruff** is the single linting and formatting tool — no black, no flake8.
- Root SDK: line length **120**
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
- **isort** with `profile = "black"` for import sorting.
- Ruff excludes `openmemory/` from root config.
### TypeScript Conventions
- **Build:** tsup across all packages.
- **Package manager:** pnpm everywhere (no npm, no yarn).
- **TypeScript strict mode** across all packages.
- **Linting varies by package:**
| Package | Linter | Formatter | Test Framework |
|---------|--------|-----------|---------------|
| `mem0-ts/` | — | Prettier | jest |
| `cli/node/` | Biome | Biome | vitest |
| `integrations/vercel-ai-sdk/` | ESLint | Prettier | jest + vitest |
| `integrations/openclaw/` | — | — | vitest |
### Type Checking
Always run type checking after modifying TypeScript code:
```bash
cd <package> && pnpm run typecheck # or: tsc --noEmit
```
## Architecture
### Provider Pattern
The SDK uses a consistent plugin architecture across 5 categories. Each category has a `base.py` abstract class and concrete provider implementations:
| Category | Count | Examples |
|----------|-------|---------|
| **LLMs** | 24 | OpenAI, Anthropic, AWS Bedrock, Azure OpenAI, Gemini, Groq, Ollama, Together, DeepSeek, vLLM, LiteLLM, LM Studio, xAI |
| **Vector Stores** | 30 | Qdrant, Pinecone, Chroma, Weaviate, Milvus, MongoDB, Redis, Elasticsearch, pgvector, Supabase, Faiss, S3 Vectors |
| **Embeddings** | 15 | OpenAI, Azure OpenAI, Gemini, HuggingFace, FastEmbed, Together, AWS Bedrock, Ollama, Vertex AI |
| **Graph Stores** | 4 | Neo4j, Memgraph, Kuzu, Apache AGE |
| **Rerankers** | 5 | Cohere, HuggingFace, LLM-based, Sentence Transformer, Zero Entropy |
### Two Usage Modes
Self-hosted `Memory` / `AsyncMemory` classes and hosted-platform `MemoryClient` — both in Python and TypeScript.
### Graph Memory
Optional layer on top of vector memory for relationship-aware retrieval. Configured via the `graph` section of `MemoryConfig`.
### MCP Integration
Model Context Protocol support in multiple places:
- **Remote:** MCP server at `mcp.mem0.ai`
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
- **Plugin:** MCP tools in `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
- `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); `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
To add a new LLM, embedding, vector store, or reranker provider:
1. Create `mem0/<category>/<provider_name>.py`
2. Inherit from the abstract base class in `mem0/<category>/base.py`
3. Add configuration to `mem0/<category>/configs.py` (if the category uses one)
4. Register the provider in `mem0/<category>/__init__.py`
5. Add tests in `tests/<category>/<provider_name>/`
6. Add any new dependencies to the appropriate optional group in `pyproject.toml` (never to core `dependencies`)
7. Follow the exact pattern of existing providers in the same category — match method signatures, error handling, and config structure
### Adding a New Integration
Agent/editor integrations live under `integrations/`. Each is a self-contained directory (its own `package.json`/lockfile, build, and tests). To add one:
1. Create `integrations/<name>/` and build the integration there.
2. If it publishes to a registry, set `repository.directory: "integrations/<name>"` in its `package.json` so npm provenance links to the correct subdirectory.
3. Add CI/CD under `.github/workflows/` (`<name>-checks.yml`, `<name>-cd.yml`). Use `integrations/<name>` in `paths:` triggers, `working-directory`, and `cache-dependency-path`. Register the release tag prefix in the `case` block in `release.yml` (keep the bare `v*` arm last). Keep workflow **filenames** stable — npm OIDC trusted publishing is pinned to repo + workflow filename.
4. If it is a Claude Code / editor marketplace plugin, register its path in the five `marketplace.json` files (root + `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.agents/plugins/`).
5. Document it under `docs/integrations/` and add the page to `docs/docs.json` and `docs/llms.txt`.
6. Add rows to the "Key Directories" table and the CI/CD tables in this file.
## CI/CD
### CI Workflows (automated testing)
PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)** runs on every PR, detects which packages changed, and invokes only the relevant package workflows below as reusable workflows (`workflow_call`). Its final **`CI Gate`** job aggregates the results (skipped pipelines pass; failed or cancelled ones fail) and is the **only status check that needs to be required** in branch protection. Package workflows keep their own push-to-main and manual triggers; their `pull_request` triggers moved into the gate's path filters.
| Workflow | File | Standalone Triggers | Tests |
|----------|------|---------------------|-------|
| CI Gate | `ci-gate.yml` | All PRs | Routes to and aggregates the workflows below |
| Python SDK | `ci.yml` | Push to main | Ruff lint + pytest on Python 3.10, 3.11, 3.12 |
| TypeScript SDK | `ts-sdk-ci.yml` | Push to main (on `mem0-ts/`) | Prettier + build + jest on Node 20, 22 |
| Python CLI | `cli-python-ci.yml` | Push to main (on `cli/python/`), manual | Ruff lint + pytest + hatch build on Python 3.10, 3.11, 3.12 |
| Node CLI | `cli-node-ci.yml` | Push to main (on `cli/node/`), manual | Biome lint + tsc + vitest + tsup build on Node 20, 22 |
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| 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
| Workflow | File | Purpose |
|----------|------|---------|
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
## Task Completion Guidelines
These guidelines outline typical artifacts for different task types. Use judgment to adapt based on scope and context.
### Bug Fixes
1. **Unit tests**: Add tests that would fail without the fix (regression tests)
2. **Implementation**: Fix the bug
3. **Manual verification**: Run the relevant test suite to confirm the fix
4. **Lint**: Run the appropriate linter for the package you modified
### New Features
1. **Implementation**: Build the feature following existing patterns
2. **Unit tests**: Comprehensive test coverage for new functionality
3. **Documentation**: Update relevant docs in `docs/` for public APIs
4. **Examples**: Add usage examples if the feature introduces new user-facing behavior
5. **llms.txt**: Any new `.mdx` page under `docs/` must be linked in `docs/llms.txt` with a scope tag (`[Platform]` / `[OSS]` / `[Both]`) and a `Use when ...` description. The `docs-llms-txt-check.yml` workflow runs on every PR that touches docs and **fails the check** if the index is out of sync. To fix: run `python scripts/check-llms-txt-coverage.py --write` locally to scaffold placeholders under `## Unclassified - needs triage`, then replace the `[TODO: ...]` tags, rewrite descriptions as `Use when ...`, move entries into the right section, and delete the triage heading when empty.
### New Provider (LLM / Embedding / Vector Store / Reranker)
1. **Implementation**: Follow the "Adding a New Provider" steps above
2. **Tests**: Add unit tests matching the pattern of existing providers
3. **Configuration**: Add to the appropriate `configs.py` and `__init__.py`
4. **Dependencies**: Add to the correct optional group in `pyproject.toml`
5. **Documentation**: Add an integration guide in `docs/integrations/`
### Refactoring / Internal Changes
- Unit tests for any changed behavior
- No documentation needed for internal-only changes
- Ensure all existing tests still pass
### When to Deviate
These are guidelines, not rigid rules. Adjust based on:
- **Scope**: Trivial fixes (typos, comments) may not need tests
- **Visibility**: Internal changes may not need documentation
- **Context**: Some changes span multiple categories — use judgment
When uncertain about expected artifacts, ask for clarification.
## Contributing Guidelines
### Workflow
1. Fork and clone the repository.
2. Create a feature branch from `main` (e.g., `feature/my-new-feature`).
3. Make your changes — add tests, docs, and examples as appropriate.
4. Run linting and tests for every package you modified (see commands above).
5. Run `pre-commit install` on first setup — hooks run ruff + isort automatically.
6. Commit with a clear message following [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat:`, `fix:`, `docs:`, `refactor:`).
7. Push and open a Pull Request against `main`.
### Pull Request Requirements
Every PR must follow the repo's PR template (`.github/PULL_REQUEST_TEMPLATE.md`):
1. **Linked Issue** — Reference the issue with `Closes #<number>`. If no issue exists, create one first or explain why in the description.
2. **Description** — Explain what the PR does and why it's needed.
3. **Type of Change** — Check the appropriate box:
- Bug fix / New feature / Breaking change / Refactor / Documentation update
4. **Breaking Changes** — If applicable, describe what breaks and the migration path.
5. **Test Coverage** — Check what applies:
- Added/updated unit tests
- Added/updated integration tests
- Tested manually (describe how)
- No tests needed (explain why)
6. **Checklist** — All must be checked before merge:
- [ ] Code follows the project's style guidelines
- [ ] Self-review performed
- [ ] Tests added that prove the fix/feature works
- [ ] New and existing tests pass locally
- [ ] Documentation updated if needed
### PR Description Template
```markdown
## Linked Issue
Closes #<!-- issue number -->
## Description
<!-- What does this PR do? Why is it needed? -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Refactor (no functional changes)
- [ ] Documentation update
## Breaking Changes
N/A
## Test Coverage
- [ ] I added/updated unit tests
- [ ] I added/updated integration tests
- [ ] I tested manually (describe below)
- [ ] No tests needed (explain why)
## Checklist
- [ ] My code follows the project's style guidelines
- [ ] I have performed a self-review of my code
- [ ] I have added tests that prove my fix/feature works
- [ ] New and existing tests pass locally
- [ ] I have updated documentation if needed
```
### General Rules
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
- For `server/` and `openmemory/` work, use Docker Compose for local development.
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
### Contributing Guides
| Task | Guide |
|------|-------|
| Code contributions | `docs/contributing/development.mdx` |
| Documentation contributions | `docs/contributing/documentation.mdx` |
| PR template | `.github/PULL_REQUEST_TEMPLATE.md` |
| Bug reports | `.github/ISSUE_TEMPLATE/bug_report.yml` |
| Feature requests | `.github/ISSUE_TEMPLATE/feature_request.yml` |
| Documentation issues | `.github/ISSUE_TEMPLATE/documentation_issue.yml` |
## Do NOT
- Modify CI/CD workflows without explicit approval.
- Add new Python dependencies to the core `dependencies` list in `pyproject.toml` without discussion — use optional dependency groups instead.
- Commit `.env` files, API keys, or credentials.
- Skip pre-commit hooks.
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
- Change public APIs without updating documentation in `docs/`.
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+156 -41
View File
@@ -1,63 +1,178 @@
# 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()`.
## Good Contribution Practices
- **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`.
## Pull Request Checklist
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 |
|---------|----------|------------|---------|
| `mem0ai` (Python SDK) | PyPI | `v*` | `v0.1.31` |
| `mem0-cli` (Python CLI) | PyPI | `cli-v*` | `cli-v0.2.1` |
| `mem0ai` (TypeScript SDK) | npm | `ts-v*` | `ts-v2.4.6` |
| `@mem0/cli` (Node CLI) | npm | `cli-node-v*` | `cli-node-v0.1.2` |
| `@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
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
- **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!
+5 -4
View File
@@ -266,7 +266,7 @@ config = MemoryConfig(
graph_store=GraphStoreConfig(provider="neo4j", config={...}), # optional
history_db_path="~/.mem0/history.db",
version="v1.1",
custom_fact_extraction_prompt="Custom prompt...",
custom_instructions="Custom prompt...",
custom_update_memory_prompt="Custom prompt..."
)
```
@@ -684,7 +684,7 @@ Conversation: {messages}
"""
config = MemoryConfig(
custom_fact_extraction_prompt=custom_extraction_prompt
custom_instructions=custom_extraction_prompt
)
memory = Memory(config)
```
@@ -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
@@ -1313,7 +1314,7 @@ async def delete_memory(memory_id: str):
- **Documentation**: https://docs.mem0.ai
- **GitHub Repository**: https://github.com/mem0ai/mem0
- **Discord Community**: https://mem0.dev/DiG
- **Platform**: https://app.mem0.ai
- **Platform**: https://app.mem0.ai?utm_source=oss&utm_medium=llm
- **Research Paper**: https://mem0.ai/research
- **Examples**: https://github.com/mem0ai/mem0/tree/main/examples
-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!")
```
-3
View File
@@ -42,9 +42,6 @@ clean:
test:
hatch run test
test-py-3.9:
hatch run dev_py_3_9:test
test-py-3.10:
hatch run dev_py_3_10:test
+107 -21
View File
@@ -39,18 +39,33 @@
</p>
<p align="center">
<a href="https://mem0.ai/research"><strong>📄 Building Production-Ready AI Agents with Scalable Long-Term Memory →</strong></a>
</p>
<p align="center">
<strong>⚡ +26% Accuracy vs. OpenAI Memory • 🚀 91% Faster • 💰 90% Fewer Tokens</strong>
<a href="https://mem0.ai/research"><strong>📄 Benchmarking Mem0's token-efficient memory algorithm →</strong></a>
</p>
> **🎉 mem0ai v1.0.0 is now available!** This major release includes API modernization, improved vector store support, and enhanced GCP integration. [See migration guide →](MIGRATION_GUIDE_v1.0.md)
## New Memory Algorithm (April 2026)
## 🔥 Research Highlights
- **+26% Accuracy** over OpenAI Memory on the LOCOMO benchmark
- **91% Faster Responses** than full-context, ensuring low-latency at scale
- **90% Lower Token Usage** than full-context, cutting costs without compromise
| Benchmark | Old | New | Tokens | Latency p50 |
| --- | --- | --- | --- | --- |
| **LoCoMo** | 71.4 | **92.5** | 7.0K | 0.88s |
| **LongMemEval** | 67.8 | **94.4** | 6.8K | 1.09s |
| **BEAM (1M)** | — | **64.1** | 6.7K | 1.00s |
| **BEAM (10M)** | — | **48.6** | 6.9K | 1.05s |
All benchmarks run on the same production-representative model stack. Single-pass retrieval (one call, no agentic loops) at a top_200 retrieval budget. Scores reflect Mem0's managed platform, which includes proprietary optimizations not available in the open-source SDK; open-source users should expect directionally similar gains but not identical numbers.
**What changed:**
- **Single-pass ADD-only extraction** -- one LLM call, no UPDATE/DELETE. Memories accumulate; nothing is overwritten.
- **Agent-generated facts are first-class** -- when an agent confirms an action, that information is now stored with equal weight.
- **Entity linking** -- entities are extracted, embedded, and linked across memories for retrieval boosting.
- **Multi-signal retrieval** -- semantic, BM25 keyword, and entity matching scored in parallel and fused.
- **Temporal Reasoning** -- time-aware retrieval that ranks the right dated instance for queries about current state, past events, and upcoming plans.
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for upgrade instructions. The [evaluation framework](https://github.com/mem0ai/memory-benchmarks) is open-sourced so anyone can reproduce the numbers.
## Research Highlights
- **92.5 on LoCoMo** -- +21 points over the previous algorithm
- **94.4 on LongMemEval** -- +27 points, with 98.2 on assistant memory recall
- **64.1 on BEAM (1M)** -- production-scale memory evaluation at 1M tokens
- [Read the full paper](https://mem0.ai/research)
# Introduction
@@ -71,28 +86,75 @@
## 🚀 Quickstart Guide <a name="quickstart"></a>
Choose between our hosted platform or self-hosted package:
### Sign up as an agent
### Hosted Platform
AI agents can mint a working Mem0 API key in under five seconds — no email, no dashboard, no OTP. Four commands end-to-end:
Get up and running in minutes with automatic updates, analytics, and enterprise security.
```bash
# 1. Install
npm install -g @mem0/cli # or: pip install mem0-cli
1. Sign up on [Mem0 Platform](https://app.mem0.ai)
2. Embed the memory layer via SDK or API keys
# 2. Sign up as an agent (replace `claude-code` with your name)
mem0 init --agent --agent-caller claude-code
### Self-Hosted (Open Source)
# 3. Add a memory
mem0 add "I am using mem0"
Install the sdk via pip:
# 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 |
| **Setup** | `pip install mem0ai` | `docker compose up` | Sign up at [app.mem0.ai](https://app.mem0.ai?utm_source=oss&utm_medium=readme) |
| **Dashboard** | -- | [Yes](https://docs.mem0.ai/open-source/setup) | Yes |
| **Auth & API Keys** | -- | Yes | Yes |
| **Advanced Features** | -- | Teasers | All included |
Just testing? Use the library. Building for a team? Self-hosted. Want zero ops? Cloud.
### Library (pip / npm)
```bash
pip install mem0ai
```
For enhanced hybrid search with BM25 keyword matching and entity extraction, install with NLP support:
```bash
pip install mem0ai[nlp]
python -m spacy download en_core_web_sm
```
Install sdk via npm:
```bash
npm install mem0ai
```
### Self-Hosted Server
> **Note:** Self-hosted auth is on by default. Upgrading from a pre-auth build? Set `ADMIN_API_KEY`, register an admin through the wizard, or `AUTH_DISABLED=true` for local dev only. See [upgrade notes](https://docs.mem0.ai/open-source/setup#upgrade-notes).
```bash
# Recommended: one command — start the stack, create an admin, issue the first API key.
cd server && make bootstrap
# Manual: start the stack and finish setup via the browser wizard.
cd server && docker compose up -d # http://localhost:3000
```
See the [self-hosted docs](https://docs.mem0.ai/open-source/overview) for configuration.
### Cloud Platform
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
Manage memories from your terminal:
@@ -107,9 +169,33 @@ mem0 search "What does Alice prefer?" --user-id alice
See the [CLI documentation](https://docs.mem0.ai/platform/cli) for the full command reference.
### Agent Skills
Teach your AI coding assistant (Claude Code, Codex, Cursor, Windsurf, OpenCode, OpenClaw, and any tool that supports the skills standard) how to build with Mem0. Two categories:
**Reference skills — always on** (SDK knowledge loaded into the assistant's context):
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0
npx skills add https://github.com/mem0ai/mem0 --skill mem0-cli
npx skills add https://github.com/mem0ai/mem0 --skill mem0-vercel-ai-sdk
```
**Pipeline skills — run on demand** (execute an end-to-end workflow in an existing repo):
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0-integrate
npx skills add https://github.com/mem0ai/mem0 --skill mem0-test-integration
npx skills add https://github.com/mem0ai/mem0 --skill mem0-oss-to-platform
```
Use `/mem0-integrate` to wire Mem0 into an existing repo via a test-first pipeline, then `/mem0-test-integration` to verify. Use `/mem0-oss-to-platform` to migrate an existing project from Mem0 OSS to the hosted Platform SDK. See the [skills catalog](./skills/) or [Vibecoding with Mem0](https://docs.mem0.ai/vibecoding) for the full picture.
### Basic Usage
Mem0 requires an LLM to function, with `gpt-4.1-nano-2025-04-14 from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
Mem0 requires an LLM to function, with `gpt-5-mini` from OpenAI as the default. However, it supports a variety of LLMs; for details, refer to our [Supported LLMs documentation](https://docs.mem0.ai/components/llms/overview).
Mem0 uses `text-embedding-3-small` from OpenAI as the default embedding model. For best results with hybrid search (semantic + keyword + entity boosting), we recommend using at least [Qwen 600M](https://huggingface.co/Alibaba-NLP/gte-Qwen2-1.5B-instruct) or a comparable embedding model. See [Supported Embeddings](https://docs.mem0.ai/components/embedders/overview) for configuration details.
First step is to instantiate the memory:
@@ -122,13 +208,13 @@ memory = Memory()
def chat_with_memories(message: str, user_id: str = "default_user") -> str:
# Retrieve relevant memories
relevant_memories = memory.search(query=message, user_id=user_id, limit=3)
relevant_memories = memory.search(query=message, filters={"user_id": user_id}, top_k=3)
memories_str = "\n".join(f"- {entry['memory']}" for entry in relevant_memories["results"])
# Generate Assistant response
system_prompt = f"You are a helpful AI. Answer the question based on query and memories.\nUser Memories:\n{memories_str}"
messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": message}]
response = openai_client.chat.completions.create(model="gpt-4.1-nano-2025-04-14", messages=messages)
response = openai_client.chat.completions.create(model="gpt-5-mini", messages=messages)
assistant_response = response.choices[0].message.content
# Create new memories from the conversation
@@ -162,7 +248,7 @@ For detailed integration steps, see the [Quickstart](https://docs.mem0.ai/quicks
## 📚 Documentation & Support
- Full docs: https://docs.mem0.ai
- Community: [Discord](https://mem0.dev/DiG) · [Twitter](https://x.com/mem0ai)
- Community: [Discord](https://mem0.dev/DiG) · [X (formerly Twitter)](https://x.com/mem0ai)
- Contact: founders@mem0.ai
## Citation
@@ -180,4 +266,4 @@ We now have a paper you can cite:
## ⚖️ License
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
Apache 2.0 — see the [LICENSE](https://github.com/mem0ai/mem0/blob/main/LICENSE) file for details.
+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.
+188 -15
View File
@@ -28,7 +28,7 @@ mem0 CLI is the official command-line interface for [mem0](https://mem0.ai) -- t
### Who is it for?
- Developers integrating mem0 into their workflows
- AI agents that need persistent memory (the CLI is designed with `--output json` and `help --json` specifically for machine consumption)
- AI agents that need persistent memory (the CLI is designed with `--json`/`--agent` global flags and `help --json` specifically for machine consumption)
- DevOps/CI pipelines that need to manage memories programmatically
### Project Structure
@@ -79,6 +79,7 @@ Apache-2.0
│ ├── init_cmd.py # run_init (interactive wizard)
│ ├── config_cmd.py # cmd_config_show, cmd_config_get, cmd_config_set
│ ├── entities.py # cmd_entities_list, cmd_entities_delete
│ ├── events_cmd.py # cmd_event_list, cmd_event_status
│ └── utils.py # cmd_status, cmd_version, cmd_import
└── node/
├── package.json # Node package config (tsup build)
@@ -88,6 +89,7 @@ Apache-2.0
├── config.ts # Config loading/saving, env var overrides
├── branding.ts # Colors, icons, banner, timedStatus, print helpers
├── output.ts # Output formatting (text, json, table, quiet)
├── state.ts # Agent mode flag (setAgentMode, isAgentMode)
├── help.ts # Rich-style help formatter (panels, command ordering)
├── backend/
│ ├── index.ts # Re-exports
@@ -98,6 +100,7 @@ Apache-2.0
├── init.ts # runInit (interactive wizard)
├── config.ts # cmdConfigShow, cmdConfigGet, cmdConfigSet
├── entities.ts # cmdEntitiesList, cmdEntitiesDelete
├── events.ts # cmdEventList, cmdEventStatus
└── utils.ts # cmdStatus, cmdVersion, cmdImport
```
@@ -153,8 +156,25 @@ Interactive setup wizard for mem0 CLI.
|-----------------|--------|----------|---------|------|
| `--api-key` | string | No | - | API key (skip prompt). |
| `-u, --user-id` | string | No | - | Default user ID (skip prompt). |
| `--email` | string | No | - | Login via email verification code. |
| `--code` | string | No | - | Verification code (use with --email for non-interactive login). |
| `--force` | bool | No | false | Overwrite existing config without confirmation. |
**Behavior:**
*Existing config protection:*
- If `~/.mem0/config.json` exists with an API key, the CLI warns and asks for confirmation before overwriting.
- In non-TTY mode, this is a hard error unless `--force` is passed.
- `--force` skips the confirmation in both TTY and non-TTY modes.
*Email login flow (when `--email` is provided):*
- Sends a 6-digit verification code to the email via `POST /api/v1/auth/email_code/`.
- If `--code` is also provided, verifies immediately (fully non-interactive).
- If `--code` is not provided, prompts for the code interactively.
- On success: receives API key, org_id, project_id. Saves to config. Creates account if email is new.
- Cannot be combined with `--api-key`.
*API key flow (existing behavior):*
- If both `--api-key` and `--user-id` are provided, runs non-interactively (no prompts).
- If running in a non-TTY without both flags, prints an error with usage hint and exits.
- Interactive mode: prints banner, prompts for API key (masked with `*`), prompts for default user ID (default: `mem0-cli`), validates connection, saves config.
@@ -164,6 +184,9 @@ Interactive setup wizard for mem0 CLI.
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --api-key m0-xxx --user-id alice --force
mem0 init --email alice@company.com
mem0 init --email alice@company.com --code 482901
```
---
@@ -687,7 +710,114 @@ mem0 entity delete --user-id alice --dry-run
---
### 3.14 `status`
### 3.14 `event list`
List recent background processing events.
| Property | Value |
|------------------|-------|
| Usage | `mem0 event list [OPTIONS]` |
| needsBackend | Yes |
| needsConfig | Yes |
| resolveIds | No |
| resolveGraph | No |
| confirmDangerous | No |
| Output formats | text (table), json |
| Default output | table |
| API endpoint | `GET /v1/events/` |
**Options:**
| Flag | Type | Default | Panel | Help |
|----------------|--------|---------|------------|------|
| `-o, --output` | string | "table" | Output | Output: text, json. |
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** Fetches all background events for the project. Displays as a table with columns: Event ID (first 8 chars), Type, Status (color-coded), Latency, Created. Status values: `PENDING` (accent), `SUCCEEDED` (green), `FAILED` (red), `PROCESSING` (yellow).
**JSON output envelope:**
```json
{
"status": "success",
"command": "event list",
"count": 3,
"duration_ms": 87,
"data": [
{ "id": "evt-abc", "event_type": "ADD", "status": "SUCCEEDED", "latency": 412.0, "created_at": "2026-01-01T10:00:00Z" }
]
}
```
**Examples:**
```bash
mem0 event list
mem0 event list --output json
```
---
### 3.15 `event status`
Get the status and results of a specific background event.
| Property | Value |
|------------------|-------|
| Usage | `mem0 event status <event_id> [OPTIONS]` |
| needsBackend | Yes |
| needsConfig | Yes |
| resolveIds | No |
| resolveGraph | No |
| confirmDangerous | No |
| Output formats | text, json |
| Default output | text |
| API endpoint | `GET /v1/events/{event_id}/` |
**Arguments:**
| Name | Type | Required | Help |
|------------|--------|----------|------|
| `event_id` | string | Yes | Event ID to inspect. |
**Options:**
| Flag | Type | Default | Panel | Help |
|----------------|--------|---------|------------|------|
| `-o, --output` | string | "text" | Output | Output: text, json. |
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** Fetches the event by ID and displays: Event ID, Type, Status (color-coded), Latency, Created, Updated, and a numbered list of result memories (event type, memory text, user_id, truncated memory ID). Displayed in a boxed panel (text) or JSON envelope.
**JSON output envelope:**
```json
{
"status": "success",
"command": "event status",
"duration_ms": 65,
"data": {
"id": "evt-abc",
"event_type": "ADD",
"status": "SUCCEEDED",
"latency": 412.0,
"created_at": "2026-01-01T10:00:00Z",
"updated_at": "2026-01-01T10:00:01Z",
"results": [
{ "id": "mem-xyz", "event": "ADD", "user_id": "alice", "memory": "User prefers dark mode" }
]
}
}
```
**Examples:**
```bash
mem0 event status evt-abc-123
mem0 event status evt-abc-123 --output json
```
---
### 3.16 `status`
Check connectivity and authentication.
@@ -708,20 +838,19 @@ Check connectivity and authentication.
| `--api-key` | string | - | Connection | Override API key. |
| `--base-url` | string | - | Connection | Override API base URL. |
**Behavior:** If config has a default `user_id` or `agent_id`, validates by making a minimal `POST /v2/memories/` with `page=1&page_size=1`. Otherwise validates via `GET /v1/entities/`. Displays connection status in a boxed panel (text) or JSON envelope.
**Behavior:** Validates connectivity by calling `GET /v1/ping/`. Displays connection status in a boxed panel (text) or JSON envelope. The ping endpoint is lightweight and does not require any entity scope.
**JSON output:**
```json
{
"status": "success",
"command": "status",
"duration_ms": 112,
"data": {
"connected": true,
"backend": "platform",
"base_url": "https://api.mem0.ai",
"latency_ms": 245
},
"duration_ms": 245
"base_url": "https://api.mem0.ai"
}
}
```
@@ -733,7 +862,7 @@ mem0 status -o json
---
### 3.15 `help`
### 3.17 `help`
Show help. Use `--json` for machine-readable output (for LLM agents).
@@ -793,6 +922,9 @@ The auth header name is `Authorization` and the scheme is `Token` (not Bearer).
| Delete all | `DELETE` | `/v1/memories/` | - | entity ID params |
| List entities | `GET` | `/v1/entities/` | - | - |
| Delete entities | `DELETE` | `/v1/entities/` | - | entity ID params |
| List events | `GET` | `/v1/events/` | - | - |
| Get event | `GET` | `/v1/events/{event_id}/` | - | - |
| Ping (status) | `GET` | `/v1/ping/` | - | - |
### How Filters Are Built (`_buildFilters` / `_build_filters`)
@@ -1095,6 +1227,8 @@ For `PENDING` events, displays "Processing in background" with the event ID.
### 7.1 Supported Modes Per Command
All commands also support `agent` mode via the global `--json`/`--agent` flag, which wraps output in a structured JSON envelope with sanitized fields.
| Command | text | json | table | quiet |
|----------------|------|------|-------|-------|
| add | Y | Y | - | Y |
@@ -1109,12 +1243,16 @@ For `PENDING` events, displays "Processing in background" with the event ID.
| config set | (success msg) | - | - | - |
| entity list | - | Y | Y (default) | - |
| entity delete | Y | Y | - | Y |
| event list | Y (table) | Y | - | - |
| event status | Y | Y | - | - |
| status | Y | Y | - | - |
| help | Y | Y (--json) | - | - |
### 7.2 JSON Envelope Format (`formatJsonEnvelope`)
### 7.2 JSON Envelope Format
Used by `config show`, `status`, and `import` for structured JSON output:
There are two related envelope formats:
**`formatJsonEnvelope`** — used by `config show`, `status`, and `import` for `--output json`:
```json
{
@@ -1128,14 +1266,39 @@ Used by `config show`, `status`, and `import` for structured JSON output:
}
```
**`formatAgentEnvelope`** — used by all commands in agent mode (`--json`/`--agent`). Same structure, but `data` is passed through `sanitizeAgentData(command, data)` to project only the most relevant fields:
| Command | Fields in `data` |
|---------------|-----------------|
| add | `[{id, memory, event}]` or `[{status, event_id}]` for PENDING |
| search | `[{id, memory, score, created_at, categories}]` |
| list | `[{id, memory, created_at, categories}]` |
| get | `{id, memory, created_at, updated_at, categories, metadata}` |
| update | `{id, memory}` |
| delete | (raw API response) |
| entity list | `[{name, type, count}]` |
| event list | `[{id, event_type, status, latency, created_at}]` |
| event status | `{id, event_type, status, latency, created_at, updated_at, results: [{id, event, user_id, memory}]}` |
| status/config/import | (pass-through) |
Error envelopes (on non-zero exit):
```json
{
"status": "error",
"command": "<command_name>",
"error": "Authentication failed. Your API key may be invalid or expired.",
"data": null
}
```
Fields:
- `status`: Always `"success"` (errors go to stderr before exit).
- `command`: The command name (e.g. `"status"`, `"config show"`, `"import"`).
- `status`: `"success"` or `"error"`.
- `command`: The command name.
- `duration_ms`: Optional, elapsed time in milliseconds.
- `scope`: Optional, active entity scope.
- `scope`: Optional, active entity scope (omitted if empty).
- `count`: Optional, result count.
- `error`: Optional, error message string.
- `data`: The primary payload.
- `error`: Only present when `status` is `"error"`.
- `data`: The primary payload (sanitized in agent mode).
### 7.3 Text Output
@@ -1206,6 +1369,16 @@ Destructive commands (`delete --all`, `delete --entity`, `entity delete`) requir
- CI/CD pipelines
- Scripting
### Why `--json`/`--agent` global flags exist
The `--json` and `--agent` flags (aliases of each other) activate agent mode globally. When set:
1. All output becomes a structured JSON envelope (`{status, command, duration_ms, scope, count, data}`).
2. The `data` field is sanitized via `sanitizeAgentData` — only the most relevant fields are included per command, reducing noise for agents parsing the output.
3. All human-readable output (spinners, colors, banners, timing lines) is suppressed.
4. Errors are emitted as JSON to stdout with a non-zero exit code, not to stderr as text.
This is distinct from `--output json`, which returns the raw API response without sanitization.
### Why `--output json` is on every command
Every data-returning command supports `--output json` (or `--json` for `help`). This enables machine consumption by AI agents and scripts. JSON output goes to stdout while human-readable spinners/timing go to stderr, so piping `mem0 list -o json | jq .` works cleanly.
+46 -8
View File
@@ -2,6 +2,8 @@
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Works with the Mem0 Platform API. Available in Python and Node.js.
> **For AI agents:** pass `--agent` (or `--json`) on any command for structured JSON output purpose-built for tool loops — sanitized fields, no colors or spinners, errors as JSON. See [Agent mode](#agent-mode) below.
## Installation
```bash
@@ -17,9 +19,15 @@ Both packages install a `mem0` binary with identical behavior.
## Quick start
```bash
# Authenticate and save config
# Interactive setup wizard
mem0 init
# Or login via email (get a new API key)
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
@@ -40,7 +48,7 @@ mem0 delete <memory-id>
| Command | Description |
|---------|-------------|
| `mem0 init` | Interactive setup wizard — configures API key and default user ID |
| `mem0 init` | Setup wizard — login via email or configure API key manually |
| `mem0 add` | Add a memory from text, JSON messages, a file, or stdin |
| `mem0 search` | Search memories using natural language |
| `mem0 list` | List memories with optional filters and pagination |
@@ -49,12 +57,45 @@ mem0 delete <memory-id>
| `mem0 delete` | Delete a memory, all memories for a scope, or an entity |
| `mem0 import` | Bulk import memories from a JSON file |
| `mem0 config` | View or modify CLI configuration |
| `mem0 entities` | List or delete entities (users, agents, apps) |
| `mem0 entity` | List or delete entities (users, agents, apps, runs) |
| `mem0 event` | Inspect background processing events (bulk deletes, large add jobs) |
| `mem0 status` | Verify API connection and display current project |
| `mem0 version` | Print the CLI version |
Run `mem0 <command> --help` for detailed usage on any command.
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
```bash
mem0 --agent search "user preferences" --user-id alice
mem0 --agent add "User prefers dark mode" --user-id alice
mem0 --agent list --user-id alice
```
Every command returns the same envelope shape:
```json
{
"status": "success",
"command": "search",
"duration_ms": 134,
"scope": { "user_id": "alice" },
"count": 2,
"data": [
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
]
}
```
What agent mode does differently from `--output json`:
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
- **No human output**: spinners, colors, and banners are suppressed entirely
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
## Output formats
Control how results are displayed with `--output`:
@@ -62,13 +103,10 @@ Control how results are displayed with `--output`:
| Format | Description |
|--------|-------------|
| `text` | Human-readable with colors and formatting (default) |
| `json` | Structured JSON for piping to `jq` or agent consumption |
| `json` | Structured JSON for piping to `jq` (raw API response) |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
```bash
mem0 search "preferences" --user-id alice --output json | jq '.data.results[].memory'
```
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
## Environment variables
+11 -3
View File
@@ -503,15 +503,23 @@
},
{
"name": "init",
"description": "Interactive setup wizard for mem0 CLI.",
"usage": "mem0 init",
"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,
"resolveIds": false,
"resolveGraph": false,
"confirmDangerous": false,
"arguments": [],
"options": []
"options": [
{ "name": "api-key", "flags": ["--api-key"], "type": "string", "default": null, "help": "API key (skip prompt)." },
{ "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": "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)." }
]
},
{
"name": "status",
+299 -37
View File
@@ -2,10 +2,12 @@
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. TypeScript implementation.
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
## Prerequisites
- Node.js **18+**
- pnpm (`npm install -g pnpm`)
- pnpm (`npm install -g pnpm`) — for development only
## Installation
@@ -13,22 +15,307 @@ The official command-line interface for [mem0](https://mem0.ai) — the memory l
npm install -g @mem0/cli
```
Or from source:
## Quick start
```bash
cd node
pnpm install
pnpm build
pnpm link --global
# Interactive setup wizard
mem0 init
# Now use it like a normal CLI
mem0 --help
# Or login via email
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories for a user
mem0 list --user-id alice
# Get a specific memory
mem0 get <memory-id>
# Update a memory
mem0 update <memory-id> "I switched to light mode"
# Delete a memory
mem0 delete <memory-id>
```
## Running during development
## Commands
### `mem0 init`
Interactive setup wizard. Prompts for your API key and default user ID.
```bash
cd node
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
```
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
```bash
mem0 init --api-key m0-xxx --user-id alice --force
```
| Flag | Description |
|------|-------------|
| `--api-key` | API key (skip prompt) |
| `-u, --user-id` | Default user ID (skip prompt) |
| `--email` | Login via email verification code |
| `--code` | Verification code (use with `--email` for non-interactive login) |
| `--force` | Overwrite existing config without confirmation |
### `mem0 add`
Add a memory from text, a JSON messages array, a file, or stdin.
```bash
mem0 add "I prefer dark mode" --user-id alice
mem0 add --file conversation.json --user-id alice
echo "Loves hiking on weekends" | mem0 add --user-id alice
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Scope to a user |
| `--agent-id` | Scope to an agent |
| `--messages` | Conversation messages as JSON |
| `-f, --file` | Read messages from a JSON file |
| `-m, --metadata` | Custom metadata as JSON |
| `--categories` | Categories (JSON array or comma-separated) |
| `--graph / --no-graph` | Enable or disable graph memory extraction |
| `-o, --output` | Output format: `text`, `json`, `quiet` |
### `mem0 search`
Search memories using natural language.
```bash
mem0 search "dietary restrictions" --user-id alice
mem0 search "preferred tools" --user-id alice --output json --top-k 5
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Filter by user |
| `-k, --top-k` | Number of results (default: 10) |
| `--threshold` | Minimum similarity score (default: 0.3) |
| `--rerank` | Enable reranking |
| `--keyword` | Use keyword search instead of semantic |
| `--filter` | Advanced filter expression (JSON) |
| `--graph / --no-graph` | Enable or disable graph in search |
| `-o, --output` | Output format: `text`, `json`, `table` |
### `mem0 list`
List memories with optional filters and pagination.
```bash
mem0 list --user-id alice
mem0 list --user-id alice --category preferences --output json
mem0 list --user-id alice --after 2024-01-01 --page-size 50
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Filter by user |
| `--page` | Page number (default: 1) |
| `--page-size` | Results per page (default: 100) |
| `--category` | Filter by category |
| `--after` | Created after date (YYYY-MM-DD) |
| `--before` | Created before date (YYYY-MM-DD) |
| `-o, --output` | Output format: `text`, `json`, `table` |
### `mem0 get`
Retrieve a specific memory by ID.
```bash
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
```
### `mem0 update`
Update the text or metadata of an existing memory.
```bash
mem0 update <memory-id> "Updated preference text"
mem0 update <memory-id> --metadata '{"priority": "high"}'
echo "new text" | mem0 update <memory-id>
```
### `mem0 delete`
Delete a single memory, all memories for a scope, or an entire entity.
```bash
# Delete a single memory
mem0 delete <memory-id>
# Delete all memories for a user
mem0 delete --all --user-id alice --force
# Delete all memories project-wide
mem0 delete --all --project --force
# Preview what would be deleted
mem0 delete --all --user-id alice --dry-run
```
| Flag | Description |
|------|-------------|
| `--all` | Delete all memories matching scope filters |
| `--entity` | Delete the entity and all its memories |
| `--project` | With `--all`: delete all memories project-wide |
| `--dry-run` | Preview without deleting |
| `--force` | Skip confirmation prompt |
### `mem0 import`
Bulk import memories from a JSON file.
```bash
mem0 import data.json --user-id alice
```
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
### `mem0 config`
View or modify the local CLI configuration.
```bash
mem0 config show # Display current config (secrets redacted)
mem0 config get api_key # Get a specific value
mem0 config set user_id bob # Set a value
```
### `mem0 entity`
List or delete entities (users, agents, apps, runs).
```bash
mem0 entity list users
mem0 entity list agents --output json
mem0 entity delete --user-id alice --force
```
### `mem0 event`
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
```bash
# List recent events
mem0 event list
# Check the status of a specific event
mem0 event status <event-id>
```
| Flag | Description |
|------|-------------|
| `-o, --output` | Output format: `text`, `json` |
### `mem0 status`
Verify your API connection and display the current project.
```bash
mem0 status
```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
```bash
mem0 --agent search "user preferences" --user-id alice
mem0 --agent add "User prefers dark mode" --user-id alice
mem0 --agent list --user-id alice
mem0 --agent delete --all --user-id alice --force
```
Every command returns the same envelope shape:
```json
{
"status": "success",
"command": "search",
"duration_ms": 134,
"scope": { "user_id": "alice" },
"count": 2,
"data": [
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
]
}
```
What agent mode does differently from `--output json`:
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
- **No human output**: spinners, colors, and banners are suppressed entirely
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
## Output formats
Control how results are displayed with `--output`:
| Format | Description |
|--------|-------------|
| `text` | Human-readable with colors and formatting (default) |
| `json` | Structured JSON for piping to `jq` (raw API response) |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
## Global flags
These flags are available on all commands:
| Flag | Description |
|------|-------------|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
| `--agent` | Alias for `--json` |
| `--api-key` | Override the configured API key for this request |
| `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format |
## Environment variables
| Variable | Description |
|----------|-------------|
| `MEM0_API_KEY` | API key (overrides config file) |
| `MEM0_BASE_URL` | API base URL |
| `MEM0_USER_ID` | Default user ID |
| `MEM0_AGENT_ID` | Default agent ID |
| `MEM0_APP_ID` | Default app ID |
| `MEM0_RUN_ID` | Default run ID |
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
Environment variables take precedence over values in the config file, which take precedence over defaults.
## Development
```bash
cd cli/node
pnpm install
# Development mode (runs TypeScript directly, no build needed)
@@ -39,36 +326,11 @@ pnpm dev search "test" --user-id alice
# Or build first, then run the compiled JS
pnpm build
node dist/index.js --help
node dist/index.js add "test memory" --user-id alice
```
## Quick Start
## Documentation
```bash
# Set up your configuration
mem0 init
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories
mem0 list --user-id alice
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MEM0_API_KEY` | API key (overrides config file) |
| `MEM0_BASE_URL` | API base URL |
| `MEM0_USER_ID` | Default user ID |
| `MEM0_AGENT_ID` | Default agent ID |
| `MEM0_APP_ID` | Default app ID |
| `MEM0_RUN_ID` | Default run ID |
| `MEM0_ENABLE_GRAPH` | Enable graph memory (true/false) |
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
## License
+19 -3
View File
@@ -1,13 +1,13 @@
{
"name": "@mem0/cli",
"version": "0.1.0",
"version": "0.2.11",
"description": "The official CLI for mem0 — the memory layer for AI agents",
"type": "module",
"bin": {
"mem0": "./dist/index.js"
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts --clean",
"build": "tsup",
"dev": "tsx src/index.ts",
"test": "vitest run",
"test:watch": "vitest",
@@ -20,6 +20,11 @@
},
"license": "Apache-2.0",
"author": "mem0.ai <founders@mem0.ai>",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "cli/node"
},
"keywords": ["mem0", "memory", "ai", "agents", "cli"],
"publishConfig": {
"access": "public"
@@ -35,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;
}
+83 -71
View File
@@ -6,110 +6,122 @@ import type { Mem0Config } from "../config.js";
import { PlatformBackend } from "./platform.js";
export interface AddOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
metadata?: Record<string, unknown>;
immutable?: boolean;
infer?: boolean;
expires?: string;
categories?: string[];
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
metadata?: Record<string, unknown>;
immutable?: boolean;
infer?: boolean;
expires?: string;
categories?: string[];
}
export interface SearchOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK?: number;
threshold?: number;
rerank?: boolean;
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
topK?: number;
threshold?: number;
rerank?: boolean;
keyword?: boolean;
filters?: Record<string, unknown>;
fields?: string[];
}
export interface ListOptions {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
enableGraph?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
page?: number;
pageSize?: number;
category?: string;
after?: string;
before?: string;
}
export interface DeleteOptions {
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
all?: boolean;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface EntityIds {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
}
export interface Backend {
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
add(
content?: string,
messages?: Record<string, unknown>[],
opts?: AddOptions,
): Promise<Record<string, unknown>>;
search(query: string, opts?: SearchOptions): Promise<Record<string, unknown>[]>;
search(
query: string,
opts?: SearchOptions,
): Promise<Record<string, unknown>[]>;
get(memoryId: string): Promise<Record<string, unknown>>;
get(memoryId: string): Promise<Record<string, unknown>>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
listMemories(opts?: ListOptions): Promise<Record<string, unknown>[]>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>;
update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>>;
delete(memoryId?: string, opts?: DeleteOptions): Promise<Record<string, unknown>>;
delete(
memoryId?: string,
opts?: DeleteOptions,
): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
deleteEntities(opts: EntityIds): Promise<Record<string, unknown>>;
status(opts?: { userId?: string; agentId?: string }): Promise<Record<string, unknown>>;
ping(): Promise<Record<string, unknown>>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
status(opts?: { userId?: string; agentId?: string }): Promise<
Record<string, unknown>
>;
entities(entityType: string): Promise<Record<string, unknown>[]>;
listEvents(): Promise<Record<string, unknown>[]>;
getEvent(eventId: string): Promise<Record<string, unknown>>;
}
export class AuthError extends Error {
constructor(message = "Authentication failed. Your API key may be invalid or expired.") {
super(message);
this.name = "AuthError";
}
constructor(
message = "Authentication failed. Your API key may be invalid or expired.",
) {
super(message);
this.name = "AuthError";
}
}
export class NotFoundError extends Error {
constructor(path: string) {
super(`Resource not found: ${path}`);
this.name = "NotFoundError";
}
constructor(path: string) {
super(`Resource not found: ${path}`);
this.name = "NotFoundError";
}
}
export class APIError extends Error {
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
constructor(path: string, detail: string) {
super(`Bad request to ${path}: ${detail}`);
this.name = "APIError";
}
}
export function getBackend(config: Mem0Config): Backend {
return new PlatformBackend(config.platform);
return new PlatformBackend(config.platform);
}
+8 -1
View File
@@ -3,5 +3,12 @@
*/
export { getBackend } from "./base.js";
export type { Backend, AddOptions, SearchOptions, ListOptions, DeleteOptions, EntityIds } from "./base.js";
export type {
Backend,
AddOptions,
SearchOptions,
ListOptions,
DeleteOptions,
EntityIds,
} from "./base.js";
export { AuthError, NotFoundError, APIError } from "./base.js";
+401 -294
View File
@@ -3,301 +3,408 @@
*/
import type { PlatformConfig } from "../config.js";
import { captureNotice, isAgentMode } from "../state.js";
import { CLI_VERSION } from "../version.js";
import {
type AddOptions,
APIError,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
APIError,
type AddOptions,
AuthError,
type Backend,
type DeleteOptions,
type EntityIds,
type ListOptions,
NotFoundError,
type SearchOptions,
} from "./base.js";
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
};
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
const fetchOpts: RequestInit = {
method,
headers: this.headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const resp = await fetch(url, fetchOpts);
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
const body = await resp.json();
detail = (body as Record<string, string>).detail ?? resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
}
if (resp.status === 204) {
return {};
}
return resp.json();
}
async add(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
if (opts.enableGraph) payload.enable_graph = true;
return (await this._request("POST", "/v1/memories/", { json: payload })) as Record<
string,
unknown
>;
}
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (opts.extraFilters && ("AND" in opts.extraFilters || "OR" in opts.extraFilters)) {
return opts.extraFilters;
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
async search(query: string, opts: SearchOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request("GET", `/v1/memories/${memoryId}/`)) as Record<string, unknown>;
}
async listMemories(opts: ListOptions = {}): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), gte: opts.after };
}
if (opts.before) {
extra.created_at = { ...(extra.created_at as Record<string, unknown> | undefined), lte: opts.before };
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.enableGraph) payload.enable_graph = true;
const result = (await this._request("POST", "/v2/memories/", { json: payload, params })) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
return (await this._request("PUT", `/v1/memories/${memoryId}/`, {
json: payload,
})) as Record<string, unknown>;
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
return (await this._request("DELETE", "/v1/memories/", { params })) as Record<
string,
unknown
>;
}
if (memoryId) {
return (await this._request("DELETE", `/v1/memories/${memoryId}/`)) as Record<
string,
unknown
>;
}
throw new Error("Either memoryId or --all is required");
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
const params: Record<string, string> = {};
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
if (Object.keys(params).length === 0) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
return (await this._request("DELETE", "/v1/entities/", { params })) as Record<
string,
unknown
>;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
if (opts.userId || opts.agentId) {
const payload: Record<string, unknown> = {};
const statusParams: Record<string, string> = { page: "1", page_size: "1" };
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
});
if (apiFilters) payload.filters = apiFilters;
await this._request("POST", "/v2/memories/", { json: payload, params: statusParams });
} else {
await this._request("GET", "/v1/entities/");
}
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<string, unknown>[];
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
function encodePathSegment(value: unknown): string {
return encodeURIComponent(String(value));
}
export class PlatformBackend implements Backend {
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: PlatformConfig) {
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
this.headers = {
Authorization: `Token ${config.apiKey}`,
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
"X-Mem0-Client-Version": CLI_VERSION,
};
}
private async _request(
method: string,
path: string,
opts?: { json?: unknown; params?: Record<string, string> },
): Promise<unknown> {
let url = `${this.baseUrl}${path}`;
if (opts?.params) {
const qs = new URLSearchParams(opts.params).toString();
url += `?${qs}`;
}
const headers = {
...this.headers,
"X-Mem0-Caller-Type": isAgentMode() ? "agent" : "user",
};
const fetchOpts: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30_000),
};
if (opts?.json) {
fetchOpts.body = JSON.stringify(opts.json);
}
const resp = await fetch(url, fetchOpts);
if (resp.status === 401) {
throw new AuthError();
}
if (resp.status === 404) {
throw new NotFoundError(path);
}
if (resp.status === 400) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail =
((body.detail ?? body.message ?? JSON.stringify(body)) as string) ??
resp.statusText;
} catch {
detail = resp.statusText;
}
throw new APIError(path, detail);
}
if (!resp.ok) {
let detail: string = resp.statusText;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.detail ?? body.message ?? resp.statusText) as string;
} catch {
/* ignore */
}
throw new Error(`HTTP ${resp.status}: ${detail}`);
}
if (resp.status === 204) {
return {};
}
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(
content?: string,
messages?: Record<string, unknown>[],
opts: AddOptions = {},
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (messages) {
payload.messages = messages;
} else if (content) {
payload.messages = [{ role: "user", content }];
}
if (opts.userId) payload.user_id = opts.userId;
if (opts.agentId) payload.agent_id = opts.agentId;
if (opts.appId) payload.app_id = opts.appId;
if (opts.runId) payload.run_id = opts.runId;
if (opts.metadata) payload.metadata = opts.metadata;
if (opts.immutable) payload.immutable = true;
if (opts.infer === false) payload.infer = false;
if (opts.expires) payload.expiration_date = opts.expires;
if (opts.categories) payload.categories = opts.categories;
payload.source = "CLI";
return (await this._request("POST", "/v3/memories/add/", {
json: payload,
})) as Record<string, unknown>;
}
private _buildFilters(opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
extraFilters?: Record<string, unknown>;
}): Record<string, unknown> | undefined {
// If caller passed a pre-built filter structure, use it directly
if (
opts.extraFilters &&
("AND" in opts.extraFilters || "OR" in opts.extraFilters)
) {
return opts.extraFilters;
}
const andConditions: Record<string, unknown>[] = [];
if (opts.userId) andConditions.push({ user_id: opts.userId });
if (opts.agentId) andConditions.push({ agent_id: opts.agentId });
if (opts.appId) andConditions.push({ app_id: opts.appId });
if (opts.runId) andConditions.push({ run_id: opts.runId });
if (opts.extraFilters) {
for (const [k, v] of Object.entries(opts.extraFilters)) {
andConditions.push({ [k]: v });
}
}
if (andConditions.length === 1) return andConditions[0];
if (andConditions.length > 1) return { AND: andConditions };
return undefined;
}
async search(
query: string,
opts: SearchOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {
query,
top_k: opts.topK ?? 10,
threshold: opts.threshold ?? 0.3,
};
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: opts.filters,
});
if (apiFilters) payload.filters = apiFilters;
if (opts.rerank) payload.rerank = true;
if (opts.keyword) payload.keyword_search = true;
if (opts.fields) payload.fields = opts.fields;
payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/search/", {
json: payload,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async get(memoryId: string): Promise<Record<string, unknown>> {
return (await this._request(
"GET",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
}
async listMemories(
opts: ListOptions = {},
): Promise<Record<string, unknown>[]> {
const payload: Record<string, unknown> = {};
const params: Record<string, string> = {
page: String(opts.page ?? 1),
page_size: String(opts.pageSize ?? 100),
};
const extra: Record<string, unknown> = {};
if (opts.category) {
extra.categories = { contains: opts.category };
}
if (opts.after) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
gte: opts.after,
};
}
if (opts.before) {
extra.created_at = {
...(extra.created_at as Record<string, unknown> | undefined),
lte: opts.before,
};
}
const apiFilters = this._buildFilters({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
extraFilters: Object.keys(extra).length > 0 ? extra : undefined,
});
if (apiFilters) payload.filters = apiFilters;
payload.source = "CLI";
const result = (await this._request("POST", "/v3/memories/", {
json: payload,
params,
})) as unknown;
if (Array.isArray(result)) return result;
const obj = result as Record<string, unknown>;
return (obj.results ?? obj.memories ?? []) as Record<string, unknown>[];
}
async update(
memoryId: string,
content?: string,
metadata?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const payload: Record<string, unknown> = {};
if (content) payload.text = content;
if (metadata) payload.metadata = metadata;
payload.source = "CLI";
return (await this._request(
"PUT",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
json: payload,
},
)) as Record<string, unknown>;
}
async delete(
memoryId?: string,
opts: DeleteOptions = {},
): Promise<Record<string, unknown>> {
if (opts.all) {
const params: Record<string, string> = { source: "CLI" };
if (opts.userId) params.user_id = opts.userId;
if (opts.agentId) params.agent_id = opts.agentId;
if (opts.appId) params.app_id = opts.appId;
if (opts.runId) params.run_id = opts.runId;
return (await this._request("DELETE", "/v1/memories/", {
params,
})) as Record<string, unknown>;
}
if (memoryId) {
return (await this._request(
"DELETE",
`/v1/memories/${encodePathSegment(memoryId)}/`,
{
params: { source: "CLI" },
},
)) as Record<string, unknown>;
}
throw new Error("Either memoryId or --all is required");
}
async deleteEntities(opts: EntityIds): Promise<Record<string, unknown>> {
// v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
const typeMap: [string, string | undefined][] = [
["user", opts.userId],
["agent", opts.agentId],
["app", opts.appId],
["run", opts.runId],
];
const entities = typeMap.filter(([, v]) => v) as [string, string][];
if (entities.length === 0) {
throw new Error("At least one entity ID is required for deleteEntities.");
}
// Delete each provided entity via the v2 path-based endpoint. Key each
// response by entity type so a multi-entity delete (e.g. --user-id and
// --agent-id together) doesn't discard everything but the last result.
const results: Record<string, unknown> = {};
for (const [entityType, entityId] of entities) {
results[entityType] = (await this._request(
"DELETE",
`/v2/entities/${encodePathSegment(entityType)}/${encodePathSegment(entityId)}/`,
{ params: { source: "CLI" } },
)) as Record<string, unknown>;
}
return results;
}
async ping(): Promise<Record<string, unknown>> {
return (await this._request("GET", "/v1/ping/")) as Record<string, unknown>;
}
async status(
opts: { userId?: string; agentId?: string } = {},
): Promise<Record<string, unknown>> {
try {
await this.ping();
return { connected: true, backend: "platform", base_url: this.baseUrl };
} catch (e) {
return {
connected: false,
backend: "platform",
error: e instanceof Error ? e.message : String(e),
};
}
}
async entities(entityType: string): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/entities/")) as unknown;
let items: Record<string, unknown>[];
if (Array.isArray(result)) {
items = result;
} else {
items = ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
const typeMap: Record<string, string> = {
users: "user",
agents: "agent",
apps: "app",
runs: "run",
};
const targetType = typeMap[entityType];
if (targetType) {
items = items.filter(
(e) => (e.type as string | undefined)?.toLowerCase() === targetType,
);
}
return items;
}
async listEvents(): Promise<Record<string, unknown>[]> {
const result = (await this._request("GET", "/v1/events/")) as unknown;
if (Array.isArray(result)) return result;
return ((result as Record<string, unknown>).results ?? []) as Record<
string,
unknown
>[];
}
async getEvent(eventId: string): Promise<Record<string, unknown>> {
return (await this._request(
"GET",
`/v1/event/${encodePathSegment(eventId)}/`,
)) as Record<string, unknown>;
}
}
+96 -69
View File
@@ -4,10 +4,8 @@
import chalk from "chalk";
import ora, { type Ora } from "ora";
import { createRequire } from "node:module";
const _require = createRequire(import.meta.url);
const PKG_VERSION: string = _require("../package.json").version;
import { getCurrentCommand, isAgentMode } from "./state.js";
import { CLI_VERSION } from "./version.js";
export const LOGO = `
███╗ ███╗███████╗███╗ ███╗ ██████╗ ██████╗██╗ ██╗
@@ -40,75 +38,96 @@ const dim = chalk.hex(DIM_COLOR);
* plain-text for piped/non-TTY or NO_COLOR environments.
*/
export function sym(fancy: string, plain: string): string {
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
if (!process.stdout.isTTY || process.env.NO_COLOR) return plain;
return fancy;
}
export function printBanner(): void {
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${PKG_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
if (isAgentMode()) return;
const pad = 3; // horizontal padding each side (matches Rich's padding=(0, 2))
const logoLines = LOGO.trimEnd().split("\n");
const tagline = ` ${TAGLINE}`;
const subtitle = `Node.js SDK · v${CLI_VERSION}`;
const contentLines = ["", ...logoLines, "", tagline, ""];
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
// Compute inner width from longest content line + padding both sides
const maxContent = Math.max(...contentLines.map((l) => l.length));
const innerWidth = maxContent + pad * 2;
const totalWidth = innerWidth + 2; // + 2 for │ borders
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`);
const topBorder = brand(`╭${"─".repeat(totalWidth - 2)}╮`);
const subtitleFill = totalWidth - 2 - subtitle.length - 3; // 3 = "─ " before subtitle + "─" after
const bottomBorder = brand(
`╰${"─".repeat(subtitleFill)} ${dim(subtitle)} ${"─"}╯`,
);
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] = `${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
const body = contentLines.map((line) => {
const rightPad = innerWidth - pad - line.length;
return `${brand("│")}${" ".repeat(pad)}${brand.bold(line)}${" ".repeat(Math.max(rightPad, 0))}${brand("│")}`;
});
// Re-color tagline line with accent instead of brand.bold
const taglineIdx = body.length - 2; // second-to-last (before trailing empty line)
const taglineRightPad = innerWidth - pad - tagline.length;
body[taglineIdx] =
`${brand("│")}${" ".repeat(pad)}${accent(tagline)}${" ".repeat(Math.max(taglineRightPad, 0))}${brand("│")}`;
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
console.log(topBorder);
for (const line of body) console.log(line);
console.log(bottomBorder);
}
export function printSuccess(message: string): void {
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
if (isAgentMode()) return;
console.log(`${success(sym("✓", "[ok]"))} ${message}`);
}
export function printError(message: string, hint?: string): void {
console.error(`${error(sym("✗", "[error]") + " Error:")} ${message}`);
if (hint) {
console.error(` ${dim(hint)}`);
}
if (isAgentMode()) {
const envelope = {
status: "error",
command: getCurrentCommand(),
error: message,
data: null,
};
console.log(JSON.stringify(envelope));
return;
}
console.error(`${error(`${sym("✗", "[error]")} Error:`)} ${message}`);
const resolvedHint =
hint ??
(message.includes("Authentication failed")
? `Run ${brand("mem0 init")} to reconfigure your API key · https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node`
: undefined);
if (resolvedHint) {
console.error(` ${dim(resolvedHint)}`);
}
}
export function printWarning(message: string): void {
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
console.error(`${warning(sym("⚠", "[warn]"))} ${message}`);
}
export function printInfo(message: string): void {
console.log(`${brand(sym("◆", "*"))} ${message}`);
if (isAgentMode()) return;
console.error(`${brand(sym("◆", "*"))} ${message}`);
}
export function printScope(ids: Record<string, string | undefined>): void {
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
const label = key.replace(/_/g, " ").replace("id", "ID").trim();
parts.push(`${label}=${val}`);
}
}
if (parts.length > 0) {
console.log(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
if (isAgentMode()) return;
const parts: string[] = [];
for (const [key, val] of Object.entries(ids)) {
if (val) {
parts.push(`${key}=${val}`);
}
}
if (parts.length > 0) {
console.error(` ${dim(`Scope: ${parts.join(", ")}`)}`);
}
}
export interface TimedStatusContext {
successMsg: string;
errorMsg: string;
successMsg: string;
errorMsg: string;
}
/**
@@ -116,29 +135,37 @@ export interface TimedStatusContext {
* Equivalent to Python's timed_status context manager.
*/
export async function timedStatus<T>(
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
message: string,
fn: (ctx: TimedStatusContext) => Promise<T>,
): Promise<T> {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({ text: dim(message), color: "magenta", stream: process.stderr }).start();
const start = performance.now();
if (isAgentMode()) {
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
return fn(ctx);
}
const ctx: TimedStatusContext = { successMsg: "", errorMsg: "" };
const spinner = ora({
text: dim(message),
color: "yellow",
stream: process.stderr,
}).start();
const start = performance.now();
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
console.error(`${error("✗ Error:")} ${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
try {
const result = await fn(ctx);
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.successMsg) {
console.error(`${success("✓")} ${ctx.successMsg} (${elapsed}s)`);
}
return result;
} catch (err) {
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
spinner.stop();
if (ctx.errorMsg) {
printError(`${ctx.errorMsg} (${elapsed}s)`);
}
throw err;
}
}
/** Format helpers using brand colors for external use. */
+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)}`);
});
}
+84 -65
View File
@@ -3,88 +3,107 @@
*/
import Table from "cli-table3";
import { printError, printSuccess, colors } from "../branding.js";
import { colors, printError, printSuccess } from "../branding.js";
import {
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
getNestedValue,
loadConfig,
redactKey,
saveConfig,
setNestedValue,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { isAgentMode, setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
export function cmdConfigShow(opts: { output?: string } = {}): void {
const config = loadConfig();
setCurrentCommand("config show");
const config = loadConfig();
if (opts.output === "json") {
formatJsonEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "config show",
data: {
defaults: {
user_id: config.defaults.userId || null,
agent_id: config.defaults.agentId || null,
app_id: config.defaults.appId || null,
run_id: config.defaults.runId || null,
},
platform: {
api_key: redactKey(config.platform.apiKey),
base_url: config.platform.baseUrl,
},
},
});
return;
}
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
console.log();
console.log(` ${brand("◆ mem0 Configuration")}\n`);
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Key"), accent("Value")],
style: { head: [], border: [] },
});
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push(["defaults.agent_id", config.defaults.agentId || dim("(not set)")]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["defaults.enable_graph", String(config.defaults.enableGraph)]);
table.push(["", ""]);
// Defaults
table.push(["defaults.user_id", config.defaults.userId || dim("(not set)")]);
table.push([
"defaults.agent_id",
config.defaults.agentId || dim("(not set)"),
]);
table.push(["defaults.app_id", config.defaults.appId || dim("(not set)")]);
table.push(["defaults.run_id", config.defaults.runId || dim("(not set)")]);
table.push(["", ""]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
// Platform
table.push(["platform.api_key", redactKey(config.platform.apiKey)]);
table.push(["platform.base_url", config.platform.baseUrl]);
console.log(table.toString());
console.log();
console.log(table.toString());
console.log();
}
export function cmdConfigGet(key: string): void {
const config = loadConfig();
const value = getNestedValue(config, key);
setCurrentCommand("config get");
const config = loadConfig();
const value = getNestedValue(config, key);
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
if (key.includes("api_key") || key.split(".").pop() === "key") {
console.log(redactKey(String(value)));
} else {
console.log(String(value));
}
}
if (value === undefined) {
printError(`Unknown config key: ${key}`);
} else {
// Redact secrets
const displayValue =
key.includes("api_key") || key.split(".").pop() === "key"
? redactKey(String(value))
: String(value);
if (isAgentMode()) {
formatAgentEnvelope({
command: "config get",
data: { key, value: displayValue },
});
} else {
console.log(displayValue);
}
}
}
export function cmdConfigSet(key: string, value: string): void {
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
printSuccess(`${key} = ${display}`);
} else {
printError(`Unknown config key: ${key}`);
}
setCurrentCommand("config set");
const config = loadConfig();
if (setNestedValue(config, key, value)) {
saveConfig(config);
const display = key.includes("key") ? redactKey(value) : value;
if (isAgentMode()) {
formatAgentEnvelope({
command: "config set",
data: { key, value: display },
});
} else {
printSuccess(`${key} = ${display}`);
}
} else {
printError(`Unknown config key: ${key}`);
}
}
+138 -109
View File
@@ -4,136 +4,165 @@
import readline from "node:readline";
import Table from "cli-table3";
import { printError, printInfo, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJson } from "../output.js";
import {
colors,
printError,
printInfo,
printSuccess,
timedStatus,
} from "../branding.js";
import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, dim } = colors;
const VALID_TYPES = new Set(["users", "agents", "apps", "runs"]);
export async function cmdEntitiesList(
backend: Backend,
entityType: string,
opts: { output: string },
backend: Backend,
entityType: string,
opts: { output: string },
): Promise<void> {
if (!VALID_TYPES.has(entityType)) {
printError(`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`);
process.exit(1);
}
setCurrentCommand("entity list");
if (!VALID_TYPES.has(entityType)) {
printError(
`Invalid entity type: ${entityType}. Use: ${[...VALID_TYPES].join(", ")}`,
);
process.exit(1);
}
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus(`Fetching ${entityType}...`, async () => {
return backend.entities(entityType);
});
} catch (e) {
printError(
e instanceof Error ? e.message : String(e),
"This feature may require the mem0 Platform.",
);
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(results);
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "entity list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
if (!results.length) {
printInfo(`No ${entityType} found.`);
return;
}
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
const table = new Table({
head: [accent("Name / ID"), accent("Created")],
style: { head: [], border: [] },
});
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
for (const entity of results) {
const name = String(entity.name ?? entity.id ?? "—");
const created = String(entity.created_at ?? "—").slice(0, 10);
table.push([name, created]);
}
console.log();
console.log(table.toString());
console.log(` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`);
console.log();
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} ${entityType} (${elapsed.toFixed(2)}s)`)}`,
);
console.log();
}
export async function cmdEntitiesDelete(
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
backend: Backend,
opts: {
userId?: string;
agentId?: string;
appId?: string;
runId?: string;
dryRun?: boolean;
force: boolean;
output: string;
},
): Promise<void> {
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError("Provide at least one of --user-id, --agent-id, --app-id, --run-id.");
process.exit(1);
}
setCurrentCommand("entity delete");
const { isAgentMode } = await import("../state.js");
if (isAgentMode() && !opts.force) {
printError("Destructive operation requires --force in agent mode.");
process.exit(1);
}
if (!opts.userId && !opts.agentId && !opts.appId && !opts.runId) {
printError(
"Provide at least one of --user-id, --agent-id, --app-id, --run-id.",
);
process.exit(1);
}
if (opts.dryRun) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
printInfo(`Would delete entity ${scopeParts.join(", ")} and all its memories.`);
printInfo("No changes made.");
return;
}
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
if (!opts.force) {
const scopeParts: string[] = [];
if (opts.userId) scopeParts.push(`user=${opts.userId}`);
if (opts.agentId) scopeParts.push(`agent=${opts.agentId}`);
if (opts.appId) scopeParts.push(`app=${opts.appId}`);
if (opts.runId) scopeParts.push(`run=${opts.runId}`);
const scope = scopeParts.join(", ");
if (opts.dryRun) {
printInfo(`Would delete entity ${scope} and all its memories.`);
printInfo("No changes made.");
return;
}
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
if (!opts.force) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`\n \u26a0 Delete entity ${scope} AND all its memories? This cannot be undone. [y/N] `,
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled.");
process.exit(0);
}
}
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Deleting entity...", async () => {
return backend.deleteEntities({
userId: opts.userId,
agentId: opts.agentId,
appId: opts.appId,
runId: opts.runId,
});
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
if (opts.output === "agent") {
formatAgentEnvelope({
command: "entity delete",
data: { deleted: true },
durationMs: Math.round(elapsed * 1000),
});
} else if (opts.output === "json") {
formatJson(result);
} else if (opts.output !== "quiet") {
printSuccess(`Entity deleted with all memories (${elapsed.toFixed(2)}s)`);
}
}
+169
View File
@@ -0,0 +1,169 @@
/**
* Event commands: list and status.
*/
import boxen from "boxen";
import Table from "cli-table3";
import type { Backend } from "../backend/base.js";
import { colors, printError, printInfo, timedStatus } from "../branding.js";
import { formatAgentEnvelope, formatJson } from "../output.js";
import { setCurrentCommand } from "../state.js";
const { brand, accent, success, error: errorColor, warning, dim } = colors;
function statusStyled(status: string): string {
switch (status.toUpperCase()) {
case "SUCCEEDED":
return success("SUCCEEDED");
case "PENDING":
return accent("PENDING");
case "FAILED":
return errorColor("FAILED");
case "PROCESSING":
return warning("PROCESSING");
default:
return status;
}
}
export async function cmdEventList(
backend: Backend,
opts: { output: string },
): Promise<void> {
setCurrentCommand("event list");
const start = performance.now();
let results: Record<string, unknown>[];
try {
results = await timedStatus("Fetching events...", async () => {
return backend.listEvents();
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "event list",
data: results,
count: results.length,
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (results.length === 0) {
console.log();
printInfo("No events found.");
console.log();
return;
}
const table = new Table({
head: [
accent("Event ID"),
accent("Type"),
accent("Status"),
accent("Latency"),
accent("Created"),
],
colWidths: [12, 14, 14, 10, 22],
wordWrap: true,
style: { head: [], border: [] },
});
for (const ev of results) {
const evId = String(ev.id ?? "").slice(0, 8);
const evType = String(ev.event_type ?? "—");
const status = String(ev.status ?? "—");
const latency = ev.latency as number | undefined;
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
table.push([dim(evId), evType, statusStyled(status), latencyStr, created]);
}
console.log();
console.log(table.toString());
console.log(
` ${dim(`${results.length} event${results.length !== 1 ? "s" : ""}`)}`,
);
console.log();
}
export async function cmdEventStatus(
backend: Backend,
eventId: string,
opts: { output: string },
): Promise<void> {
setCurrentCommand("event status");
const start = performance.now();
let ev: Record<string, unknown>;
try {
ev = await timedStatus("Fetching event...", async () => {
return backend.getEvent(eventId);
});
} catch (e) {
printError(e instanceof Error ? e.message : String(e));
process.exit(1);
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "event status",
data: ev,
durationMs: Math.round(elapsed * 1000),
});
return;
}
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency = ev.latency as number | undefined;
const latencyStr = latency !== undefined ? `${Math.round(latency)}ms` : "—";
const created = String(ev.created_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const updated = String(ev.updated_at ?? "—")
.slice(0, 19)
.replace("T", " ");
const results = ev.results as Record<string, unknown>[] | undefined;
const lines: string[] = [];
lines.push(` ${dim("Event ID:")} ${eventId}`);
lines.push(` ${dim("Type:")} ${evType}`);
lines.push(` ${dim("Status:")} ${statusStyled(status)}`);
lines.push(` ${dim("Latency:")} ${latencyStr}`);
lines.push(` ${dim("Created:")} ${created}`);
lines.push(` ${dim("Updated:")} ${updated}`);
if (results && results.length > 0) {
lines.push("");
lines.push(` ${dim(`Results (${results.length}):`)}`);
for (const r of results) {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ? String(data.memory) : "";
const evName = String(r.event ?? "");
const user = String(r.user_id ?? "");
let detail = `${evName} ${memory}`;
if (user) detail += ` ${dim(`(user_id=${user})`)}`;
lines.push(` ${success("·")} ${detail} ${dim(`(${memId})`)}`);
}
}
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Event Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
+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}.`);
}
+573 -143
View File
@@ -2,181 +2,611 @@
* mem0 init — interactive setup wizard.
*/
import fs from "node:fs";
import readline from "node:readline";
import {
printBanner,
printError,
printInfo,
printSuccess,
colors,
} from "../branding.js";
import { type Mem0Config, createDefaultConfig, saveConfig } from "../config.js";
import { PlatformBackend } from "../backend/platform.js";
import {
colors,
printBanner,
printError,
printInfo,
printSuccess,
} from "../branding.js";
import {
CONFIG_FILE,
DEFAULT_BASE_URL,
type Mem0Config,
createDefaultConfig,
loadConfig,
redactKey,
saveConfig,
} from "../config.js";
import { formatJsonEnvelope } from "../output.js";
import { isAgentMode } from "../state.js";
const { brand, dim } = colors;
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
function validateEmail(email: string): void {
if (!EMAIL_RE.test(email)) {
printError(`Invalid email address: ${JSON.stringify(email)}`);
process.exit(1);
}
}
/** @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,
baseUrl: string,
): Promise<Record<string, unknown>> {
const url = baseUrl.replace(/\/+$/, "");
let codeValue = code;
const sourceHeaders = {
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "node",
};
if (!codeValue) {
const resp = await fetch(`${url}/api/v1/auth/email_code/`, {
method: "POST",
headers: sourceHeaders,
body: JSON.stringify({ email }),
signal: AbortSignal.timeout(30_000),
});
if (resp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!resp.ok) {
let detail: string;
try {
const body = (await resp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? resp.statusText) as string;
} catch {
detail = resp.statusText;
}
printError(`Failed to send code: ${detail}`);
process.exit(1);
}
printSuccess("Verification code sent! Check your email.");
if (!process.stdin.isTTY) {
printError(
"No --code provided and terminal is non-interactive.",
"Run: mem0 init --email <email> --code <code>",
);
process.exit(1);
}
console.log();
const entered = await promptLine(` ${brand("Verification Code")}`);
if (!entered) {
printError("Code is required.");
process.exit(1);
}
codeValue = entered;
}
const verifyResp = await fetch(`${url}/api/v1/auth/email_code/verify/`, {
method: "POST",
headers: sourceHeaders,
body: JSON.stringify({ email, code: codeValue.trim() }),
signal: AbortSignal.timeout(30_000),
});
if (verifyResp.status === 429) {
printError("Too many attempts. Try again in a few minutes.");
process.exit(1);
}
if (!verifyResp.ok) {
let detail: string;
try {
const body = (await verifyResp.json()) as Record<string, unknown>;
detail = (body.error ?? body.detail ?? verifyResp.statusText) as string;
} catch {
detail = verifyResp.statusText;
}
printError(`Verification failed: ${detail}`);
process.exit(1);
}
return verifyResp.json() as Promise<Record<string, unknown>>;
}
function promptSecret(label: string): Promise<string> {
return new Promise((resolve, reject) => {
process.stdout.write(label);
return new Promise((resolve, reject) => {
process.stdout.write(label);
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.resume();
process.stdin.setEncoding("utf-8");
const chars: string[] = [];
const chars: string[] = [];
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const onData = (key: string) => {
for (const ch of key) {
if (ch === "\r" || ch === "\n") {
cleanup();
process.stdout.write("\n");
resolve(chars.join(""));
return;
}
if (ch === "\x03") {
cleanup();
reject(new Error("Interrupted"));
return;
}
if (ch === "\x7f" || ch === "\x08") {
// backspace
if (chars.length > 0) {
chars.pop();
process.stdout.write("\b \b");
}
} else if (ch === "\x15") {
// Ctrl+U — clear line
process.stdout.write("\b \b".repeat(chars.length));
chars.length = 0;
} else if (ch >= " ") {
chars.push(ch);
process.stdout.write("*");
}
}
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
const cleanup = () => {
process.stdin.removeListener("data", onData);
if (process.stdin.isTTY) {
process.stdin.setRawMode(false);
}
process.stdin.pause();
};
process.stdin.on("data", onData);
});
process.stdin.on("data", onData);
});
}
function promptLine(label: string, defaultValue?: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const prompt = defaultValue ? `${label} [${defaultValue}]: ` : `${label}: `;
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim() || defaultValue || "");
});
});
}
async function setupPlatform(config: Mem0Config): Promise<void> {
console.log();
console.log(` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys")}`);
console.log();
console.log();
console.log(
` ${dim("Get your API key at https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node")}`,
);
console.log();
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
process.stdout.write(` ${brand("API Key")}: `);
const apiKey = await promptSecret("");
if (!apiKey) {
printError("API key is required.");
process.exit(1);
}
config.platform.apiKey = apiKey;
config.platform.createdVia = "api_key";
}
async function setupDefaults(config: Mem0Config): Promise<void> {
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
console.log();
printInfo("Set default entity IDs (press Enter to skip).\n");
const userId = await promptLine(` ${brand("Default User ID")} ${dim("(recommended)")}`, "mem0-cli");
if (userId) config.defaults.userId = userId;
const _systemUser = process.env.USER || process.env.USERNAME || "mem0-cli";
const userId = await promptLine(
` ${brand("Default User ID")} ${dim("(recommended)")}`,
_systemUser,
);
if (userId) config.defaults.userId = userId;
}
async function validatePlatform(config: Mem0Config): Promise<void> {
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Check your API key and try again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
console.log();
printInfo("Validating connection...");
try {
const backend = new PlatformBackend(config.platform);
const status = await backend.status({
userId: config.defaults.userId || undefined,
agentId: config.defaults.agentId || undefined,
});
if (status.connected) {
printSuccess("Connected to mem0 Platform!");
// Cache user_email from ping response for telemetry distinct_id
try {
const pingData = (await backend.ping()) as Record<string, unknown>;
const userEmail = pingData?.user_email as string | undefined;
if (userEmail) {
config.platform.userEmail = userEmail;
}
} catch {
/* ignore — telemetry ID will fall back to API key hash */
}
} else {
printError(
`Could not connect: ${status.error ?? "Unknown error"}`,
"Visit https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node to get a new key, or run mem0 init again.",
);
}
} catch (e) {
printError(`Connection test failed: ${e instanceof Error ? e.message : e}`);
}
}
export async function runInit(opts: { apiKey?: string; userId?: string } = {}): Promise<void> {
const config = createDefaultConfig();
export async function runInit(
opts: {
apiKey?: string;
userId?: string;
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");
// Non-interactive: both flags provided
if (opts.apiKey && opts.userId) {
config.platform.apiKey = opts.apiKey;
config.defaults.userId = opts.userId;
await validatePlatform(config);
saveConfig(config);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
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);
};
// Non-TTY without full flags: error with usage hint
if (!process.stdin.isTTY && (!opts.apiKey || !opts.userId)) {
printError(
"Non-interactive terminal detected and missing required flags.",
"Usage: mem0 init --api-key <key> --user-id <id>",
);
process.exit(1);
}
const config = createDefaultConfig();
const savedConfig = loadConfig();
const baseUrl =
process.env.MEM0_BASE_URL ||
savedConfig.platform.baseUrl ||
DEFAULT_BASE_URL;
config.platform.baseUrl = baseUrl;
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Guards
if (opts.code && !opts.email) {
printError("--code requires --email.");
process.exit(1);
}
if (opts.email && opts.apiKey) {
printError("Cannot use both --api-key and --email.");
process.exit(1);
}
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
await setupPlatform(config);
}
// ── 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;
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
// ── 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;
}
await validatePlatform(config);
// Warn if an existing config with an API key would be overwritten
if (
!opts.force &&
fs.existsSync(CONFIG_FILE) &&
savedConfig.platform.apiKey
) {
console.log(
`\n ${brand("Existing configuration found")} ${dim(`(API key: ${redactKey(savedConfig.platform.apiKey)})`)}`,
);
if (process.stdin.isTTY) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
" Overwrite existing config? This cannot be undone. [y/N] ",
resolve,
);
});
rl.close();
if (answer.toLowerCase() !== "y") {
printInfo("Cancelled. Use --force to skip this check.");
process.exit(0);
}
} else {
printError(
"Existing config would be overwritten.",
"Use --force to overwrite.",
);
process.exit(1);
}
}
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
// ── Email login flow ──────────────────────────────────────────────────────
if (opts.email) {
const email = opts.email.trim().toLowerCase();
validateEmail(email);
printBanner();
console.log();
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, opts.code, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
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";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// ── 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>, --email <addr>, or --agent for unattended Agent Mode bootstrap.",
);
process.exit(1);
}
opts.userId =
opts.userId || process.env.USER || process.env.USERNAME || "mem0-cli";
}
// 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);
printSuccess("Configuration saved to ~/.mem0/config.json");
return;
}
printBanner();
console.log();
printInfo("Welcome! Let's set up your mem0 CLI.\n");
// Use provided API key or prompt
if (opts.apiKey) {
config.platform.apiKey = opts.apiKey;
} else {
console.log(` ${brand("How would you like to authenticate?")}`);
console.log(` ${dim("1.")} Login with email ${dim("(recommended)")}`);
console.log(` ${dim("2.")} Enter API key manually`);
console.log();
const choice = await promptLine(` ${brand("Choose")} [1/2]`, "1");
if (choice === "1") {
console.log();
const emailAddr = await promptLine(` ${brand("Email")}`);
if (!emailAddr) {
printError("Email is required.");
process.exit(1);
}
const email = emailAddr.trim().toLowerCase();
validateEmail(email);
printInfo(`Logging in as ${email}...\n`);
const result = await emailLogin(email, undefined, baseUrl);
const apiKeyVal = result.api_key as string | undefined;
if (!apiKeyVal) {
printError(
"Auth succeeded but no API key was returned. Contact support.",
);
process.exit(1);
}
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";
saveConfig(config);
console.log();
printSuccess("Authenticated! Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
console.log();
return;
}
// choice === "2": fall through to API key prompt
await setupPlatform(config);
}
// Use provided user ID or prompt
if (opts.userId) {
config.defaults.userId = opts.userId;
} else {
await setupDefaults(config);
}
await validatePlatform(config);
saveConfig(config);
console.log();
printSuccess("Configuration saved to ~/.mem0/config.json");
console.log();
console.log(` ${dim("Get started:")}`);
if (config.defaults.userId) {
console.log(` ${dim(' mem0 add "I prefer dark mode"')}`);
console.log(` ${dim(' mem0 search "preferences"')}`);
} else {
console.log(` ${dim(' mem0 add "I prefer dark mode" --user-id alice')}`);
console.log(` ${dim(' mem0 search "preferences" --user-id alice')}`);
}
console.log();
}
File diff suppressed because it is too large Load Diff
+125 -109
View File
@@ -3,137 +3,153 @@
*/
import fs from "node:fs";
import { createRequire } from "node:module";
import { printError, printSuccess, timedStatus, colors } from "../branding.js";
import type { Backend } from "../backend/base.js";
import { formatJsonEnvelope } from "../output.js";
import boxen from "boxen";
import type { Backend } from "../backend/base.js";
import { colors, printError, printSuccess, timedStatus } from "../branding.js";
import { formatAgentEnvelope, formatJsonEnvelope } from "../output.js";
import { setCurrentCommand } from "../state.js";
import { CLI_VERSION } from "../version.js";
const { brand, dim, success, error: errorColor } = colors;
const _require = createRequire(import.meta.url);
const VERSION: string = _require("../../package.json").version;
export async function cmdStatus(
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
backend: Backend,
opts: { userId?: string; agentId?: string; output?: string } = {},
): Promise<void> {
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = { connected: false, error: e instanceof Error ? e.message : String(e) };
}
const elapsed = (performance.now() - start) / 1000;
setCurrentCommand("status");
const start = performance.now();
let result: Record<string, unknown>;
try {
result = await timedStatus("Checking connection...", async () => {
return backend.status({ userId: opts.userId, agentId: opts.agentId });
});
} catch (e) {
result = {
connected: false,
error: e instanceof Error ? e.message : String(e),
};
}
const elapsed = (performance.now() - start) / 1000;
if (opts.output === "json") {
formatJsonEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
latency_ms: Math.round(elapsed * 1000),
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "status",
data: {
connected: result.connected,
backend: result.backend ?? null,
base_url: result.base_url ?? null,
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
const lines: string[] = [];
if (result.connected) {
lines.push(` ${success("\u25cf")} Connected`);
} else {
lines.push(` ${errorColor("\u25cf")} Disconnected`);
}
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
lines.push(` ${dim("Backend:")} ${result.backend ?? "?"}`);
if (result.base_url) {
lines.push(` ${dim("API URL:")} ${result.base_url}`);
}
if (result.error) {
lines.push(` ${errorColor("Error:")} ${result.error}`);
if (String(result.error).includes("Authentication failed")) {
lines.push("");
lines.push(
` ${dim("Run")} ${brand("mem0 init")} ${dim("to reconfigure your API key")}`,
);
lines.push(
` ${dim("Get a key at")} ${brand("https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-node")}`,
);
}
}
lines.push(` ${dim("Latency:")} ${elapsed.toFixed(2)}s`);
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Connection Status"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function cmdVersion(): void {
console.log(` ${brand("◆ Mem0")} CLI v${VERSION}`);
console.log(` ${brand("◆ Mem0")} CLI v${CLI_VERSION}`);
}
export async function cmdImport(
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
backend: Backend,
filePath: string,
opts: { userId?: string; agentId?: string; output?: string },
): Promise<void> {
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
setCurrentCommand("import");
let data: Record<string, unknown>[];
try {
const raw = fs.readFileSync(filePath, "utf-8");
const parsed = JSON.parse(raw);
data = Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
printError(`Failed to read file: ${e instanceof Error ? e.message : e}`);
process.exit(1);
}
let added = 0;
let failed = 0;
const start = performance.now();
let added = 0;
let failed = 0;
const start = performance.now();
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
for (let i = 0; i < data.length; i++) {
const item = data[i];
const content = (item.memory ?? item.text ?? item.content ?? "") as string;
if (!content) {
failed++;
continue;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
try {
await backend.add(content, undefined, {
userId: opts.userId ?? (item.user_id as string | undefined),
agentId: opts.agentId ?? (item.agent_id as string | undefined),
metadata: item.metadata as Record<string, unknown> | undefined,
});
added++;
} catch {
failed++;
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`);
}
}
// Simple progress indicator
if ((i + 1) % 10 === 0 || i === data.length - 1) {
process.stdout.write(
`\r ${dim(`Importing memories... ${i + 1}/${data.length}`)}`,
);
}
}
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
const elapsed = (performance.now() - start) / 1000;
console.log(); // Clear progress line
if (opts.output === "json") {
formatJsonEnvelope({
command: "import",
data: { added, failed, duration_s: parseFloat(elapsed.toFixed(2)) },
durationMs: Math.round(elapsed * 1000),
});
return;
}
if (opts.output === "agent" || opts.output === "json") {
formatAgentEnvelope({
command: "import",
data: {
added,
failed,
},
durationMs: Math.round(elapsed * 1000),
});
return;
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
printSuccess(`Imported ${added} memories (${elapsed.toFixed(2)}s)`);
if (failed > 0) {
printError(`${failed} memories failed to import.`);
}
}
+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");
}
+173 -100
View File
@@ -18,142 +18,215 @@ export const DEFAULT_BASE_URL = "https://api.mem0.ai";
export const CONFIG_VERSION = 1;
export interface PlatformConfig {
apiKey: string;
baseUrl: string;
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 {
userId: string;
agentId: string;
appId: string;
runId: string;
enableGraph: boolean;
userId: string;
agentId: string;
appId: string;
runId: string;
}
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;
version: number;
defaults: DefaultsConfig;
platform: PlatformConfig;
telemetry: TelemetryConfig;
agentRush: AgentRushConfig;
}
export function createDefaultConfig(): Mem0Config {
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
enableGraph: false,
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
},
};
return {
version: CONFIG_VERSION,
defaults: {
userId: "",
agentId: "",
appId: "",
runId: "",
},
platform: {
apiKey: "",
baseUrl: DEFAULT_BASE_URL,
userEmail: "",
agentMode: false,
createdVia: "",
agentCaller: "",
claimedAt: "",
defaultUserId: "",
},
telemetry: {
anonymousId: "",
},
agentRush: {
acknowledgedAt: "",
},
};
}
export function ensureConfigDir(): string {
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
return CONFIG_DIR;
}
export function loadConfig(): Mem0Config {
const config = createDefaultConfig();
const config = createDefaultConfig();
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
const data = JSON.parse(raw);
config.version = data.version ?? CONFIG_VERSION;
config.version = data.version ?? CONFIG_VERSION;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
const plat = data.platform ?? {};
config.platform.apiKey = plat.api_key ?? "";
config.platform.baseUrl = plat.base_url ?? DEFAULT_BASE_URL;
config.platform.userEmail = plat.user_email ?? "";
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 ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
config.defaults.runId = defaults.run_id ?? "";
config.defaults.enableGraph = defaults.enable_graph ?? false;
}
const defaults = data.defaults ?? {};
config.defaults.userId = defaults.user_id ?? "";
config.defaults.agentId = defaults.agent_id ?? "";
config.defaults.appId = defaults.app_id ?? "";
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
if (process.env.MEM0_API_KEY) config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL) config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID) config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID) config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
if (process.env.MEM0_ENABLE_GRAPH) {
config.defaults.enableGraph = ["true", "1", "yes"].includes(
process.env.MEM0_ENABLE_GRAPH.toLowerCase(),
);
}
return config;
// Environment variable overrides
if (process.env.MEM0_API_KEY)
config.platform.apiKey = process.env.MEM0_API_KEY;
if (process.env.MEM0_BASE_URL)
config.platform.baseUrl = process.env.MEM0_BASE_URL;
if (process.env.MEM0_USER_ID)
config.defaults.userId = process.env.MEM0_USER_ID;
if (process.env.MEM0_AGENT_ID)
config.defaults.agentId = process.env.MEM0_AGENT_ID;
if (process.env.MEM0_APP_ID) config.defaults.appId = process.env.MEM0_APP_ID;
if (process.env.MEM0_RUN_ID) config.defaults.runId = process.env.MEM0_RUN_ID;
return config;
}
export function saveConfig(config: Mem0Config): void {
ensureConfigDir();
ensureConfigDir();
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
enable_graph: config.defaults.enableGraph,
},
platform: {
api_key: config.platform.apiKey,
base_url: config.platform.baseUrl,
},
};
const data = {
version: config.version,
defaults: {
user_id: config.defaults.userId,
agent_id: config.defaults.agentId,
app_id: config.defaults.appId,
run_id: config.defaults.runId,
},
platform: {
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);
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 {
if (!key) return "(not set)";
if (key.length <= 8) return key.slice(0, 2) + "***";
return key.slice(0, 4) + "..." + key.slice(-4);
if (!key) return "(not set)";
if (key.length <= 8) return `${key.slice(0, 2)}***`;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}
/** Key map from dotted config path to the config object fields. */
const KEY_MAP: Record<string, [keyof Mem0Config, string]> = {
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"],
"defaults.enable_graph": ["defaults", "enableGraph"],
"platform.api_key": ["platform", "apiKey"],
"platform.base_url": ["platform", "baseUrl"],
"platform.user_email": ["platform", "userEmail"],
"defaults.user_id": ["defaults", "userId"],
"defaults.agent_id": ["defaults", "agentId"],
"defaults.app_id": ["defaults", "appId"],
"defaults.run_id": ["defaults", "runId"],
// Short-form aliases
api_key: ["platform", "apiKey"],
base_url: ["platform", "baseUrl"],
user_email: ["platform", "userEmail"],
user_id: ["defaults", "userId"],
agent_id: ["defaults", "agentId"],
app_id: ["defaults", "appId"],
run_id: ["defaults", "runId"],
};
export function getNestedValue(config: Mem0Config, dottedKey: string): unknown {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
const mapping = KEY_MAP[dottedKey];
if (!mapping) return undefined;
const [section, field] = mapping;
return (config[section] as unknown as Record<string, unknown>)[field];
}
export function setNestedValue(config: Mem0Config, dottedKey: string, value: string): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
export function setNestedValue(
config: Mem0Config,
dottedKey: string,
value: string,
): boolean {
const mapping = KEY_MAP[dottedKey];
if (!mapping) return false;
const [section, field] = mapping;
const obj = config[section] as unknown as Record<string, unknown>;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
const current = obj[field];
if (typeof current === "boolean") {
obj[field] = ["true", "1", "yes"].includes(value.toLowerCase());
} else if (typeof current === "number") {
obj[field] = Number.parseInt(value, 10);
} else {
obj[field] = value;
}
return true;
}
+2
View File
@@ -0,0 +1,2 @@
/** Injected by tsup at build time from package.json version field. Undefined in dev/test. */
declare const __CLI_VERSION__: string | undefined;
+284 -280
View File
@@ -4,40 +4,40 @@
*/
import chalk from "chalk";
import type { Command, Help, Option, Argument } from "commander";
import type { Argument, Command, Help, Option } from "commander";
// Colors imported from chalk directly to match Typer/Rich defaults
// ── Colors (matching Typer/Rich defaults) ────────────────────────────────
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
const cyanBold = chalk.cyan.bold; // option flags, command names
const greenBold = chalk.green.bold; // switch flags (boolean --force etc)
const yellowBold = chalk.yellow.bold; // metavar <value>
const yellow = chalk.yellow; // "Usage:" label
const bold = chalk.bold; // command name in usage
const dim = chalk.dim; // defaults, descriptions
const dimBorder = chalk.dim; // panel borders
// ── Strip ANSI ───────────────────────────────────────────────────────────
// eslint-disable-next-line no-control-regex
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape sequence is intentional
const ANSI_RE = /\x1b\[[0-9;]*m/g;
function stripAnsi(str: string): number {
return str.replace(ANSI_RE, "").length;
return str.replace(ANSI_RE, "").length;
}
// ── Command display order (matches Python CLI) ──────────────────────────
/** Commands grouped into panels, matching Python CLI's rich_help_panel. */
const COMMAND_GROUPS: { panel: string; commands: string[] }[] = [
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "config"],
},
{
panel: "Memory",
commands: ["add", "search", "get", "list", "update", "delete"],
},
{
panel: "Management",
commands: ["init", "status", "import", "help", "entity", "event", "config"],
},
];
/** Flat order derived from COMMAND_GROUPS. */
@@ -46,88 +46,88 @@ const COMMAND_ORDER: string[] = COMMAND_GROUPS.flatMap((g) => g.commands);
// ── Option-to-panel mapping (derived from Python's rich_help_panel) ─────
const OPTION_PANELS: Record<string, Record<string, string>> = {
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
add: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
search: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--top-k": "Search",
"--threshold": "Search",
"--rerank": "Search",
"--keyword": "Search",
"--filter": "Search",
"--fields": "Search",
"--graph": "Search",
"--no-graph": "Search",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
get: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
list: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--page": "Pagination",
"--page-size": "Pagination",
"--category": "Filters",
"--after": "Filters",
"--before": "Filters",
"--graph": "Filters",
"--no-graph": "Filters",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
update: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
delete: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--app-id": "Scope",
"--run-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
status: {
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
import: {
"--user-id": "Scope",
"--agent-id": "Scope",
"--output": "Output",
"--api-key": "Connection",
"--base-url": "Connection",
},
};
const PANEL_ORDER: string[] = [
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
"Scope",
"Search",
"Pagination",
"Filters",
"Output",
"Connection",
];
// ── Panel rendering ─────────────────────────────────────────────────────
@@ -141,234 +141,238 @@ const PANEL_ORDER: string[] = [
* ╰────────────────────────────────╯
* ```
*/
function renderPanel(
title: string,
rows: string[],
width: number,
): string {
if (rows.length === 0) return "";
function renderPanel(title: string, rows: string[], width: number): string {
if (rows.length === 0) return "";
// Inner width is total width minus the two border chars
const inner = width - 2;
// Inner width is total width minus the two border chars
const inner = width - 2;
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Top border: ╭─ Title ─...─╮
const titleStr = ` ${title} `;
const fillLen = Math.max(0, inner - 1 - titleStr.length);
const topLine =
dimBorder("╭─") +
dimBorder(titleStr) +
dimBorder("─".repeat(fillLen)) +
dimBorder("╮");
// Bottom border: ╰─...─╯
const bottomLine = dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Bottom border: ╰─...─╯
const bottomLine =
dimBorder("╰") + dimBorder("─".repeat(inner)) + dimBorder("╯");
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return dimBorder("│") + " " + row + " ".repeat(pad) + dimBorder("│");
});
// Content rows
const contentLines = rows.map((row) => {
const visLen = stripAnsi(row);
const pad = Math.max(0, inner - 1 - visLen);
return `${dimBorder("│")} ${row}${" ".repeat(pad)}${dimBorder("│")}`;
});
return [topLine, ...contentLines, bottomLine].join("\n");
return [topLine, ...contentLines, bottomLine].join("\n");
}
// ── Format an option term (short + long) ────────────────────────────────
function formatOptionTerm(opt: Option): string {
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
const parts: string[] = [];
if (opt.short) parts.push(opt.short);
if (opt.long) parts.push(opt.long);
let term = parts.join(", ");
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += " " + match[0];
}
}
return term;
// Append value placeholder for non-boolean options
if (opt.flags) {
const match = opt.flags.match(/<[^>]+>|\[[^\]]+\]/);
if (match) {
term += ` ${match[0]}`;
}
}
return term;
}
// ── Get the long flag name for panel lookup ─────────────────────────────
function getLongFlag(opt: Option): string {
if (opt.long) return opt.long;
return opt.short || "";
if (opt.long) return opt.long;
return opt.short || "";
}
// ── Format a default value ──────────────────────────────────────────────
function formatDefault(opt: Option): string {
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
if (opt.defaultValue !== undefined && opt.defaultValue !== false) {
return dim(` [default: ${opt.defaultValue}]`);
}
return "";
}
// ── The main help formatter ─────────────────────────────────────────────
export function richFormatHelp(cmd: Command, helper: Help): string {
const width = process.stdout.columns || 80;
const lines: string[] = [];
const width = process.stdout.columns || 80;
const lines: string[] = [];
const isRoot = !cmd.parent;
const isRoot = !cmd.parent;
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`);
}
lines.push("");
// ── Usage line ──
const usage = helper.commandUsage(cmd);
lines.push("");
if (isRoot) {
// Root: "Usage: mem0 <command> [options]" — <command> yellow, [options] bold
lines.push(
` ${yellow("Usage:")} ${bold(cmd.name())} ${yellow("<command>")} ${bold("[options]")}`,
);
} else {
// Subcommands: split into command path (bold) and args (yellow)
const usageParts = usage.split(" ");
const cmdPath: string[] = [];
const argParts: string[] = [];
let pastCmd = false;
for (const part of usageParts) {
if (!pastCmd && !part.startsWith("[") && !part.startsWith("<")) {
cmdPath.push(part);
} else {
pastCmd = true;
argParts.push(part);
}
}
lines.push(
` ${yellow("Usage:")} ${bold(cmdPath.join(" "))} ${yellow(argParts.join(" "))}`,
);
}
lines.push("");
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Description ──
const desc = helper.commandDescription(cmd);
if (desc) {
// Split multi-line descriptions (e.g., title + tagline)
const descLines = desc.split("\n");
for (let i = 0; i < descLines.length; i++) {
const dLine = descLines[i];
// First line is the title, subsequent non-empty lines are tagline (dimmed)
if (i === 0 || dLine.trim() === "") {
lines.push(` ${dLine}`);
} else {
lines.push(` ${dim(dLine)}`);
}
}
lines.push("");
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(...visibleArgs.map((a: Argument) => a.name().length));
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Arguments panel (subcommands only) ──
if (!isRoot) {
const visibleArgs = helper.visibleArguments(cmd);
if (visibleArgs.length > 0) {
const maxLen = Math.max(
...visibleArgs.map((a: Argument) => a.name().length),
);
const argRows = visibleArgs.map((a: Argument) => {
const name = cyanBold(a.name().padEnd(maxLen));
const description = helper.argumentDescription(a);
return ` ${name} ${description}`;
});
const panel = renderPanel("Arguments", argRows, width);
if (panel) lines.push(panel);
}
}
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap = (!isRoot && OPTION_PANELS[cmdName]) ? OPTION_PANELS[cmdName] : {};
// ── Collect options (grouped into panels for subcommands) ──
const visibleOpts = helper.visibleOptions(cmd);
const cmdName = cmd.name();
const panelMap =
!isRoot && OPTION_PANELS[cmdName] ? OPTION_PANELS[cmdName] : {};
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
const grouped: Record<string, Option[]> = { Options: [] };
for (const panelName of PANEL_ORDER) {
grouped[panelName] = [];
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped["Options"].push(opt);
}
}
for (const opt of visibleOpts) {
const flag = getLongFlag(opt);
const panel = panelMap[flag];
if (panel && PANEL_ORDER.includes(panel)) {
grouped[panel].push(opt);
} else {
grouped.Options.push(opt);
}
}
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
// ── Collect commands ──
const visibleCmds = helper.visibleCommands(cmd);
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped["Options"].length > 0) {
const optRows = formatOptionRows(grouped["Options"]);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
if (isRoot) {
// ROOT: Options first, then command groups (matches Python/Typer ordering)
if (grouped.Options.length > 0) {
const optRows = formatOptionRows(grouped.Options);
const panel = renderPanel("Options", optRows, width);
if (panel) lines.push(panel);
}
if (visibleCmds.length > 0) {
const cmdMap = new Map(visibleCmds.map((c) => [c.name(), c]));
for (const group of COMMAND_GROUPS) {
const groupCmds = group.commands
.map((name) => cmdMap.get(name))
.filter((c): c is Command => c !== undefined);
if (groupCmds.length === 0) continue;
const maxLen = Math.max(...groupCmds.map((c) => c.name().length));
const cmdRows = groupCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel(group.panel, cmdRows, width);
if (panel) lines.push(panel);
}
}
} else {
// SUBCOMMANDS: Options/panels first, then sub-subcommands
const panelSequence = ["Options", ...PANEL_ORDER];
for (const panelName of panelSequence) {
const opts = grouped[panelName];
if (opts && opts.length > 0) {
const optRows = formatOptionRows(opts);
const panel = renderPanel(panelName, optRows, width);
if (panel) lines.push(panel);
}
}
// Sub-subcommands (e.g., config show/get/set, entity list/delete)
if (visibleCmds.length > 0) {
const maxLen = Math.max(...visibleCmds.map((c) => c.name().length));
const cmdRows = visibleCmds.map((c) => {
const name = cyanBold(c.name().padEnd(maxLen));
const description = helper.subcommandDescription(c);
return ` ${name} ${description}`;
});
const panel = renderPanel("Commands", cmdRows, width);
if (panel) lines.push(panel);
}
}
lines.push("");
return lines.join("\n");
lines.push("");
return lines.join("\n");
}
// ── Format option rows with aligned columns ─────────────────────────────
function formatOptionRows(opts: Option[]): string[] {
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
const terms = opts.map((o) => formatOptionTerm(o));
const maxTermLen = Math.max(...terms.map((t) => t.length));
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
return opts.map((opt, i) => {
const term = cyanBold(terms[i].padEnd(maxTermLen));
const desc = opt.description || "";
const def = formatDefault(opt);
return ` ${term} ${desc}${def}`;
});
}
// ── Sort commands by COMMAND_ORDER ──────────────────────────────────────
function sortCommands(cmds: Command[]): Command[] {
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
return [...cmds].sort((a, b) => {
const ai = COMMAND_ORDER.indexOf(a.name());
const bi = COMMAND_ORDER.indexOf(b.name());
// Unknown commands go to end, preserving original order
const aIdx = ai === -1 ? COMMAND_ORDER.length : ai;
const bIdx = bi === -1 ? COMMAND_ORDER.length : bi;
return aIdx - bIdx;
});
}
+780 -401
View File
File diff suppressed because it is too large Load Diff
+344 -177
View File
@@ -2,229 +2,396 @@
* Output formatting for mem0 CLI — text, JSON, table, quiet modes.
*/
import Table from "cli-table3";
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;
function formatDate(dtStr?: string): string | undefined {
if (!dtStr) return undefined;
try {
const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10);
} catch {
return dtStr?.slice(0, 10);
}
if (!dtStr) return undefined;
try {
const dt = new Date(dtStr.replace("Z", "+00:00"));
return dt.toISOString().slice(0, 10);
} catch {
return dtStr?.slice(0, 10);
}
}
export function formatMemoriesText(
memories: Record<string, unknown>[],
title = "memories",
memories: Record<string, unknown>[],
title = "memories",
): void {
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
const count = memories.length;
console.log(`\n${brand(`Found ${count} ${title}:`)}\n`);
for (let i = 0; i < memories.length; i++) {
const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined;
const cats = mem.categories;
if (Array.isArray(cats)) {
category = cats[0] as string | undefined;
}
for (let i = 0; i < memories.length; i++) {
const mem = memories[i];
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = ((mem.id as string) ?? "").slice(0, 8);
const score = mem.score as number | undefined;
const created = formatDate(mem.created_at as string | undefined);
let category: string | undefined;
const cats = mem.categories;
if (Array.isArray(cats)) {
category = cats[0] as string | undefined;
}
console.log(` ${i + 1}. ${memoryText}`);
console.log(` ${i + 1}. ${memoryText}`);
const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`);
const details: string[] = [];
if (score !== undefined) details.push(`Score: ${score.toFixed(2)}`);
if (memId) details.push(`ID: ${memId}`);
if (created) details.push(`Created: ${created}`);
if (category) details.push(`Category: ${category}`);
if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`);
}
console.log();
}
if (details.length > 0) {
console.log(` ${dim(details.join(" · "))}`);
}
console.log();
}
}
export function formatMemoriesTable(memories: Record<string, unknown>[]): void {
const table = new Table({
head: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")],
colWidths: [12, 52, 16, 14],
wordWrap: true,
style: { head: [], border: [] },
});
export function formatMemoriesTable(
memories: Record<string, unknown>[],
opts: { showScore?: boolean } = {},
): void {
const head = opts.showScore
? [
accent("ID"),
accent("Score"),
accent("Memory"),
accent("Category"),
accent("Created"),
]
: [accent("ID"), accent("Memory"), accent("Category"), accent("Created")];
const colWidths = opts.showScore ? [38, 8, 40, 16, 14] : [38, 40, 16, 14];
const table = new Table({
head,
colWidths,
wordWrap: true,
style: { head: [], border: [] },
});
for (const mem of memories) {
const memId = ((mem.id as string) ?? "").slice(0, 8);
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = memoryText.slice(0, 57) + "...";
}
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
table.push([dim(memId), memoryText, cat, created]);
}
for (const mem of memories) {
const memId = (mem.id as string) ?? "";
let memoryText = (mem.memory ?? mem.text ?? "") as string;
if (memoryText.length > 60) {
memoryText = `${memoryText.slice(0, 57)}...`;
}
const categories = mem.categories;
const cat =
Array.isArray(categories) && categories.length > 0
? categories.length > 1
? `${categories[0]} (+${categories.length - 1})`
: (categories[0] as string)
: "—";
const created = formatDate(mem.created_at as string | undefined) ?? "—";
if (opts.showScore) {
const score = mem.score as number | undefined;
const scoreStr = score !== undefined ? score.toFixed(2) : "—";
table.push([dim(memId), scoreStr, memoryText, cat, created]);
} else {
table.push([dim(memId), memoryText, cat, created]);
}
}
console.log();
console.log(table.toString());
console.log();
console.log();
console.log(table.toString());
console.log();
}
export function formatJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
console.log(JSON.stringify(data, null, 2));
}
export function formatSingleMemory(
mem: Record<string, unknown>,
output = "text",
mem: Record<string, unknown>,
output = "text",
): void {
if (output === "json") {
formatJson(mem);
return;
}
if (output === "json") {
formatJson(mem);
return;
}
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const memoryText = (mem.memory ?? mem.text ?? "") as string;
const memId = (mem.id ?? "") as string;
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
const lines: string[] = [];
lines.push(` ${memoryText}`);
lines.push("");
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories;
if (categories) {
const catStr = Array.isArray(categories) ? categories.join(", ") : String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
if (memId) lines.push(` ${dim("ID:")} ${memId}`);
const created = formatDate(mem.created_at as string | undefined);
if (created) lines.push(` ${dim("Created:")} ${created}`);
const updated = formatDate(mem.updated_at as string | undefined);
if (updated) lines.push(` ${dim("Updated:")} ${updated}`);
const meta = mem.metadata;
if (meta) lines.push(` ${dim("Metadata:")} ${JSON.stringify(meta)}`);
const categories = mem.categories;
if (categories) {
const catStr = Array.isArray(categories)
? categories.join(", ")
: String(categories);
lines.push(` ${dim("Categories:")} ${catStr}`);
}
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Memory"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
const content = lines.join("\n");
console.log();
console.log(
boxen(content, {
title: brand("Memory"),
titleAlignment: "left",
borderColor: "magenta",
padding: 1,
}),
);
console.log();
}
export function formatAddResult(
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
result: Record<string, unknown> | Record<string, unknown>[],
output = "text",
): void {
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
if (output === "json") {
formatJson(result);
return;
}
if (output === "quiet") return;
const results: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
const results: Record<string, unknown>[] = Array.isArray(result)
? result
: ((result.results as Record<string, unknown>[]) ?? [result]);
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
if (!results.length) {
console.log(` ${dim("No memories extracted.")}`);
return;
}
console.log();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = ((r.event_id as string) ?? "").slice(0, 8);
const icon = accent(sym("⧗", "..."));
const parts = [` ${icon} ${dim("Queued".padEnd(10))}`, "Processing in background"];
if (eventId) parts.push(dim(`(event ${eventId})`));
console.log(parts.join(" "));
continue;
}
console.log();
const seenPendingEvents = new Set<string>();
for (const r of results) {
// Detect async PENDING response
if (r.status === "PENDING") {
const eventId = (r.event_id as string) ?? "";
// Deduplicate PENDING entries with the same event_id
if (eventId && seenPendingEvents.has(eventId)) continue;
if (eventId) seenPendingEvents.add(eventId);
const icon = accent(sym("⧗", "..."));
const parts = [
` ${icon} ${dim("Queued".padEnd(10))}`,
"Processing in background",
];
console.log(parts.join(" "));
if (eventId) {
console.log(` ${dim(` event_id: ${eventId}`)}`);
console.log(
` ${dim(` → Check status: mem0 event status ${eventId}`)}`,
);
}
continue;
}
const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(0, 8);
const event = (r.event ?? "ADD") as string;
const memory = (r.memory ?? r.text ?? r.content ?? r.data ?? "") as string;
const memId = ((r.id as string) ?? (r.memory_id as string) ?? "").slice(
0,
8,
);
let icon: string;
let label: string;
if (event === "ADD") {
icon = success("+");
label = "Added";
} else if (event === "UPDATE") {
icon = accent("~");
label = "Updated";
} else if (event === "DELETE") {
icon = errorColor("-");
label = "Deleted";
} else if (event === "NOOP") {
icon = dim("·");
label = "No change";
} else {
icon = dim("?");
label = event;
}
let icon: string;
let label: string;
if (event === "ADD") {
icon = success("+");
label = "Added";
} else if (event === "UPDATE") {
icon = accent("~");
label = "Updated";
} else if (event === "DELETE") {
icon = errorColor("-");
label = "Deleted";
} else if (event === "NOOP") {
icon = dim("·");
label = "No change";
} else {
icon = dim("?");
label = event;
}
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" "));
}
console.log();
const parts = [` ${icon} ${dim(label.padEnd(10))}`];
if (memory) parts.push(memory);
if (memId) parts.push(dim(`(${memId})`));
console.log(parts.join(" "));
}
console.log();
}
export function formatJsonEnvelope(opts: {
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
status?: string;
error?: string;
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
status?: string;
error?: string;
}): void {
const envelope: Record<string, unknown> = {
status: opts.status ?? "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope;
if (opts.count !== undefined) envelope.count = opts.count;
if (opts.error) envelope.error = opts.error;
envelope.data = opts.data;
console.log(JSON.stringify(envelope, null, 2));
const envelope: Record<string, unknown> = {
status: opts.status ?? "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope !== undefined) envelope.scope = opts.scope;
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));
}
function pick(
obj: Record<string, unknown>,
keys: string[],
): Record<string, unknown> {
const result: Record<string, unknown> = {};
for (const key of keys) {
if (key in obj) result[key] = obj[key];
}
return result;
}
export function sanitizeAgentData(command: string, data: unknown): unknown {
if (data === null || data === undefined) return data;
switch (command) {
case "add": {
const items = Array.isArray(data) ? data : [data];
return items.map((item) => {
const r = item as Record<string, unknown>;
if (r.status === "PENDING") return pick(r, ["status", "event_id"]);
return pick(r, ["id", "memory", "event"]);
});
}
case "search":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "memory", "score", "created_at", "categories"]),
);
case "list":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "memory", "created_at", "categories"]),
);
case "get": {
const r = data as Record<string, unknown>;
return pick(r, [
"id",
"memory",
"created_at",
"updated_at",
"categories",
"metadata",
]);
}
case "update": {
const r = data as Record<string, unknown>;
return pick(r, ["id", "memory"]);
}
case "delete":
case "delete-all":
case "entity delete":
return data;
case "entity list":
return (data as Record<string, unknown>[]).map((r) => ({
name: (r.name ?? r.id) as string,
...pick(r, ["type", "count"]),
}));
case "event list":
return (data as Record<string, unknown>[]).map((r) =>
pick(r, ["id", "event_type", "status", "latency", "created_at"]),
);
case "event status": {
const ev = data as Record<string, unknown>;
const rawResults =
(ev.results as Record<string, unknown>[] | undefined) ?? [];
const sanitizedResults = rawResults.map((r) => {
const nested = r.data as Record<string, unknown> | undefined;
return {
id: r.id,
event: r.event,
user_id: r.user_id,
memory: nested?.memory ?? null,
};
});
return {
...pick(ev, [
"id",
"event_type",
"status",
"latency",
"created_at",
"updated_at",
]),
results: sanitizedResults,
};
}
default:
return data;
}
}
export function formatAgentEnvelope(opts: {
command: string;
data: unknown;
durationMs?: number;
scope?: Record<string, string | undefined>;
count?: number;
}): void {
const envelope: Record<string, unknown> = {
status: "success",
command: opts.command,
};
if (opts.durationMs !== undefined) envelope.duration_ms = opts.durationMs;
if (opts.scope) {
const filtered = Object.fromEntries(
Object.entries(opts.scope).filter(([, v]) => v),
);
if (Object.keys(filtered).length > 0) envelope.scope = filtered;
}
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));
}
export function printResultSummary(opts: {
count: number;
durationSecs?: number;
page?: number;
scopeIds?: Record<string, string | undefined>;
count: number;
durationSecs?: number;
page?: number;
scopeIds?: Record<string, string | undefined>;
}): void {
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k.replace(/_/g, " ")}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined) parts.push(`${opts.durationSecs.toFixed(2)}s`);
const parts = [`${opts.count} result${opts.count !== 1 ? "s" : ""}`];
if (opts.page !== undefined) parts.push(`page ${opts.page}`);
if (opts.scopeIds) {
const scopeParts = Object.entries(opts.scopeIds)
.filter(([, v]) => v)
.map(([k, v]) => `${k}=${v}`);
if (scopeParts.length > 0) parts.push(scopeParts.join(", "));
}
if (opts.durationSecs !== undefined)
parts.push(`${opts.durationSecs.toFixed(2)}s`);
console.log(` ${dim(parts.join(" · "))}`);
console.log();
console.log(` ${dim(parts.join(" · "))}`);
console.log();
}
+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;
}
}
+40
View File
@@ -0,0 +1,40 @@
/**
* Agent mode state — set by the root program option handler,
* read by commands and branding functions.
*/
let _agentMode = false;
let _currentCommand = "";
let _pendingNotice = "";
export function isAgentMode(): boolean {
return _agentMode;
}
export function setAgentMode(val: boolean): void {
_agentMode = val;
}
export function getCurrentCommand(): string {
return _currentCommand;
}
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;
}
+157
View File
@@ -0,0 +1,157 @@
/**
* CLI telemetry — anonymous usage tracking via PostHog.
*
* Sends fire-and-forget events by spawning a detached child process
* (telemetry-sender.cjs). The parent CLI process exits immediately;
* the child handles email resolution, caching, and the HTTP POST.
*
* Disable with: MEM0_TELEMETRY=false
*/
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { CONFIG_FILE, loadConfig, saveConfig } from "./config.js";
import { CLI_VERSION } from "./version.js";
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SENDER_SCRIPT = path.join(__dirname, "..", "telemetry-sender.cjs");
function isTelemetryEnabled(): boolean {
try {
return process.env.MEM0_TELEMETRY !== "false";
} catch {
return true;
}
}
/**
* Return a persistent per-machine anonymous ID, generating one if needed.
*
* Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
* repeat runs on the same machine share one PostHog identity instead of
* collapsing into a single shared fallback string.
*/
function getOrCreateAnonymousId(): string {
const config = loadConfig();
if (config.telemetry.anonymousId) {
return config.telemetry.anonymousId;
}
const newId = `cli-anon-${randomUUID().replace(/-/g, "")}`;
config.telemetry.anonymousId = newId;
try {
saveConfig(config);
} catch {
/* ignore persistence failure — still return the generated ID */
}
return newId;
}
/**
* Return a stable anonymous identifier for the current user.
*
* Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
* persistent per-machine anonymous ID.
*/
function getDistinctId(): string {
try {
const config = loadConfig();
if (config.platform.userEmail) {
return config.platform.userEmail;
}
if (config.platform.apiKey) {
return createHash("md5").update(config.platform.apiKey).digest("hex");
}
} catch {
/* ignore */
}
try {
return getOrCreateAnonymousId();
} catch {
return `cli-anon-${randomUUID().replace(/-/g, "")}`;
}
}
/**
* Fire a PostHog event (non-blocking, returns void, never throws).
* Spawns telemetry-sender.cjs as a detached subprocess.
*
* When `preResolvedEmail` is provided (e.g. from an upfront ping
* validation), it is used directly as the PostHog distinct ID and the
* subprocess skips its own `/v1/ping/` call.
*/
export function captureEvent(
eventName: string,
properties: Record<string, unknown> = {},
preResolvedEmail?: string,
): void {
if (!isTelemetryEnabled()) return;
try {
const config = loadConfig();
const distinctId = preResolvedEmail || getDistinctId();
// Detect anonymous → identified transition. If a stored anonymous_id
// exists and we just resolved to a real identity, fire a one-shot
// $identify event so PostHog stitches the pre-signup history onto
// the authenticated profile. Clear the stored id so we don't re-alias.
let anonIdToAlias: string | null = null;
if (
distinctId &&
!distinctId.startsWith("cli-anon-") &&
config.telemetry.anonymousId
) {
anonIdToAlias = config.telemetry.anonymousId;
config.telemetry.anonymousId = "";
try {
saveConfig(config);
} catch {
/* ignore — alias may double-fire next run, harmless */
}
}
// 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,
event: eventName,
properties: {
source: "CLI",
language: "node",
cli_version: CLI_VERSION,
agent_mode: Boolean(config.platform.agentMode),
node_version: process.version,
os: process.platform,
...properties,
$process_person_profile: false,
$lib: "posthog-node",
},
};
const context = {
payload,
posthogHost: POSTHOG_HOST,
needsEmail: !distinctId || !distinctId.includes("@"),
mem0ApiKey: config.platform.apiKey || "",
mem0BaseUrl: config.platform.baseUrl || "https://api.mem0.ai",
configPath: CONFIG_FILE,
anonDistinctIdToAlias: anonIdToAlias,
};
const child = spawn(process.execPath, [SENDER_SCRIPT], {
detached: true,
stdio: ["pipe", "ignore", "ignore"],
});
child.stdin?.end(JSON.stringify(context));
child.unref();
} catch {
/* silently swallow */
}
}
+10
View File
@@ -0,0 +1,10 @@
import { createRequire } from "node:module";
// __CLI_VERSION__ is replaced at build time by tsup (see tsup.config.ts).
// When running via tsx in dev/test mode, fall back to reading package.json.
// typeof is safe to use on undeclared identifiers — it returns 'undefined' without throwing.
export const CLI_VERSION: string =
typeof __CLI_VERSION__ !== "undefined"
? (__CLI_VERSION__ as string)
: (createRequire(import.meta.url)("../package.json") as { version: string })
.version;
+155
View File
@@ -0,0 +1,155 @@
/**
* Standalone telemetry sender — runs as a detached child process.
*
* Usage: node telemetry-sender.cjs (JSON context is read from stdin; a single
* argv argument is still accepted as a legacy fallback)
*
* This script is spawned by telemetry.captureEvent() and runs independently
* of the parent CLI process. It:
*
* 1. Resolves the user's email via /v1/ping/ if not already cached
* 2. Caches the email in ~/.mem0/config.json for future runs
* 3. Sends the PostHog event
*
* All errors are silently swallowed — this process must never produce output
* or affect the user experience.
*/
"use strict";
const https = require("https");
const fs = require("fs");
function 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);
const opts = {
hostname: u.hostname,
path: u.pathname + u.search,
method,
headers,
timeout: 10000,
};
const req = https.request(opts, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch {
resolve({});
}
});
});
req.on("error", reject);
req.on("timeout", () => {
req.destroy();
reject(new Error("timeout"));
});
if (body) {
req.end(body);
} else {
req.end();
}
});
}
async function resolveAndCacheEmail(ctx, payload) {
try {
const pingUrl = ctx.mem0BaseUrl.replace(/\/+$/, "") + "/v1/ping/";
const data = await httpsRequest(pingUrl, "GET", {
Authorization: "Token " + ctx.mem0ApiKey,
"Content-Type": "application/json",
});
if (data.user_email) {
payload.distinct_id = data.user_email;
cacheEmail(ctx.configPath, data.user_email);
}
} catch {
// silently swallow
}
}
function cacheEmail(configPath, email) {
if (!configPath) return;
try {
const raw = fs.readFileSync(configPath, "utf-8");
const cfg = JSON.parse(raw);
if (!cfg.platform) cfg.platform = {};
cfg.platform.user_email = email;
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
} catch {
// silently swallow
}
}
async function sendPosthogEvent(posthogHost, payload) {
try {
const body = JSON.stringify(payload);
await httpsRequest(posthogHost, "POST", {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body),
}, body);
} catch {
// silently swallow
}
}
async function sendIdentifyEvent(ctx, payload, anonId) {
const identifyPayload = {
api_key: payload.api_key,
event: "$identify",
distinct_id: payload.distinct_id,
properties: {
$anon_distinct_id: anonId,
$lib: (payload.properties && payload.properties.$lib) || "posthog-node",
},
};
await sendPosthogEvent(ctx.posthogHost, identifyPayload);
}
async function main() {
const ctx = await loadContext();
const payload = ctx.payload;
if (ctx.needsEmail && ctx.mem0ApiKey) {
await resolveAndCacheEmail(ctx, payload);
}
// Fire $identify *after* email resolution so PostHog links the stored
// anonymous id directly to the final identity (email, not the api-key
// hash). The regular event is sent next so it lands under the merged
// profile.
if (ctx.anonDistinctIdToAlias) {
await sendIdentifyEvent(ctx, payload, ctx.anonDistinctIdToAlias);
}
await sendPosthogEvent(ctx.posthogHost, payload);
}
main().catch(() => {});
+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");
});
});
+4 -4
View File
@@ -80,19 +80,19 @@ describe("printWarning", () => {
describe("printInfo", () => {
it("prints info message", () => {
printInfo("Important note");
expect(output).toContain("Important note");
expect(errOutput).toContain("Important note");
});
});
describe("printScope", () => {
it("prints scope when IDs present", () => {
printScope({ user_id: "alice", agent_id: "bot" });
expect(output).toContain("alice");
expect(output).toContain("bot");
expect(errOutput).toContain("alice");
expect(errOutput).toContain("bot");
});
it("prints nothing when no IDs", () => {
printScope({});
expect(output).toBe("");
expect(errOutput).toBe("");
});
});
+6 -13
View File
@@ -44,13 +44,6 @@ describe("CLI Integration — help and version", () => {
expect(result.stdout).toContain("search");
});
it("shows version with --version", () => {
const result = run(["--version"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("0.1.0");
});
it("help --json produces valid JSON", () => {
const result = run(["help", "--json"]);
expect(result.exitCode).toBe(0);
@@ -114,22 +107,22 @@ describe("CLI Integration — help and version", () => {
expect(result.exitCode).toBe(0);
});
it("add help has --graph flag", () => {
it("add help has --output flag", () => {
const result = run(["add", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
expect(result.stdout).toContain("--output");
});
it("search help has --graph flag", () => {
it("search help has --rerank flag", () => {
const result = run(["search", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
expect(result.stdout).toContain("--rerank");
});
it("list help has --graph flag", () => {
it("list help has --category flag", () => {
const result = run(["list", "--help"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--graph");
expect(result.stdout).toContain("--category");
});
});
+267 -22
View File
@@ -3,8 +3,10 @@
*/
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";
let mockBackend: Backend;
@@ -31,6 +33,7 @@ import { afterEach } from "vitest";
afterEach(() => {
console.log = originalLog;
console.error = originalError;
setAgentMode(false);
});
describe("cmdAdd", () => {
@@ -39,8 +42,6 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "I prefer dark mode", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
@@ -52,8 +53,6 @@ describe("cmdAdd", () => {
userId: "alice",
messages: JSON.stringify([{ role: "user", content: "I love Python" }]),
immutable: false,
noInfer: false,
enableGraph: false,
output: "text",
});
expect(mockBackend.add).toHaveBeenCalledOnce();
@@ -64,8 +63,6 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: false,
output: "json",
});
expect(output).toContain("results");
@@ -76,14 +73,106 @@ describe("cmdAdd", () => {
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
noInfer: false,
enableGraph: 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: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
};
it("text shows one pending block", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "text",
});
expect(output.match(/Queued/g)?.length).toBe(1);
});
it("json shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "json",
});
const data = JSON.parse(output);
const pending = data.results.filter((r: Record<string, unknown>) => r.status === "PENDING");
expect(pending).toHaveLength(1);
});
it("agent shows one pending entry", async () => {
(mockBackend.add as ReturnType<typeof vi.fn>).mockResolvedValue(DUPLICATE_PENDING);
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test", {
userId: "alice",
immutable: false,
output: "agent",
});
const data = JSON.parse(output);
expect(data.count).toBe(1);
expect(data.data).toHaveLength(1);
});
});
describe("cmdSearch", () => {
it("searches and shows results in text mode", async () => {
const { cmdSearch } = await import("../src/commands/memory.js");
@@ -93,7 +182,7 @@ describe("cmdSearch", () => {
threshold: 0.3,
rerank: false,
keyword: false,
enableGraph: false,
output: "text",
});
expect(output).toContain("Found 2");
@@ -107,7 +196,7 @@ describe("cmdSearch", () => {
threshold: 0.3,
rerank: false,
keyword: false,
enableGraph: false,
output: "json",
});
expect(output).toContain("memory");
@@ -122,10 +211,10 @@ describe("cmdSearch", () => {
threshold: 0.3,
rerank: false,
keyword: false,
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
@@ -150,7 +239,7 @@ describe("cmdList", () => {
userId: "alice",
page: 1,
pageSize: 100,
enableGraph: false,
output: "table",
});
expect(output).toContain("dark mode");
@@ -163,10 +252,10 @@ describe("cmdList", () => {
userId: "alice",
page: 1,
pageSize: 100,
enableGraph: false,
output: "text",
});
expect(output).toContain("No memories found");
expect(errOutput).toContain("No memories found");
});
});
@@ -198,13 +287,6 @@ describe("cmdDeleteAll", () => {
});
});
describe("cmdVersion", () => {
it("shows version", async () => {
const { cmdVersion } = await import("../src/commands/utils.js");
cmdVersion();
expect(output).toContain("0.1.0");
});
});
describe("cmdEntitiesList", () => {
it("lists users in table mode", async () => {
@@ -219,3 +301,166 @@ describe("cmdEntitiesList", () => {
expect(output).toContain("alice");
});
});
describe("cmdEventList", () => {
it("lists events in table mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect(output).toContain("evt-abc-");
expect(output).toContain("ADD");
expect(output).toContain("SUCCEEDED");
});
it("lists events in json mode", async () => {
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("evt-def-456-ghi-789");
});
it("shows empty message when no events", async () => {
(mockBackend.listEvents as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "table" });
expect((output + errOutput).toLowerCase()).toContain("no events");
});
});
describe("cmdEventStatus", () => {
it("shows event details in text mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "text" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("SUCCEEDED");
});
it("shows event details in json mode", async () => {
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "json" });
expect(output).toContain("evt-abc-123-def-456");
expect(output).toContain("ADD");
});
});
describe("agent mode", () => {
it("cmdAdd outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdAdd } = await import("../src/commands/memory.js");
await cmdAdd(mockBackend, "test preference", {
userId: "alice",
immutable: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("add");
expect(parsed.data).toBeDefined();
expect(parsed.scope).toMatchObject({ user_id: "alice" });
expect(Object.keys(parsed.data[0]).sort()).toEqual(["event", "id", "memory"].sort());
});
it("cmdSearch outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdSearch } = await import("../src/commands/memory.js");
await cmdSearch(mockBackend, "preferences", {
userId: "alice",
topK: 10,
threshold: 0.3,
rerank: false,
keyword: false,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("search");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
const keys = Object.keys(parsed.data[0]);
expect(keys).toContain("id");
expect(keys).toContain("memory");
expect(keys).toContain("score");
expect(keys).toContain("created_at");
expect(keys).toContain("categories");
expect(keys).not.toContain("user_id");
expect(keys).not.toContain("agent_id");
});
it("cmdList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdList } = await import("../src/commands/memory.js");
await cmdList(mockBackend, {
userId: "alice",
page: 1,
pageSize: 100,
output: "agent",
});
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("cmdGet outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdGet } = await import("../src/commands/memory.js");
await cmdGet(mockBackend, "abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("get");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "abc-123-def-456" });
expect(Object.keys(parsed.data)).not.toContain("user_id");
});
it("cmdUpdate outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdUpdate } = await import("../src/commands/memory.js");
await cmdUpdate(mockBackend, "abc-123", "Updated text", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("update");
expect(parsed.data).toBeDefined();
});
it("cmdDelete outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdDelete } = await import("../src/commands/memory.js");
await cmdDelete(mockBackend, "abc-123", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("delete");
expect(parsed.data).toBeDefined();
});
it("cmdEventList outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventList } = await import("../src/commands/events.js");
await cmdEventList(mockBackend, { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event list");
expect(Array.isArray(parsed.data)).toBe(true);
expect(parsed.count).toBe(2);
expect(Object.keys(parsed.data[0]).sort()).toEqual(
["created_at", "event_type", "id", "latency", "status"],
);
expect(Object.keys(parsed.data[0])).not.toContain("updated_at");
});
it("cmdEventStatus outputs JSON envelope", async () => {
setAgentMode(true);
const { cmdEventStatus } = await import("../src/commands/events.js");
await cmdEventStatus(mockBackend, "evt-abc-123-def-456", { output: "agent" });
const parsed = JSON.parse(output.trim());
expect(parsed.status).toBe("success");
expect(parsed.command).toBe("event status");
expect(parsed.data).toBeDefined();
expect(parsed.data).toMatchObject({ id: "evt-abc-123-def-456" });
expect(parsed.data.results[0]).toHaveProperty("memory");
expect(parsed.data.results[0]).not.toHaveProperty("data");
});
});
-6
View File
@@ -64,7 +64,6 @@ describe("createDefaultConfig", () => {
expect(config.platform.baseUrl).toBe("https://api.mem0.ai");
expect(config.platform.apiKey).toBe("");
expect(config.defaults.userId).toBe("");
expect(config.defaults.enableGraph).toBe(false);
});
});
@@ -105,9 +104,4 @@ describe("setNestedValue", () => {
expect(config.defaults.userId).toBe("bob");
});
it("coerces boolean for enable_graph", () => {
const config = createDefaultConfig();
expect(setNestedValue(config, "defaults.enable_graph", "true")).toBe(true);
expect(config.defaults.enableGraph).toBe(true);
});
});
+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);
});
});
+83
View File
@@ -10,6 +10,7 @@ import {
formatSingleMemory,
formatAddResult,
printResultSummary,
sanitizeAgentData,
} from "../src/output.js";
let output: string;
@@ -98,6 +99,18 @@ describe("formatAddResult", () => {
});
expect(output).toContain("Queued");
});
it("deduplicates PENDING entries with same event_id", () => {
formatAddResult({
results: [
{ status: "PENDING", event_id: "evt-dup" },
{ status: "PENDING", event_id: "evt-dup" },
],
});
// Should show only one PENDING block despite two entries with same event_id
expect(output.match(/Queued/g)?.length).toBe(1);
expect(output.match(/evt-dup/g)?.length).toBe(2); // event_id line + status hint line
});
});
describe("printResultSummary", () => {
@@ -113,3 +126,73 @@ describe("printResultSummary", () => {
expect(output).not.toContain("results");
});
});
describe("sanitizeAgentData", () => {
it("projects add results", () => {
const raw = [{ id: "abc", memory: "test", event: "ADD", metadata: { x: 1 }, categories: ["a"] }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ id: "abc", memory: "test", event: "ADD" }]);
});
it("passes through PENDING add items", () => {
const raw = [{ status: "PENDING", event_id: "evt-123", noise: "x" }];
const result = sanitizeAgentData("add", raw) as Record<string, unknown>[];
expect(result).toEqual([{ status: "PENDING", event_id: "evt-123" }]);
});
it("projects search results", () => {
const raw = [{ id: "abc", memory: "test", score: 0.9, created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("search", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("user_id");
expect(result[0]).toHaveProperty("score");
});
it("projects list results", () => {
const raw = [{ id: "abc", memory: "test", created_at: "2026-01-01", categories: ["a"], user_id: "u1" }];
const result = sanitizeAgentData("list", raw) as Record<string, unknown>[];
expect(Object.keys(result[0]).sort()).toEqual(["categories", "created_at", "id", "memory"]);
});
it("projects get result", () => {
const raw = { id: "abc", memory: "test", created_at: "2026-01-01", updated_at: "2026-01-02", categories: ["a"], metadata: { k: "v" }, user_id: "u1" };
const result = sanitizeAgentData("get", raw) as Record<string, unknown>;
expect(result).not.toHaveProperty("user_id");
expect(result).toHaveProperty("metadata");
});
it("projects update result", () => {
const raw = { id: "abc", memory: "updated", extra: "noise" };
const result = sanitizeAgentData("update", raw);
expect(result).toEqual({ id: "abc", memory: "updated" });
});
it("projects event list results", () => {
const raw = [{ id: "evt-1", event_type: "ADD", status: "SUCCEEDED", graph_status: null, latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02" }];
const result = sanitizeAgentData("event list", raw) as Record<string, unknown>[];
expect(result[0]).not.toHaveProperty("updated_at");
expect(result[0]).not.toHaveProperty("graph_status");
});
it("flattens event status results", () => {
const raw = {
id: "evt-1", event_type: "ADD", status: "SUCCEEDED",
latency: 100, created_at: "2026-01-01", updated_at: "2026-01-02",
results: [{ id: "mem-1", event: "ADD", user_id: "alice", data: { memory: "dark mode" } }],
};
const result = sanitizeAgentData("event status", raw) as Record<string, unknown>;
const firstResult = (result.results as Record<string, unknown>[])[0];
expect(firstResult).toHaveProperty("memory", "dark mode");
expect(firstResult).not.toHaveProperty("data");
});
it("passes through status/config/import commands unchanged", () => {
const data = { key: "value", other: "stuff" };
for (const cmd of ["status", "import", "config show", "config get", "config set"]) {
expect(sanitizeAgentData(cmd, data)).toEqual(data);
}
});
it("handles null data", () => {
expect(sanitizeAgentData("add", null)).toBeNull();
});
});
+100
View File
@@ -0,0 +1,100 @@
/**
* Tests for the Platform backend (mem0 Platform API client).
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PlatformBackend } from "../src/backend/platform.js";
import { createDefaultConfig } from "../src/config.js";
function makeBackend(): PlatformBackend {
// apiKey/baseUrl only build request headers; every test spies on _request,
// so no real network calls are made.
return new PlatformBackend(createDefaultConfig().platform);
}
function mockFetch() {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
headers: { get: vi.fn().mockReturnValue(null) },
json: vi.fn().mockResolvedValue({ message: "ok" }),
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("deleteEntities", () => {
it("returns all results keyed by entity type for a multi-entity delete", async () => {
const backend = makeBackend();
const responses: Record<string, unknown> = {
"/v2/entities/user/alice/": { message: "user deleted" },
"/v2/entities/agent/bob/": { message: "agent deleted" },
};
const spy = vi
// biome-ignore lint/suspicious/noExplicitAny: spying on a private method
.spyOn(backend as any, "_request")
.mockImplementation(async (_method: string, path: string) => responses[path]);
const result = await backend.deleteEntities({ userId: "alice", agentId: "bob" });
// 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",
);
});
});
describe("PlatformBackend path encoding", () => {
it("encodes memory IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.get("mem/a?b#c");
await backend.update("mem/a?b#c", "updated");
await backend.delete("mem/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/",
"https://api.mem0.ai/v1/memories/mem%2Fa%3Fb%23c/?source=CLI",
]);
});
it("encodes entity and event IDs before interpolating them into paths", async () => {
const fetchMock = mockFetch();
const backend = makeBackend();
await backend.deleteEntities({ userId: "org/team?active#frag" });
await backend.getEvent("evt/a?b#c");
const urls = fetchMock.mock.calls.map((call) => call[0]);
expect(urls).toEqual([
"https://api.mem0.ai/v2/entities/user/org%2Fteam%3Factive%23frag/?source=CLI",
"https://api.mem0.ai/v1/event/evt%2Fa%3Fb%23c/",
]);
});
});
+37
View File
@@ -71,5 +71,42 @@ export function createMockBackend(): Backend {
{ name: "alice", count: 5 },
{ name: "bob", count: 3 },
]),
listEvents: vi.fn().mockResolvedValue([
{
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: null,
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
},
{
id: "evt-def-456-ghi-789",
event_type: "SEARCH",
status: "PENDING",
graph_status: null,
latency: null,
created_at: "2026-04-01T10:01:00Z",
updated_at: "2026-04-01T10:01:00Z",
},
]),
getEvent: vi.fn().mockResolvedValue({
id: "evt-abc-123-def-456",
event_type: "ADD",
status: "SUCCEEDED",
graph_status: "SUCCEEDED",
latency: 1234.5,
created_at: "2026-04-01T10:00:00Z",
updated_at: "2026-04-01T10:00:01Z",
results: [
{
id: "mem-abc-123",
event: "ADD",
user_id: "alice",
data: { memory: "User prefers dark mode" },
},
],
}),
};
}
+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);
});
});
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'tsup';
import { createRequire } from 'node:module';
const _require = createRequire(import.meta.url);
const pkg = _require('./package.json');
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
define: {
__CLI_VERSION__: JSON.stringify(pkg.version),
},
});
+17
View File
@@ -0,0 +1,17 @@
import { createRequire } from "node:module";
import { defineConfig } from "vitest/config";
const _require = createRequire(import.meta.url);
const pkg = _require("./package.json") as { version: string };
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,
},
});
+315 -5
View File
@@ -1,6 +1,12 @@
# mem0 CLI
# mem0 CLI (Python)
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents.
The official command-line interface for [mem0](https://mem0.ai) — the memory layer for AI agents. Python implementation.
> **Built for AI agents.** Pass `--agent` (or `--json`) as a global flag on any command to get structured JSON output optimized for programmatic consumption — sanitized fields, no colors or spinners, and errors as JSON too.
## Prerequisites
- Python **3.10+**
## Installation
@@ -18,22 +24,326 @@ pip install mem0-cli
> **Note:** On macOS with Homebrew Python, `pip install` outside a virtual environment will fail with an `externally-managed-environment` error ([PEP 668](https://peps.python.org/pep-0668/)). Use `pipx` instead, or install inside a virtual environment.
## Quick Start
## Quick start
```bash
# Set up your configuration
# Interactive setup wizard
mem0 init
# Or login via email
mem0 init --email alice@company.com
# Or authenticate with an existing API key
mem0 init --api-key m0-xxx
# Add a memory
mem0 add "I prefer dark mode and use vim keybindings" --user-id alice
# Search memories
mem0 search "What are Alice's preferences?" --user-id alice
# List all memories
# List all memories for a user
mem0 list --user-id alice
# Get a specific memory
mem0 get <memory-id>
# Update a memory
mem0 update <memory-id> "I switched to light mode"
# Delete a memory
mem0 delete <memory-id>
```
## Commands
### `mem0 init`
Interactive setup wizard. Prompts for your API key and default user ID.
```bash
mem0 init
mem0 init --api-key m0-xxx --user-id alice
mem0 init --email alice@company.com
```
If an existing configuration is detected, the CLI asks for confirmation before overwriting. Use `--force` to skip the prompt (useful in CI/CD).
```bash
mem0 init --api-key m0-xxx --user-id alice --force
```
| Flag | Description |
|------|-------------|
| `--api-key` | API key (skip prompt) |
| `-u, --user-id` | Default user ID (skip prompt) |
| `--email` | Login via email verification code |
| `--code` | Verification code (use with `--email` for non-interactive login) |
| `--force` | Overwrite existing config without confirmation |
### `mem0 add`
Add a memory from text, a JSON messages array, a file, or stdin.
```bash
mem0 add "I prefer dark mode" --user-id alice
mem0 add --file conversation.json --user-id alice
echo "Loves hiking on weekends" | mem0 add --user-id alice
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Scope to a user |
| `--agent-id` | Scope to an agent |
| `--messages` | Conversation messages as JSON |
| `-f, --file` | Read messages from a JSON file |
| `-m, --metadata` | Custom metadata as JSON |
| `--categories` | Categories (JSON array or comma-separated) |
| `--graph / --no-graph` | Enable or disable graph memory extraction |
| `-o, --output` | Output format: `text`, `json`, `quiet` |
### `mem0 search`
Search memories using natural language.
```bash
mem0 search "dietary restrictions" --user-id alice
mem0 search "preferred tools" --user-id alice --output json --top-k 5
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Filter by user |
| `-k, --top-k` | Number of results (default: 10) |
| `--threshold` | Minimum similarity score (default: 0.3) |
| `--rerank` | Enable reranking |
| `--keyword` | Use keyword search instead of semantic |
| `--filter` | Advanced filter expression (JSON) |
| `--graph / --no-graph` | Enable or disable graph in search |
| `-o, --output` | Output format: `text`, `json`, `table` |
### `mem0 list`
List memories with optional filters and pagination.
```bash
mem0 list --user-id alice
mem0 list --user-id alice --category preferences --output json
mem0 list --user-id alice --after 2024-01-01 --page-size 50
```
| Flag | Description |
|------|-------------|
| `-u, --user-id` | Filter by user |
| `--page` | Page number (default: 1) |
| `--page-size` | Results per page (default: 100) |
| `--category` | Filter by category |
| `--after` | Created after date (YYYY-MM-DD) |
| `--before` | Created before date (YYYY-MM-DD) |
| `-o, --output` | Output format: `text`, `json`, `table` |
### `mem0 get`
Retrieve a specific memory by ID.
```bash
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789
mem0 get 7b3c1a2e-4d5f-6789-abcd-ef0123456789 --output json
```
### `mem0 update`
Update the text or metadata of an existing memory.
```bash
mem0 update <memory-id> "Updated preference text"
mem0 update <memory-id> --metadata '{"priority": "high"}'
echo "new text" | mem0 update <memory-id>
```
### `mem0 delete`
Delete a single memory, all memories for a scope, or an entire entity.
```bash
# Delete a single memory
mem0 delete <memory-id>
# Delete all memories for a user
mem0 delete --all --user-id alice --force
# Delete all memories project-wide
mem0 delete --all --project --force
# Preview what would be deleted
mem0 delete --all --user-id alice --dry-run
```
| Flag | Description |
|------|-------------|
| `--all` | Delete all memories matching scope filters |
| `--entity` | Delete the entity and all its memories |
| `--project` | With `--all`: delete all memories project-wide |
| `--dry-run` | Preview without deleting |
| `--force` | Skip confirmation prompt |
### `mem0 import`
Bulk import memories from a JSON file.
```bash
mem0 import data.json --user-id alice
```
The file should be a JSON array where each item has a `memory` (or `text` or `content`) field and optional `user_id`, `agent_id`, and `metadata` fields.
### `mem0 config`
View or modify the local CLI configuration.
```bash
mem0 config show # Display current config (secrets redacted)
mem0 config get api_key # Get a specific value
mem0 config set user_id bob # Set a value
```
### `mem0 entity`
List or delete entities (users, agents, apps, runs).
```bash
mem0 entity list users
mem0 entity list agents --output json
mem0 entity delete --user-id alice --force
```
### `mem0 event`
Inspect background processing events created by async operations (e.g. bulk deletes, large add jobs).
```bash
# List recent events
mem0 event list
# Check the status of a specific event
mem0 event status <event-id>
```
| Flag | Description |
|------|-------------|
| `-o, --output` | Output format: `text`, `json` |
### `mem0 status`
Verify your API connection and display the current project.
```bash
mem0 status
```
### `mem0 version`
Print the CLI version.
```bash
mem0 version
```
## Agent mode
Pass `--agent` (or its alias `--json`) as a **global flag** on any command to get output designed for AI agent tool loops:
```bash
mem0 --agent search "user preferences" --user-id alice
mem0 --agent add "User prefers dark mode" --user-id alice
mem0 --agent list --user-id alice
mem0 --agent delete --all --user-id alice --force
```
Every command returns the same envelope shape:
```json
{
"status": "success",
"command": "search",
"duration_ms": 134,
"scope": { "user_id": "alice" },
"count": 2,
"data": [
{ "id": "abc-123", "memory": "User prefers dark mode", "score": 0.97, "created_at": "2026-01-15", "categories": ["preferences"] }
]
}
```
What agent mode does differently from `--output json`:
- **Sanitized `data`**: only the fields an agent needs (id, memory, score, etc.) — no internal API noise
- **No human output**: spinners, colors, and banners are suppressed entirely
- **Errors as JSON**: errors go to stdout as `{"status": "error", "command": "...", "error": "..."}` with a non-zero exit code
Use `mem0 help --json` to get the full command tree as JSON — useful for agents that need to self-discover available commands.
## Output formats
Control how results are displayed with `--output`:
| Format | Description |
|--------|-------------|
| `text` | Human-readable with colors and formatting (default) |
| `json` | Structured JSON for piping to `jq` (raw API response) |
| `table` | Tabular format (default for `list`) |
| `quiet` | Minimal — just IDs or status codes |
| `agent` | Structured JSON envelope with sanitized fields (set by `--agent`/`--json`) |
## Global flags
These flags are available on all commands:
| Flag | Description |
|------|-------------|
| `--json` | Enable agent mode: structured JSON envelope output, no colors or spinners |
| `--agent` | Alias for `--json` |
| `--api-key` | Override the configured API key for this request |
| `--base-url` | Override the configured API base URL for this request |
| `-o, --output` | Set the output format |
## Environment variables
| Variable | Description |
|----------|-------------|
| `MEM0_API_KEY` | API key (overrides config file) |
| `MEM0_BASE_URL` | API base URL |
| `MEM0_USER_ID` | Default user ID |
| `MEM0_AGENT_ID` | Default agent ID |
| `MEM0_APP_ID` | Default app ID |
| `MEM0_RUN_ID` | Default run ID |
| `MEM0_ENABLE_GRAPH` | Enable graph memory (`true` / `false`) |
Environment variables take precedence over values in the config file, which take precedence over defaults.
## Development
```bash
cd cli/python
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Run during development
python -m mem0_cli --help
mem0 add "test memory" --user-id alice
```
## Releasing
1. Update `version` in `pyproject.toml`
2. Create a GitHub Release with tag `cli-v<version>` (e.g. `cli-v0.2.1`)
For a pre-release, use a beta version like `0.2.1b1` and check the **pre-release** checkbox.
## Documentation
Full documentation is available at [docs.mem0.ai/platform/cli](https://docs.mem0.ai/platform/cli).
## License
Apache-2.0
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0-cli"
version = "0.1.0"
version = "0.2.10"
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.1.0"
__version__ = "0.2.10"
+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
+369 -57
View File
@@ -2,7 +2,10 @@
from __future__ import annotations
import contextlib
import json as _json
import os
import stat as _stat_mod
import sys
from pathlib import Path
@@ -10,7 +13,7 @@ import typer
from rich.console import Console
from mem0_cli import __version__
from mem0_cli.branding import BRAND_COLOR, print_error
from mem0_cli.branding import BRAND_COLOR, print_error, print_warning
console = Console()
err_console = Console(stderr=True)
@@ -43,7 +46,52 @@ entity_app = typer.Typer(
no_args_is_help=True,
rich_markup_mode="rich",
)
# entity_app registered after Memory commands to control panel ordering
event_app = typer.Typer(
name="event",
help="Inspect background processing events.",
no_args_is_help=True,
rich_markup_mode="rich",
)
# entity_app and event_app registered after Memory commands to control panel ordering
# ── Validated user identity (set by _get_backend_and_config) ──────────────
_validated_user_email: str | None = None
# ── Telemetry helper ─────────────────────────────────────────────────────
def _fire_telemetry(command_name: str, extra: dict | None = None) -> None:
"""Fire a PostHog telemetry event (non-blocking, never fails)."""
try:
from mem0_cli.telemetry import capture_event
props = {"command": command_name}
if extra:
props.update(extra)
capture_event(f"cli.{command_name}", props, pre_resolved_email=_validated_user_email)
except Exception:
pass
@config_app.callback(invoke_without_command=True)
def _config_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"config.{ctx.invoked_subcommand}")
@entity_app.callback(invoke_without_command=True)
def _entity_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"entity.{ctx.invoked_subcommand}")
@event_app.callback(invoke_without_command=True)
def _event_callback(ctx: typer.Context) -> None:
if ctx.invoked_subcommand:
_fire_telemetry(f"event.{ctx.invoked_subcommand}")
# ── Helpers ───────────────────────────────────────────────────────────────
@@ -53,9 +101,16 @@ def _get_backend_and_config(
api_key: str | None = None,
base_url: str | None = None,
):
"""Build and return the Platform backend plus the loaded config."""
"""Build and return the Platform backend plus the loaded config.
Validates the API key upfront via ``/v1/ping/`` and caches the
resolved user email for telemetry.
"""
global _validated_user_email
from mem0_cli.backend import get_backend
from mem0_cli.config import load_config
from mem0_cli.backend.platform import AuthError
from mem0_cli.config import load_config, save_config
config = load_config()
@@ -72,7 +127,29 @@ def _get_backend_and_config(
)
raise typer.Exit(1)
return get_backend(config), config
backend = get_backend(config)
# Validate the API key upfront with a fast timeout
try:
ping_data = backend.ping(timeout=5.0)
email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
if email:
_validated_user_email = email
if config.platform.user_email != email:
config.platform.user_email = email
with contextlib.suppress(Exception):
save_config(config)
except AuthError:
print_error(
err_console,
"Invalid or expired API key.",
hint="Run 'mem0 init' or set MEM0_API_KEY environment variable.",
)
raise typer.Exit(1) from None
except Exception:
print_warning(err_console, "Could not validate API key (network issue). Proceeding anyway.")
return backend, config
def _get_backend(
@@ -114,9 +191,22 @@ def _resolve_ids(
}
def _stdin_is_piped() -> bool:
"""Return True only when stdin is an actual pipe or file redirect — not a bare open fd."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return False
try:
mode = os.fstat(sys.stdin.fileno()).st_mode
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
except Exception:
return False
def _read_stdin() -> str | None:
"""Read from stdin if it is piped (not a TTY)."""
if not sys.stdin.isatty():
"""Read from stdin if it is an actual pipe or file redirect (not a TTY, not agent mode)."""
if _stdin_is_piped():
return sys.stdin.read().strip() or None
return None
@@ -128,12 +218,34 @@ def _read_stdin() -> str | None:
def main_callback(
ctx: typer.Context,
version: bool = typer.Option(False, "--version", help="Show version and exit."),
json_agent: bool = typer.Option(
False,
"--json",
"--agent",
help="Output as JSON for agent/programmatic use.",
is_eager=False,
),
) -> None:
if json_agent:
from mem0_cli.state import set_agent_mode
set_agent_mode(True)
if version:
from mem0_cli.commands.utils import cmd_version
_fire_telemetry("version")
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)
# ── Memory: add ───────────────────────────────────────────────────────────
@@ -163,8 +275,6 @@ def add(
categories: str | None = typer.Option(
None, "--categories", help="Categories (JSON array or comma-separated)."
),
graph: bool = typer.Option(False, "--graph", help="Enable graph memory extraction."),
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph memory extraction."),
output: str = typer.Option(
"text", "--output", "-o", help="Output format: text, json, quiet.", rich_help_panel="Output"
),
@@ -191,13 +301,6 @@ def add(
backend, config = _get_backend_and_config(api_key, base_url)
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
if no_graph:
graph_enabled = False
elif graph:
graph_enabled = True
else:
graph_enabled = config.defaults.enable_graph
cmd_add(
backend,
text,
@@ -209,7 +312,6 @@ def add(
no_infer=no_infer,
expires=expires,
categories=categories,
enable_graph=graph_enabled,
output=output,
)
@@ -253,8 +355,6 @@ def search(
help="Specific fields to return (comma-separated).",
rich_help_panel="Search",
),
graph: bool = typer.Option(False, "--graph", help="Enable graph in search.", rich_help_panel="Search"),
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in search.", rich_help_panel="Search"),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -269,7 +369,7 @@ def search(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""Search memories by semantic query.
"""Query your memory store — semantic, keyword, or hybrid retrieval.
Examples:
mem0 search "preferences" --user-id alice
@@ -281,20 +381,13 @@ def search(
# STEP 7: stdin fallback for query
if query is None:
query = _read_stdin()
if query is None:
print_error(err_console, "No query provided. Pass a query argument or pipe via stdin.")
if not query or not query.strip():
print_error(err_console, "Search query cannot be empty.")
raise typer.Exit(1)
backend, config = _get_backend_and_config(api_key, base_url)
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
if no_graph:
graph_enabled = False
elif graph:
graph_enabled = True
else:
graph_enabled = config.defaults.enable_graph
cmd_search(
backend,
query,
@@ -305,7 +398,6 @@ def search(
keyword=keyword,
filter_json=filter_json,
fields=fields,
enable_graph=graph_enabled,
output=output,
)
@@ -372,8 +464,6 @@ def list_cmd(
before: str | None = typer.Option(
None, "--before", help="Created before (YYYY-MM-DD).", rich_help_panel="Filters"
),
graph: bool = typer.Option(False, "--graph", help="Enable graph in listing.", rich_help_panel="Filters"),
no_graph: bool = typer.Option(False, "--no-graph", help="Disable graph in listing.", rich_help_panel="Filters"),
output: str = typer.Option(
"table", "--output", "-o", help="Output: text, json, table.", rich_help_panel="Output"
),
@@ -399,13 +489,6 @@ def list_cmd(
backend, config = _get_backend_and_config(api_key, base_url)
ids = _resolve_ids(config, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
if no_graph:
graph_enabled = False
elif graph:
graph_enabled = True
else:
graph_enabled = config.defaults.enable_graph
cmd_list(
backend,
**ids,
@@ -414,7 +497,6 @@ def list_cmd(
category=category,
after=after,
before=before,
enable_graph=graph_enabled,
output=output,
)
@@ -463,11 +545,19 @@ def update(
@app.command(rich_help_panel="Memory")
def delete(
memory_id: str | None = typer.Argument(None, help="Memory ID to delete (omit when using --all or --entity)."),
memory_id: str | None = typer.Argument(
None, help="Memory ID to delete (omit when using --all or --entity)."
),
all_: bool = typer.Option(False, "--all", help="Delete all memories matching scope filters."),
entity: bool = typer.Option(False, "--entity", help="Delete the entity itself and all its memories (cascade)."),
project: bool = typer.Option(False, "--project", help="With --all: delete ALL memories project-wide."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
entity: bool = typer.Option(
False, "--entity", help="Delete the entity itself and all its memories (cascade)."
),
project: bool = typer.Option(
False, "--project", help="With --all: delete ALL memories project-wide."
),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Scope to user.", rich_help_panel="Scope"
@@ -522,12 +612,14 @@ def delete(
# ── Dispatch ─────────────────────────────────────────────────────
if memory_id is not None:
_fire_telemetry("delete", {"delete_mode": "single"})
from mem0_cli.commands.memory import cmd_delete
backend = _get_backend(api_key, base_url)
cmd_delete(backend, memory_id, dry_run=dry_run, force=force, output=output)
elif all_:
_fire_telemetry("delete", {"delete_mode": "all"})
from mem0_cli.commands.memory import cmd_delete_all
backend, config = _get_backend_and_config(api_key, base_url)
@@ -535,6 +627,7 @@ def delete(
cmd_delete_all(backend, force=force, dry_run=dry_run, all_=project, **ids, output=output)
else: # --entity
_fire_telemetry("delete", {"delete_mode": "entity"})
from mem0_cli.commands.entities import cmd_entities_delete
backend = _get_backend(api_key, base_url)
@@ -641,14 +734,12 @@ def entity_delete(
agent_id: str | None = typer.Option(
None, "--agent-id", help="Agent ID.", rich_help_panel="Scope"
),
app_id: str | None = typer.Option(
None, "--app-id", help="App ID.", rich_help_panel="Scope"
),
run_id: str | None = typer.Option(
None, "--run-id", help="Run ID.", rich_help_panel="Scope"
),
app_id: str | None = typer.Option(None, "--app-id", help="App ID.", rich_help_panel="Scope"),
run_id: str | None = typer.Option(None, "--run-id", help="Run ID.", rich_help_panel="Scope"),
force: bool = typer.Option(False, "--force", help="Skip confirmation."),
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting."),
dry_run: bool = typer.Option(
False, "--dry-run", help="Show what would be deleted without deleting."
),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json, quiet.", rich_help_panel="Output"
),
@@ -688,23 +779,198 @@ def entity_delete(
app.add_typer(entity_app, name="entity", rich_help_panel="Management")
# ── Event subcommands ─────────────────────────────────────────────────────
@event_app.command("list")
def event_list(
output: str = typer.Option(
"table", "--output", "-o", help="Output: table, json.", rich_help_panel="Output"
),
api_key: str | None = typer.Option(
None,
"--api-key",
help="Override API key.",
envvar="MEM0_API_KEY",
rich_help_panel="Connection",
),
base_url: str | None = typer.Option(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""List recent background processing events.
Examples:
mem0 event list
mem0 event list -o json
"""
from mem0_cli.commands.events_cmd import cmd_event_list
backend = _get_backend(api_key, base_url)
cmd_event_list(backend, output=output)
@event_app.command("status")
def event_status(
event_id: str = typer.Argument(..., help="Event ID to inspect."),
output: str = typer.Option(
"text", "--output", "-o", help="Output: text, json.", rich_help_panel="Output"
),
api_key: str | None = typer.Option(
None,
"--api-key",
help="Override API key.",
envvar="MEM0_API_KEY",
rich_help_panel="Connection",
),
base_url: str | None = typer.Option(
None, "--base-url", help="Override API base URL.", rich_help_panel="Connection"
),
) -> None:
"""Check the status of a specific background event.
Examples:
mem0 event status <event-id>
mem0 event status <event-id> -o json
"""
from mem0_cli.commands.events_cmd import cmd_event_status
backend = _get_backend(api_key, base_url)
cmd_event_status(backend, event_id, output=output)
# ── Event subgroup ──
app.add_typer(event_app, name="event", rich_help_panel="Management")
# ── Management commands ───────────────────────────────────────────────────
@app.command(rich_help_panel="Management")
def init(
api_key: str | None = typer.Option(None, "--api-key", help="API key (skip prompt)."),
user_id: str | None = typer.Option(None, "--user-id", "-u", help="Default user ID (skip prompt)."),
user_id: str | None = typer.Option(
None, "--user-id", "-u", help="Default user ID (skip prompt)."
),
email: str | None = typer.Option(None, "--email", help="Login via email verification code."),
code: str | None = typer.Option(
None, "--code", help="Verification code (use with --email for non-interactive login)."
),
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.
Examples:
mem0 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)
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)
@@ -743,7 +1009,6 @@ def status(
)
@app.command("import", rich_help_panel="Management")
def import_cmd(
file_path: str = typer.Argument(..., help="JSON file to import."),
@@ -810,7 +1075,7 @@ def _build_help_json() -> dict:
},
},
"search": {
"description": "Search memories by semantic query.",
"description": "Query your memory store — semantic, keyword, or hybrid retrieval.",
"usage": "mem0 search <query> [OPTIONS]",
"arguments": {"query": {"description": "Search query.", "required": False}},
"options": {
@@ -914,6 +1179,24 @@ def _build_help_json() -> dict:
"value": {"description": "Value to set.", "required": True},
},
},
"event": {
"description": "Inspect background processing events.",
"subcommands": {
"list": {
"description": "List recent background processing events.",
"usage": "mem0 event list [OPTIONS]",
"options": {"--output, -o": "Output format: table, json."},
},
"status": {
"description": "Check the status of a specific background event.",
"usage": "mem0 event status <event_id> [OPTIONS]",
"arguments": {
"event_id": {"description": "Event ID to inspect.", "required": True}
},
"options": {"--output, -o": "Output format: text, json."},
},
},
},
"entity": {
"description": "Manage entities.",
"subcommands": {
@@ -965,6 +1248,7 @@ def _build_help_json() -> dict:
"global_options": {
"--api-key": "Override API key (env: MEM0_API_KEY).",
"--base-url": "Override API base URL.",
"--json / --agent": "Output as JSON for agent/programmatic use.",
"--help": "Show help for a command.",
"--version": "Show version and exit.",
},
@@ -994,7 +1278,7 @@ def help(
console.print("Usage: mem0 <command> [OPTIONS]\n")
console.print("[bold]Commands:[/]")
console.print(" add Add a memory from text, messages, file, or stdin")
console.print(" search Search memories by semantic query")
console.print(" search Query your memory store (semantic, keyword, hybrid)")
console.print(" get Get a specific memory by ID")
console.print(" list List memories with optional filters")
console.print(" update Update a memory's text or metadata")
@@ -1002,6 +1286,7 @@ def help(
console.print(" import Import memories from a JSON file")
console.print(" config Manage configuration (show, get, set)")
console.print(" entity Manage entities (list, delete)")
console.print(" event Inspect background events (list, status)")
console.print(" init Interactive setup wizard")
console.print(" status Check connectivity and authentication")
console.print()
@@ -1018,4 +1303,31 @@ app.add_typer(config_app, name="config", rich_help_panel="Management")
def main() -> None:
app()
import sys
# Allow --json/--agent anywhere in the command line (not just before subcommand).
# 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 argv_rest if a not in _global_flags]
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")
+5 -3
View File
@@ -26,7 +26,6 @@ class Backend(ABC):
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
enable_graph: bool = False,
) -> dict: ...
@abstractmethod
@@ -44,7 +43,6 @@ class Backend(ABC):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
enable_graph: bool = False,
) -> list[dict]: ...
@abstractmethod
@@ -63,7 +61,6 @@ class Backend(ABC):
category: str | None = None,
after: str | None = None,
before: str | None = None,
enable_graph: bool = False,
) -> list[dict]: ...
@abstractmethod
@@ -104,6 +101,11 @@ class Backend(ABC):
@abstractmethod
def entities(self, entity_type: str) -> list[dict]: ...
@abstractmethod
def list_events(self) -> list[dict]: ...
@abstractmethod
def get_event(self, event_id: str) -> dict: ...
def get_backend(config: Mem0Config) -> Backend:
+99 -42
View File
@@ -3,13 +3,19 @@
from __future__ import annotations
from typing import Any
from urllib.parse import quote
import httpx
from mem0_cli import __version__
from mem0_cli.backend.base import Backend
from mem0_cli.config import PlatformConfig
def _encode_path_segment(value: Any) -> str:
return quote(str(value), safe="")
class PlatformBackend(Backend):
"""Backend that talks to the mem0 Platform API."""
@@ -21,11 +27,17 @@ class PlatformBackend(Backend):
headers={
"Authorization": f"Token {config.api_key}",
"Content-Type": "application/json",
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
"X-Mem0-Client-Version": __version__,
},
timeout=30.0,
)
def _request(self, method: str, path: str, **kwargs: Any) -> Any:
from mem0_cli.state import 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)
if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
@@ -41,7 +53,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,
@@ -57,7 +88,6 @@ class PlatformBackend(Backend):
infer: bool = True,
expires: str | None = None,
categories: list[str] | None = None,
enable_graph: bool = False,
) -> dict:
payload: dict[str, Any] = {}
@@ -84,10 +114,9 @@ class PlatformBackend(Backend):
payload["expiration_date"] = expires
if categories:
payload["categories"] = categories
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
return self._request("POST", "/v1/memories/", json=payload)
return self._request("POST", "/v3/memories/add/", json=payload)
def _build_filters(
self,
@@ -98,7 +127,7 @@ class PlatformBackend(Backend):
run_id: str | None = None,
extra_filters: dict | None = None,
) -> dict | None:
"""Build a filters dict for v2 API endpoints.
"""Build a filters dict for v3 API endpoints.
Entity IDs are ANDed (all provided IDs must match).
Extra filters (date ranges, categories) are also ANDed.
@@ -144,7 +173,6 @@ class PlatformBackend(Backend):
keyword: bool = False,
filters: dict | None = None,
fields: list[str] | None = None,
enable_graph: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {"query": query, "top_k": top_k, "threshold": threshold}
@@ -163,10 +191,9 @@ class PlatformBackend(Backend):
payload["keyword_search"] = True
if fields:
payload["fields"] = fields
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/search/", json=payload)
result = self._request("POST", "/v3/memories/search/", json=payload)
return (
result
if isinstance(result, list)
@@ -174,7 +201,11 @@ class PlatformBackend(Backend):
)
def get(self, memory_id: str) -> dict:
return self._request("GET", f"/v1/memories/{memory_id}/")
return self._request(
"GET",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
def list_memories(
self,
@@ -188,12 +219,11 @@ class PlatformBackend(Backend):
category: str | None = None,
after: str | None = None,
before: str | None = None,
enable_graph: bool = False,
) -> list[dict]:
payload: dict[str, Any] = {}
params = {"page": str(page), "page_size": str(page_size)}
# Build filters for v2 API — entity IDs and date filters go inside "filters"
# Build filters — entity IDs and date filters go inside "filters"
extra: dict[str, Any] = {}
if category:
extra["categories"] = {"contains": category}
@@ -211,10 +241,9 @@ class PlatformBackend(Backend):
)
if api_filters:
payload["filters"] = api_filters
if enable_graph:
payload["enable_graph"] = True
payload["source"] = "CLI"
result = self._request("POST", "/v2/memories/", json=payload, params=params)
result = self._request("POST", "/v3/memories/", json=payload, params=params)
return (
result
if isinstance(result, list)
@@ -229,7 +258,12 @@ class PlatformBackend(Backend):
payload["text"] = content
if metadata:
payload["metadata"] = metadata
return self._request("PUT", f"/v1/memories/{memory_id}/", json=payload)
payload["source"] = "CLI"
return self._request(
"PUT",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
json=payload,
)
def delete(
self,
@@ -242,7 +276,7 @@ class PlatformBackend(Backend):
run_id: str | None = None,
) -> dict:
if all:
params: dict[str, str] = {}
params: dict[str, str] = {"source": "CLI"}
if user_id:
params["user_id"] = user_id
if agent_id:
@@ -253,7 +287,11 @@ class PlatformBackend(Backend):
params["run_id"] = run_id
return self._request("DELETE", "/v1/memories/", params=params)
elif memory_id:
return self._request("DELETE", f"/v1/memories/{memory_id}/")
return self._request(
"DELETE",
f"/v1/memories/{_encode_path_segment(memory_id)}/",
params={"source": "CLI"},
)
else:
raise ValueError("Either memory_id or --all is required")
@@ -265,18 +303,41 @@ class PlatformBackend(Backend):
app_id: str | None = None,
run_id: str | None = None,
) -> dict:
params: dict[str, str] = {}
if user_id:
params["user_id"] = user_id
if agent_id:
params["agent_id"] = agent_id
if app_id:
params["app_id"] = app_id
if run_id:
params["run_id"] = run_id
if not params:
# v2 endpoint: DELETE /v2/entities/{entity_type}/{entity_id}/
type_map = {
"user": user_id,
"agent": agent_id,
"app": app_id,
"run": run_id,
}
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.")
return self._request("DELETE", "/v1/entities/", params=params)
# 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():
results[entity_type] = self._request(
"DELETE",
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/",
params={"source": "CLI"},
)
return results
def ping(self, timeout: float | None = None) -> dict:
"""Call the ping endpoint and return the raw response.
When *timeout* is given it overrides the client-level timeout so that
validation pings can fail fast without blocking the user.
"""
if timeout is not None:
resp = self._client.get("/v1/ping/", timeout=timeout)
if resp.status_code == 401:
raise AuthError("Authentication failed. Your API key may be invalid or expired.")
resp.raise_for_status()
return resp.json()
return self._request("GET", "/v1/ping/")
def status(
self,
@@ -284,19 +345,9 @@ class PlatformBackend(Backend):
user_id: str | None = None,
agent_id: str | None = None,
) -> dict[str, Any]:
"""Check connectivity by making a lightweight API call."""
"""Check connectivity using the ping endpoint."""
try:
# If entity IDs are available, validate with a minimal memories list
if user_id or agent_id:
payload: dict[str, Any] = {}
params = {"page": "1", "page_size": "1"}
api_filters = self._build_filters(user_id=user_id, agent_id=agent_id)
if api_filters:
payload["filters"] = api_filters
self._request("POST", "/v2/memories/", json=payload, params=params)
else:
# No entity IDs — use entities endpoint to validate API key
self._request("GET", "/v1/entities/")
self.ping()
return {"connected": True, "backend": "platform", "base_url": self.base_url}
except Exception as e:
return {"connected": False, "backend": "platform", "error": str(e)}
@@ -311,6 +362,12 @@ class PlatformBackend(Backend):
items = [e for e in items if e.get("type", "").lower() == target_type]
return items
def list_events(self) -> list[dict]:
result = self._request("GET", "/v1/events/")
return result if isinstance(result, list) else result.get("results", [])
def get_event(self, event_id: str) -> dict:
return self._request("GET", f"/v1/event/{_encode_path_segment(event_id)}/")
class AuthError(Exception):
+52 -4
View File
@@ -43,6 +43,10 @@ def _sym(fancy: str, plain: str) -> str:
def print_banner(console: Console) -> None:
"""Print the mem0 welcome banner."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
logo_text = Text(LOGO, style=f"bold {BRAND_COLOR}")
tagline = Text(f" {TAGLINE}\n", style=f"{ACCENT_COLOR}")
@@ -61,23 +65,50 @@ def print_banner(console: Console) -> None:
def print_success(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("✓", "[ok]")
console.print(f"[{SUCCESS_COLOR}]{sym}[/] {message}")
def print_error(console: Console, message: str, hint: str | None = None) -> None:
from mem0_cli.state import get_current_command, is_agent_mode
if is_agent_mode():
import json as _json
envelope = {
"status": "error",
"command": get_current_command(),
"error": message,
"data": 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:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("⚠", "[warn]")
console.print(f"[{WARNING_COLOR}]{sym}[/] {message}")
def print_info(console: Console, message: str) -> None:
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
sym = _sym("◆", "*")
console.print(f"[{BRAND_COLOR}]{sym}[/] {message}")
@@ -89,7 +120,9 @@ def timed_status(console: Console, message: str):
The spinner and timing output are sent to stderr (via ``_err``) so they
never contaminate machine-readable stdout. The *console* parameter is
kept for backward compatibility but is not used for spinner output.
In agent mode the spinner is suppressed entirely.
"""
from mem0_cli.state import is_agent_mode
class _Ctx:
def __init__(self):
@@ -97,6 +130,13 @@ def timed_status(console: Console, message: str):
self.error_msg = ""
ctx = _Ctx()
if is_agent_mode():
try:
yield ctx
except Exception:
raise
return
start = time.perf_counter()
try:
with Status(f"[{DIM_COLOR}]{message}[/]", console=_err):
@@ -105,6 +145,11 @@ def timed_status(console: Console, message: str):
elapsed = time.perf_counter() - start
if ctx.error_msg:
print_error(_err, f"{ctx.error_msg} ({elapsed:.2f}s)")
if "Authentication failed" in ctx.error_msg:
_err.print(
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key"
f" · [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
)
raise
else:
elapsed = time.perf_counter() - start
@@ -114,11 +159,14 @@ def timed_status(console: Console, message: str):
def print_scope(console: Console, **ids: str | None) -> None:
"""Show active entity scope if any IDs are set."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return
parts = []
for key, val in ids.items():
if val:
label = key.replace("_", " ").replace("id", "ID").strip()
parts.append(f"{label}={val}")
parts.append(f"{key}={val}")
if parts:
scope_str = ", ".join(parts)
console.print(f" [{DIM_COLOR}]Scope: {scope_str}[/]")
@@ -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}")
+33 -14
View File
@@ -20,12 +20,17 @@ err_console = Console(stderr=True)
def cmd_config_show(*, output: str = "text") -> None:
"""Display current configuration (secrets redacted)."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config show")
if is_agent_mode():
output = "agent"
config = load_config()
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_envelope(
console,
command="config show",
data={
@@ -34,7 +39,6 @@ def cmd_config_show(*, output: str = "text") -> None:
"agent_id": config.defaults.agent_id or None,
"app_id": config.defaults.app_id or None,
"run_id": config.defaults.run_id or None,
"enable_graph": config.defaults.enable_graph,
},
"platform": {
"api_key": redact_key(config.platform.api_key),
@@ -68,10 +72,6 @@ def cmd_config_show(*, output: str = "text") -> None:
"defaults.run_id",
config.defaults.run_id or f"[{DIM_COLOR}](not set)[/]",
)
table.add_row(
"defaults.enable_graph",
str(config.defaults.enable_graph).lower(),
)
table.add_row("", "")
# Platform
@@ -84,25 +84,44 @@ def cmd_config_show(*, output: str = "text") -> None:
def cmd_config_get(key: str) -> None:
"""Get a config value."""
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config get")
config = load_config()
value = get_nested_value(config, key)
if value is None:
print_error(err_console, f"Unknown config key: {key}")
return
display_value = (
redact_key(str(value)) if ("api_key" in key or "key" in key.split(".")[-1:]) else str(value)
)
if is_agent_mode():
format_agent_envelope(
console, command="config get", data={"key": key, "value": display_value}
)
else:
# Redact secrets
if "api_key" in key or "key" in key.split(".")[-1:]:
console.print(redact_key(str(value)))
else:
console.print(str(value))
console.print(display_value)
def cmd_config_set(key: str, value: str) -> None:
"""Set a config value."""
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("config set")
config = load_config()
if set_nested_value(config, key, value):
save_config(config)
display = redact_key(value) if "key" in key else value
print_success(console, f"{key} = {display}")
if is_agent_mode():
format_agent_envelope(
console, command="config set", data={"key": key, "value": display}
)
else:
print_success(console, f"{key} = {display}")
else:
print_error(err_console, f"Unknown config key: {key}")
+62 -27
View File
@@ -18,7 +18,7 @@ from mem0_cli.branding import (
print_success,
timed_status,
)
from mem0_cli.output import format_json
from mem0_cli.output import format_agent_envelope, format_json
console = Console()
err_console = Console(stderr=True)
@@ -26,9 +26,16 @@ err_console = Console(stderr=True)
def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> None:
"""List entities of a given type."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("entity list")
if is_agent_mode():
output = "agent"
valid_types = {"users", "agents", "apps", "runs"}
if entity_type not in valid_types:
print_error(err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}")
print_error(
err_console, f"Invalid entity type: {entity_type}. Use: {', '.join(valid_types)}"
)
raise typer.Exit(1)
_start = _time.perf_counter()
@@ -40,6 +47,16 @@ def cmd_entities_list(backend: Backend, entity_type: str, *, output: str) -> Non
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="entity list",
data=results,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
return
@@ -75,39 +92,39 @@ def cmd_entities_delete(
output: str,
) -> None:
"""Delete an entity and all its memories (cascade delete)."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("entity delete")
if is_agent_mode():
output = "agent"
if not force:
print_error(err_console, "Destructive operation requires --force in agent mode.")
raise typer.Exit(1)
if not any([user_id, agent_id, app_id, run_id]):
print_error(err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id.")
print_error(
err_console, "Provide at least one of --user-id, --agent-id, --app-id, --run-id."
)
raise typer.Exit(1)
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope_str = ", ".join(scope_parts)
if dry_run:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
print_info(console, f"Would delete entity {scope} and all its memories.")
print_info(console, f"Would delete entity {scope_str} and all its memories.")
print_info(console, "No changes made (dry run).")
return
if not force:
scope_parts = []
if user_id:
scope_parts.append(f"user={user_id}")
if agent_id:
scope_parts.append(f"agent={agent_id}")
if app_id:
scope_parts.append(f"app={app_id}")
if run_id:
scope_parts.append(f"run={run_id}")
scope = ", ".join(scope_parts)
confirm = typer.confirm(
f"\n \u26a0 Delete entity {scope} AND all its memories? This cannot be undone."
f"\n \u26a0 Delete entity {scope_str} AND all its memories? This cannot be undone."
)
if not confirm:
print_info(console, "Cancelled.")
@@ -127,7 +144,25 @@ def cmd_entities_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
if output == "agent":
format_agent_envelope(
console,
command="entity delete",
data={"deleted": True},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Entity deleted with all memories ({_elapsed:.2f}s)")
@@ -0,0 +1,176 @@
"""Event commands: list and status."""
from __future__ import annotations
import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from mem0_cli.backend.base import Backend
from mem0_cli.branding import (
ACCENT_COLOR,
BRAND_COLOR,
DIM_COLOR,
ERROR_COLOR,
SUCCESS_COLOR,
WARNING_COLOR,
print_info,
timed_status,
)
from mem0_cli.output import format_agent_envelope, format_json
console = Console()
err_console = Console(stderr=True)
_STATUS_STYLE = {
"SUCCEEDED": f"[{SUCCESS_COLOR}]SUCCEEDED[/]",
"PENDING": f"[{ACCENT_COLOR}]PENDING[/]",
"FAILED": f"[{ERROR_COLOR}]FAILED[/]",
"PROCESSING": f"[{WARNING_COLOR}]PROCESSING[/]",
}
def _status_styled(status: str) -> str:
return _STATUS_STYLE.get(status.upper(), status)
def cmd_event_list(backend: Backend, *, output: str = "table") -> None:
"""List recent background events."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("event list")
if is_agent_mode():
output = "agent"
import time as _time
_start = _time.perf_counter()
with timed_status(err_console, "Fetching events...") as _ts:
try:
results = backend.list_events()
except Exception as e:
_ts.error_msg = str(e)
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="event list",
data=results,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
return
if not results:
console.print()
print_info(console, "No events found.")
console.print()
return
table = Table(
border_style=BRAND_COLOR,
header_style=f"bold {ACCENT_COLOR}",
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("Event ID", style="dim", max_width=10, no_wrap=True)
table.add_column("Type", max_width=14)
table.add_column("Status", max_width=12)
table.add_column("Latency", max_width=10, justify="right")
table.add_column("Created", max_width=20)
for ev in results:
ev_id = str(ev.get("id", ""))[:8]
ev_type = str(ev.get("event_type", "—"))
status = str(ev.get("status", "—"))
latency = ev.get("latency")
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
table.add_row(ev_id, ev_type, _status_styled(status), latency_str, created)
console.print()
console.print(table)
console.print(f" [{DIM_COLOR}]{len(results)} event{'s' if len(results) != 1 else ''}[/]")
console.print()
def cmd_event_status(backend: Backend, event_id: str, *, output: str = "text") -> None:
"""Get the status of a specific background event."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("event status")
if is_agent_mode():
output = "agent"
import time as _time
_start = _time.perf_counter()
with timed_status(err_console, "Fetching event...") as _ts:
try:
ev = backend.get_event(event_id)
except Exception as e:
_ts.error_msg = str(e)
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "agent":
format_agent_envelope(
console,
command="event status",
data=ev,
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, ev)
return
status = str(ev.get("status", "—"))
ev_type = str(ev.get("event_type", "—"))
latency = ev.get("latency")
latency_str = f"{latency:.0f}ms" if isinstance(latency, (int, float)) else "—"
created = str(ev.get("created_at", "—"))[:19].replace("T", " ")
updated = str(ev.get("updated_at", "—"))[:19].replace("T", " ")
results = ev.get("results")
lines = []
lines.append(f" [{DIM_COLOR}]Event ID:[/] {event_id}")
lines.append(f" [{DIM_COLOR}]Type:[/] {ev_type}")
lines.append(f" [{DIM_COLOR}]Status:[/] {_status_styled(status)}")
lines.append(f" [{DIM_COLOR}]Latency:[/] {latency_str}")
lines.append(f" [{DIM_COLOR}]Created:[/] {created}")
lines.append(f" [{DIM_COLOR}]Updated:[/] {updated}")
if results:
lines.append("")
lines.append(f" [{DIM_COLOR}]Results ({len(results)}):[/]")
for r in results:
mem_id = str(r.get("id", ""))[:8]
data = r.get("data", {})
memory = data.get("memory", "") if isinstance(data, dict) else str(data)
ev_name = str(r.get("event", ""))
user = str(r.get("user_id", ""))
detail = f"{ev_name} {memory}"
if user:
detail += f" [{DIM_COLOR}](user_id={user})[/]"
lines.append(f" [{SUCCESS_COLOR}]·[/] {detail} [{DIM_COLOR}]({mem_id})[/]")
content = "\n".join(lines)
panel = Panel(
content,
title=f"[{BRAND_COLOR}]Event Status[/]",
title_align="left",
border_style=BRAND_COLOR,
padding=(1, 1),
)
console.print()
console.print(panel)
console.print()
@@ -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}.")
+387 -15
View File
@@ -2,8 +2,11 @@
from __future__ import annotations
import os
import re
import sys
import httpx
import typer
from rich.console import Console
from rich.prompt import Prompt
@@ -16,7 +19,13 @@ from mem0_cli.branding import (
print_info,
print_success,
)
from mem0_cli.config import Mem0Config, save_config
from mem0_cli.config import (
CONFIG_FILE,
DEFAULT_BASE_URL,
Mem0Config,
load_config,
save_config,
)
console = Console()
err_console = Console(stderr=True)
@@ -84,40 +93,391 @@ def _prompt_secret(label: str) -> str:
return "".join(chars)
def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
def _validate_email(email: str) -> None:
"""Exit with an error if *email* doesn't look like a valid address."""
if not _EMAIL_RE.match(email):
print_error(err_console, f"Invalid email address: {email!r}")
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,
base_url: str,
) -> dict:
"""Run the email verification code login flow.
Returns the parsed JSON response from the verify endpoint.
The caller expects at minimum an ``api_key`` field.
"""
url = base_url.rstrip("/")
_source_headers = {
"X-Mem0-Source": "cli",
"X-Mem0-Client-Language": "python",
}
with httpx.Client(timeout=30.0) as client:
# If code is already provided, skip sending — user already has a code
if not code:
# Step 1: Request verification code
resp = client.post(
f"{url}/api/v1/auth/email_code/",
json={"email": email},
headers=_source_headers,
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Failed to send code: {detail}")
raise typer.Exit(1)
print_success(console, "Verification code sent! Check your email.")
# Step 2: Get code from user
if not sys.stdin.isatty():
print_error(
err_console,
"No --code provided and terminal is non-interactive.",
hint="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 3: Verify code
resp = client.post(
f"{url}/api/v1/auth/email_code/verify/",
json={"email": email, "code": code.strip()},
headers=_source_headers,
)
if resp.status_code == 429:
print_error(err_console, "Too many attempts. Try again in a few minutes.")
raise typer.Exit(1)
if resp.status_code != 200:
try:
detail = resp.json().get("error", resp.text)
except Exception:
detail = resp.text
print_error(err_console, f"Verification failed: {detail}")
raise typer.Exit(1)
return resp.json()
def run_init(
*,
api_key: str | None = None,
user_id: str | None = None,
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()
if existing.platform.api_key:
from mem0_cli.config import redact_key
console.print(
f"\n [{BRAND_COLOR}]Existing configuration found[/] "
f"[{DIM_COLOR}](API key: {redact_key(existing.platform.api_key)})[/]"
)
if sys.stdin.isatty():
confirm = typer.confirm(" Overwrite existing config? This cannot be undone.")
if not confirm:
print_info(console, "Cancelled. Use --force to skip this check.")
raise typer.Exit(0)
else:
print_error(
err_console,
"Existing config would be overwritten.",
hint="Use --force to overwrite.",
)
raise typer.Exit(1)
# ── Email login flow ──────────────────────────────────────────────
if email:
if api_key:
print_error(err_console, "Cannot use both --api-key and --email.")
raise typer.Exit(1)
email = email.strip().lower()
_validate_email(email)
print_banner(console)
console.print()
print_info(console, f"Logging in as {email}...\n")
result = _email_login(email, code, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(err_console, "Auth succeeded but no API key was returned. Contact support.")
raise typer.Exit(1)
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"
)
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
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():
if not api_key:
print_error(
err_console,
"Non-interactive terminal detected and --api-key is required.",
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"
# 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)
print_success(console, "Configuration saved to ~/.mem0/config.json")
return
# Non-TTY without full flags -> error
if not sys.stdin.isatty() and (not api_key or not user_id):
print_error(
err_console,
"Non-interactive terminal detected and required flags missing.",
hint="Run: mem0 init --api-key <key> --user-id <id>",
)
raise typer.Exit(1)
print_banner(console)
console.print()
print_info(console, "Welcome! Let's set up your mem0 CLI.\n")
# Use provided flags or prompt
# If no flags at all, ask user how they want to authenticate
if not api_key:
console.print(f" [{BRAND_COLOR}]How would you like to authenticate?[/]")
console.print(f" [{DIM_COLOR}]1.[/] Login with email [{DIM_COLOR}](recommended)[/]")
console.print(f" [{DIM_COLOR}]2.[/] Enter API key manually")
console.print()
choice = Prompt.ask(f" [{BRAND_COLOR}]Choose[/]", choices=["1", "2"], default="1")
if choice == "1":
console.print()
email_addr = Prompt.ask(f" [{BRAND_COLOR}]Email[/]")
if not email_addr:
print_error(err_console, "Email is required.")
raise typer.Exit(1)
email_addr = email_addr.strip().lower()
_validate_email(email_addr)
print_info(console, f"Logging in as {email_addr}...\n")
result = _email_login(email_addr, None, base_url)
api_key_val = result.get("api_key")
if not api_key_val:
print_error(
err_console, "Auth succeeded but no API key was returned. Contact support."
)
raise typer.Exit(1)
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"
)
save_config(config)
console.print()
print_success(console, "Authenticated! Configuration saved to ~/.mem0/config.json")
console.print()
console.print(f" [{DIM_COLOR}]Get started:[/]")
console.print(f' [{DIM_COLOR}] mem0 add "I prefer dark mode"[/]')
console.print(f' [{DIM_COLOR}] mem0 search "preferences"[/]')
console.print()
return
# API key flow
if api_key:
config.platform.api_key = api_key
config.platform.created_via = "api_key"
else:
_setup_platform(config)
@@ -145,7 +505,9 @@ def run_init(*, api_key: str | None = None, user_id: str | None = None) -> None:
def _setup_platform(config: Mem0Config) -> None:
"""Platform setup flow."""
console.print()
console.print(f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys[/]")
console.print(
f" [{DIM_COLOR}]Get your API key at https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/]"
)
console.print()
console.print(f" [{BRAND_COLOR}]API Key[/]: ", end="")
@@ -155,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:
@@ -162,9 +525,10 @@ def _setup_defaults(config: Mem0Config) -> None:
console.print()
print_info(console, "Set default entity IDs (press Enter to skip).\n")
_default_user = os.environ.get("USER") or os.environ.get("USERNAME") or "mem0-cli"
user_id = Prompt.ask(
f" [{BRAND_COLOR}]Default User ID[/] [{DIM_COLOR}](recommended)[/]",
default="mem0-cli",
default=_default_user,
)
if user_id:
config.defaults.user_id = user_id
@@ -184,11 +548,19 @@ def _validate_platform(config: Mem0Config) -> None:
)
if status.get("connected"):
print_success(console, "Connected to mem0 Platform!")
# Cache user_email from ping response for telemetry distinct_id
try:
ping_data = backend.ping()
user_email = ping_data.get("user_email") if isinstance(ping_data, dict) else None
if user_email:
config.platform.user_email = user_email
except Exception:
pass
else:
print_error(
err_console,
f"Could not connect: {status.get('error', 'Unknown error')}",
hint="Check your API key and try again.",
hint="Visit https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python to get a new key, then run mem0 init again.",
)
except Exception as e:
print_error(err_console, f"Connection test failed: {e}")
+229 -27
View File
@@ -3,6 +3,8 @@
from __future__ import annotations
import json
import os
import stat as _stat_mod
import sys
import time as _time
from pathlib import Path
@@ -20,6 +22,7 @@ from mem0_cli.branding import (
)
from mem0_cli.output import (
format_add_result,
format_agent_envelope,
format_json,
format_memories_table,
format_memories_text,
@@ -31,6 +34,19 @@ console = Console()
err_console = Console(stderr=True)
def _stdin_is_piped() -> bool:
"""Return True only when stdin is an actual pipe or file redirect."""
from mem0_cli.state import is_agent_mode
if is_agent_mode():
return False
try:
mode = os.fstat(sys.stdin.fileno()).st_mode
return _stat_mod.S_ISFIFO(mode) or _stat_mod.S_ISREG(mode)
except Exception:
return False
def cmd_add(
backend: Backend,
text: str | None,
@@ -46,10 +62,14 @@ def cmd_add(
no_infer: bool,
expires: str | None,
categories: str | None,
enable_graph: bool = False,
output: str = "text",
) -> None:
"""Add a memory."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("add")
if is_agent_mode():
output = "agent"
msgs = None
content = text
@@ -70,8 +90,8 @@ def cmd_add(
print_error(err_console, f"Invalid JSON in --messages: {e}")
raise typer.Exit(1) from None
# Read from stdin if no text and stdin is piped
elif not content and not sys.stdin.isatty():
# Read from stdin only if stdin is an actual pipe or file redirect
elif not content and _stdin_is_piped():
content = sys.stdin.read().strip()
if not content and not msgs:
@@ -95,6 +115,21 @@ def cmd_add(
except json.JSONDecodeError:
cats = [c.strip() for c in categories.split(",")]
# Validate --expires
if expires:
import re
if not re.match(r"^\d{4}-\d{2}-\d{2}$", expires):
print_error(
err_console, "Invalid date format for --expires. Use YYYY-MM-DD (e.g. 2025-12-31)."
)
raise typer.Exit(1)
from datetime import date
if date.fromisoformat(expires) <= date.today():
print_error(err_console, "--expires date must be in the future.")
raise typer.Exit(1)
with timed_status(err_console, "Adding memory...") as ts:
try:
result = backend.add(
@@ -109,28 +144,69 @@ def cmd_add(
infer=not no_infer,
expires=expires,
categories=cats,
enable_graph=enable_graph,
)
except Exception as e:
ts.error_msg = str(e)
print_error(err_console, str(e))
raise typer.Exit(1) from None
if output == "quiet":
return
# Deduplicate PENDING entries sharing the same event_id across all output modes
results_list = result if isinstance(result, list) else result.get("results", [result])
seen_events: set[str] = set()
deduped: list[dict] = []
for r in results_list:
if r.get("status") == "PENDING":
eid = r.get("event_id", "")
if eid and eid in seen_events:
continue
if eid:
seen_events.add(eid)
deduped.append(r)
# Write back so downstream formatters see deduplicated data
if isinstance(result, dict) and "results" in result:
result = {**result, "results": deduped}
else:
result = deduped
if output == "agent":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="add",
data=deduped,
scope=scope or None,
count=len(deduped),
)
return
if output == "json":
format_add_result(console, result, output)
return
console.print()
print_scope(console, user_id=user_id, agent_id=agent_id, app_id=app_id, run_id=run_id)
# Count results
results = result if isinstance(result, list) else result.get("results", [result])
count = len(results) if results else 0
print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
)
count = len(deduped)
all_pending = count > 0 and all(r.get("status") == "PENDING" for r in deduped)
if all_pending:
print_success(
console,
f"Memory queued — {count} event{'s' if count != 1 else ''} pending",
)
else:
print_success(
console, f"Memory processed — {count} memor{'y' if count == 1 else 'ies'} extracted"
)
format_add_result(console, result, output)
@@ -148,10 +224,14 @@ def cmd_search(
keyword: bool,
filter_json: str | None,
fields: str | None,
enable_graph: bool = False,
output: str = "text",
) -> None:
"""Search memories."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("search")
if is_agent_mode():
output = "agent"
filters = None
if filter_json:
try:
@@ -164,6 +244,13 @@ def cmd_search(
if fields:
field_list = [f.strip() for f in fields.split(",")]
if top_k < 1:
print_error(err_console, "--top-k must be >= 1.")
raise typer.Exit(1)
if not (0.0 <= threshold <= 1.0):
print_error(err_console, "--threshold must be between 0.0 and 1.0.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Searching memories...") as _ts:
try:
@@ -179,18 +266,41 @@ def cmd_search(
keyword=keyword,
filters=filters,
fields=field_list,
enable_graph=enable_graph,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "quiet":
return
if output == "agent":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="search",
data=results,
scope=scope or None,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
return
if output == "json":
format_json(console, results)
elif output == "table":
if results:
format_memories_table(console, results)
format_memories_table(console, results, show_score=True)
print_result_summary(
console, len(results), duration_secs=_elapsed, user_id=user_id, agent_id=agent_id
)
@@ -212,6 +322,11 @@ def cmd_search(
def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
"""Get a specific memory by ID."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("get")
if is_agent_mode():
output = "agent"
with timed_status(err_console, "Fetching memory...") as _ts:
try:
result = backend.get(memory_id)
@@ -219,7 +334,10 @@ def cmd_get(backend: Backend, memory_id: str, *, output: str) -> None:
print_error(err_console, str(e))
raise typer.Exit(1) from None
format_single_memory(console, result, output)
if output == "agent":
format_agent_envelope(console, command="get", data=result)
else:
format_single_memory(console, result, output)
def cmd_list(
@@ -234,10 +352,21 @@ def cmd_list(
category: str | None,
after: str | None,
before: str | None,
enable_graph: bool = False,
output: str = "table",
) -> None:
"""List memories."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("list")
if is_agent_mode():
output = "agent"
if page_size < 1:
print_error(err_console, "--page-size must be >= 1.")
raise typer.Exit(1)
if page < 1:
print_error(err_console, "--page must be >= 1.")
raise typer.Exit(1)
_start = _time.perf_counter()
with timed_status(err_console, "Listing memories...") as _ts:
try:
@@ -251,15 +380,34 @@ def cmd_list(
category=category,
after=after,
before=before,
enable_graph=enable_graph,
)
except Exception as e:
print_error(err_console, str(e))
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json(console, results)
if output == "quiet":
return
if output in ("json", "agent"):
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
format_agent_envelope(
console,
command="list",
data=results,
scope=scope or None,
count=len(results),
duration_ms=int(_elapsed * 1000),
)
elif output == "table":
if results:
format_memories_table(console, results)
@@ -301,6 +449,11 @@ def cmd_update(
output: str,
) -> None:
"""Update a memory."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("update")
if is_agent_mode():
output = "agent"
meta = None
if metadata:
try:
@@ -318,7 +471,14 @@ def cmd_update(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="update",
data=result,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} updated ({_elapsed:.2f}s)")
@@ -333,6 +493,11 @@ def cmd_delete(
output: str,
) -> None:
"""Delete a single memory by ID."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("delete")
if is_agent_mode():
output = "agent"
if dry_run:
# Fetch and display what would be deleted
try:
@@ -353,7 +518,14 @@ def cmd_delete(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete",
data={"id": memory_id, "deleted": True},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
print_success(console, f"Memory {memory_id[:8]} deleted ({_elapsed:.2f}s)")
@@ -372,12 +544,17 @@ def cmd_delete_all(
output: str,
) -> None:
"""Delete all memories matching a scope."""
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("delete-all")
if is_agent_mode():
output = "agent"
if not force:
print_error(err_console, "Destructive operation requires --force in agent mode.")
raise typer.Exit(1)
if all_:
# Project-wide wipe using wildcard entity IDs
if dry_run:
print_info(console, "Would delete ALL memories project-wide.")
print_info(console, "No changes made (dry run).")
return
# Note: --dry-run is ignored here because the API has no count-before-delete endpoint.
if not force:
confirm = typer.confirm(
@@ -402,7 +579,14 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
if output == "agent":
format_agent_envelope(
console,
command="delete-all",
data={"deleted": True, "scope": "project"},
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
@@ -460,7 +644,25 @@ def cmd_delete_all(
raise typer.Exit(1) from None
_elapsed = _time.perf_counter() - _start
if output == "json":
scope = {
k: v
for k, v in {
"user_id": user_id,
"agent_id": agent_id,
"app_id": app_id,
"run_id": run_id,
}.items()
if v
}
if output == "agent":
format_agent_envelope(
console,
command="delete-all",
data={"deleted": True},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
elif output == "json":
format_json(console, result)
elif output != "quiet":
if isinstance(result, dict) and "message" in result:
+30 -9
View File
@@ -35,22 +35,26 @@ def cmd_status(
output: str = "text",
) -> None:
"""Check connectivity and auth."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("status")
if is_agent_mode():
output = "agent"
_start = _time.perf_counter()
with timed_status(err_console, "Checking connection...") as _ts:
result = backend.status(user_id=user_id, agent_id=agent_id)
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
format_agent_envelope(
console,
command="status",
data={
"connected": result.get("connected", False),
"backend": result.get("backend", "?"),
"base_url": result.get("base_url", ""),
"latency_ms": int(_elapsed * 1000),
},
duration_ms=int(_elapsed * 1000),
)
@@ -67,6 +71,14 @@ def cmd_status(
lines.append(f" [{DIM_COLOR}]API URL:[/] {result['base_url']}")
if result.get("error"):
lines.append(f" [{ERROR_COLOR}]Error:[/] {result['error']}")
if "Authentication failed" in str(result["error"]):
lines.append("")
lines.append(
f" [{DIM_COLOR}]Run [bold]mem0 init[/bold] to reconfigure your API key[/]"
)
lines.append(
f" [{DIM_COLOR}]Get a key at [bold]https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cli-python[/bold][/]"
)
lines.append(f" [{DIM_COLOR}]Latency:[/] {_elapsed:.2f}s")
content = "\n".join(lines)
@@ -96,7 +108,12 @@ def cmd_import(
output: str = "text",
) -> None:
"""Import memories from a JSON file."""
from mem0_cli.output import format_json_envelope
from mem0_cli.output import format_agent_envelope
from mem0_cli.state import is_agent_mode, set_current_command
set_current_command("import")
if is_agent_mode():
output = "agent"
try:
data = json.loads(Path(file_path).read_text())
@@ -110,7 +127,9 @@ def cmd_import(
added = 0
failed = 0
_start = _time.perf_counter()
for item in track(data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console):
for item in track(
data, description=f"[{DIM_COLOR}]Importing memories...[/]", console=err_console
):
content = item.get("memory", item.get("text", item.get("content", "")))
if not content:
failed += 1
@@ -127,11 +146,13 @@ def cmd_import(
failed += 1
_elapsed = _time.perf_counter() - _start
if output == "json":
format_json_envelope(
if output in ("json", "agent"):
scope = {k: v for k, v in {"user_id": user_id, "agent_id": agent_id}.items() if v}
format_agent_envelope(
console,
command="import",
data={"added": added, "failed": failed, "duration_s": round(_elapsed, 2)},
data={"added": added, "failed": failed},
scope=scope or None,
duration_ms=int(_elapsed * 1000),
)
return
@@ -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")
+77 -9
View File
@@ -27,6 +27,15 @@ CONFIG_VERSION = 1
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
@@ -35,7 +44,18 @@ class DefaultsConfig:
agent_id: str = ""
app_id: str = ""
run_id: str = ""
enable_graph: bool = False
@dataclass
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
@@ -43,6 +63,19 @@ 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] = {
"api_key": "platform.api_key",
"base_url": "platform.base_url",
"user_email": "platform.user_email",
"user_id": "defaults.user_id",
"agent_id": "defaults.agent_id",
"app_id": "defaults.app_id",
"run_id": "defaults.run_id",
}
def ensure_config_dir() -> Path:
@@ -65,13 +98,23 @@ def load_config() -> Mem0Config:
plat = data.get("platform", {})
config.platform.api_key = plat.get("api_key", "")
config.platform.base_url = plat.get("base_url", DEFAULT_BASE_URL)
config.platform.user_email = plat.get("user_email", "")
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", "")
config.defaults.agent_id = defaults.get("agent_id", "")
config.defaults.app_id = defaults.get("app_id", "")
config.defaults.run_id = defaults.get("run_id", "")
config.defaults.enable_graph = defaults.get("enable_graph", False)
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")
@@ -98,10 +141,6 @@ def load_config() -> Mem0Config:
if env_run_id:
config.defaults.run_id = env_run_id
env_graph = os.environ.get("MEM0_ENABLE_GRAPH")
if env_graph:
config.defaults.enable_graph = env_graph.lower() in ("true", "1", "yes")
return config
@@ -116,11 +155,22 @@ def save_config(config: Mem0Config) -> None:
"agent_id": config.defaults.agent_id,
"app_id": config.defaults.app_id,
"run_id": config.defaults.run_id,
"enable_graph": config.defaults.enable_graph,
},
"platform": {
"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,
},
}
@@ -129,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"""
@@ -140,7 +203,8 @@ def redact_key(key: str) -> str:
def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
"""Get a config value by dotted path, e.g. 'platform.api_key'."""
"""Get a config value by dotted path, e.g. 'platform.api_key' or short form 'api_key'."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts:
@@ -153,6 +217,7 @@ def get_nested_value(config: Mem0Config, dotted_key: str) -> Any:
def set_nested_value(config: Mem0Config, dotted_key: str, value: str) -> bool:
"""Set a config value by dotted path. Returns True on success."""
dotted_key = SHORT_KEY_ALIASES.get(dotted_key, dotted_key)
parts = dotted_key.split(".")
obj: Any = config
for part in parts[:-1]:
@@ -170,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
+150 -14
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])
@@ -49,7 +49,9 @@ def format_memories_text(console: Console, memories: list[dict], title: str = "m
console.print()
def format_memories_table(console: Console, memories: list[dict]) -> None:
def format_memories_table(
console: Console, memories: list[dict], *, show_score: bool = False
) -> None:
"""Render memories in a rich table."""
table = Table(
border_style=BRAND_COLOR,
@@ -57,20 +59,34 @@ def format_memories_table(console: Console, memories: list[dict]) -> None:
row_styles=["", "dim"],
padding=(0, 1),
)
table.add_column("ID", style="dim", max_width=10)
table.add_column("ID", style="dim", max_width=38, no_wrap=True)
if show_score:
table.add_column("Score", max_width=7, justify="right")
table.add_column("Memory", max_width=50, no_wrap=False)
table.add_column("Category", max_width=14)
table.add_column("Created", max_width=12)
for mem in memories:
mem_id = mem.get("id", "")[:8]
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", [])
cat = categories[0] if isinstance(categories, list) and categories else "—"
if isinstance(categories, list) and categories:
cat = (
categories[0]
if len(categories) == 1
else f"{categories[0]} (+{len(categories) - 1})"
)
else:
cat = "—"
created = _format_date(mem.get("created_at")) or "—"
table.add_row(mem_id, memory_text, cat, created)
if show_score:
score = mem.get("score")
score_str = f"{score:.2f}" if score is not None else "—"
table.add_row(mem_id, score_str, memory_text, cat, created)
else:
table.add_row(mem_id, memory_text, cat, created)
console.print()
console.print(table)
@@ -88,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}[/]")
@@ -139,16 +155,23 @@ def format_add_result(console: Console, result: dict | list, output: str = "text
return
console.print()
seen_pending_events: set[str] = set()
for r in results:
# Detect async PENDING response from Platform API
if r.get("status") == "PENDING":
event_id = r.get("event_id", "")[:8]
event_id = r.get("event_id", "")
# Deduplicate PENDING entries with the same event_id
if event_id and event_id in seen_pending_events:
continue
if event_id:
seen_pending_events.add(event_id)
icon = f"[{ACCENT_COLOR}]{_sym('⧗', '...')}[/]"
parts = [f" {icon} [{DIM_COLOR}]{'Queued':<10}[/]"]
parts.append("[white]Processing in background[/]")
if event_id:
parts.append(f"[{DIM_COLOR}](event {event_id})[/]")
console.print(" ".join(parts))
if event_id:
console.print(f" [{DIM_COLOR}] event_id: {event_id}[/]")
console.print(f" [{DIM_COLOR}] → Check status: mem0 event status {event_id}[/]")
continue
event = r.get("event", "ADD")
@@ -206,6 +229,119 @@ 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))
def sanitize_agent_data(command: str, data: Any) -> Any:
"""Project API response data to minimal relevant fields for agent consumption."""
def pick(obj: dict, keys: list) -> dict:
return {k: obj[k] for k in keys if k in obj}
if data is None:
return data
if command == "add":
items = data if isinstance(data, list) else [data]
result = []
for item in items:
if item.get("status") == "PENDING":
result.append(pick(item, ["status", "event_id"]))
else:
result.append(pick(item, ["id", "memory", "event"]))
return result
if command == "search":
return [pick(r, ["id", "memory", "score", "created_at", "categories"]) for r in data]
if command == "list":
return [pick(r, ["id", "memory", "created_at", "categories"]) for r in data]
if command == "get":
return pick(data, ["id", "memory", "created_at", "updated_at", "categories", "metadata"])
if command == "update":
return pick(data, ["id", "memory"])
if command in ("delete", "delete-all", "entity delete"):
return data
if command == "entity list":
result = []
for r in data:
item = pick(r, ["type", "count"])
item["name"] = r.get("name") or r.get("id", "")
result.append(item)
return result
if command == "event list":
return [pick(r, ["id", "event_type", "status", "latency", "created_at"]) for r in data]
if command == "event status":
ev = data
raw_results = ev.get("results") or []
sanitized_results = []
for r in raw_results:
nested = r.get("data") or {}
memory = nested.get("memory") if isinstance(nested, dict) else None
sanitized_results.append(
{
"id": r.get("id"),
"event": r.get("event"),
"user_id": r.get("user_id"),
"memory": memory,
}
)
result = pick(ev, ["id", "event_type", "status", "latency", "created_at", "updated_at"])
result["results"] = sanitized_results
return result
# Pass-through: status, import, config show/get/set
return data
def format_agent_envelope(
console: Console,
*,
command: str,
data: Any,
duration_ms: int | None = None,
scope: dict | None = None,
count: int | None = None,
) -> None:
"""Output structured JSON envelope for agent/programmatic use (--json/--agent mode)."""
envelope: dict[str, Any] = {
"status": "success",
"command": command,
}
if duration_ms is not None:
envelope["duration_ms"] = duration_ms
if scope:
filtered = {k: v for k, v in scope.items() if v}
if filtered:
envelope["scope"] = filtered
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))
@@ -221,7 +357,7 @@ def print_result_summary(
parts = [f"{count} result{'s' if count != 1 else ''}"]
if page is not None:
parts.append(f"page {page}")
scope_parts = [f"{k.replace('_', ' ')}={v}" for k, v in scope_ids.items() if v]
scope_parts = [f"{k}={v}" for k, v in scope_ids.items() if v]
if scope_parts:
parts.append(", ".join(scope_parts))
if duration_secs is not None:
+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
+45
View File
@@ -0,0 +1,45 @@
"""Agent mode state — set by the root callback, read by commands and branding."""
from __future__ import annotations
_agent_mode: bool = False
_current_command: str = ""
_pending_notice: str = ""
def is_agent_mode() -> bool:
return _agent_mode
def set_agent_mode(val: bool) -> None:
global _agent_mode
_agent_mode = val
def get_current_command() -> str:
return _current_command
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
+155
View File
@@ -0,0 +1,155 @@
"""CLI telemetry — anonymous usage tracking via PostHog.
Sends fire-and-forget events to PostHog by spawning a detached subprocess
(telemetry_sender.py). The parent CLI process exits immediately; the
subprocess handles email resolution, caching, and the HTTP POST.
Disable with: MEM0_TELEMETRY=false
"""
from __future__ import annotations
import contextlib
import hashlib
import json
import os
import platform
import subprocess
import sys
import uuid
from typing import Any
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
def _is_telemetry_enabled() -> bool:
val = os.environ.get("MEM0_TELEMETRY", "true").lower()
return val not in ("false", "0", "no")
def _get_or_create_anonymous_id() -> str:
"""Return a persistent per-machine anonymous ID, generating one if needed.
Stored in ~/.mem0/config.json under `telemetry.anonymous_id` so that
repeat runs on the same machine share one PostHog identity instead of
collapsing into a single shared fallback string.
"""
from mem0_cli.config import load_config, save_config
config = load_config()
if config.telemetry.anonymous_id:
return config.telemetry.anonymous_id
new_id = f"cli-anon-{uuid.uuid4().hex}"
config.telemetry.anonymous_id = new_id
with contextlib.suppress(Exception):
save_config(config)
return new_id
def _get_distinct_id() -> str:
"""Return a stable anonymous identifier for the current user.
Priority: cached user_email (from /v1/ping/) > MD5(api_key) >
persistent per-machine anonymous ID.
"""
try:
from mem0_cli.config import load_config
config = load_config()
if config.platform.user_email:
return config.platform.user_email
if config.platform.api_key:
return hashlib.md5(config.platform.api_key.encode()).hexdigest()
except Exception:
pass
try:
return _get_or_create_anonymous_id()
except Exception:
return f"cli-anon-{uuid.uuid4().hex}"
def capture_event(
event_name: str,
properties: dict[str, Any] | None = None,
pre_resolved_email: str | None = None,
) -> None:
"""Fire a PostHog event via a detached subprocess (non-blocking).
When *pre_resolved_email* is provided (e.g. from an upfront ping
validation), it is used directly as the PostHog distinct ID and the
subprocess skips its own ``/v1/ping/`` call.
"""
if not _is_telemetry_enabled():
return
try:
from mem0_cli import __version__
from mem0_cli.config import CONFIG_FILE, load_config, save_config
config = load_config()
distinct_id = pre_resolved_email or _get_distinct_id()
# Detect anonymous → identified transition. If a stored anonymous_id
# exists and we just resolved to a real identity, fire a one-shot
# $identify event so PostHog stitches the pre-signup history onto
# the authenticated profile. Clear the stored id so we don't re-alias.
anon_id_to_alias: str | None = None
if (
distinct_id
and not distinct_id.startswith("cli-anon-")
and config.telemetry.anonymous_id
):
anon_id_to_alias = config.telemetry.anonymous_id
config.telemetry.anonymous_id = ""
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,
"event": event_name,
"properties": {
"source": "CLI",
"language": "python",
"cli_version": __version__,
"agent_mode": bool(config.platform.agent_mode),
"python_version": sys.version,
"os": sys.platform,
"os_version": platform.version(),
"$process_person_profile": False,
"$lib": "posthog-python",
**(properties or {}),
},
}
context = {
"payload": payload,
"posthog_host": POSTHOG_HOST,
"needs_email": not distinct_id or "@" not in distinct_id,
"mem0_api_key": config.platform.api_key or "",
"mem0_base_url": config.platform.base_url or "https://api.mem0.ai",
"config_path": str(CONFIG_FILE),
"anon_distinct_id_to_alias": anon_id_to_alias,
}
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
+119
View File
@@ -0,0 +1,119 @@
"""Standalone telemetry sender — runs as a detached subprocess.
Usage: python -m mem0_cli.telemetry_sender (JSON context is read from stdin;
a single argv argument is still accepted as a legacy fallback)
This module is spawned by telemetry.capture_event() and runs independently
of the parent CLI process. It:
1. Resolves the user's email via /v1/ping/ if not already cached
2. Caches the email in ~/.mem0/config.json for future runs
3. Sends the PostHog event
All errors are silently swallowed — this process must never produce output
or affect the user experience.
"""
from __future__ import annotations
import json
import sys
import urllib.request
def _load_context() -> dict:
"""Load telemetry context from stdin, falling back to argv for compatibility."""
raw = ""
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if not raw and len(sys.argv) > 1:
raw = sys.argv[1]
return json.loads(raw)
def main() -> None:
ctx = _load_context()
payload = ctx["payload"]
if ctx.get("needs_email") and ctx.get("mem0_api_key"):
_resolve_and_cache_email(ctx, payload)
# Fire $identify *after* email resolution so PostHog links the stored
# anonymous id directly to the final identity (email, not the api-key
# hash). The regular event is sent next so it lands under the merged
# profile.
anon_id = ctx.get("anon_distinct_id_to_alias")
if anon_id:
_send_identify_event(ctx, payload, anon_id)
_send_posthog_event(ctx["posthog_host"], payload)
def _send_identify_event(ctx: dict, payload: dict, anon_id: str) -> None:
"""Send a PostHog $identify event aliasing anon_id → payload['distinct_id']."""
identify_payload = {
"api_key": payload["api_key"],
"event": "$identify",
"distinct_id": payload["distinct_id"],
"properties": {
"$anon_distinct_id": anon_id,
"$lib": payload.get("properties", {}).get("$lib", "posthog-python"),
},
}
_send_posthog_event(ctx["posthog_host"], identify_payload)
def _resolve_and_cache_email(ctx: dict, payload: dict) -> None:
"""Call /v1/ping/ to get the user's email, update the payload, and cache it."""
try:
ping_url = ctx["mem0_base_url"].rstrip("/") + "/v1/ping/"
req = urllib.request.Request(
ping_url,
headers={
"Authorization": "Token " + ctx["mem0_api_key"],
"Content-Type": "application/json",
},
)
resp = urllib.request.urlopen(req, timeout=10)
data = json.loads(resp.read())
email = data.get("user_email")
if email:
payload["distinct_id"] = email
_cache_email(ctx.get("config_path"), email)
except Exception:
pass
def _cache_email(config_path: str | None, email: str) -> None:
"""Write user_email into the config file for future runs."""
if not config_path:
return
try:
with open(config_path) as f:
cfg = json.load(f)
cfg.setdefault("platform", {})["user_email"] = email
with open(config_path, "w") as f:
json.dump(cfg, f, indent=2)
except Exception:
pass
def _send_posthog_event(posthog_host: str, payload: dict) -> None:
"""POST the event to PostHog."""
try:
body = json.dumps(payload).encode()
req = urllib.request.Request(
posthog_host,
data=body,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=10)
except Exception:
pass
if __name__ == "__main__":
import contextlib
with contextlib.suppress(Exception):
main()

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