refactor: consolidate agent/editor plugins under integrations/ (#5491)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Kartik
2026-06-12 10:31:35 +05:30
committed by GitHub
co-authored by Claude
parent c676c2c458
commit 2c796d144f
295 changed files with 94 additions and 81 deletions
@@ -0,0 +1,22 @@
{
"name": "mem0",
"version": "0.2.9",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
"email": "support@mem0.ai"
},
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"license": "Apache-2.0",
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
"userConfig": {
"api_key": {
"type": "string",
"title": "Mem0 API Key",
"description": "Your Mem0 Platform API key (starts with m0-). Get one at https://app.mem0.ai/dashboard/api-keys",
"sensitive": true,
"required": true
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mem0": {
"url": "https://mcp.mem0.ai/mcp",
"bearer_token_env_var": "MEM0_API_KEY"
}
}
}
@@ -0,0 +1,35 @@
{
"name": "mem0",
"version": "0.2.9",
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
"email": "support@mem0.ai",
"url": "https://mem0.ai"
},
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"license": "Apache-2.0",
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
"skills": "./skills/",
"mcpServers": "./.codex-mcp.json",
"hooks": "./hooks/codex-hooks.json",
"interface": {
"displayName": "Mem0",
"shortDescription": "Persistent memory layer for AI coding workflows",
"longDescription": "Mem0 adds long-term memory to Codex. Store decisions, user preferences, project context, and session state across conversations. Memories are automatically retrieved via semantic search so Codex always has the right context.",
"developerName": "Mem0",
"category": "Productivity",
"capabilities": ["Read", "Write"],
"websiteURL": "https://mem0.ai",
"privacyPolicyURL": "https://mem0.ai/privacy",
"termsOfServiceURL": "https://mem0.ai/terms",
"defaultPrompt": [
"Search my memories for recent project decisions",
"Remember that I prefer TypeScript over JavaScript",
"What do you know about my coding preferences?"
],
"brandColor": "#FBBF24",
"logo": "./logo.svg"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"mcpServers": {
"mem0": {
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${env:MEM0_API_KEY}"
}
}
}
}
@@ -0,0 +1,17 @@
{
"name": "mem0",
"version": "0.2.9",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
"email": "support@mem0.ai"
},
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"logo": "logo.svg",
"license": "Apache-2.0",
"keywords": ["mem0", "memory", "mcp", "personalization", "semantic-search"],
"skills": "./skills/",
"hooks": "./hooks/cursor-hooks.json",
"mcpServers": ".cursor-mcp.json"
}
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"mem0": {
"type": "http",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${MEM0_API_KEY}"
}
}
}
}
@@ -0,0 +1,47 @@
# Changelog
All notable changes to the `@mem0/opencode-plugin` will be documented in this file.
## 0.1.3 — File-context injection, session summaries & activity timeline
### Added
- **File-context injection (`tool.execute.before` / Read):** Before the agent reads a file, the plugin searches mem0 for memories referencing that file path and injects prior work as system context. Gates on file size (>= 1,500 bytes). Gives the agent "I've worked on this file before" awareness automatically.
- **Stop hook session summary (`experimental.session.compacting`):** Enhanced session compaction to store a structured `session_summary` memory with `infer=True`, letting the mem0 backend AI extract key facts (request, decisions, learnings, next steps). Previously only stored a raw stats string.
- **SessionStart activity timeline:** The initial memory loading now formats recent memories with type icons (⚖️ decision, 🔴 bug_fix, 🔵 task_learning, etc.) and relative age indicators (2h ago, 1d ago) instead of bare text. Provides a visual "Recent Activity" timeline on first message.
### Changed
- **`experimental.session.compacting` handler:** Now stores `metadata.type=session_summary` with `metadata.source=opencode-stop` instead of `metadata.type=session_state` with `metadata.source=pre-compaction`. Includes a structured prompt that instructs mem0's AI to extract request, decisions, learnings, and next steps.
- **Initial context formatting:** Memories shown on first message now include type icons and age labels for quick scanning.
## 0.1.2 — Automatic coding categories & global search
### Added
- **Auto-configured coding categories:** The plugin now automatically sets up 17 coding categories (e.g. `architecture_decisions`, `api_design`, `security`, `debugging_notes`) on the Mem0 project at startup. Runs in the background on every session start via `autoSetupCategories()`, is fully idempotent, and never blocks initialization. Uses SHA-256 fingerprints of the category list and API key — stored in `~/.mem0/categories_setup.json` — to skip redundant API calls on subsequent sessions.
- **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, all `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Applies to all plugin search paths: initial load, per-message recall, resume detection, error-pattern lookup, and compaction context.
- **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed.
- **`MEM0_GLOBAL_SEARCH` environment variable:** Exported to child shells via the `shell.env` hook (`"true"` or `"false"`).
### Changed
- **Search filters are now dynamic:** All search paths throughout the plugin construct filters based on the `global_search` setting instead of always using `AND [user_id, app_id]`.
- **Resume-context searches broadened:** Resume and error-pattern searches no longer include `metadata.type` sub-filters (`session_state`, `decision`, `anti_pattern`, `bug_fix`), broadening recall.
- **System context message updated:** Informs the model when global search is active (`"Global search is ON — searches return all memories across all users and projects. Writes still use user_id=..., app_id=..."`).
- **`/mem0:onboard` Step 5 is no longer interactive:** Removed the manual category installation prompt. Categories now configure automatically in the background; the onboarding step only verifies status and stores a fallback `project_profile` memory if the background run hasn't finished yet.
- **`/mem0:switch-project` skill expanded:** Description and execution updated to document the `--global` and `--no-global` flags alongside the existing project-name argument.
## 0.1.1
- CI/CD publish flow test (`#5288`).
- Fixed tsconfig, added `publishConfig` and bun lockfile (`#5273`).
- Renamed package to `@mem0/opencode-plugin` (`#5272`).
- Added plugin array to bundled `opencode.json` (`#5271`).
## 0.1.0 — Initial release
- **OpenCode plugin** (`@mem0/opencode-plugin` on npm): Pure TypeScript plugin using the `mem0ai` TS SDK — no Python, no shell scripts. Hooks into all 6 OpenCode events (`chat.message`, `tool.execute.before`, `tool.execute.after`, `experimental.chat.system.transform`, `experimental.session.compacting`, `shell.env`). Features: session start memory loading, per-prompt semantic search, error pattern detection with memory lookup, resume/remember intent detection, auto-capture every 3rd message, periodic save nudges, full metadata defaults injection (confidence, source, type, session_id, files, branch), identity injection for search/get/delete filters, type-filtered error pre-fetch (anti_pattern + bug_fix), pre-compaction memory capture, MEMORY.md write blocking, and secret redaction.
- **16 OpenCode-native skills** bundled in `opencode-skills/`: `context-loader`, `dream`, `export`, `forget`, `health`, `import`, `list-projects`, `mem0` (SDK reference), `memory-reviewer`, `onboard`, `peek`, `pin`, `remember`, `stats`, `switch-project`, `tour`. All skills are pure MCP-tool-based — no Python scripts, no shell scripts, no Claude Code dependencies.
- **Auto-install skills and commands (`installSkills()`):** On plugin load, copies all 16 skills to `.opencode/skills/` and creates command wrapper files in `.opencode/commands/` so they appear in the OpenCode `/` palette.
- **CLI installer (`cli.ts`):** `bunx @mem0/opencode-plugin install` auto-configures plugin and MCP server in `~/.config/opencode/opencode.json`.
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2026] [Taranjeet Singh]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,84 @@
# @mem0/opencode-plugin
Persistent memory for [OpenCode](https://opencode.ai). Your agent remembers decisions, preferences, and learnings across sessions automatically.
## Install
```bash
bunx @mem0/opencode-plugin@latest install
```
Or using OpenCode's built-in CLI:
```bash
opencode plugin @mem0/opencode-plugin
```
**Or let your agent do it** — paste this into OpenCode:
```
Install @mem0/opencode-plugin by following https://raw.githubusercontent.com/mem0ai/mem0/main/integrations/mem0-plugin/.opencode-plugin/README.md
```
All commands auto-add the plugin and MCP server to your `~/.config/opencode/opencode.json`. No manual config needed.
Get your API key (free): [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys)
```bash
echo 'export MEM0_API_KEY="m0-your-key"' >> ~/.zshrc && source ~/.zshrc
```
Restart OpenCode.
## What's included
| Component | Description |
|-----------|-------------|
| **MCP Server** | 9 memory tools — add, search, get, update, delete memories |
| **Lifecycle Hooks** | Auto-search on session start and every prompt, metadata enforcement, error memory lookup, compaction context |
| **16 Slash Commands** | `/mem0:remember`, `/mem0:tour`, `/mem0:stats`, `/mem0:health`, `/mem0:dream`, and more |
## Hooks
Pure TypeScript — no Python, no shell scripts. Uses the [mem0ai](https://www.npmjs.com/package/mem0ai) SDK directly.
| Hook | Event | What it does |
|------|-------|-------------|
| **Chat message** | `chat.message` | Loads prior memories on session start, searches relevant memories before each prompt, auto-captures learnings periodically |
| **Pre-tool** | `tool.execute.before` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
| **Post-tool** | `tool.execute.after` | Tracks stats, scans bash errors for related memories |
| **System transform** | `experimental.chat.system.transform` | Injects memory context (session memories, search results, error lookups) into system prompt |
| **Compaction** | `experimental.session.compacting` | Stores session state memory, then injects prior memories into compaction context so nothing is lost |
| **Shell env** | `shell.env` | Exports `MEM0_USER_ID`, `MEM0_APP_ID`, `MEM0_SESSION_ID`, and `MEM0_BRANCH` to shell |
## MCP Tools
| Tool | Description |
|------|-------------|
| `add_memory` | Save text or conversation history |
| `search_memories` | Semantic search across memories |
| `get_memories` | List memories with filters and pagination |
| `get_memory` | Retrieve a specific memory by ID |
| `update_memory` | Overwrite a memory's text by ID |
| `delete_memory` | Delete a single memory by ID |
| `delete_all_memories` | Bulk delete all memories in scope |
| `delete_entities` | Delete an entity and its memories |
| `list_entities` | List users/agents/apps stored in Mem0 |
## Verify
Start OpenCode and ask: *"Search my memories for recent decisions"*
If the `mem0` tools respond, you're all set.
## Troubleshooting
| Problem | Fix |
|---------|-----|
| No tools appearing | Restart OpenCode after installing |
| 401 Unauthorized | `echo $MEM0_API_KEY` must print your `m0-` key |
| Plugin not loading | Run `opencode plugin @mem0/opencode-plugin` again |
## License
Apache-2.0
@@ -0,0 +1,765 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@mem0/opencode-plugin",
"dependencies": {
"@opencode-ai/plugin": "^1.0.162",
"mem0ai": "^3.0.5",
},
"devDependencies": {
"bun-types": ">=1.3.14",
"typescript": "^5.7.3",
},
"peerDependencies": {
"bun": ">=1.0.0",
},
},
},
"packages": {
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.40.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w=="],
"@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="],
"@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="],
"@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="],
"@azure/core-http-compat": ["@azure/core-http-compat@2.4.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2" }, "peerDependencies": { "@azure/core-client": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0" } }, "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA=="],
"@azure/core-paging": ["@azure/core-paging@1.6.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA=="],
"@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.23.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ=="],
"@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="],
"@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="],
"@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="],
"@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="],
"@azure/msal-browser": ["@azure/msal-browser@5.11.0", "", { "dependencies": { "@azure/msal-common": "16.6.2" } }, "sha512-zkGNYS3TwY8lUpPIafAmsFCYZbgFixY9y/LZB9GUg0IILoHTqpN26j5OrkL1AQThh/YdZsawe4iWXfp85lFVxg=="],
"@azure/msal-common": ["@azure/msal-common@16.6.2", "", {}, "sha512-hQjjsekAjB00cM1EmatWJlzhEoK2Qhz7Rj5gvM6tYf8iL7RM3tkxlpU9fG0+ofkulzg9AEEA6dIEnSmDr5ZqUA=="],
"@azure/msal-node": ["@azure/msal-node@5.2.2", "", { "dependencies": { "@azure/msal-common": "16.6.2", "jsonwebtoken": "^9.0.0" } }, "sha512-toS+2AePxqyzb0YOKttDOOiSl3jrkK9aiqIvpurpis0O34QcIS5gToqrgT39p04Dpxw3YoUU0lxJKTpSFFfA6Q=="],
"@azure/search-documents": ["@azure/search-documents@12.2.0", "", { "dependencies": { "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-http-compat": "^2.1.2", "@azure/core-paging": "^1.6.2", "@azure/core-rest-pipeline": "^1.18.0", "@azure/core-tracing": "^1.2.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.1.4", "events": "^3.0.0", "tslib": "^2.8.1" } }, "sha512-4+Qw+qaGqnkdUCq/vEFzk/bkROogTvdbPb1fmI8poxNfDDN1q2WHxBmhI7CYwesrBj1yXC4i5E0aISBxZqZi0g=="],
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260527.1", "", {}, "sha512-GK+C05N4fGptp1uG+pbjC/7Udonz8EhMEYvYFnVKdLLnEGS9nsmVOFi/RLQr7tq9l/0UEzcWjudzeY2ssuqyIA=="],
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
"@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="],
"@jest/expect-utils": ["@jest/expect-utils@29.7.0", "", { "dependencies": { "jest-get-type": "^29.6.3" } }, "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA=="],
"@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
"@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="],
"@langchain/core": ["@langchain/core@1.1.48", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "@standard-schema/spec": "^1.1.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "zod": "^3.25.76 || ^4" } }, "sha512-fQU6Guyb1pwc2fEplmA8FPbKfOMAofjnyJzExevro0FxEiuGHE18Ov/ZHmT9trWCDTZRI9eW1VIc6aChxV8pAQ=="],
"@mistralai/mistralai": ["@mistralai/mistralai@1.15.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-fb995eiz3r0KsBGtRjFV+/iLbX+UpfalxpF+YitT3R6ukrPD4PN+FGwwmYcRFhNAzVzDUtTVxQYnjQWEnwV5nw=="],
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.4.11", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA=="],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
"@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="],
"@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="],
"@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="],
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.15.11", "", { "dependencies": { "@opencode-ai/sdk": "1.15.11", "effect": "4.0.0-beta.66", "zod": "4.1.8" }, "peerDependencies": { "@opentui/core": ">=0.2.15", "@opentui/keymap": ">=0.2.15", "@opentui/solid": ">=0.2.15" }, "optionalPeers": ["@opentui/core", "@opentui/keymap", "@opentui/solid"] }, "sha512-RDvYDCHO0+3OAGD590oDQqtryrENrUW04SjtoA4sgRV2efpZeBFjx3TnonBsXKq6nWqYg172nhltUEZsihGnXQ=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.15.11", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-IyYyDVsO8SKbKbkSadHpDuYnYC+2vmEeLU+rW+rH2M54Sigq6l3gDHno16+U6SRut+lowbph7v/ry3WbV67V3w=="],
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Omj20SuiHBOUjUBIyqtkNjSUIjOtEOJwmbix/ZyFH4BaQ6OZTaaRWIR4TjHVz0yadHgli6lLTiAh1uarnvD49A=="],
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-FFj3QdU/OhlDyZOJ8CWfN5eWLpRlT4qjZg7lMQi7jA6GuoY5ajlO1zWLP/MuHYRSbXQUvV52RejNi8DVnAp13w=="],
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-OSfsTZstc898HHElhU4NccaBGOSSDn5VfahiVTnidZ9B/+wb7WTyfZJaBeJcfjwJ9H2W9uTh2TGtl3UfcXgV9g=="],
"@oven/bun-freebsd-aarch64": ["@oven/bun-freebsd-aarch64@1.3.14", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-LIKrXaFxAHybVO5Pf+9XP2FHUj/5APvXTUKk9dqHm5iFz4oH+W24cmhjkJirNujh9hKeTyrpWSe3no9JZKowIw=="],
"@oven/bun-freebsd-x64": ["@oven/bun-freebsd-x64@1.3.14", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uwD+fGUH1ADpIF3B1U2jWzzb20QwRLZfj5QZ28GUCGrAJ/nTmWrD6YYGsblCY1wuhldRez3lU40AyuvSCyLYmw=="],
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-X5SsPZHs+iYO8R/efIcRtc7gT2Q2DgPfliCxEkx4cXBumwkw0c/EsHMNwH3EgGpCDaZ7IYVPhpCG/xBOQHEwZw=="],
"@oven/bun-linux-aarch64-android": ["@oven/bun-linux-aarch64-android@1.3.14", "", { "os": "android", "cpu": "arm64" }, "sha512-y4kq5b85lsrmFb9Xvi4w9mA5IEFJkLMrSmYn06q24KjL9rUWDWO3VFZEtteZxUN5+ec3Zm5S8OnJw1umaCbVjA=="],
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmqOA92Cd1NL/1XBd4bFkJLxQ86K0RW7ohxS2qzzAvuitO4JiIxjjTeCspoU44zCozH72HpfZfUE2On31OjnWA=="],
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-7OVTAKvwfPmSbIV1HpdOoVVx5VRc427GuPPne93N6vk4eQBPId9nXmZDh9/zGaKPdbVjVtQSZafWQoUjx38Utw=="],
"@oven/bun-linux-x64-android": ["@oven/bun-linux-x64-android@1.3.14", "", { "os": "android", "cpu": "x64" }, "sha512-qe9e1d+3VAEU7nAA2ol9Jvmy/o99PVMSgZhHn7Q/9O3YcDrfEqyQ8zm4zoe5qTEo8HZH0dN03Le0Ys2eQPs7eg=="],
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-q/8EdOC0yUE8FPeoOVq8/Pw5I9/tJaYmUfO/uDUAREx8IUnOJH1RJ5A3BjFqre8pvJoiZA9AovPJq5FnNNjSxA=="],
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-GBCB/k/sIqcr06eTNgg7g46qiUv35Jasx4XiccJ/n7RGqrE4RWUD/XJBbWFprVPjvqd59+QtSnS99XGqvftHfg=="],
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.14", "", { "os": "linux", "cpu": "x64" }, "sha512-n6iE71G4lQE4XkrZhQQcL5YUlxDbnq6nqV7zeQi33PMsLT/0kYE+RvHOtBWZ3w0wMdXZfINmp63hIb9ijUBGtw=="],
"@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-T7s3x/BsVKQObGU6QDkZeI6wKynzqGbBH1yI77jrrj5siElclxr3DQrDIk8CV4G5/SJq2HHq4kpLyYY2DKCSmA=="],
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-mUFWL3BoYkNpjd8e9PqROiFF/1Xeotq20mABJsiQH62jM1g5zqWh4khw1RZ6bX8Q8fWvlPaxG1PjofkmjUi3vg=="],
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.14", "", { "os": "win32", "cpu": "x64" }, "sha512-uIjLUC1S9DWgICzuoMba7vurBJnBruE4S5CxnvmZkdqWVXRzx1Rgu636HoH+k0qeaQCFh3jeG3JQ1y6fRHv0sw=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
"@qdrant/js-client-rest": ["@qdrant/js-client-rest@1.13.0", "", { "dependencies": { "@qdrant/openapi-typescript-fetch": "1.2.6", "@sevinf/maybe": "0.5.0", "undici": "~5.28.4" }, "peerDependencies": { "typescript": ">=4.7" } }, "sha512-bewMtnXlGvhhnfXsp0sLoLXOGvnrCM15z9lNlG0Snp021OedNAnRtKkerjk5vkOcbQWUmJHXYCuxDfcT93aSkA=="],
"@qdrant/openapi-typescript-fetch": ["@qdrant/openapi-typescript-fetch@1.2.6", "", {}, "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA=="],
"@redis/bloom": ["@redis/bloom@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="],
"@redis/client": ["@redis/client@1.6.1", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw=="],
"@redis/graph": ["@redis/graph@1.1.1", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="],
"@redis/json": ["@redis/json@1.0.7", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="],
"@redis/search": ["@redis/search@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="],
"@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="],
"@sevinf/maybe": ["@sevinf/maybe@0.5.0", "", {}, "sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@supabase/auth-js": ["@supabase/auth-js@2.106.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-VcAjUErkHkhC5Jaf+g/G1qbkQrFh8edaCdHa7pxJmHUjkWKjT7UnYCtPA89XV0N0GIYRkEqJZw5V62CtOxTmBQ=="],
"@supabase/functions-js": ["@supabase/functions-js@2.106.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-oRnr0QrL8H+zTO1YyQ1QjiHZU/957jvubbxSJTUm2XLAgzoGGV9Tahfyd+uvLsBLRVmXLtpU3oyCjdQIvkGMOA=="],
"@supabase/phoenix": ["@supabase/phoenix@0.4.2", "", {}, "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A=="],
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.106.2", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-tDOzyPgp9pIRMR2x6C9+uDSJrnXSzxLtt3d7nC+Lrsy3jnJDHYfdQC/xcRyhJE/TOBJ0heSqRKR3UmejDjZxsw=="],
"@supabase/realtime-js": ["@supabase/realtime-js@2.106.2", "", { "dependencies": { "@supabase/phoenix": "^0.4.2", "tslib": "2.8.1" } }, "sha512-LdRGT7DNhyZkPjubUv5bSdAZ0jSEX8wTHvx7htj7+K59TOZRvz4TuQK7tL2RWxyIZVeFMRluL04SzWS61rKnUA=="],
"@supabase/storage-js": ["@supabase/storage-js@2.106.2", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-xgKCSYuev1YarV+iVqr+zlfgSyremnJtn8T0NCT8L4XmMv1CLtESc0Q6kNp8+mKWdX/8ND0nzm7OMKx08kwNAw=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.106.2", "", { "dependencies": { "@supabase/auth-js": "2.106.2", "@supabase/functions-js": "2.106.2", "@supabase/postgrest-js": "2.106.2", "@supabase/realtime-js": "2.106.2", "@supabase/storage-js": "2.106.2" } }, "sha512-2/RZ/1fmJx/MRSEDG2Xk8+J4JVk5clM9V0uSI6kUTrcS32KA89DtqI5RUOC9r6mzY3WBC9qexLjssIHjbLyVJA=="],
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
"@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
"@types/jest": ["@types/jest@29.5.14", "", { "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ=="],
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
"@types/pg": ["@types/pg@8.11.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw=="],
"@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="],
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
"@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="],
"@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="],
"@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="],
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
"@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="],
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"afinn-165": ["afinn-165@2.0.2", "", {}, "sha512-mJ/RLUfpXfQA6bzugv+bBsc/QYkVrKaLYeS8fWBpKbTCsonv4iuV9ET0fgReEunm9vKLkaNgnekuSNlTC3WQ1Q=="],
"afinn-165-financialmarketnews": ["afinn-165-financialmarketnews@3.0.0", "", {}, "sha512-0g9A1S3ZomFIGDTzZ0t6xmv4AuokBvBmpes8htiyHpH7N4xDmvSQL6UxL/Zcs2ypRb3VwgCscaD8Q3zEawKYhw=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"apparatus": ["apparatus@0.0.10", "", { "dependencies": { "sylvester": ">= 0.0.8" } }, "sha512-KLy/ugo33KZA7nugtQ7O0E1c8kQ52N3IvD/XgIh4w/Nr28ypfkwDfA67F1ev4N1m5D+BOk1+b2dEJDfpj/VvZg=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="],
"base-64": ["base-64@0.1.0", "", {}, "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"better-sqlite3": ["better-sqlite3@12.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"bson": ["bson@7.2.0", "", {}, "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"buffer-writer": ["buffer-writer@2.0.0", "", {}, "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="],
"bun": ["bun@1.3.14", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.14", "@oven/bun-darwin-x64": "1.3.14", "@oven/bun-darwin-x64-baseline": "1.3.14", "@oven/bun-freebsd-aarch64": "1.3.14", "@oven/bun-freebsd-x64": "1.3.14", "@oven/bun-linux-aarch64": "1.3.14", "@oven/bun-linux-aarch64-android": "1.3.14", "@oven/bun-linux-aarch64-musl": "1.3.14", "@oven/bun-linux-x64": "1.3.14", "@oven/bun-linux-x64-android": "1.3.14", "@oven/bun-linux-x64-baseline": "1.3.14", "@oven/bun-linux-x64-musl": "1.3.14", "@oven/bun-linux-x64-musl-baseline": "1.3.14", "@oven/bun-windows-aarch64": "1.3.14", "@oven/bun-windows-x64": "1.3.14", "@oven/bun-windows-x64-baseline": "1.3.14" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-aB6GVd42x1Y5ie1K16SF+oLGtgSkwX9hgoDdIW88pjvfTccU8F1vfpoOt34QLv0dZ1v3XimtaxPlZUG81Gx9Zg=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
"cloudflare": ["cloudflare@4.5.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"compromise": ["compromise@14.15.1", "", { "dependencies": { "efrt": "2.7.0", "grad-school": "0.0.5", "suffix-thumb": "5.0.2" } }, "sha512-9F3UkUaEU1PPz2fgStkE/TI4tk++0wHxS8xfWq9PQWL/v28dy8bEcPVVSLh3dISIRD7PEhJ8YTzHRKF8y9tnLA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diff-sequences": ["diff-sequences@29.6.3", "", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
"digest-fetch": ["digest-fetch@1.3.0", "", { "dependencies": { "base-64": "^0.1.0", "md5": "^2.3.0" } }, "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA=="],
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"effect": ["effect@4.0.0-beta.66", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw=="],
"efrt": ["efrt@2.7.0", "", {}, "sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
"follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
"generic-pool": ["generic-pool@3.9.0", "", {}, "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"grad-school": ["grad-school@0.0.5", "", {}, "sha512-rXunEHF9M9EkMydTBux7+IryYXEZinRk6g8OBOGDBzo/qWJjhTxy86i5q7lQYpCLHN8Sqv1XX3OIOc7ka2gtvQ=="],
"groq-sdk": ["groq-sdk@0.3.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "digest-fetch": "^1.3.0", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7", "web-streams-polyfill": "^3.2.1" } }, "sha512-Cdgjh4YoSBE2X4S9sxPGXaAy1dlN4bRtAaDZ3cnq+XsxhhN9WSBeHF64l7LWwuD5ntmw7YC5Vf4Ff1oHCg1LOg=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
"is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jest-diff": ["jest-diff@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw=="],
"jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="],
"jest-matcher-utils": ["jest-matcher-utils@29.7.0", "", { "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" } }, "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g=="],
"jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="],
"jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="],
"js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"kareem": ["kareem@3.3.0", "", {}, "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw=="],
"kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="],
"langsmith": ["langsmith@0.7.2", "", { "dependencies": { "p-queue": "6.6.2" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*", "ws": ">=7" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai", "ws"] }, "sha512-3dZUwQDJluxi2ih5eIygFODtlrQKrs3Tua0Ck3l+DAUkSGFkB1Dc0JPqkGbqiVSN+TQDElnkev/ydAOAz6jndA=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
"mem0ai": ["mem0ai@3.0.5", "", { "dependencies": { "axios": "^1.15.2", "openai": "^4.93.0", "uuid": "9.0.1", "zod": "^3.24.1" }, "peerDependencies": { "@anthropic-ai/sdk": "^0.40.1", "@azure/identity": "^4.0.0", "@azure/search-documents": "^12.0.0", "@cloudflare/workers-types": "^4.20250504.0", "@google/genai": "^1.2.0", "@langchain/core": "^1.1.47", "@mistralai/mistralai": "^1.5.2", "@qdrant/js-client-rest": "1.13.0", "@supabase/supabase-js": "^2.49.1", "@types/jest": "29.5.14", "@types/pg": "8.11.0", "better-sqlite3": "^12.6.2", "cloudflare": "^4.2.0", "compromise": "^14.0.0", "groq-sdk": "0.3.0", "natural": "^8.0.1", "ollama": "^0.5.14", "pg": "8.11.3", "redis": "^4.6.13" } }, "sha512-W/R59d5fMpUGHhPEnyoo36GSz5NFJbAs+vS4BxoIvE+t19mIJfoz/2FJSKIw80mT8AkeNYpDfzc/DYRu2IzYIw=="],
"memjs": ["memjs@1.3.2", "", {}, "sha512-qUEg2g8vxPe+zPn09KidjIStHPtoBO8Cttm8bgJFWWabbsjQ9Av9Ky+6UcvKx6ue0LLb/LEhtcyQpRyKfzeXcg=="],
"memory-pager": ["memory-pager@1.5.0", "", {}, "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"mongodb": ["mongodb@7.2.0", "", { "dependencies": { "@mongodb-js/saslprep": "^1.3.0", "bson": "^7.2.0", "mongodb-connection-string-url": "^7.0.0" }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.806.0", "@mongodb-js/zstd": "^7.0.0", "gcp-metadata": "^7.0.1", "kerberos": "^7.0.0", "mongodb-client-encryption": ">=7.0.0 <7.1.0", "snappy": "^7.3.2", "socks": "^2.8.6" }, "optionalPeers": ["@aws-sdk/credential-providers", "@mongodb-js/zstd", "gcp-metadata", "kerberos", "mongodb-client-encryption", "snappy", "socks"] }, "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw=="],
"mongodb-connection-string-url": ["mongodb-connection-string-url@7.0.1", "", { "dependencies": { "@types/whatwg-url": "^13.0.0", "whatwg-url": "^14.1.0" } }, "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ=="],
"mongoose": ["mongoose@9.6.3", "", { "dependencies": { "kareem": "3.3.0", "mongodb": "~7.2", "mpath": "0.9.0", "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" } }, "sha512-vI6dTTlQnfMCyyQ5TrvhG0bCRs4dq5e1uFNPtOOWsOhn0fSg8AoIHjfyyCYr8aybyvPs845dRHGxsC3w/fHcBA=="],
"mpath": ["mpath@0.9.0", "", {}, "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew=="],
"mquery": ["mquery@6.0.0", "", {}, "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@1.11.12", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg=="],
"msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="],
"multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="],
"mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"natural": ["natural@8.1.1", "", { "dependencies": { "afinn-165": "^2.0.2", "afinn-165-financialmarketnews": "^3.0.0", "apparatus": "^0.0.10", "dotenv": "^17.3.1", "memjs": "^1.3.2", "mongoose": "^9.2.1", "pg": "^8.18.0", "redis": "^5.11.0", "safe-stable-stringify": "^2.5.0", "stopwords-iso": "^1.1.0", "sylvester": "^0.0.21", "underscore": "^1.13.0", "uuid": "^13.0.0", "wordnet-db": "^3.1.14" } }, "sha512-Ucb+lsUcGxUqu3rn8cwHjT6gJQosO63nIX/aBQXB3+IDkNbFV7PuviysO+Rzz3aKn7PZhPj3bNF4PS9gDVjYCQ=="],
"node-abi": ["node-abi@3.92.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="],
"ollama": ["ollama@0.5.18", "", { "dependencies": { "whatwg-fetch": "^3.6.20" } }, "sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
"openai": ["openai@4.104.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA=="],
"p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
"p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
"p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="],
"p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
"packet-reader": ["packet-reader@1.0.0", "", {}, "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"pg": ["pg@8.11.3", "", { "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", "pg-connection-string": "^2.6.2", "pg-pool": "^3.6.1", "pg-protocol": "^1.6.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g=="],
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-numeric": ["pg-numeric@1.0.2", "", {}, "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw=="],
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
"pg-types": ["pg-types@4.1.0", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
"postgres-bytea": ["postgres-bytea@3.0.0", "", { "dependencies": { "obuf": "~1.1.2" } }, "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw=="],
"postgres-date": ["postgres-date@2.1.0", "", {}, "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA=="],
"postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="],
"postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
"protobufjs": ["protobufjs@7.6.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg=="],
"proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
"pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"sift": ["sift@17.1.3", "", {}, "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"sparse-bitfield": ["sparse-bitfield@3.0.3", "", { "dependencies": { "memory-pager": "^1.0.2" } }, "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"stopwords-iso": ["stopwords-iso@1.1.0", "", {}, "sha512-I6GPS/E0zyieHehMRPQcqkiBMJKGgLta+1hREixhoLPqEA0AlVFiC43dl8uPpmkkeRdDMzYRWFWk5/l9x7nmNg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"suffix-thumb": ["suffix-thumb@5.0.2", "", {}, "sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"sylvester": ["sylvester@0.0.21", "", {}, "sha512-yUT0ukFkFEt4nb+NY+n2ag51aS/u9UHXoZw+A4jgD77/jzZsBoSDHuqysrVCBC4CYR4TYvUJq54ONpXgDBH8tA=="],
"tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
"undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="],
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wordnet-db": ["wordnet-db@3.1.14", "", {}, "sha512-zVyFsvE+mq9MCmwXUWHIcpfbrHHClZWZiVOzKSxNJruIcFn2RbY55zkhiAMMxM8zCVSmtNiViq8FsAZSFpMYag=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"effect/uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="],
"formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
"gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"gaxios/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"groq-sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"mem0ai/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"mongodb-connection-string-url/whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
"natural/pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
"natural/redis": ["redis@5.12.1", "", { "dependencies": { "@redis/bloom": "5.12.1", "@redis/client": "5.12.1", "@redis/json": "5.12.1", "@redis/search": "5.12.1", "@redis/time-series": "5.12.1" } }, "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g=="],
"natural/uuid": ["uuid@13.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw=="],
"openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"@anthropic-ai/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"mongodb-connection-string-url/whatwg-url/tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
"mongodb-connection-string-url/whatwg-url/webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
"natural/pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"natural/redis/@redis/bloom": ["@redis/bloom@5.12.1", "", { "peerDependencies": { "@redis/client": "^5.12.1" } }, "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA=="],
"natural/redis/@redis/client": ["@redis/client@5.12.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA=="],
"natural/redis/@redis/json": ["@redis/json@5.12.1", "", { "peerDependencies": { "@redis/client": "^5.12.1" } }, "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg=="],
"natural/redis/@redis/search": ["@redis/search@5.12.1", "", { "peerDependencies": { "@redis/client": "^5.12.1" } }, "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w=="],
"natural/redis/@redis/time-series": ["@redis/time-series@5.12.1", "", { "peerDependencies": { "@redis/client": "^5.12.1" } }, "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ=="],
"openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"natural/pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"natural/pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"natural/pg/pg-types/postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"natural/pg/pg-types/postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
}
}
@@ -0,0 +1,254 @@
#!/usr/bin/env bun
import {
readFileSync,
writeFileSync,
existsSync,
mkdirSync,
copyFileSync,
readdirSync,
rmSync,
statSync,
} from "fs";
import { join, dirname } from "path";
import { homedir } from "os";
const PLUGIN_NAME = "@mem0/opencode-plugin";
const MCP_CONFIG = {
mem0: {
type: "remote",
url: "https://mcp.mem0.ai/mcp/",
headers: {
Authorization: "Token {env:MEM0_API_KEY}",
},
oauth: false,
},
};
const SKILLS_NAMESPACE = "mem0";
function getConfigDir(): string {
const dir = join(homedir(), ".config", "opencode");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
return dir;
}
function getConfigPath(): string {
const configDir = getConfigDir();
const jsonc = join(configDir, "opencode.jsonc");
if (existsSync(jsonc)) return jsonc;
return join(configDir, "opencode.json");
}
function stripJsonComments(text: string): string {
let result = "";
let i = 0;
let inString = false;
let escape = false;
while (i < text.length) {
const ch = text[i];
if (escape) {
result += ch;
escape = false;
i++;
continue;
}
if (inString) {
if (ch === "\\") escape = true;
else if (ch === '"') inString = false;
result += ch;
i++;
continue;
}
if (ch === '"') {
inString = true;
result += ch;
i++;
continue;
}
if (ch === "/" && text[i + 1] === "/") {
while (i < text.length && text[i] !== "\n") i++;
continue;
}
if (ch === "/" && text[i + 1] === "*") {
i += 2;
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
i += 2;
continue;
}
result += ch;
i++;
}
return result;
}
function resolvePluginDir(): string {
try {
return dirname(new URL(import.meta.url).pathname);
} catch {}
return __dirname ?? process.cwd();
}
function findSkillsDir(): string {
const base = resolvePluginDir();
const candidates = [
join(base, "opencode-skills"),
join(dirname(base), "opencode-skills"),
join(base, "..", "opencode-skills"),
];
for (const c of candidates) {
try {
if (existsSync(c) && statSync(c).isDirectory()) return c;
} catch {}
}
return "";
}
function installSkills(): number {
const skillsSource = findSkillsDir();
if (!skillsSource) {
console.log(" ! Skills directory not found — skipping slash command install");
return 0;
}
const skillsTarget = join(getConfigDir(), "skills");
if (!existsSync(skillsTarget)) mkdirSync(skillsTarget, { recursive: true });
let count = 0;
const entries = readdirSync(skillsSource);
for (const name of entries) {
const skillDir = join(skillsSource, name);
try {
if (!statSync(skillDir).isDirectory()) continue;
} catch {
continue;
}
const skillFile = join(skillDir, "SKILL.md");
if (!existsSync(skillFile)) continue;
const targetDir = join(skillsTarget, `${SKILLS_NAMESPACE}-${name}`);
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
copyFileSync(skillFile, join(targetDir, "SKILL.md"));
count++;
}
return count;
}
function uninstallSkills(): number {
const skillsTarget = join(getConfigDir(), "skills");
if (!existsSync(skillsTarget)) return 0;
let count = 0;
const entries = readdirSync(skillsTarget);
for (const name of entries) {
if (!name.startsWith(`${SKILLS_NAMESPACE}-`)) continue;
const fullPath = join(skillsTarget, name);
try {
if (!statSync(fullPath).isDirectory()) continue;
rmSync(fullPath, { recursive: true });
count++;
} catch {}
}
return count;
}
function install() {
console.log("Installing Mem0 plugin for OpenCode...\n");
const configPath = getConfigPath();
let config: any = {};
if (existsSync(configPath)) {
try {
const raw = readFileSync(configPath, "utf-8");
config = JSON.parse(stripJsonComments(raw));
} catch {
console.log(` ! Could not parse ${configPath}, creating fresh config`);
config = {};
}
}
if (!Array.isArray(config.plugin)) config.plugin = [];
if (!config.plugin.includes(PLUGIN_NAME)) {
config.plugin.push(PLUGIN_NAME);
console.log(` + Added "${PLUGIN_NAME}" to plugin array`);
} else {
console.log(` ~ "${PLUGIN_NAME}" already in plugin array`);
}
if (!config.mcp) config.mcp = {};
if (!config.mcp.mem0) {
config.mcp.mem0 = MCP_CONFIG.mem0;
console.log(" + Added mem0 MCP server config");
} else {
console.log(" ~ mem0 MCP server already configured");
}
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
console.log(`\n Wrote ${configPath}`);
const skillCount = installSkills();
if (skillCount > 0) {
console.log(` + Installed ${skillCount} slash commands to ~/.config/opencode/skills/`);
}
console.log("");
if (!process.env.MEM0_API_KEY) {
console.log(" ! MEM0_API_KEY is not set in your environment.");
console.log(
' Run: echo \'export MEM0_API_KEY="m0-your-key"\' >> ~/.zshrc && source ~/.zshrc',
);
console.log(
" Get a free key at: https://app.mem0.ai/dashboard/api-keys\n",
);
} else {
console.log(" MEM0_API_KEY detected\n");
}
console.log("Done! Restart OpenCode to activate Mem0.");
console.log(" Then run /mem0-onboard in the TUI to complete setup.\n");
}
function uninstall() {
const configPath = getConfigPath();
if (!existsSync(configPath)) {
console.log("No OpenCode config found. Nothing to remove.");
return;
}
const raw = readFileSync(configPath, "utf-8");
const config = JSON.parse(stripJsonComments(raw));
if (Array.isArray(config.plugin)) {
config.plugin = config.plugin.filter((p: string) => p !== PLUGIN_NAME);
if (config.plugin.length === 0) delete config.plugin;
}
if (config.mcp?.mem0) {
delete config.mcp.mem0;
if (Object.keys(config.mcp).length === 0) delete config.mcp;
}
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
console.log(` Removed plugin + MCP from ${configPath}`);
const skillCount = uninstallSkills();
if (skillCount > 0) {
console.log(` Removed ${skillCount} slash commands from ~/.config/opencode/skills/`);
}
console.log("Restart OpenCode to complete removal.");
}
const cmd = process.argv[2];
if (cmd === "uninstall" || cmd === "remove") {
uninstall();
} else {
install();
}
@@ -0,0 +1,744 @@
// Mem0 memory plugin for OpenCode: captures and recalls memories across sessions
// (add / search / manage) via the Mem0 platform, wired through OpenCode plugin hooks.
import type { Plugin } from "@opencode-ai/plugin";
import { MemoryClient } from "mem0ai";
import { userInfo } from "os";
import { basename, resolve, dirname } from "path";
import { randomBytes } from "crypto";
import { existsSync, readdirSync, cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { join } from "path";
import { createHash } from "crypto";
async function getUserId(): Promise<string> {
if (process.env.MEM0_USER_ID) return process.env.MEM0_USER_ID;
try {
return userInfo().username;
} catch {}
return process.env.USER || process.env.USERNAME || "unknown";
}
async function getProjectId($: any): Promise<string> {
if (process.env.MEM0_APP_ID) return process.env.MEM0_APP_ID;
try {
const r = await $`git remote get-url origin`.quiet();
const remote = r.stdout.toString().trim();
const m = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
if (m) return m[1].replace("/", "-");
} catch {}
return basename(process.cwd());
}
async function getBranch($: any): Promise<string> {
try {
const r = await $`git branch --show-current`.quiet();
return r.stdout.toString().trim() || "main";
} catch {}
return "main";
}
function extractMemories(res: any): Array<{ memory: string; id: string }> {
const arr = res?.results ?? res;
if (!Array.isArray(arr)) return [];
return arr.map((m: any) => ({ memory: m.memory ?? "", id: m.id ?? "" }));
}
function generateSessionId(): string {
const ts = Math.floor(Date.now() / 1000);
const rnd = randomBytes(3).toString("hex");
return `ses_${ts}_${rnd}`;
}
const SECRET_PATTERNS = [
/sk-[A-Za-z0-9]{20,}/g,
/m0-[A-Za-z0-9]{20,}/g,
/AKIA[0-9A-Z]{16}/g,
/xox[baprs]-[A-Za-z0-9-]{20,}/g,
/ghp_[A-Za-z0-9]{36,}/g,
/gho_[A-Za-z0-9]{36,}/g,
];
function redact(text: string): string {
let out = text;
for (const re of SECRET_PATTERNS) {
out = out.replace(re, "[REDACTED]");
}
return out;
}
function formatAge(createdAt: string): string {
try {
const dt = new Date(createdAt);
const now = Date.now();
const seconds = Math.floor((now - dt.getTime()) / 1000);
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
const days = Math.floor(seconds / 86400);
if (days === 1) return "1d ago";
if (days < 30) return `${days}d ago`;
return `${Math.floor(days / 30)}mo ago`;
} catch {
return "";
}
}
const TYPE_ICONS: Record<string, string> = {
decision: "⚖️",
anti_pattern: "🔴",
bug_fix: "🔴",
convention: "🔄",
task_learning: "🔵",
user_preference: "🟣",
session_summary: "📋",
session_state: "📋",
project_profile: "📖",
compact_summary: "📋",
auto_capture: "✅",
};
const FILE_READ_GATE_MIN_BYTES = 1500;
function loadGlobalSearch(): boolean {
try {
const settingsPath = join(homedir(), ".mem0", "settings.json");
if (!existsSync(settingsPath)) return false;
const settings = JSON.parse(readFileSync(settingsPath, "utf8"));
return settings.global_search === true;
} catch {}
return false;
}
const CODING_CATEGORIES = [
"architecture_decisions", "api_design", "data_models", "algorithms",
"dependencies", "environment_setup", "testing_strategy", "debugging_notes",
"performance", "security", "deployment", "code_conventions",
"error_handling", "refactoring_history", "integrations", "onboarding",
"project_meta",
];
function categoriesFingerprint(): string {
const sorted = [...CODING_CATEGORIES].sort();
return createHash("sha256").update(sorted.join("\n")).digest("hex").slice(0, 16);
}
function apiKeyFingerprint(apiKey: string): string {
return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
}
async function autoSetupCategories(mem0: MemoryClient, apiKey: string): Promise<void> {
const stateDir = join(homedir(), ".mem0");
const stateFile = join(stateDir, "categories_setup.json");
const keyFp = apiKeyFingerprint(apiKey);
const catFp = categoriesFingerprint();
let state: Record<string, string> = {};
try {
if (existsSync(stateFile)) {
state = JSON.parse(readFileSync(stateFile, "utf8"));
}
} catch {}
if (state[keyFp] === catFp) return;
try {
const project = await mem0.getProject({ fields: ["customCategories"] });
const existing: string[] = (project as any)?.custom_categories ?? (project as any)?.customCategories ?? [];
const sortedExisting = [...existing].sort();
const sortedTarget = [...CODING_CATEGORIES].sort();
if (JSON.stringify(sortedExisting) === JSON.stringify(sortedTarget)) {
state[keyFp] = catFp;
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n");
return;
}
await mem0.updateProject({ customCategories: CODING_CATEGORIES as any });
state[keyFp] = catFp;
mkdirSync(stateDir, { recursive: true });
writeFileSync(stateFile, JSON.stringify(state, null, 2) + "\n");
} catch {}
}
const NUDGE_RE =
/\b(remember\s+(this|that)|memorize|save\s+this|note\s+(this|that)|don'?t\s+forget|always\s+remember|never\s+forget|keep\s+(this|that)\s+in\s+(mind|memory)|store\s+(this|that))\b/i;
const RESUME_RE =
/where\s+(did\s+)?(we|I)\s+(leave|left)\s+off|continue\s+(from\s+)?(where|last)|what\s+were\s+we\s+(working|doing)|pick\s+up\s+where|resume\s+(from\s+|where\s+)|what.s\s+the\s+(current|latest)\s+(state|status)|catch\s+me\s+up|where\s+are\s+we/i;
const ERROR_STRONG_RE =
/Traceback \(most recent call last\)|panic: |FATAL:|error\[E\d+\]/;
const ERROR_MULTI_RE = /(Error:|Exception:)/g;
const MEM0_MCP_RE = /mem0.*(?:add_memory|search_memories|get_memor|delete_memor|update_memory|delete_entities|list_entities)/i;
const WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "write", "edit", "multiEdit"]);
function isMem0Tool(name: string): boolean {
return MEM0_MCP_RE.test(name);
}
function isMem0AddMemory(name: string): boolean {
return /mem0.*add_memory/i.test(name);
}
function isMem0SearchOrGet(name: string): boolean {
return /mem0.*(search_memories|get_memories)/i.test(name);
}
function isMem0DeleteAll(name: string): boolean {
return /mem0.*delete_all_memories/i.test(name);
}
function extractUserText(input: any, output: any): string {
const parts: any[] = output?.parts;
if (Array.isArray(parts)) {
return parts
.filter((p: any) => p.type === "text" && !p.synthetic)
.map((p: any) => p.text ?? "")
.join("\n");
}
const msg = output?.message ?? input?.message;
if (typeof msg?.content === "string") return msg.content;
if (typeof msg?.text === "string") return msg.text;
return "";
}
function installSkills(projectDir: string): void {
const pluginDir = dirname(dirname(import.meta.filename));
const srcSkills = resolve(pluginDir, "opencode-skills");
if (!existsSync(srcSkills)) return;
const destSkills = resolve(projectDir, ".opencode", "skills");
const destCommands = resolve(projectDir, ".opencode", "commands");
try {
const skills = readdirSync(srcSkills, { withFileTypes: true });
for (const entry of skills) {
if (!entry.isDirectory()) continue;
const dest = resolve(destSkills, `mem0-${entry.name}`);
if (existsSync(dest)) continue;
cpSync(resolve(srcSkills, entry.name), dest, { recursive: true });
}
for (const entry of skills) {
if (!entry.isDirectory()) continue;
const cmdFile = resolve(destCommands, `mem0-${entry.name}.md`);
if (existsSync(cmdFile)) continue;
const skillMd = resolve(srcSkills, entry.name, "SKILL.md");
if (!existsSync(skillMd)) continue;
let desc = "Mem0 " + entry.name + " skill";
try {
const content = readFileSync(skillMd, "utf8");
const m = content.match(/^description:\s*(.+)$/m);
if (m) desc = m[1].trim();
} catch {}
const cmdContent = `---\ndescription: ${desc}\n---\nLoad and follow the skill at .opencode/skills/mem0-${entry.name}/SKILL.md\n\nUse the mem0 MCP tools (search_memories, get_memories, add_memory, delete_memory, update_memory, list_entities, delete_entities, get_event_status) to execute the skill instructions.\n\nIdentity context (from environment):\n- user_id: Use MEM0_USER_ID env var, or fall back to $USER\n- app_id: Use MEM0_APP_ID env var\n- session_id: Use MEM0_SESSION_ID env var\n- branch: Use MEM0_BRANCH env var\n`;
try {
mkdirSync(destCommands, { recursive: true });
writeFileSync(cmdFile, cmdContent, "utf8");
} catch {}
}
} catch {}
}
const Mem0Plugin: Plugin = async (ctx) => {
const { $, client } = ctx;
try {
const projectDir = (ctx as any).directory ?? process.cwd();
installSkills(projectDir);
} catch {}
const apiKey = process.env.MEM0_API_KEY;
if (!apiKey) {
try {
await client.app.log({
body: {
service: "mem0",
level: "warn",
message:
"MEM0_API_KEY not set. Get one at https://app.mem0.ai/dashboard/api-keys",
},
});
} catch {}
return {};
}
const mem0 = new MemoryClient({ apiKey });
const userId = await getUserId();
const appId = await getProjectId($);
const branch = await getBranch($);
const stats = { adds: 0, searches: 0, messages: 0 };
const sessionId = generateSessionId();
const globalSearch = loadGlobalSearch();
let initialized = false;
let memoryCount = 0;
let msgCount = 0;
const systemContext: string[] = [];
// Auto-configure coding categories in background (idempotent, never blocks)
Promise.resolve().then(() => autoSetupCategories(mem0, apiKey)).catch(() => {});
return {
"chat.message": async (input: any, output: any) => {
const userText = extractUserText(input, output);
if (!userText || userText.length < 10) return;
const safeText = redact(userText);
msgCount++;
stats.messages++;
if (!initialized) {
initialized = true;
const searchFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
try {
const all = await mem0.getAll({
filters: searchFilters,
page: 1,
pageSize: 1,
});
memoryCount =
(all as any)?.count ??
(all as any)?.results?.length ??
0;
if (globalSearch) {
systemContext.push(
`Global search is ON — searches return all memories across all users and projects. Writes still use user_id="${userId}", app_id="${appId}".`,
);
} else {
systemContext.push(
`Always include user_id="${userId}" and app_id="${appId}" in every search_memories filter and add_memory call.`,
);
}
if (memoryCount === 0) {
systemContext.push(
"New project with 0 memories. Suggest running /mem0:onboard to import project files and install coding categories.",
);
}
if (memoryCount > 0) {
systemContext.push(
"Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type.",
);
try {
const res = await mem0.search(
"recent session state decisions and learnings",
{
filters: searchFilters,
topK: 5,
},
);
stats.searches++;
const memories = extractMemories(res);
if (memories.length > 0) {
const memLines = memories
.map((m) => {
const meta = (m as any).metadata ?? {};
const cat = meta.type ?? "unknown";
const icon = TYPE_ICONS[cat] ?? "❓";
const age = (m as any).created_at ? formatAge((m as any).created_at) : "";
const ageStr = age ? ` (${age})` : "";
return `- ${icon} [${cat}]${ageStr} ${m.memory.slice(0, 120)}`;
})
.join("\n");
systemContext.push(`### Recent Activity\n\n${memLines}`);
}
} catch {}
}
systemContext.push(
"Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id.",
);
} catch (err: any) {
try {
await client.app.log({
body: {
service: "mem0",
level: "error",
message: `Session init error: ${err?.message}`,
},
});
} catch {}
}
}
if (NUDGE_RE.test(safeText)) {
systemContext.push(
"[MEMORY TRIGGER] User asked to remember something. Call add_memory with the user's statement, confidence=1.0, infer=false.",
);
}
const hasResume = RESUME_RE.test(safeText);
if (hasResume) {
try {
const resumeFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: {
AND: [
{ user_id: userId },
{ app_id: appId },
],
};
const [stateRes, decisionsRes] = await Promise.all([
mem0.search("session state current task", {
filters: resumeFilters,
topK: 3,
}),
mem0.search("recent decisions and learnings", {
filters: resumeFilters,
topK: 3,
}),
]);
stats.searches += 2;
const all = [
...extractMemories(stateRes),
...extractMemories(decisionsRes),
];
const seen = new Set<string>();
const unique = all.filter((m) => {
if (seen.has(m.id)) return false;
seen.add(m.id);
return true;
});
if (unique.length > 0) {
const memLines = unique.map((m) => `- ${m.memory}`).join("\n");
systemContext.push(
`Session resume context:\n${memLines}\n\nThese memories provide context for resuming work.`,
);
}
} catch {}
}
if (!hasResume && memoryCount > 0) {
try {
const msgFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const res = await mem0.search(safeText, {
filters: msgFilters,
topK: 5,
});
stats.searches++;
const memories = extractMemories(res);
if (memories.length > 0) {
const memLines = memories.map((m) => `- ${m.memory}`).join("\n");
systemContext.push(`Relevant memories:\n${memLines}`);
}
} catch {}
}
if (msgCount % 3 === 0) {
Promise.resolve().then(async () => {
try {
await mem0.add([{ role: "user", content: safeText }], {
user_id: userId,
app_id: appId,
metadata: {
type: "auto_capture",
source: "opencode",
confidence: 0.7,
session_id: sessionId,
branch,
},
infer: true,
} as any);
stats.adds++;
} catch {}
});
}
if (msgCount % 5 === 0 && stats.adds < Math.floor(msgCount / 3)) {
systemContext.push(
"After responding, store any new decisions, learnings, or preferences from this exchange via add_memory. Keep it to 1 sentence per memory.",
);
}
},
"experimental.chat.messages.transform": async (
_input: any,
output: { messages: { info: any; parts: any[] }[] },
) => {
if (systemContext.length === 0 || !output?.messages?.length) return;
const firstUser = output.messages.find(
(m) => m.info.role === "user",
);
if (!firstUser || !firstUser.parts.length) return;
const marker = "## Mem0 Memory Context";
if (firstUser.parts.some((p: any) => p.type === "text" && p.text?.includes(marker))) return;
const block = `${marker}\n\n${systemContext.join("\n\n")}`;
const ref = firstUser.parts[0];
firstUser.parts.unshift({ ...ref, type: "text", text: block });
},
"tool.execute.before": async (input: any, output: any) => {
const toolName: string = input?.tool ?? "";
// File-context injection: before reading a file, search mem0 for prior work on it
if (toolName === "read" || toolName === "Read") {
const filePath = String(output?.args?.file_path ?? output?.args?.filePath ?? "");
if (filePath && filePath.length > 0) {
try {
const absPath = filePath.startsWith("/") ? filePath : resolve(process.cwd(), filePath);
const { statSync } = await import("fs");
const stat = statSync(absPath);
if (stat.isFile() && stat.size >= FILE_READ_GATE_MIN_BYTES) {
const searchFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const relPath = filePath.startsWith("/")
? filePath.replace(process.cwd() + "/", "")
: filePath;
const res = await mem0.search(relPath, {
filters: searchFilters,
topK: 5,
});
stats.searches++;
const memories = extractMemories(res);
if (memories.length > 0) {
const lines = memories.map((m) => {
const text = m.memory.slice(0, 150).replace(/\n/g, " ");
return `- ${text} [mem0:${m.id.slice(0, 8)}]`;
});
systemContext.push(
`Prior work on \`${relPath}\`:\n${lines.join("\n")}`,
);
}
}
} catch {}
}
}
if (WRITE_TOOLS.has(toolName)) {
const fp = String(
output?.args?.file_path ?? output?.args?.filePath ?? "",
);
if (/MEMORY\.md|\.claude\/memory/i.test(fp)) {
throw new Error(
"Use the add_memory MCP tool instead of writing to MEMORY.md",
);
}
}
if (isMem0Tool(toolName) && output?.args) {
if (!output.args.user_id) output.args.user_id = userId;
if (!output.args.app_id) output.args.app_id = appId;
if (isMem0AddMemory(toolName)) {
if (!output.args.metadata) output.args.metadata = {};
const meta = output.args.metadata;
if (meta.confidence === undefined) meta.confidence = 0.7;
if (!meta.source) meta.source = "opencode";
if (!meta.type) meta.type = "task_learning";
if (!meta.session_id) meta.session_id = sessionId;
if (!meta.files) meta.files = ["*"];
if (!meta.branch) meta.branch = branch;
if (meta.confidence >= 1.0 && output.args.infer === undefined) {
output.args.infer = false;
}
}
if (isMem0SearchOrGet(toolName)) {
if (globalSearch) {
output.args.filters = { OR: [{ user_id: "*" }] };
} else {
const existingFilters = output.args.filters;
if (existingFilters === undefined || existingFilters === null) {
output.args.filters = {
AND: [{ user_id: userId }, { app_id: appId }],
};
} else if (typeof existingFilters === "object") {
const andClauses: any[] = existingFilters.AND;
if (Array.isArray(andClauses)) {
const hasUid = andClauses.some(
(c: any) => c && typeof c === "object" && "user_id" in c,
);
const hasAid = andClauses.some(
(c: any) => c && typeof c === "object" && "app_id" in c,
);
if (!hasUid) andClauses.push({ user_id: userId });
if (!hasAid) andClauses.push({ app_id: appId });
} else if (andClauses === undefined) {
const hasUid = "user_id" in existingFilters;
const hasAid = "app_id" in existingFilters;
if (!hasUid || !hasAid) {
const existing = Object.entries(existingFilters).map(
([k, v]) => ({ [k]: v }),
);
if (!hasUid) existing.push({ user_id: userId });
if (!hasAid) existing.push({ app_id: appId });
output.args.filters = { AND: existing };
}
}
}
}
}
if (isMem0DeleteAll(toolName)) {
if (!output.args.user_id) output.args.user_id = userId;
if (!output.args.app_id) output.args.app_id = appId;
}
if (!isMem0AddMemory(toolName) && !output.args.metadata) {
output.args.metadata = { source: "opencode", branch };
}
}
},
"tool.execute.after": async (input: any, _output: any) => {
const toolName: string = input?.tool ?? "";
const toolOutput: string = input?.output ?? _output?.output ?? "";
if (MEM0_MCP_RE.test(toolName)) {
if (toolName.includes("add_memory")) stats.adds++;
if (toolName.includes("search")) stats.searches++;
}
if (toolName === "bash" && toolOutput.length >= 50) {
const command: string = input?.args?.command ?? "";
if (/git\s+(commit|merge|rebase)/.test(command)) return;
const hasStrongError = ERROR_STRONG_RE.test(toolOutput);
const multiErrors = (toolOutput.match(ERROR_MULTI_RE) ?? []).length;
if (!hasStrongError && multiErrors < 2) return;
try {
const errorLine =
toolOutput
.split("\n")
.find((l: string) =>
/Error:|Exception:|panic:|FAIL:|fatal:/i.test(l),
)
?.replace(/^\s+/, "")
.slice(0, 120) ?? "";
const traceFiles = [
...new Set(
toolOutput.match(
/[a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh)(:\d+)?/g,
) ?? [],
),
].slice(0, 5);
const errorQuery = errorLine.slice(0, 80);
if (errorQuery.length < 10) return;
const errorFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: {
AND: [
{ user_id: userId },
{ app_id: appId },
],
};
const [antiPatternRes, bugFixRes] = await Promise.all([
mem0.search(`error: ${errorQuery}`, {
filters: errorFilters,
topK: 3,
}),
mem0.search(`error: ${errorQuery}`, {
filters: errorFilters,
topK: 3,
}),
]);
stats.searches += 2;
const allResults = [
...extractMemories(antiPatternRes),
...extractMemories(bugFixRes),
];
const seen = new Set<string>();
const unique = allResults.filter((m) => {
if (seen.has(m.id)) return false;
seen.add(m.id);
return true;
});
let ctx = `Error detected: \`${command.slice(0, 100)}\` produced:\n> ${errorLine}`;
if (traceFiles.length > 0) {
ctx += `\nFiles in stack trace: ${traceFiles.join(", ")}`;
}
if (unique.length > 0) {
const lines = unique.map((m) => `- ${m.memory}`).join("\n");
ctx += `\nPrior error memories:\n${lines}`;
}
ctx +=
"\nStore resolved errors as anti_pattern or bug_fix memories for future reference.";
systemContext.push(ctx);
} catch {}
}
},
"experimental.session.compacting": async (
input: { sessionID?: string },
output: { context: string[]; prompt?: string },
) => {
try {
const compactSessionId = input?.sessionID ?? sessionId;
// Session summary capture: store a structured summary of the session
const summaryPrompt = [
`Session summary for project ${appId} (branch: ${branch}).`,
`Session: ${compactSessionId}.`,
`Stats: ${stats.adds} memories stored, ${stats.searches} searches, ${stats.messages} messages.`,
`Extract and remember: what was requested, what was investigated, key decisions made, what was completed, and what needs to happen next.`,
].join(" ");
Promise.resolve().then(async () => {
try {
await mem0.add([{ role: "user", content: summaryPrompt }], {
user_id: userId,
app_id: appId,
metadata: {
type: "session_summary",
source: "opencode-stop",
session_id: compactSessionId,
branch,
},
infer: true,
} as any);
} catch {}
});
const compactFilters = globalSearch
? { OR: [{ user_id: "*" }] }
: { AND: [{ user_id: userId }, { app_id: appId }] };
const res = await mem0.search("session state decisions learnings", {
filters: compactFilters,
topK: 10,
});
const memories = extractMemories(res);
if (memories.length > 0 && output?.context) {
const lines = memories.map((m) => `- ${m.memory}`).join("\n");
output.context.push(
`## Mem0 Memories (preserve across compaction)\n\n${lines}\n\nIMPORTANT: After compaction, store any key decisions or learnings using the add_memory MCP tool.`,
);
}
} catch {}
},
"shell.env": async (
_input: { cwd: string; sessionID?: string },
output: { env: Record<string, string> },
) => {
if (output?.env) {
output.env.MEM0_USER_ID = userId;
output.env.MEM0_APP_ID = appId;
output.env.MEM0_SESSION_ID = sessionId;
output.env.MEM0_BRANCH = branch;
output.env.MEM0_GLOBAL_SEARCH = globalSearch ? "true" : "false";
}
},
};
};
export default Mem0Plugin;
@@ -0,0 +1,52 @@
---
name: context-loader
description: Searches and injects relevant memories into context before starting work on a task. Use when beginning a new task, switching context, or when project history, past decisions, or coding conventions need to be loaded.
---
# Context Loader
Pre-fetches relevant memories to prime context before working on a task.
## When to use
- Session start (invoke manually or auto-triggered by skill description matching)
- User starts work on a specific feature or file set
- Complex multi-step task begins
- User says "what do we know about X" or "context for X"
## Steps
1. **Extract topics** from current message/task. Identify: file paths, module names, feature areas, error patterns.
2. **Run 2-4 parallel `search_memories` calls** with different angles:
| Query angle | Filter | Purpose |
|---|---|---|
| Feature/module name | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}` | Architecture decisions |
| File paths mentioned | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "convention"}}]}` | Coding patterns |
| Error keywords (if any) | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "anti_pattern"}}]}` | Known pitfalls |
| Broad project context | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}` | Catch-all |
3. **Deduplicate** results by memory ID across all search responses.
4. **Output compact context block** (max 10 memories):
```
context-loader: loaded <N> memories for "<task summary>"
- [decision] <content> [mem0:<short_id>]
- [convention] <content> [mem0:<short_id>]
- [anti_pattern] <content> [mem0:<short_id>]
```
5. If **zero results**: output nothing. Don't announce empty context.
## Constraints
- **Read-only** — never modify or delete memories
- **Max 10 memories** returned (most relevant only)
- **Silent on empty** — only surfaces findings if relevant context exists
- Skip memories already visible in current session context
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,237 @@
---
name: dream
description: Consolidates stored memories by merging duplicates, resolving contradictions, and pruning stale entries. Use when memory count is high, search results feel noisy or repetitive, or periodic cleanup is needed to maintain memory quality.
---
# Mem0 Dream — Memory Consolidation
This skill performs a memory consolidation pass: it fetches all project memories,
identifies near-duplicates, flags contradictions, and prunes stale entries based on
configured retention policies. All proposed changes are shown as a diff for user
approval before anything is modified.
**IMPORTANT: Execute steps strictly in order (1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead.**
## Step 1: Load Retention Policies
Check for a project config file in the project root (current working directory):
1. Look for `.mem0.json` first. If it exists, parse it as JSON and read the
`retention` field (a dict of `category → days | null`).
2. If `.mem0.json` is not present, look for `.mem0.md`. If it exists, scan it
for a `retention:` section or YAML front matter with retention settings and
parse what you find.
3. If neither file exists, skip config loading entirely.
If no config is found or the config contains no retention settings, fall back to
these built-in defaults:
| `metadata.type` | Default retention |
|---|---|
| `session_state` | 90 days |
| `compact_summary` | 90 days |
| all others | no pruning |
Store the resolved policies for use in Step 3.
---
## Step 2: Fetch ALL Project Memories
Call `get_memories` to retrieve every memory for the active project:
```python
get_memories(
filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]},
page_size=200,
)
```
If the response indicates more pages exist, paginate until all memories are fetched.
Collect the full list before proceeding. If zero memories are found, print:
```
No memories found for project <project_id>. Nothing to consolidate.
```
…and stop.
---
## Step 3: Analyze — Find Issues
Work entirely in-memory; do not modify anything yet.
Group memories by `metadata.type` (use `"unknown"` when the field is absent).
For each group, identify the following:
### 3a. Near-duplicate pairs (merge candidates)
Two memories are near-duplicates when they express the same fact or decision but
phrased differently (e.g., "Use PostgreSQL for auth" and "Auth DB is PostgreSQL").
Heuristics — two memories are near-duplicates if **all** of these hold:
- Similarity threshold: estimated cosine similarity > 0.9 (use noun/keyword overlap as proxy — if >60% of significant nouns overlap, treat as >0.9 similarity).
- Same `metadata.type`.
- Neither memory is pinned (`metadata.pinned != true`).
For each qualifying pair, draft a merged version that is more complete and specific
than either original.
### 3b. Contradictions
Two memories contradict when they assert opposing facts about the same topic
(e.g., "Deploy to ECS" vs. "Deploy to Vercel").
Identify the likely winner: the more recent memory with higher confidence wins.
Store both IDs and their content for user review.
### 3c. Prune candidates
A memory is a prune candidate when **any** of the following is true:
1. Its `metadata.type` has a retention policy and the memory is older than the
configured number of days (compare `created_at` to today).
2. Its confidence score is below 0.3 AND it contains no information unique to
this project (no file paths, identifiers, or domain-specific nouns).
**Always skip memories where `metadata.pinned == true`**, regardless of age or
confidence.
---
## Step 4: Print Diff Report
Print a structured diff to the terminal before making any changes. Use exactly
this format:
```
## dream — consolidation report
Merges (<N>):
[mem0:<id1>] + [mem0:<id2>] → "<merged content, 100 chars>"
Conflicts (<N>):
[mem0:<idA>] vs [mem0:<idB>] — "<topic>" [A/B/skip]
Prune (<N>):
[mem0:<id>] — <type>, <age>d old
Proposed: <N> merges, <N> prunes, <N> conflicts. Apply? [Y/n]
```
If there are zero items in any category, omit that section entirely.
If there are zero total proposals (no merges, no prunes, no conflicts), print:
```
Dream complete. No duplicate, contradictory, or stale memories found.
```
…and stop.
---
## Step 5: Wait for User Input and Apply
### 5a. Contradictions
For each `CONFLICT` pair in the report, wait for the user to type `A`, `B`, or
`skip` (case-insensitive). If they enter nothing (empty), treat as `skip`.
Record the winner for each pair before proceeding to the final apply confirmation.
### 5b. Final confirmation
After all conflict resolutions are collected, prompt:
```
Apply? [Y/n]
```
If the user types `n` or `no` (case-insensitive), print `Cancelled. No changes made.`
and stop.
If the user confirms (`Y`, `yes`, or empty / Enter), apply all changes in this order:
#### Merges
For each approved merge pair:
1. `delete_memory(<id1>)`
2. `delete_memory(<id2>)`
3. `add_memory` with:
- `text="<merged content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>` (top-level, not in metadata)
- `metadata={"type": "<original type>", "branch": "<active_branch>", "confidence": <higher of the two original scores>, "source": "mem0-dream"}`
- `infer=False`
#### Contradictions (resolved)
For each resolved conflict where the user chose A or B:
- Delete the loser (the non-chosen memory): `delete_memory(memory_id=<loser_id>)`
Contradictions where the user chose `skip` are left untouched.
#### Prunes
For each prune candidate:
- `delete_memory(<memory_id>)`
---
## Step 6: Print Summary
After all changes are applied, print:
```
Dream complete — merged: <N>, pruned: <N>, conflicts resolved: <N>, skipped: <N>
```
---
## Auto mode
When invoked with `--auto` (e.g., `/mem0:dream --auto`), run non-interactively:
- **Merges**: applied automatically (no contradiction, both are compatible).
- **Prunes**: applied automatically (age/confidence-based, no ambiguity).
- **Contradictions**: skipped — they require human judgment.
### Concurrency guard
Before doing any work, check for a lock file at `/tmp/mem0_dream_auto.lock`:
- If the lock file exists and is less than 10 minutes old, print `[mem0-dream --auto] Another run in progress — skipping.` and stop.
- Otherwise, create the lock file (write the current timestamp). Delete it when done (in all exit paths).
### Execution
In auto mode:
1. Load policies and fetch memories (Steps 1–3) as normal.
2. Apply merges and prunes silently without printing the diff or prompting.
3. Print a compact summary:
```
[mem0-dream --auto] project=<id> merged=<N> pruned=<N> conflicts_skipped=<N>
```
4. If contradictions were detected but skipped, check if a `mem0-dream-auto` reminder already exists before storing one:
- Search for existing reminders: `search_memories(query="mem0-dream contradictions manual review", filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}, {"metadata": {"source": "mem0-dream-auto"}}]}, top_k=1)`
- If a result exists with similarity > 0.9, skip storing the reminder (one already exists).
- If no match, store the reminder:
```python
add_memory(
text="mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively.",
user_id="<active_user_id>",
app_id="<active_project_id>",
metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": "<active_branch>"},
infer=False,
)
```
## See also
- `/mem0:forget` — targeted deletion of specific memories (search + confirm + delete)
- `/mem0:health --deep` — quick quality scan without applying changes
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,80 @@
---
name: export
description: Exports all project memories to a portable Markdown file for backup or migration. Use when backing up memories, migrating to another project, sharing memory state with teammates, or archiving before cleanup.
---
# Mem0 Export
Export all memories for the current project to a portable Markdown file.
## Execution
### Step 1: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 2: Fetch all memories
Call `get_memories` with:
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=200`
If the response is paginated (i.e. the result contains a `next` cursor or the count equals `page_size`), continue fetching pages until all memories are retrieved.
### Step 3: Format each memory as a YAML-frontmatter block
For each memory record, produce a block in this exact format:
```
---
id: <memory.id>
created_at: <memory.created_at>
type: <memory.metadata.type or "">
confidence: <memory.metadata.confidence or "">
branch: <memory.metadata.branch or "">
files: <memory.metadata.files joined with ", " or "">
categories: <memory.categories joined with ", " or "">
---
<memory.memory or memory content string>
```
Notes:
- The `---` delimiters must be on their own lines with no extra whitespace.
- `files` and `categories` are written as comma-separated values on a single line.
- Leave a blank line after the content before the next `---` (for readability).
- If a field is missing or null, write an empty string (not "null").
### Step 4: Write the export file
Determine the output filename:
```
mem0-export-<project_id>-<YYYY-MM-DD>.md
```
Where `<YYYY-MM-DD>` is today's date in UTC.
Write all formatted blocks to this file using the Write tool (or equivalent). The file is written to the current working directory.
### Step 5: Print summary
```
Exported <N> memories to <filename>
```
Where `<N>` is the total number of memory blocks written.
## Error Handling
- If `get_memories` returns an error or zero memories, print:
```
No memories found for project <project_id>. Nothing exported.
```
- If the write fails, report the error to the user.
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,74 @@
---
name: forget
description: Deletes memories by search query or memory ID with confirmation before removal. Use when removing outdated decisions, incorrect memories, sensitive data, or cleaning up after experiments. Also handles undo of recent additions.
---
# Mem0 Forget
Delete specific memories from mem0.
## Execution
### Step 1: Parse input
The user provides either:
- A search query: `/mem0:forget auth module decisions`
- A memory ID: `/mem0:forget <memory_id>`
If no argument, ask: "What should I forget? Provide a search query or memory ID."
### Step 2: Find memories
**If memory ID provided** (looks like a UUID or hex string):
- Call `get_memory` with the ID to verify it exists.
- Show: `Found: "<memory content first 120 chars>" (created <date>)`
**If search query provided:**
- Call `search_memories` with:
- `query=<user's query>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<project_id>"}]}`
- `top_k=10`
- Show numbered list:
```
Found <N> memories matching "<query>":
1. <content, 120 chars> (type: <type>, created: <date>) [ID: <short_id>]
2. ...
```
### Step 3: Confirm
Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'."
For a single memory ID, ask: "Delete this memory? [y/N]"
**Never delete without confirmation.** This is destructive.
### Step 4: Delete
For each confirmed memory, call `delete_memory` with the memory ID.
### Step 5: Report
```
Deleted <N> memories.
```
If any deletions failed, report which ones and why.
## Undo recent writes
If the user says "undo last N memories" or "undo last write":
1. Check the `MEM0_SESSION_ID` environment variable (set by the plugin's shell.env hook).
2. If `MEM0_SESSION_ID` is set, call `search_memories` with:
- `query="recently added"`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<project_id>"}, {"metadata": {"session_id": "<MEM0_SESSION_ID>"}}]}`
- `top_k=20`
3. Sort results by creation time descending and show the last N entries (default 1). Ask for confirmation.
4. Delete confirmed entries via `delete_memory`.
If `MEM0_SESSION_ID` is not set or the search returns no results, tell the user: "No recent memory IDs tracked this session. Try `/mem0:tour` to browse recent memories, or `/mem0:forget <search query>` to find specific ones."
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,159 @@
---
name: health
description: Diagnoses mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, add_memory errors occur, MCP connection drops, or to verify the plugin is working correctly.
---
# Mem0 Health Check
Run a diagnostic check on the mem0 plugin. Useful for troubleshooting.
## Execution
Run ALL checks, then display a single summary. Do not stop on the first failure.
### Check 1: API key
```bash
_KEY="${MEM0_API_KEY:-}"
[ -n "$_KEY" ] && echo "${_KEY:0:6}..." || echo "NOT_SET"
```
- If `NOT_SET`: FAIL — "No API key configured"
- If set: PASS — the command already prints only the first 6 chars
### Check 2: Identity resolution
Resolve identity from environment variables set by the plugin's `shell.env` hook:
```bash
echo "user_id=${MEM0_USER_ID:-${USER:-}}"
echo "project_id=${MEM0_APP_ID:-}"
echo "branch=$(git branch --show-current 2>/dev/null || echo '')"
```
- `user_id`: from `MEM0_USER_ID`, falling back to `$USER`
- `project_id`: from `MEM0_APP_ID`
- `branch`: from `git branch --show-current`
PASS if all three are non-empty. WARN if any falls back to defaults.
### Check 3: MCP server connectivity
Call `search_memories` with:
- `query="health check"`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `top_k=1`
- If returns successfully (even empty): PASS
- If errors: FAIL — show the error message
### Check 4: Memory write capability
Call `add_memory` with:
- `text="Health check probe — safe to delete."`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "health_check", "probe": true}`
- `infer=False`
The response returns `event_id` (v3 writes are async). Call `get_event_status(event_id=<event_id>)` to check processing.
- If status is `SUCCEEDED`: PASS — extract the memory ID from the event result, then call `delete_memory` with that ID to clean up.
- If status is `PENDING` after 5 seconds: PASS (write accepted, processing delayed)
- If errors: FAIL — show the error.
### Check 5: Session context
Check that the plugin's `shell.env` hook has injected session context into the environment:
```bash
echo "session_id=${MEM0_SESSION_ID:-}"
echo "app_id=${MEM0_APP_ID:-}"
echo "branch=${MEM0_BRANCH:-}"
```
- If all three are non-empty: PASS — "Session active"
- If any are missing: WARN — "Plugin env vars not set; shell.env hook may not have fired"
### Display
```
## mem0 health
PASS API Key m0-dVe...
PASS Identity user=kartik, project=mem0, branch=main
PASS MCP Connection 142ms
PASS Write/Read write + delete OK
PASS Session session_id=abc123, app_id=mem0, branch=main
All checks passed.
```
If any check fails, add a `## Troubleshooting` section with specific fix steps for each failure.
## Extended mode: Memory Quality Analysis
When invoked with `--deep` (e.g., `/mem0:health --deep`), run the standard 5 checks above **plus** a memory quality scan.
### Quality Check 1: Duplicates
Call `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Compare all pairs within the same `metadata.type` group for high textual overlap (shared nouns/keywords > 60%). Report:
```
Potential duplicates: <N> pairs
[mem0:<id1>] ≈ [mem0:<id2>] — both about "<shared topic>"
```
### Quality Check 2: Stale memories
Flag memories where:
- `metadata.type` is `session_state` or `compact_summary` AND older than 90 days
- `metadata.confidence` < 0.3 AND older than 30 days
```
Stale candidates: <N>
[mem0:<id>] — session_state, 142d old
```
### Quality Check 2b: Low-confidence memories
Flag memories where `metadata.confidence` < 0.5 (regardless of age). Report separately from stale:
```
Low-confidence memories: <N>
[mem0:<id>] — confidence=0.3, "<content preview>"
```
### Quality Check 3: Contradictions
Within each `metadata.type` group, flag pairs that assert opposing facts about the same topic. Use semantic judgment — look for negation patterns, conflicting tool/framework choices, or reversed decisions.
```
Possible contradictions: <N>
[mem0:<idA>] vs [mem0:<idB>] — conflicting on "<topic>"
```
### Quality Check 4: Orphan memories
Memories with no `metadata.type` set, or with `metadata.type` not in the 17 known coding categories. These were likely written without proper tagging.
```
Untagged/orphan memories: <N>
```
### Quality summary
```
## Memory Quality
Duplicates: <N> · Stale: <N> · Contradictions: <N> · Orphans: <N>
```
If all counts are 0: `Memory quality: clean.`
If any non-zero: append `Run /mem0:dream to fix.`
To fix issues found by `--deep`, run `/mem0:dream` for automated consolidation (merges, prunes, conflict resolution).
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,185 @@
---
name: import
description: Imports memories from an exported Markdown file or MEMORY.md into the current project. Use when migrating from another project, restoring from backup, importing Claude Code native MEMORY.md content, or setting up a new project with existing knowledge.
---
# Mem0 Import
Import memories from a mem0 export file into the current project.
## Execution
### Step 1: Determine the export file to import
If the user provided a filename as an argument to `/mem0:import <filename>`, use that file.
Otherwise, list `.md` files in the current directory whose names contain `mem0-export`:
```bash
ls -1 *.md 2>/dev/null | grep mem0-export || echo "No export files found"
```
If multiple files are found, ask the user which one to import. If none are found, print:
```
No mem0-export files found in the current directory.
Run /mem0:export first, or provide the filename: /mem0:import <path-to-file>
```
### Step 2: Parse the export file
Read the export file directly. It is a JSON file containing a top-level `memories` array. Each element has:
- `id` — original memory ID (for reference only; a new ID will be assigned on import)
- `type` — metadata type
- `confidence` — metadata confidence value
- `branch` — metadata branch
- `files` — list of associated files
- `categories` — list of categories
- `content` — the memory text
Parse the JSON in-memory (do not run any external script). If the file cannot be read or parsed, or if the `memories` array is missing or empty, print:
```
Failed to parse <filename> or file contains no valid memory blocks.
```
and stop.
### Step 3: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 4: Import each memory
For each record in the parsed JSON array, call `add_memory` (MCP tool) with:
- `text="<record.content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={`
- `"type": "<record.type>"` (if non-empty)
- `"confidence": "<record.confidence>"` (if non-empty)
- `"branch": "<record.branch>"` (if non-empty)
- `"files": <record.files>` (the list, if non-empty)
- `"source": "import"`
- `}`
- `infer=False`
Notes:
- Do NOT pass the original `id` — the platform assigns a new ID.
- Skip records where `content` is empty.
- Continue importing even if individual records fail; track the count of successes.
### Step 5: Print results
```
Imported <N> memories into project <project_id>
```
Where `<N>` is the number of successfully imported memories.
If any failed:
```
Imported <N>/<total> memories into project <project_id> (<failed> failed)
```
## Importing from competing AI tools (`--tools`)
When invoked with `--tools` (e.g., `/mem0:import --tools`), detect and import
from competing AI tool configuration files:
### Supported tools
| Tool | File/directory |
|------|---------------|
| Cursor | `.cursorrules` |
| GitHub Copilot | `.github/copilot-instructions.md` |
| Cline | `memory-bank/` (directory of `.md` files) |
| Continue | `.continue/rules.md` |
### T1: Detect
```bash
test -f .cursorrules && echo "cursor: .cursorrules"
test -f .github/copilot-instructions.md && echo "copilot: .github/copilot-instructions.md"
test -d memory-bank/ && echo "cline: memory-bank/"
test -f .continue/rules.md && echo "continue: .continue/rules.md"
```
### T2: Ask user
List found files, ask which to import (numbers, comma-separated, or "all").
If none found:
```
No competing tool configuration files found.
Checked: .cursorrules, .github/copilot-instructions.md, memory-bank/, .continue/rules.md
```
### T3: Import
For each selected tool, read the file(s) directly and split the content into logical
chunks to import as individual memories. No external scripts are used — all parsing
and importing is done via MCP tools.
Chunking rules per tool:
- **cursorrules / copilot / continue**: Read the file as plain text. Split on blank
lines or section headers (`#`, `##`). Each non-empty chunk becomes one memory.
Skip chunks shorter than 50 characters.
- **cline (memory-bank/)**: List all `.md` files in the directory. Read each file.
Split each file on blank lines or headers. Each non-empty chunk becomes one memory.
Skip chunks shorter than 50 characters.
For each chunk, call `add_memory` (MCP tool) with:
- `text="<chunk text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "<tool>-import", "confidence": 0.8}`
- `infer=False`
Where `<tool>` is `cursorrules`, `copilot`, `cline`, or `continue`.
Notes: chunks longer than 10,000 characters are truncated before import. Safe to
re-run — deduplication handles repeated entries.
### T4: Report
```
Imported <N> memories into <project_id> (cursor: <N>, copilot: <N>)
```
---
## Importing Claude Code's native MEMORY.md
When invoked with a path to Claude Code's native `MEMORY.md` file (typically
`~/.claude/projects/<proj-key>/memory/MEMORY.md`), or when `on_session_start.sh`
detects native auto-memory and the user chooses to import:
1. Read the file directly. It contains newline-separated memory entries (one fact per
line, sometimes with `- ` bullet prefix).
2. Split by non-empty lines. Each line becomes one memory.
3. Skip lines shorter than 20 characters or lines that are just headers (`#`).
4. For each line, call `add_memory` (MCP tool) with:
- `text="<line>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "memory-md-import", "confidence": 0.8}`
- `infer=False`
5. Report: `Imported <N> memories from MEMORY.md into project <project_id>`
6. Suggest disabling native auto-memory:
```
To avoid duplicate memory systems, add to ~/.claude/settings.json:
"autoMemoryEnabled": false
```
This handles the cold-start gap when a user has been using Claude Code's native
memory and switches to mem0.
## Error Handling
- If `add_memory` calls fail consistently (e.g. auth error), report the issue and stop early.
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,64 @@
---
name: list-projects
description: Lists all projects with stored memories for the current user, showing memory counts and last activity dates. Use when checking which projects have memories, comparing memory distribution across repos, or finding a specific project scope.
---
# Mem0 List Projects
Show all known project scopes for the current user.
## Execution
### Step 1: Fetch memories to discover app_ids
There is no dedicated "list projects" API endpoint. Discover projects by fetching
the user's memories across all scopes.
**Important:** A filter with only `user_id` triggers implicit null scoping — it
excludes memories that have a non-null `app_id`. Run two queries and merge:
1. **Null-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}]}`, `page_size=200`
— catches memories without `app_id`
2. **App-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": {"exists": true}}]}`, `page_size=200`
— catches memories with any `app_id`
Run both calls in parallel. Merge results, deduplicate by memory `id`.
If either response indicates more pages, paginate (up to 1000 total).
### Step 2: Extract distinct projects
For each memory, determine project by:
1. Top-level `app_id` field (preferred)
2. `metadata.project_id` (legacy memories)
3. `metadata.project` (oldest format)
4. `"(unscoped)"` if none found
Group by resolved project name. For each project, count:
- Total memories
- Most recent `created_at` date
- Top 3 `metadata.type` values by frequency
### Step 3: Display
```
## mem0 projects
<app_id_1> <count> memories (last: <date>) ← current
<app_id_2> <count> memories (last: <date>)
<N> projects, <M> total memories
```
Mark current project with `← current`. Sort by memory count descending.
### Step 4: Empty state
If zero memories found:
```
No projects found. Run /mem0:onboard to get started.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,189 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but not
limited to compiled object code, generated documentation, and
conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 Mem0.ai
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,73 @@
# Mem0 Skill for Claude
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai?utm_source=oss&utm_medium=mem0-plugin-skill-readme).
## What This Skill Does
When installed, Claude can:
- **Set up Mem0** in your Python or TypeScript project
- **Integrate memory** into your existing AI app (LangChain, CrewAI, Vercel AI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
- **Generate working code** using real API references and tested patterns
- **Search live docs** on demand for the latest Mem0 documentation
## Installation
This skill is included automatically when you install the Mem0 plugin:
```
/plugin marketplace add mem0ai/mem0
/plugin install mem0@mem0-plugins
```
See the [plugin README](../../README.md) for full setup instructions.
### Prerequisites
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill-readme))
- Python 3.10+ or Node.js 18+
- Set the environment variable:
```bash
export MEM0_API_KEY="m0-your-api-key"
```
## Quick Start
After installing, just ask Claude:
- "Set up mem0 in my project"
- "Add memory to my chatbot"
- "Help me search user memories with filters"
- "Integrate mem0 with my LangChain app"
- "Add graph memory to track entity relationships"
## What's Inside
```text
skills/mem0/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
├── scripts/
│ └── mem0_doc_search.py # Search live Mem0 docs on demand
└── references/ # Documentation (loaded on demand)
├── quickstart.md # Full quickstart (Python, TS, cURL)
├── sdk-guide.md # All SDK methods (Python + TypeScript)
├── api-reference.md # REST endpoints, filters, memory object
├── architecture.md # Processing pipeline, lifecycle, scoping, performance
├── features.md # Retrieval, graph, categories, MCP, webhooks, multimodal
├── integration-patterns.md # LangChain, CrewAI, Vercel AI, LangGraph, LlamaIndex, etc.
└── use-cases.md # 7 real-world patterns with Python + TypeScript code
```
## Links
- [Mem0 Platform Dashboard](https://app.mem0.ai?utm_source=oss&utm_medium=mem0-plugin-skill-readme)
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
- [API Reference](https://docs.mem0.ai/api-reference)
## License
Apache-2.0
@@ -0,0 +1,191 @@
---
name: mem0
description: Mem0 SDK reference covering Python and TypeScript APIs, memory client methods, configuration, and framework integrations. Use when writing code that calls mem0 APIs, configuring memory providers, or integrating mem0 into an application.
license: Apache-2.0
metadata:
author: mem0ai
version: "0.1.1"
category: ai-memory
tags: "memory, personalization, ai, python, typescript, vector-search"
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var (Platform), and internet access to api.mem0.ai. Uses Mem0 v3 API.
---
# Mem0 Platform Integration
> **Skill Graph:** This skill is part of the Mem0 skill graph:
> - **mem0** (this skill) -- Platform Client SDK + OSS (Python + TypeScript)
> - **[mem0-vercel-ai-sdk](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk)** -- Vercel AI SDK provider
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy. For self-hosted usage, see the OSS section in the client references below.
## Step 1: Install and authenticate
**Python:**
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
**TypeScript/JavaScript:**
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
Get an API key at: https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill
> **Don't have a `MEM0_API_KEY`?** Sign up at https://app.mem0.ai and create one from the dashboard. Keys start with `m0-`.
## Step 2: Initialize the client
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
For async Python, use `AsyncMemoryClient`.
## Step 3: Core operations
Every Mem0 integration follows the same pattern: **retrieve → generate → store**.
### Add memories
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
### Search memories
```python
results = client.search("dietary preferences", filters={"user_id": "alice"})
for mem in results.get("results", []):
print(mem["memory"])
```
### Get all memories
```python
all_memories = client.get_all(filters={"user_id": "alice"})
```
### Update a memory
```python
client.update("memory-uuid", text="Updated: vegetarian, nut allergy, prefers organic")
```
### Delete a memory
```python
client.delete("memory-uuid")
client.delete_all(user_id="alice") # delete all for a user
```
## Common integration pattern
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, filters={"user_id": user_id})
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate response with memory context
response = openai.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"User context:\n{context}"},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
```
## Common edge cases
- **Search returns empty:** v3 processes `add()` asynchronously — returns an event ID immediately. Wait 2-3s before searching. Also verify `user_id` matches exactly (case-sensitive) and use `filters={"user_id": "..."}` syntax.
- **AND filter with user_id + agent_id returns empty:** Entities are stored separately. `{"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}` returns nothing. Use `OR` instead, or query each separately.
- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. `infer=True` extracts facts via LLM with dedup. `infer=False` stores raw — same text can be stored twice.
- **Implicit null scoping:** `filters={"user_id": "alice"}` only returns memories where `agent_id`, `app_id`, `run_id` are ALL null. Wrap in `{"OR": [...]}` to include memories with non-null scoping fields.
- **Platform vs OSS imports:** Platform: `from mem0 import MemoryClient`. OSS: `from mem0 import Memory`. Don't mix them — `MemoryClient` talks to `api.mem0.ai`, `Memory` runs locally.
- **v3 defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`. Adjust as needed.
## v3 API (Current)
Mem0 v3 uses single-pass extraction, entity linking, and multi-signal retrieval.
**Key v3 changes from v2:**
- **Endpoints:** `POST /v3/memories/add/`, `POST /v3/memories/search/`, `POST /v3/memories/` (paginated list)
- **Extraction:** Single ADD-only pass — no more UPDATE/DELETE operations during extraction. Memories accumulate rather than consolidate.
- **Entity linking:** Replaces graph memory. Auto-extracted during `add()`, no config needed. Remove `enable_graph` and `graph_store` from any old config.
- **Defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`
- **Removed params:** `org_id`, `project_id`, `enable_graph` — all removed from SDK
- **TypeScript:** Exclusively camelCase (`userId`, `agentId`, `appId`, `topK`)
- **Add response:** Async — returns event ID immediately, poll via `GET /v1/event/{event_id}/`
See the [migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for details.
## Live documentation search
For the latest docs beyond what's in the references, use the doc search tool:
```bash
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --query "topic"
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --page "/platform/features/graph-memory"
bun ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.ts --index
```
No API key needed — searches docs.mem0.ai directly.
## Client SDK References
Language-specific deep references (Platform + OSS):
| Language | File |
|----------|------|
| Python (MemoryClient + AsyncMemoryClient + Memory OSS) | [client/python.md](client/python.md) |
| TypeScript/Node.js (MemoryClient + Memory OSS) | [client/node.md](client/node.md) |
| Python vs TypeScript differences | [client/differences.md](client/differences.md) |
## Platform References
Load these on demand for deeper detail:
| Topic | File |
|-------|------|
| Quickstart (Python, TS, cURL) | [references/quickstart.md](references/quickstart.md) |
| SDK guide (all methods, both languages) | [references/sdk-guide.md](references/sdk-guide.md) |
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
| Framework integrations (LangChain, CrewAI, OpenAI Agents, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
## Related Mem0 Skills
| Skill | When to use | Link |
|-------|-------------|------|
| mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,129 @@
# Python vs TypeScript SDK Differences
Quick-reference cheatsheet for developers working across both Mem0 SDKs.
## Constructor
| Aspect | Python | TypeScript |
|--------|--------|------------|
| Import (Platform) | `from mem0 import MemoryClient` | `import MemoryClient from 'mem0ai'` |
| Import (OSS) | `from mem0 import Memory` | `import { Memory } from 'mem0ai/oss'` |
| Constructor | `MemoryClient(api_key="m0-xxx")` | `new MemoryClient({ apiKey: 'm0-xxx' })` |
| Required param | `api_key` (positional or kwarg) | `apiKey` (in options object) |
Both read from `MEM0_API_KEY` env var if no key provided.
## Method Naming
| Operation | Python | TypeScript |
|-----------|--------|------------|
| Add | `add()` | `add()` |
| Search | `search()` | `search()` |
| Get | `get()` | `get()` |
| Get all | `get_all()` | `getAll()` |
| Update | `update()` | `update()` |
| Delete | `delete()` | `delete()` |
| Delete all | `delete_all()` | `deleteAll()` |
| History | `history()` | `history()` |
| Batch update | `batch_update()` | `batchUpdate()` |
| Batch delete | `batch_delete()` | `batchDelete()` |
| List users | `users()` | `users()` |
| Delete users | `delete_users()` | `deleteUsers()` |
| Get project | `project.get()` | `getProject()` |
| Update project | `project.update()` | `updateProject()` |
| Create webhook | `create_webhook()` | `createWebhook()` |
| Get webhooks | `get_webhooks()` | `getWebhooks()` |
| Update webhook | `update_webhook()` | `updateWebhook()` |
| Delete webhook | `delete_webhook()` | `deleteWebhook()` |
| Create export | `create_memory_export()` | `createMemoryExport()` |
| Get export | `get_memory_export()` | `getMemoryExport()` |
| Feedback | `feedback()` | `feedback()` |
**Rule:** Python uses `snake_case`, TypeScript uses `camelCase` for method names.
## Parameter Passing
```python
# Python: kwargs
client.add(messages, user_id="alice", metadata={"source": "chat"})
client.search("query", filters={"user_id": "alice"}, top_k=5, rerank=True)
```
```typescript
// TypeScript: options object with camelCase for top-level params, snake_case for filter keys
await client.add(messages, { userId: 'alice', metadata: { source: 'chat' } });
await client.search('query', { filters: { user_id: 'alice' }, topK: 5, rerank: true });
```
**v3:** Python uses `snake_case` everywhere. TypeScript uses `camelCase` for top-level params (`userId`, `topK`) but `snake_case` for filter keys (`user_id`, `agent_id`).
## Architectural Differences
| Aspect | Python | TypeScript |
|--------|--------|------------|
| HTTP library | httpx | axios |
| Default timeout | 300s | 60s |
| Sync support | Yes (`MemoryClient`) | No (all async) |
| Async support | Yes (`AsyncMemoryClient`) | All methods are async |
| Project management | `client.project.*` (separate class) | `client.getProject()` / `client.updateProject()` |
| Context manager | `async with AsyncMemoryClient()` | Not supported |
## Platform Features: Python-only
These methods exist in Python but not TypeScript:
| Method | Description |
|--------|-------------|
| `get_summary(filters)` | Get summary of memories |
| `reset()` | Delete ALL data (users + memories) |
| `project.create(name)` | Create a new project |
| `project.delete()` | Delete current project |
| `project.get_members()` | List project members |
| `project.add_member(email, role)` | Add member to project |
| `project.update_member(email, role)` | Change member role |
| `project.remove_member(email)` | Remove member |
## Platform Features: TypeScript-only
| Method | Description |
|--------|-------------|
| `deleteUser(data)` | Convenience method for single entity deletion |
| `ping()` | Health check endpoint |
## OSS Config Naming
| Python config key | TypeScript config key |
|-------------------|----------------------|
| `vector_store` | `vectorStore` |
| `history_db_path` | `historyDbPath` |
| `custom_instructions` | `customInstructions` |
## OSS Scope Parameter Naming
| Python | TypeScript |
|--------|------------|
| `user_id="alice"` | `userId: 'alice'` |
| `agent_id="bot"` | `agentId: 'bot'` |
| `run_id="session"` | `runId: 'session'` |
## Entity ID Passing (v3)
| Method | Python | TypeScript |
|--------|--------|------------|
| add() | Top-level: `user_id="alice"` | Top-level: `{ userId: 'alice' }` |
| search() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
| get_all() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
## Common Gotcha
When searching/filtering, both Python and TypeScript use `snake_case` for filter keys. TypeScript only uses `camelCase` for top-level method parameters:
```python
# Python - snake_case in filters
results = client.search("query", filters={"user_id": "alice"})
```
```typescript
// TypeScript - snake_case in filters, camelCase for top-level params
const results = await client.search('query', { filters: { user_id: 'alice' }, topK: 20 });
```
@@ -0,0 +1,418 @@
# Mem0 Node.js / TypeScript SDK Reference
Complete reference for the `mem0ai` npm package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
**Constructor:** `new MemoryClient({ apiKey })`. If `apiKey` is not provided, reads from `MEM0_API_KEY` environment variable.
- HTTP library: `axios`
- Timeout: 60 seconds
- Base URL: `https://api.mem0.ai`
- All methods are async (return `Promise`)
---
### Memory Methods
#### add(messages, options?)
Store new memories from messages.
```typescript
const messages = [
{ role: 'user', content: "I'm a vegetarian and allergic to nuts." },
{ role: 'assistant', content: "Got it! I'll remember that." },
];
await client.add(messages, { userId: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `Message[]` | Array of `{role, content}` objects |
| `options.userId` | string | User identifier |
| `options.agentId` | string | Agent identifier |
| `options.appId` | string | Application identifier |
| `options.runId` | string | Session identifier |
| `options.metadata` | object | Custom key-value pairs |
| `options.infer` | boolean | If false, store raw text (default: true) |
**Returns:** `Promise<any>` -- list of events
#### search(query, options?)
Search memories by semantic similarity.
```typescript
const results = await client.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 20 });
for (const mem of results.results) {
console.log(mem.memory, mem.score);
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Natural language search query |
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
| `options.topK` | number | Number of results (default: 20) |
| `options.rerank` | boolean | Enable semantic reranking (default: false) |
| `options.threshold` | number | Minimum similarity (default: 0.1) |
**Returns:** `Promise<SearchResult>` -- `{results: [{id, memory, score, ...}]}`
#### get(memoryId)
```typescript
const memory = await client.get('ea925981-...');
```
#### getAll(options?)
Retrieve all memories. Requires at least one entity identifier in filters.
```typescript
const memories = await client.getAll({ filters: { user_id: 'alice' } });
// With filters
const filtered = await client.getAll({
filters: { AND: [{ user_id: 'alice' }, { categories: { contains: 'health' } }] },
});
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
| `options.page` | number | Page number |
| `options.pageSize` | number | Results per page |
#### update(memoryId, data)
```typescript
await client.update('ea925981-...', { text: 'Updated: vegan since 2024' });
await client.update('ea925981-...', { text: 'Updated', metadata: { verified: true } });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `memoryId` | string | Memory ID |
| `data.text` | string | New content |
| `data.metadata` | object | New metadata |
| `data.timestamp` | string | New timestamp |
#### delete(memoryId)
```typescript
await client.delete('ea925981-...');
```
#### deleteAll(options?)
```typescript
await client.deleteAll({ userId: 'alice' });
```
#### history(memoryId)
```typescript
const history = await client.history('ea925981-...');
// Returns: [{previousValue, newValue, action, timestamps}]
```
---
### Batch Methods
#### batchUpdate(memories)
```typescript
await client.batchUpdate([
{ memoryId: 'uuid-1', text: 'Updated text' },
{ memoryId: 'uuid-2', text: 'Another update' },
]);
```
#### batchDelete(memories)
```typescript
await client.batchDelete(['uuid-1', 'uuid-2', 'uuid-3']);
```
---
### User/Entity Management
#### users()
```typescript
const users = await client.users();
// Returns: {results: [{type: "user", name: "alice"}, ...]}
```
#### deleteUser(data) / deleteUsers(data)
```typescript
await client.deleteUser({ userId: 'alice' }); // Single entity
await client.deleteUsers({ agentId: 'bot-1' }); // Flexible
```
---
### Project Management
```typescript
// Get project config
const config = await client.getProject({ fields: ['customCategories'] });
// Update project settings
await client.updateProject({
customInstructions: 'Extract dietary preferences and health info',
customCategories: [{ health: 'Medical and dietary info' }],
});
```
---
### Webhooks
```typescript
// List
const webhooks = await client.getWebhooks({ projectId: 'proj_123' });
// Create
const webhook = await client.createWebhook({
url: 'https://your-app.com/webhook',
name: 'Memory Logger',
projectId: 'proj_123',
eventTypes: ['memory_add', 'memory_update'],
});
// Update
await client.updateWebhook({
webhookId: 'wh_123',
name: 'Updated Logger',
url: 'https://new-url.com',
});
// Delete
await client.deleteWebhook({ webhookId: 'wh_123' });
```
---
### Feedback
```typescript
await client.feedback({
memoryId: 'mem-123',
feedback: 'POSITIVE',
feedbackReason: 'Accurately captured preference',
});
```
---
### Export
```typescript
const exportReq = await client.createMemoryExport({
schema: JSON.stringify({ type: 'object', properties: { name: { type: 'string' } } }),
filters: { user_id: 'alice' },
});
const result = await client.getMemoryExport({ memoryExportId: exportReq.id });
```
---
### TypeScript Types
Key interfaces from `mem0.types.ts`:
```typescript
interface Message { role: string; content: string; }
interface Memory { id: string; memory: string; userId: string; categories: string[]; score?: number; /* ... */ }
interface MemoryOptions { userId?: string; agentId?: string; appId?: string; runId?: string; metadata?: object; /* ... */ }
interface SearchOptions { filters?: object; topK?: number; rerank?: boolean; threshold?: number; /* ... */ }
interface MemoryHistory { id: string; memoryId: string; previousValue: string; newValue: string; action: string; /* ... */ }
interface FeedbackPayload { memoryId: string; feedback: string; feedbackReason?: string; }
interface WebhookCreatePayload { url: string; name: string; projectId: string; eventTypes: string[]; }
```
---
## Open Source / Self-Hosted
### Installation
```bash
npm install mem0ai
```
### Memory Class
```typescript
import { Memory } from 'mem0ai/oss';
const m = new Memory(); // Uses default config
```
**Import:** `from 'mem0ai/oss'` (NOT the default export -- that is `MemoryClient` for Platform)
### Configuration
```typescript
const config = {
llm: {
provider: 'openai', // openai, groq, anthropic, google, ollama, lmstudio, mistral, azure
config: {
model: 'gpt-5-mini',
apiKey: 'sk-xxx',
},
},
embedder: {
provider: 'openai', // openai, ollama, lmstudio, google, azure, langchain, anthropic
config: {
model: 'text-embedding-3-small',
apiKey: 'sk-xxx',
},
},
vectorStore: {
provider: 'qdrant', // memory, qdrant, redis, supabase, langchain, azure_ai_search, pgvector
config: {
collectionName: 'my_memories',
host: 'localhost',
port: 6333,
},
},
historyDbPath: 'history.db',
customInstructions: '...',
disableHistory: false,
};
const m = new Memory(config);
// Or from dict with validation:
const m2 = Memory.fromConfig(config);
```
### Methods
All methods are async (return `Promise`):
#### add(messages, config)
```typescript
await m.add('I prefer dark mode', { userId: 'alice' });
await m.add([
{ role: 'user', content: 'I like hiking' },
{ role: 'assistant', content: 'Great outdoor activity!' },
], { userId: 'alice' });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `messages` | `string \| Message[]` | Content to store |
| `config.userId` | string | User identifier (at least one scope required) |
| `config.agentId` | string | Agent identifier |
| `config.runId` | string | Session identifier |
| `config.metadata` | object | Custom key-value pairs |
| `config.filters` | object | Additional filters |
| `config.infer` | boolean | LLM inference (default: true) |
**Returns:** `Promise<{results: [...], relations?: [...]}>`
#### search(query, config)
```typescript
const results = await m.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 5 });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string | Search query |
| `config.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, `run_id`, etc.) |
| `config.topK` | number | Max results (default: 20) |
#### get(memoryId) / getAll(config) / update(memoryId, data) / delete(memoryId) / deleteAll(config) / history(memoryId)
Same interface patterns. Note: OSS `update` takes a string for data, not an object.
```typescript
await m.update('mem-id', 'new content');
```
#### reset()
Clear the entire vector store and history.
```typescript
await m.reset();
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `import MemoryClient from 'mem0ai'` | `import { Memory } from 'mem0ai/oss'` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Param style** | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) |
| **Batch ops** | `batchUpdate`, `batchDelete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `createMemoryExport` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `getProject`, `updateProject` | Not available |
| **User listing** | `users()`, `deleteUser()` | Not available |
| **History** | Platform-managed | SQLite (configurable) |
---
## v2 Compatibility
If you're using SDK v2.x:
**Naming Changes:**
- Top-level params now use camelCase: `topK`, `rerank` (not `top_k`)
- Filter keys use snake_case: `user_id`, `agent_id`
- OSS: `limit` renamed to `topK`
**API Changes:**
```typescript
// v2 - top-level entity IDs, snake_case
await client.search("query", { user_id: "alice", top_k: 20 });
// v3 - filters object with snake_case keys, camelCase top-level params
await client.search("query", { filters: { user_id: "alice" }, topK: 20 });
```
**Default Changes:**
| Param | v2 | v3 |
|-------|----|----|
| `topK` | 100 | 20 |
| `threshold` | none | 0.1 |
| `rerank` | true | false |
**Removed:**
- `OutputFormat` and `API_VERSION` enums
- `organizationId`, `projectId` from constructor
- `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details.
@@ -0,0 +1,487 @@
# Mem0 Python SDK Reference
Complete reference for the `mem0ai` Python package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
---
## Platform Client
### Installation
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
### MemoryClient (Synchronous)
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**Constructor:** `MemoryClient(api_key=None)`. If `api_key` is not provided, reads from `MEM0_API_KEY` environment variable. Raises `ValueError` if no key found.
- HTTP library: `httpx`
- Timeout: 300 seconds
- Base URL: `https://api.mem0.ai`
### AsyncMemoryClient (Asynchronous)
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-xxx")
# Or use as context manager
async with AsyncMemoryClient(api_key="m0-xxx") as client:
results = await client.search("query", filters={"user_id": "alice"})
```
Same methods as `MemoryClient`, all `async`/`await`. Supports async context manager.
---
### Memory Methods
#### add(messages, **kwargs)
Store new memories from messages.
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `messages` | str \| dict \| list[dict] | required | Message content. Strings auto-convert to user messages |
| `user_id` | str | None | User identifier |
| `agent_id` | str | None | Agent identifier |
| `app_id` | str | None | Application identifier |
| `run_id` | str | None | Session/run identifier |
| `metadata` | dict | None | Custom key-value pairs |
| `infer` | bool | True | If False, store raw text without LLM inference |
| `custom_categories` | list | None | Override project categories |
| `custom_instructions` | str | None | Override extraction instructions |
| `timestamp` | int \| float \| str | None | Custom timestamp (Unix epoch or ISO 8601) |
**Returns:** `dict` -- list of events: `[{"id": "...", "event": "ADD", "data": {"memory": "..."}}]`
#### search(query, **kwargs)
Search memories by semantic similarity.
```python
results = client.search("dietary preferences", filters={"user_id": "alice"})
for mem in results.get("results", []):
print(mem["memory"], mem["score"])
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | str | required | Natural language search query |
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions (e.g., `{"user_id": "alice"}`) |
| `top_k` | int | 10 | Number of results |
| `rerank` | bool | False | Enable deep semantic reranking (+150-200ms) |
| `threshold` | float | 0.1 | Minimum similarity score |
| `fields` | list | None | Specific fields to return |
| `categories` | list | None | Filter by category |
**Returns:** `dict` -- `{"results": [{id, memory, user_id, categories, score, created_at, ...}]}`
#### get(memory_id)
Retrieve a single memory by ID.
```python
memory = client.get(memory_id="ea925981-...")
```
**Returns:** `dict` -- full memory object
#### get_all(**kwargs)
Retrieve all memories with optional filtering. Requires at least one entity identifier.
```python
memories = client.get_all(filters={"user_id": "alice"})
# With compound filters
memories = client.get_all(filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "health"}}]})
```
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions |
| `top_k` | int | None | Limit results |
| `page` | int | None | Page number |
| `page_size` | int | None | Results per page |
**Returns:** `dict` -- `{"results": [...]}`
#### update(memory_id, text=None, metadata=None, timestamp=None)
Update a memory's content, metadata, or timestamp. At least one parameter required.
```python
client.update("ea925981-...", text="Updated: vegan since 2024")
client.update("ea925981-...", metadata={"verified": True})
```
**Returns:** `dict` -- updated memory
#### delete(memory_id)
Permanently delete a single memory.
```python
client.delete("ea925981-...")
```
#### delete_all(**kwargs)
Delete all memories matching filters. Irreversible.
```python
client.delete_all(user_id="alice")
```
#### history(memory_id)
Get the change history of a memory.
```python
history = client.history("ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
---
### Batch Methods
#### batch_update(memories)
Update up to 1000 memories in a single request.
```python
client.batch_update([
{"memory_id": "uuid-1", "text": "Updated text"},
{"memory_id": "uuid-2", "text": "Another update", "metadata": {"verified": True}},
])
```
#### batch_delete(memories)
Delete up to 1000 memories in a single request.
```python
client.batch_delete([
{"memory_id": "uuid-1"},
{"memory_id": "uuid-2"},
])
```
---
### User/Entity Management
#### users()
List all users, agents, and sessions that have memories.
```python
users = client.users()
# Returns: {"results": [{"type": "user", "name": "alice"}, ...]}
```
#### delete_users(user_id=None, agent_id=None, app_id=None, run_id=None)
Delete a specific entity and all its memories.
```python
client.delete_users(user_id="alice")
```
#### reset()
Delete ALL users, agents, sessions, and memories. Complete data reset.
```python
client.reset()
```
---
### Export & Summary
#### create_memory_export(schema, **kwargs)
Create a structured export of memories.
```python
import json
schema = json.dumps({
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
}
})
export = client.create_memory_export(schema=schema, user_id="alice")
```
#### get_memory_export(**kwargs)
Retrieve a previously created export.
```python
result = client.get_memory_export(memory_export_id=export["id"])
```
#### get_summary(filters=None)
Get a summary of memories.
```python
summary = client.get_summary(filters={"user_id": "alice"})
```
---
### Feedback
#### feedback(memory_id, feedback=None, feedback_reason=None)
Provide quality feedback on a memory.
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE", # POSITIVE | NEGATIVE | VERY_NEGATIVE | None (clear)
feedback_reason="Accurately captured preference"
)
```
---
### Webhooks
```python
# List
webhooks = client.get_webhooks(project_id="proj_123")
# Create
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_update"]
)
# Update
client.update_webhook(webhook_id=123, name="Updated", url="https://new-url.com")
# Delete
client.delete_webhook(webhook_id=123)
```
---
### Project Management
Access via `client.project.*`:
```python
# Get project config
config = client.project.get(fields=["custom_categories", "custom_instructions"])
# Update project settings
client.project.update(
custom_instructions="Extract dietary preferences and health info",
custom_categories=[{"health": "Medical and dietary info"}],
multilingual=True,
)
# Create/delete project
client.project.create(name="My Project", description="...")
client.project.delete()
# Member management
members = client.project.get_members()
client.project.add_member(email="user@example.com", role="READER") # READER or OWNER
client.project.update_member(email="user@example.com", role="OWNER")
client.project.remove_member(email="user@example.com")
```
---
## Open Source / Self-Hosted
### Installation
```bash
pip install mem0ai
```
### Memory Class
```python
from mem0 import Memory
m = Memory() # Uses default config (OpenAI embedder + in-memory vector store)
```
**Import:** `from mem0 import Memory` (NOT `MemoryClient` -- that is the Platform client)
### Configuration
```python
config = {
"llm": {
"provider": "openai", # openai, groq, azure, ollama, lmstudio, google, anthropic, mistral
"config": {
"model": "gpt-5-mini",
"api_key": "sk-xxx",
}
},
"embedder": {
"provider": "openai", # openai, ollama, azure, lmstudio, google, huggingface
"config": {
"model": "text-embedding-3-small",
"api_key": "sk-xxx",
}
},
"vector_store": {
"provider": "qdrant", # faiss, qdrant, pgvector, redis, supabase, azure_ai_search, memory
"config": {
"collection_name": "my_memories",
"host": "localhost",
"port": 6333,
}
},
"history_db_path": "history.db", # SQLite path for change history
"custom_instructions": "...", # Custom LLM prompt for extraction
}
m = Memory.from_config(config)
```
### Context Manager
```python
with Memory(config) as m:
m.add("I prefer dark mode", user_id="alice")
results = m.search("preferences", filters={"user_id": "alice"})
# SQLite connections released automatically
```
### Methods
All methods mirror the Platform client but run locally:
#### add(messages, *, user_id, agent_id, run_id, metadata, infer=True)
```python
m.add("I'm a vegetarian", user_id="alice")
m.add([
{"role": "user", "content": "I like hiking"},
{"role": "assistant", "content": "Great outdoor activity!"}
], user_id="alice")
```
At least one of `user_id`, `agent_id`, `run_id` required.
**Returns:** `{"results": [...], "relations": [...]}`
#### search(query, *, filters=None, top_k=20, threshold=0.1, rerank=False)
```python
results = m.search("dietary preferences", filters={"user_id": "alice"}, top_k=5)
```
Entity IDs (`user_id`, `agent_id`, `run_id`) must be passed inside the `filters` dict.
Supports filter operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`.
#### get(memory_id) / get_all(**kwargs) / update(memory_id, data, metadata=None) / delete(memory_id) / delete_all(**kwargs) / history(memory_id)
Same interface as Platform client.
#### reset()
Clear the entire vector store collection and history database. Recreates the vector store.
```python
m.reset()
```
#### close()
Release SQLite connections. Called automatically when using context manager.
### AsyncMemory
```python
from mem0 import AsyncMemory
m = AsyncMemory(config)
await m.add("text", user_id="alice")
results = await m.search("query", filters={"user_id": "alice"})
```
---
## Key Differences: Platform vs OSS
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|--------|--------------------------|----------------|
| **Import** | `from mem0 import MemoryClient` | `from mem0 import Memory` |
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
| **Execution** | API calls to `api.mem0.ai` | Local execution |
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
| **Entity filtering** | `filters={"user_id": "..."}` | `filters={"user_id": "..."}` |
| **Batch ops** | `batch_update`, `batch_delete` | Not available |
| **Webhooks** | Full CRUD | Not available |
| **Export** | `create_memory_export`, `get_memory_export` | Not available |
| **Feedback** | `feedback()` | Not available |
| **Project mgmt** | `client.project.*` | Not available |
| **User listing** | `users()`, `delete_users()` | Not available |
| **Custom prompts** | Via project settings | Direct config (`custom_instructions`) |
| **History** | Platform-managed | SQLite (configurable) |
| **Async** | `AsyncMemoryClient` | `AsyncMemory` |
---
## v2 Compatibility
If you're using SDK v2.x or the v2 API:
**API Changes:**
- **Entity IDs in search/get_all:** Pass `user_id`, `agent_id` as top-level kwargs instead of inside `filters`
```python
# v2
results = client.search("query", user_id="alice")
# v3
results = client.search("query", filters={"user_id": "alice"})
```
- **add() returns:** v2 returns ADD, UPDATE, DELETE events; v3 returns ADD only
**Default Changes:**
| Param | v2 | v3 |
|-------|----|----|
| `top_k` | 100 | 20 |
| `threshold` | None | 0.1 |
| `rerank` | True | False |
**Removed Parameters:**
- Constructor: `org_id`, `project_id`
- add(): `async_mode`, `output_format`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
- search()/get_all(): `enable_graph`
- Config: `enable_graph`, `graph_store`, `custom_fact_extraction_prompt` (renamed to `custom_instructions`)
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for full details.
@@ -0,0 +1,150 @@
# Mem0 Platform API Reference
REST API endpoints for the Mem0 Platform. Base URL: `https://api.mem0.ai`
All endpoints require: `Authorization: Token <MEM0_API_KEY>`
## Endpoints
| Operation | Method | URL |
|-----------|--------|-----|
| Add Memories | `POST` | `/v3/memories/add/` |
| Search Memories | `POST` | `/v3/memories/search/` |
| Get All Memories | `POST` | `/v3/memories/` |
| Get Single Memory | `GET` | `/v1/memories/{memory_id}/` |
| Update Memory | `PUT` | `/v1/memories/{memory_id}/` |
| Delete Memory | `DELETE` | `/v1/memories/{memory_id}/` |
| Delete All Memories | `DELETE` | `/v1/memories/?user_id=X&app_id=Y` |
| Get Event Status | `GET` | `/v1/event/{event_id}/` |
## Memory Object Structure
| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Unique memory identifier |
| `memory` | string | Text content of the memory |
| `user_id` | string | Associated user |
| `agent_id` | string (nullable) | Agent identifier |
| `app_id` | string (nullable) | Application identifier |
| `run_id` | string (nullable) | Run/session identifier |
| `metadata` | object | Custom key-value pairs |
| `categories` | array of strings | Auto-assigned category tags |
| `hash` | string | Content hash |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
Search results additionally include `score` (relevance metric).
## Scoping Identifiers
Memories can be scoped to different levels:
| Scope | Parameter | Use Case |
|-------|-----------|----------|
| User | `user_id` | Per-user memory isolation |
| Agent | `agent_id` | Per-agent memory partitioning |
| Application | `app_id` | Cross-agent app-level memory |
| Run/Session | `run_id` | Session-scoped temporary memory |
**Critical:** Combining `user_id` and `agent_id` in a single AND filter yields empty results. Entities are stored separately. Use `OR` logic or separate queries.
## Processing Model
- Memories are processed **asynchronously** (v3 default)
- Add responses return queued `ADD` events only (v3 is ADD-only, no UPDATE/DELETE)
- Poll status via `GET /v1/event/{event_id}/`
## Filter System
Filters use nested JSON with a logical operator at the root:
```json
{
"AND": [
{"user_id": "alice"},
{"categories": {"contains": "finance"}},
{"created_at": {"gte": "2024-01-01"}}
]
}
```
Root must be `AND`, `OR`, or `NOT`. Simple shorthand `{"user_id": "alice"}` also works.
### Supported Operators
| Operator | Description |
|----------|-------------|
| `eq` | Equal to (default) |
| `ne` | Not equal to |
| `in` | Matches any value in array |
| `gt`, `gte` | Greater than / greater than or equal |
| `lt`, `lte` | Less than / less than or equal |
| `contains` | Case-sensitive containment |
| `icontains` | Case-insensitive containment |
| `*` | Wildcard -- matches any non-null value |
### Filterable Fields
| Field | Valid Operators |
|-------|-----------------|
| `user_id`, `agent_id`, `app_id`, `run_id` | `eq`, `ne`, `in`, `*` |
| `created_at`, `updated_at`, `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` |
| `categories` | `eq`, `ne`, `in`, `contains` |
| `metadata` | `eq`, `ne`, `contains` (top-level keys only) |
| `keywords` | `contains`, `icontains` |
| `memory_ids` | `in` |
### Filter Constraints
1. **Entity scope partitioning:** `user_id` AND `agent_id` in one `AND` block yields empty results.
2. **Metadata limitations:** Only top-level keys. Only `eq`, `contains`, `ne`. No `in` or `gt`.
3. **Operator syntax:** Use `gte`, `lt`, `ne`. SQL-style (`>=`, `!=`) rejected.
4. **Entity filter required for get-all:** At least one of `user_id`, `agent_id`, `app_id`, or `run_id`.
5. **Wildcard excludes null:** `*` matches only non-null values.
6. **Date format:** ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). Timezone-naive defaults to UTC.
## Response Formats
### Add Response (v3)
```json
{
"message": "Memory processing has been queued for background execution",
"status": "PENDING",
"event_id": "evt-uuid"
}
```
v3 is ADD-only. No UPDATE or DELETE events.
### Search Response
```json
{
"results": [
{
"id": "ea925981-...",
"memory": "Is a vegetarian and allergic to nuts.",
"user_id": "user123",
"categories": ["food", "health"],
"score": 0.89,
"created_at": "2024-07-26T10:29:36.630547-07:00"
}
]
}
```
In v3, `score` is a combined multi-signal relevance score.
### Get All Response (v3)
```json
{
"count": 123,
"next": "https://api.mem0.ai/v3/memories/?page=2&page_size=50",
"previous": null,
"results": [...]
}
```
v3 returns paginated envelope. Use `page` and `page_size` query params.
@@ -0,0 +1,330 @@
# Mem0 Platform Architecture
How Mem0 processes, stores, and retrieves memories under the hood.
## Table of Contents
- [Core Concept](#core-concept)
- [Memory Processing Pipeline](#memory-processing-pipeline)
- [Retrieval Pipeline](#retrieval-pipeline)
- [Memory Lifecycle](#memory-lifecycle)
- [Memory Object Structure](#memory-object-structure)
- [Scoping & Multi-Tenancy](#scoping--multi-tenancy)
- [Memory Layers](#memory-layers)
- [Performance Characteristics](#performance-characteristics)
---
## Core Concept
Mem0 is a managed memory layer that sits between your AI application and users. Every integration follows the same 3-step loop:
```
User Input → Retrieve relevant memories → Enrich LLM prompt → Generate response → Store new memories
```
Mem0 handles the complexity of extraction, deduplication, conflict resolution, and semantic retrieval so your application only needs to call `search()` and `add()`.
**Storage architecture:**
- **Vector store**: Embeddings for semantic similarity search
- **Entity store**: Automatic entity linking for relationship-aware retrieval
---
## Memory Processing Pipeline
### What happens when you call `client.add()`
```
Messages In
│
▼
┌─────────────────────┐
│ 1. EXTRACTION │ Single LLM call extracts all distinct new facts
│ (infer=True) │ If infer=False, stores raw text as-is
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. DEDUPLICATION │ Hash-based dedup (MD5 prevents exact duplicates)
│ │ No UPDATE/DELETE - v3 is ADD-only
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. STORAGE │ Batch embed → vector store
│ │ Entity extraction → entity store
└─────────┬───────────┘
│
▼
Memory Object
```
### Processing (v3)
v3 processes memories asynchronously by default:
- API returns immediately: `{"status": "PENDING", "event_id": "evt-..."}`
- Poll status via `GET /v1/event/{event_id}/`
- Use webhooks for completion notifications
### Extraction modes
**Inferred (`infer=True`, default):**
- LLM extracts structured facts from conversation
- Conflict resolution deduplicates and resolves contradictions
- Best for: natural conversation → memory
**Raw (`infer=False`):**
- Stores text exactly as provided, no LLM processing
- Skips conflict resolution — same fact can be stored twice
- Only `user` role messages are stored; `assistant` messages ignored
- Best for: bulk imports, pre-structured data, migrations
**Warning:** Don't mix `infer=True` and `infer=False` for the same data — the same fact will be stored twice.
---
## Retrieval Pipeline (v3)
### What happens when you call `client.search()`
```
Query In
│
▼
┌─────────────────────┐
│ 1. PREPROCESSING │ Lemmatize keywords, extract entities
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. PARALLEL SCORING │ Semantic search (vector similarity)
│ │ BM25 keyword search (term matching)
│ │ Entity matching (entity graph boost)
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. SCORE FUSION │ Combine signals into single score
│ │ Optional: rerank=True for deep reordering
└─────────┬───────────┘
│
▼
Results (combined score per memory)
```
### v3 Search Defaults
| Parameter | Default | Notes |
|-----------|---------|-------|
| `top_k` | 20 | Was 100 in v2 |
| `threshold` | 0.1 | Was None in v2 |
| `rerank` | False | Was True in v2 |
### Implicit null scoping
When you search with `filters={"user_id": "alice"}` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
To include memories with non-null fields, use explicit filters:
```python
# Gets memories for alice regardless of agent/app/run
filters={"OR": [{"user_id": "alice"}]}
```
---
## Memory Lifecycle (v3)
v3 uses ADD-only extraction. Memories accumulate over time rather than being consolidated.
### Creation
- `client.add(messages, user_id="...")`
- Single-pass extraction → deduplication → storage
- Returns `{"event_id": "...", "status": "PENDING"}`
### Updates
- `client.update(memory_id, text="...")` replaces text
- Batch: `client.batch_update([...])`
### Deletion
- Single: `client.delete(memory_id)`
- Batch: `client.batch_delete([...])`
- Bulk: `client.delete_all(filters={"user_id": "alice"})`
---
## Memory Object Structure
```json
{
"id": "uuid-string",
"memory": "Extracted memory text",
"user_id": "user-identifier",
"agent_id": null,
"app_id": null,
"run_id": null,
"metadata": { "source": "chat", "priority": "high" },
"categories": ["health", "preferences"],
"created_at": "2025-03-12T12:34:56Z",
"updated_at": "2025-03-12T12:34:56Z",
"structured_attributes": {
"day": 12, "month": 3, "year": 2025,
"hour": 12, "minute": 34,
"day_of_week": "wednesday",
"is_weekend": false,
"quarter": 1, "week_of_year": 11
},
"score": 0.85
}
```
| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Unique identifier, used for update/delete |
| `memory` | string | Extracted or stored text content |
| `user_id` | string | Primary entity scope |
| `agent_id` | string | Agent scope |
| `app_id` | string | Application scope |
| `run_id` | string | Session/run scope |
| `metadata` | object | Custom key-value pairs for filtering |
| `categories` | array | Auto-assigned or custom category tags |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
| `structured_attributes` | object | Temporal breakdown for time-based queries |
| `score` | float | Semantic similarity (search results only, 0-1) |
---
## Scoping & Multi-Tenancy
Mem0 separates memories across four dimensions to prevent data mixing:
| Dimension | Field | Purpose | Example |
|-----------|-------|---------|---------|
| User | `user_id` | Persistent persona or account | `"customer_6412"` |
| Agent | `agent_id` | Distinct agent or tool | `"meal_planner"` |
| App | `app_id` | Product surface or deployment | `"ios_retail_app"` |
| Session | `run_id` | Short-lived flow or thread | `"ticket-9241"` |
### Storage model
Each entity combination creates separate records. A memory with `user_id="alice"` is stored separately from one with `user_id="alice"` + `agent_id="bot"`.
### Critical: cross-entity queries
```python
# This returns NOTHING — user and agent memories are stored separately
filters={"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use OR to query multiple scopes
filters={"OR": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use wildcard to include any non-null value
filters={"AND": [{"user_id": "*"}]} # All users (excludes null)
```
### Recommended scoping patterns
```python
# User-level: persistent preferences
client.add(messages, user_id="alice")
# Session-level: temporary context
client.add(messages, user_id="alice", run_id="session_123")
# Clean up when done: client.delete_all(run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
# Multi-tenant: full isolation
client.add(messages, user_id="alice", agent_id="bot", app_id="acme_corp", run_id="ticket_42")
```
---
## Memory Layers
Mem0 supports three layers of memory, from shortest to longest lived:
### Conversation memory
- In-flight messages within a single turn
- Tool calls, chain-of-thought reasoning
- **Lifetime:** Single response — lost after turn finishes
- **Managed by:** Your application, not Mem0
### Session memory
- Short-lived facts for current task or channel
- Multi-step flows (onboarding, debugging, support tickets)
- **Lifetime:** Minutes to hours
- **Managed by:** Mem0 via `run_id` parameter
- Clean up with `client.delete_all(run_id="session_id")`
### User memory
- Long-lived knowledge tied to a person or account
- Personal preferences, account state, compliance details
- **Lifetime:** Weeks to forever
- **Managed by:** Mem0 via `user_id` parameter
- Persists across all sessions and interactions
### How layering works in practice
```python
def chat(user_input: str, user_id: str, session_id: str) -> str:
# 1. Retrieve user memories (long-term preferences)
user_mems = mem0.search(user_input, filters={"user_id": user_id})
# 2. Retrieve session memories (current task context)
session_mems = mem0.search(user_input, filters={
"AND": [{"user_id": user_id}, {"run_id": session_id}]
})
# 3. Combine both layers for LLM context
context = format_memories(user_mems) + format_memories(session_mems)
# 4. Generate response
response = llm.generate(context=context, input=user_input)
# 5. Store in session scope (temporary) + user scope (persistent)
messages = [{"role": "user", "content": user_input}, {"role": "assistant", "content": response}]
mem0.add(messages, user_id=user_id, run_id=session_id)
return response
```
---
## Performance Characteristics
### Latency
| Operation | Typical Latency |
|-----------|----------------|
| Hybrid search (v3 default) | ~100-150ms |
| + reranking | +150-200ms |
| Add (async) | < 50ms response |
### Processing
- **Async (default):** Returns immediately, processes in background
- **Batch operations:** Up to 1000 memories per batch_update/batch_delete
- **Webhooks:** Real-time notifications when async processing completes
### Scoping strategy for performance
- Use `user_id` for all user-facing queries (most common, fastest)
- Add `run_id` for session isolation (narrows search space)
- Avoid wildcard `"*"` filters on large datasets (scans all non-null records)
- Use `top_k` to limit result count when you only need a few memories
---
## Comparison with Alternatives
| Approach | Pros | Cons |
|----------|------|------|
| **Raw vector DB** | Fast, full control | No extraction, no dedup, no conflict resolution |
| **In-memory chat history** | Zero latency | Lost on restart, no cross-session, grows unbounded |
| **RAG over documents** | Good for static knowledge | No personalization, no memory updates |
| **Mem0 Platform** | Managed extraction + dedup + graph + scoping | External dependency, async processing delay |
Mem0 combines the best of vector search (semantic retrieval) with automatic extraction (LLM-powered), conflict resolution (deduplication), and structured scoping (multi-tenancy) — in a single managed API.
@@ -0,0 +1,425 @@
# Platform Features -- Mem0 Platform
Additional platform capabilities beyond core CRUD operations.
## Table of Contents
- [Advanced Retrieval](#advanced-retrieval)
- [Entity Linking](#entity-linking)
- [Custom Categories](#custom-categories)
- [Custom Instructions](#custom-instructions)
- [Criteria Retrieval](#criteria-retrieval)
- [Feedback Mechanism](#feedback-mechanism)
- [Memory Export](#memory-export)
- [Group Chat](#group-chat)
- [MCP Integration](#mcp-integration)
- [Webhooks](#webhooks)
- [Multimodal Support](#multimodal-support)
## Advanced Retrieval
### Hybrid Search (v3 Default)
v3 uses multi-signal hybrid search combining:
- **Semantic search** (vector similarity)
- **BM25 keyword search** (normalized term matching)
- **Entity matching** (entity graph boost)
This is automatic — no configuration needed.
### Reranking (`rerank=True`)
Deep semantic reordering of results — most relevant first.
- Latency: +150-200ms
- Default: `False` (was `True` in v2)
- Best for: user-facing results, top-N precision
**Python:**
```python
results = client.search(query, filters={"user_id": "user123"}, rerank=True)
```
**TypeScript:**
```typescript
const results = await client.search(query, {
filters: { user_id: 'user123' },
rerank: true,
});
```
---
## Entity Linking
v3 replaces graph memory with built-in entity linking. Entities (proper nouns, quoted text, compound noun phrases) are automatically extracted and linked across memories.
### How It Works
1. **Extraction**: During `add()`, entities are automatically extracted from memory text
2. **Storage**: Entities are stored in a parallel collection (`{collection}_entities`)
3. **Retrieval**: During `search()`, query entities are matched and used to boost relevant memories
Entity linking is automatic — no configuration required. The boost is folded into the combined `score` on each result.
### v2 Migration Note
If you were using `enable_graph=True` in v2:
- Remove `enable_graph` from all API calls
- Remove `graph_store` from OSS configuration
- Entity relationships are now consumed through retrieval ranking, not exposed as a separate `relations` array
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for details.
---
## Custom Categories
Replace Mem0's default 15 labels with domain-specific categories. The system automatically tags memories to the closest matching category.
### Default Categories (15)
`personal_details`, `family`, `professional_details`, `sports`, `travel`, `food`, `music`, `health`, `technology`, `hobbies`, `fashion`, `entertainment`, `milestones`, `user_preferences`, `misc`
### Configuration
**Set project-level categories:**
```python
new_categories = [
{"lifestyle_management": "Tracks daily routines, habits, wellness activities"},
{"seeking_structure": "Documents goals around creating routines and systems"},
{"personal_information": "Basic information about the user"}
]
client.project.update(custom_categories=new_categories)
```
```javascript
await client.updateProject({ customCategories: newCategories });
```
**Retrieve active categories:**
```python
categories = client.project.get(fields=["custom_categories"])
```
### Key Constraint
Per-request overrides (`custom_categories=...` on `client.add`) are **not supported** on the managed API. Only project-level configuration works. Workaround: store ad-hoc labels in `metadata` field.
---
## Custom Instructions
Natural language filters that control what information Mem0 extracts when creating memories.
### Set Instructions
```python
client.project.update(custom_instructions="Your guidelines here...")
```
```javascript
await client.updateProject({ customInstructions: "Your guidelines here..." });
```
### Template Structure
1. **Task Description** -- brief extraction overview
2. **Information Categories** -- numbered sections with specific details to capture
3. **Processing Guidelines** -- quality and handling rules
4. **Exclusion List** -- sensitive/irrelevant data to filter out
### Domain Examples
**E-commerce:** Capture product issues, preferences, service experience; exclude payment data.
**Education:** Extract learning progress, student preferences, performance patterns; exclude specific grades.
**Finance:** Track financial goals, life events, investment interests; exclude account numbers and SSNs.
### Best Practices
- Start simply, test with sample messages, iterate based on results
- Avoid overly lengthy instructions
- Be specific about what to include AND exclude
---
## Criteria Retrieval
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
### Configuration
```python
# Define criteria at project level
retrieval_criteria = [
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
]
client.project.update(retrieval_criteria=retrieval_criteria)
```
```typescript
await client.updateProject({
retrievalCriteria: [
{ name: 'joy', description: 'Positive emotions', weight: 3 },
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
],
});
```
### Usage
Once configured, `client.search()` automatically applies criteria ranking:
```python
# Criteria-weighted results returned automatically
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
```
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
---
## Feedback Mechanism
Provide feedback on extracted memories to improve system quality over time.
### Feedback Types
| Type | Meaning |
|------|---------|
| `POSITIVE` | Memory is useful and accurate |
| `NEGATIVE` | Memory is not useful |
| `VERY_NEGATIVE` | Memory is harmful or completely wrong |
| `None` | Clear existing feedback |
### Usage
**Python:**
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE",
feedback_reason="Accurately captured dietary preference"
)
# Bulk feedback
for item in feedback_data:
client.feedback(**item)
```
**TypeScript:**
```typescript
await client.feedback('mem-123', {
feedback: 'POSITIVE',
feedbackReason: 'Accurately captured dietary preference',
});
```
---
## Memory Export
Create structured exports of memories using customizable schemas with filters.
### Usage
```python
import json
# Define export schema
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
"health_info": {"type": "string"},
}
}
# Create export
response = client.create_memory_export(
schema=json.dumps(schema),
filters={"user_id": "alice"},
export_instructions="Create comprehensive profile based on all memories"
)
# Retrieve export (may take a moment to process)
result = client.get_memory_export(memory_export_id=response["id"])
```
**Best for:** Data analytics, user profile generation, compliance audits, CRM sync.
---
## Group Chat
Process multi-participant conversations and automatically attribute memories to individual speakers.
### Usage
```python
messages = [
{"role": "user", "name": "Alice", "content": "I think we should use React for the frontend"},
{"role": "user", "name": "Bob", "content": "I prefer Vue.js, it's simpler for our use case"},
{"role": "assistant", "content": "Both are great choices. Let me note your preferences."},
]
# Mem0 automatically attributes memories to each speaker
response = client.add(messages, run_id="team_meeting_1")
# Retrieve Alice's memories from that session
alice_mems = client.get_all(
filters={"AND": [{"user_id": "alice"}, {"run_id": "team_meeting_1"}]}
)
```
Use the `name` field in messages to identify speakers. Mem0 maps names to entity scopes automatically.
---
## MCP Integration
Model Context Protocol integration enables AI clients (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode) to manage Mem0 memory autonomously.
### Setup
Add Mem0 MCP to your clients with a single command:
```bash
npx mcp-add \
--name mem0-mcp \
--type http \
--url "https://mcp.mem0.ai/mcp" \
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
```
### Available MCP Tools
The MCP server exposes 9 memory tools that AI agents can use autonomously:
- Add, search, get, update, delete memories
- Get history, list users, delete users
- Search Mem0 documentation
### How It Works
1. Add Mem0 MCP to your AI client using the setup command above
2. The agent autonomously decides when to store/retrieve memories
3. No manual API calls needed — the agent manages memory as part of its reasoning
**Best for:** Universal AI client integration — one protocol works everywhere.
---
## Webhooks
Real-time event notifications for memory operations.
### Supported Events
| Event | Trigger |
|-------|---------|
| `memory_add` | Memory created |
| `memory_update` | Memory modified |
| `memory_delete` | Memory removed |
| `memory_categorize` | Memory tagged |
### Create Webhook
Note: `project_id` here refers to the Mem0 dashboard project scope for webhooks — not the deprecated client init parameter.
```python
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_categorize"]
)
```
### Manage Webhooks
```python
# Retrieve
webhooks = client.get_webhooks(project_id="proj_123")
# Update
client.update_webhook(
name="Updated Logger",
url="https://your-app.com/new-webhook",
event_types=["memory_update", "memory_add"],
webhook_id="wh_123"
)
# Delete
client.delete_webhook(webhook_id="wh_123")
```
### Payload Structure
Memory events contain: ID, data object with memory content, event type (`ADD`/`UPDATE`/`DELETE`).
Categorization events contain: memory ID, event type (`CATEGORIZE`), assigned category labels.
---
## Multimodal Support
Mem0 can process images and documents alongside text.
### Supported Media Types
- Images: JPG, PNG
- Documents: MDX, TXT, PDF
### Image via URL
```python
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": "https://example.com/image.jpg"}
}
}
client.add([image_message], user_id="alice")
```
### Image via Base64
```python
import base64
with open("photo.jpg", "rb") as f:
base64_image = base64.b64encode(f.read()).decode("utf-8")
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
}
}
client.add([image_message], user_id="alice")
```
### Document (MDX/TXT)
```python
doc_message = {
"role": "user",
"content": {"type": "mdx_url", "mdx_url": {"url": document_url}}
}
client.add([doc_message], user_id="alice")
```
### PDF Document
```python
pdf_message = {
"role": "user",
"content": {"type": "pdf_url", "pdf_url": {"url": pdf_url}}
}
client.add([pdf_message], user_id="alice")
```
@@ -0,0 +1,395 @@
# Mem0 Integration Patterns
Working code examples for integrating Mem0 Platform with popular AI frameworks.
All examples use `MemoryClient` (Platform API key).
Code examples are sourced from official Mem0 integration docs at docs.mem0.ai, simplified for quick reference.
---
## Common Pattern
Every integration follows the same 3-step loop:
1. **Retrieve** -- search relevant memories before generating a response
2. **Generate** -- include memories as context in the LLM prompt
3. **Store** -- save the interaction back to Mem0 for future use
---
## LangChain
Source: [docs.mem0.ai/integrations/langchain](https://docs.mem0.ai/integrations/langchain)
```python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from mem0 import MemoryClient
llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="You are a helpful travel agent AI. Use the provided context to personalize your responses."),
MessagesPlaceholder(variable_name="context"),
HumanMessage(content="{input}")
])
def retrieve_context(query: str, user_id: str):
"""Retrieve relevant memories from Mem0"""
memories = mem0.search(query, filters={"user_id": user_id})
memory_list = memories['results']
serialized = ' '.join([m["memory"] for m in memory_list])
return [
{"role": "system", "content": f"Relevant information: {serialized}"},
{"role": "user", "content": query}
]
def chat_turn(user_input: str, user_id: str) -> str:
# 1. Retrieve
context = retrieve_context(user_input, user_id)
# 2. Generate
chain = prompt | llm
response = chain.invoke({"context": context, "input": user_input})
# 3. Store
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response.content}],
user_id=user_id
)
return response.content
```
---
## CrewAI
Source: [docs.mem0.ai/integrations/crewai](https://docs.mem0.ai/integrations/crewai)
CrewAI has native Mem0 integration via `memory_config`:
```python
from crewai import Agent, Task, Crew, Process
from mem0 import MemoryClient
client = MemoryClient()
# Store user preferences first
messages = [
{"role": "user", "content": "I am more of a beach person than a mountain person."},
{"role": "assistant", "content": "Noted! I'll recommend beach destinations."},
{"role": "user", "content": "I like Airbnb more than hotels."},
]
client.add(messages, user_id="crew_user_1")
# Create agent
travel_agent = Agent(
role="Personalized Travel Planner",
goal="Plan personalized travel itineraries",
backstory="You are a seasoned travel planner.",
memory=True,
)
# Create task
task = Task(
description="Find places to live, eat, and visit in San Francisco.",
expected_output="A detailed list of places to live, eat, and visit.",
agent=travel_agent,
)
# Setup crew with Mem0 memory
crew = Crew(
agents=[travel_agent],
tasks=[task],
process=Process.sequential,
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "crew_user_1"},
}
)
result = crew.kickoff()
```
---
## Vercel AI SDK
> **Dedicated skill available.** For comprehensive Vercel AI SDK documentation, see the [mem0-vercel-ai-sdk skill](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk).
Install: `npm install @mem0/vercel-ai-provider`
Quick example (wrapped model with automatic memory):
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { text } = await generateText({
model: mem0("gpt-5-mini", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
```
Supported providers: `openai`, `anthropic`, `google`, `groq`, `cohere`
---
## OpenAI Agents SDK
Source: [docs.mem0.ai/integrations/openai-agents-sdk](https://docs.mem0.ai/integrations/openai-agents-sdk)
```python
from agents import Agent, Runner, function_tool
from mem0 import MemoryClient
mem0 = MemoryClient()
@function_tool
def search_memory(query: str, user_id: str) -> str:
"""Search through past conversations and memories"""
memories = mem0.search(query, filters={"user_id": user_id}, top_k=3)
if memories and memories.get('results'):
return "\n".join([f"- {mem['memory']}" for mem in memories['results']])
return "No relevant memories found."
@function_tool
def save_memory(content: str, user_id: str) -> str:
"""Save important information to memory"""
mem0.add([{"role": "user", "content": content}], user_id=user_id)
return "Information saved to memory."
agent = Agent(
name="Personal Assistant",
instructions="""You are a helpful personal assistant with memory capabilities.
Use search_memory to recall past conversations.
Use save_memory to store important information.""",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
result = Runner.run_sync(agent, "I love Italian food and I'm planning a trip to Rome next month")
print(result.final_output)
```
### Multi-Agent with Handoffs
```python
from agents import Agent, Runner, function_tool
travel_agent = Agent(
name="Travel Planner",
instructions="You are a travel planning specialist. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
health_agent = Agent(
name="Health Advisor",
instructions="You are a health and wellness advisor. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-5-mini"
)
triage_agent = Agent(
name="Personal Assistant",
instructions="""Route travel questions to Travel Planner, health questions to Health Advisor.""",
handoffs=[travel_agent, health_agent],
model="gpt-5-mini"
)
result = Runner.run_sync(triage_agent, "Plan a healthy meal for my Italy trip")
```
---
## Pipecat (Voice / Real-Time)
Source: [docs.mem0.ai/integrations/pipecat](https://docs.mem0.ai/integrations/pipecat)
```python
from pipecat.services.mem0 import Mem0MemoryService
memory = Mem0MemoryService(
api_key=os.getenv("MEM0_API_KEY"),
user_id="alice",
agent_id="voice_bot",
params={
"search_limit": 10,
"search_threshold": 0.1,
"system_prompt": "Here are your past memories:",
"add_as_system_message": True,
}
)
# Use in pipeline
pipeline = Pipeline([
transport.input(),
stt,
user_context,
memory, # Memory enhances context automatically
llm,
transport.output(),
assistant_context
])
```
---
## LangGraph
Source: [docs.mem0.ai/integrations/langgraph](https://docs.mem0.ai/integrations/langgraph)
State-based agent workflows with memory persistence. Best for complex conversation flows with branching logic.
```python
from typing import Annotated, TypedDict, List
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from mem0 import MemoryClient
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()
class State(TypedDict):
messages: Annotated[List[HumanMessage | AIMessage], add_messages]
mem0_user_id: str
def chatbot(state: State):
messages = state["messages"]
user_id = state["mem0_user_id"]
# Retrieve relevant memories
memories = mem0.search(messages[-1].content, filters={"user_id": user_id})
context = "Relevant context:\n"
for memory in memories["results"]:
context += f"- {memory['memory']}\n"
system_message = SystemMessage(content=f"""You are a helpful support assistant.
{context}""")
response = llm.invoke([system_message] + messages)
# Store the interaction
mem0.add(
[{"role": "user", "content": messages[-1].content},
{"role": "assistant", "content": response.content}],
user_id=user_id
)
return {"messages": [response]}
graph = StateGraph(State)
graph.add_node("chatbot", chatbot)
graph.add_edge(START, "chatbot")
app = graph.compile()
# Usage
result = app.invoke({
"messages": [HumanMessage(content="I need help with my order")],
"mem0_user_id": "customer_123"
})
```
---
## LlamaIndex
Source: [docs.mem0.ai/integrations/llama-index](https://docs.mem0.ai/integrations/llama-index)
Install: `pip install llama-index-core llama-index-memory-mem0`
LlamaIndex has native Mem0 support via `Mem0Memory`. Works with ReAct and FunctionCalling agents.
```python
from llama_index.memory.mem0 import Mem0Memory
context = {"user_id": "alice", "agent_id": "llama_agent_1"}
memory = Mem0Memory.from_client(
context=context,
search_msg_limit=4, # messages from chat history used for retrieval (default: 5)
)
# Use with LlamaIndex agent
from llama_index.core.agent import FunctionCallingAgent
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-5-mini")
agent = FunctionCallingAgent.from_tools(
tools=[],
llm=llm,
memory=memory,
verbose=True,
)
response = agent.chat("I prefer vegetarian restaurants")
# Memory automatically stores and retrieves context
response = agent.chat("What kind of food do I like?")
# Agent retrieves the vegetarian preference from Mem0
```
---
## AutoGen
Source: [docs.mem0.ai/integrations/autogen](https://docs.mem0.ai/integrations/autogen)
Install: `pip install autogen mem0ai`
Multi-agent conversational systems with memory persistence.
```python
from autogen import ConversableAgent
from mem0 import MemoryClient
memory_client = MemoryClient()
USER_ID = "alice"
agent = ConversableAgent(
"chatbot",
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": os.environ["OPENAI_API_KEY"]}]},
code_execution_config=False,
human_input_mode="NEVER",
)
def get_context_aware_response(question: str) -> str:
# Retrieve memories for context
relevant_memories = memory_client.search(question, filters={"user_id": USER_ID})
context = "\n".join([m["memory"] for m in relevant_memories.get("results", [])])
prompt = f"""Answer considering previous interactions:
Previous context: {context}
Question: {question}"""
reply = agent.generate_reply(messages=[{"content": prompt, "role": "user"}])
# Store the new interaction
memory_client.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=USER_ID
)
return reply
```
---
## All Supported Frameworks
Beyond the examples above, Mem0 integrates with:
| Framework | Type | Install |
|-----------|------|---------|
| [Mastra](https://docs.mem0.ai/integrations/mastra) | TS agent framework | `npm install @mastra/mem0` |
| [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs) | Voice AI | `pip install elevenlabs mem0ai` |
| [LiveKit](https://docs.mem0.ai/integrations/livekit) | Real-time voice/video | `pip install livekit-agents mem0ai` |
| [Camel AI](https://docs.mem0.ai/integrations/camel-ai) | Multi-agent framework | `pip install camel-ai[all] mem0ai` |
| [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock) | Cloud LLM provider | `pip install boto3 mem0ai` |
| [Dify](https://docs.mem0.ai/integrations/dify) | Low-code AI platform | Plugin-based |
| [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) | Google agent framework | `pip install google-adk mem0ai` |
For the general Python pattern (no framework), see the "Common integration pattern" in [SKILL.md](../SKILL.md).
@@ -0,0 +1,119 @@
# Mem0 Platform Quickstart
Get running with Mem0 in 2 minutes. No infrastructure to deploy -- just an API key.
## Prerequisites
- Python 3.10+ or Node.js 18+
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-skill-quickstart))
## Python Setup
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-api-key")
# Add a memory
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
]
client.add(messages, user_id="user123")
# Search memories
results = client.search("What are my dietary restrictions?", filters={"user_id": "user123"})
print(results)
```
### Async Client
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="your-api-key")
await client.add(messages, user_id="user123")
results = await client.search("query", filters={"user_id": "user123"})
```
## TypeScript / JavaScript Setup
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```javascript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'your-api-key' });
// Add a memory
const messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
];
await client.add(messages, { userId: "user123" });
// Search memories
const results = await client.search("What are my dietary restrictions?", {
filters: { user_id: "user123" }
});
console.log(results);
```
## cURL
```bash
export MEM0_API_KEY="m0-your-api-key"
# Add memory
curl -X POST https://api.mem0.ai/v3/memories/add/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "I am a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I will remember your dietary preferences."}
],
"user_id": "user123"
}'
# Search memories
curl -X POST https://api.mem0.ai/v3/memories/search/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are my dietary restrictions?",
"filters": {"user_id": "user123"}
}'
```
## Sample Response
```json
{
"results": [
{
"id": "14e1b28a-2014-40ad-ac42-69c9ef42193d",
"memory": "Allergic to nuts",
"user_id": "user123",
"categories": ["health"],
"created_at": "2025-10-22T04:40:22.864647-07:00",
"score": 0.30
}
]
}
```
## Next Steps
- [SDK Guide](sdk-guide.md) -- all methods for Python and TypeScript
- [API Reference](api-reference.md) -- REST endpoints and memory object structure
- [Integration Patterns](integration-patterns.md) -- LangChain, CrewAI, Vercel AI, etc.
@@ -0,0 +1,353 @@
# Mem0 SDK Guide
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
> **For language-specific deep references (including OSS):** See [client/python.md](../client/python.md) and [client/node.md](../client/node.md). For Python vs TypeScript differences: [client/differences.md](../client/differences.md).
## Initialization
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-your-api-key")
```
**Python (Async):**
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-your-api-key")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-your-api-key' });
```
Constructor accepts `apiKey` (required) and `host` (optional, default: `https://api.mem0.ai`).
---
## add() -- Store Memories
**Python:**
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
# With metadata
client.add(messages, user_id="alice", metadata={"source": "onboarding"})
```
**TypeScript:**
```typescript
await client.add(messages, { userId: "alice" });
await client.add(messages, { userId: "alice", metadata: { source: "onboarding" } });
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `messages` | array | `[{"role": "user", "content": "..."}]` |
| `user_id` | string | User identifier (recommended) |
| `agent_id` | string | Agent identifier |
| `run_id` | string | Session identifier |
| `metadata` | object | Custom key-value pairs |
| `infer` | boolean | If `false`, store raw text without inference (default: `true`) |
### Advanced Add Options
```python
# Agent + session scoping
client.add(messages, user_id="alice", agent_id="nutrition-agent", run_id="session-456")
# Raw text -- skip LLM inference
client.add(
[{"role": "user", "content": "User prefers dark mode."}],
user_id="alice",
infer=False,
)
```
---
## search() -- Find Memories
**Python:**
```python
results = client.search("dietary preferences?", filters={"user_id": "alice"})
# With filters and reranking
results = client.search(
query="work experience",
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "professional_details"}}]},
top_k=5,
rerank=True,
threshold=0.5
)
```
**TypeScript:**
```typescript
const results = await client.search("dietary preferences", { filters: { user_id: "alice" } });
const results = await client.search("work experience", {
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "professional_details" } }] },
topK: 5,
rerank: true,
});
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `query` | string | Natural language search query |
| `filters` | object | Filter object (AND/OR operators). Use `{"user_id": "..."}` to filter by user |
| `top_k` | number | Number of results (default: 10 for Platform) |
| `rerank` | boolean | Enable reranking for better relevance (default: `false`) |
| `threshold` | number | Minimum similarity score (default: 0.1) |
### Common Filter Patterns
**Python:**
```python
# Single user filter
filters={"user_id": "alice"}
# OR across agents
filters={"OR": [{"user_id": "alice"}, {"agent_id": {"in": ["travel-agent", "sports-agent"]}}]}
# Category filtering (partial match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "finance"}}]}
# Category filtering (exact match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"in": ["personal_information"]}}]}
# Wildcard (match any non-null run)
filters={"AND": [{"user_id": "alice"}, {"run_id": "*"}]}
# Date range
filters={"AND": [
{"user_id": "alice"},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}},
{"created_at": {"lt": "2024-02-01T00:00:00Z"}}
]}
# Exclude categories with NOT
filters={"AND": [{"user_id": "user_123"}, {"NOT": {"categories": {"in": ["spam", "test"]}}}]}
# Multi-dimensional query
filters={"AND": [
{"user_id": "user_123"},
{"keywords": {"icontains": "invoice"}},
{"categories": {"in": ["finance"]}},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}}
]}
```
**TypeScript:**
```typescript
// Single user filter
filters: { user_id: "alice" }
// OR across agents
filters: { OR: [{ user_id: "alice" }, { agent_id: { in: ["travel-agent", "sports-agent"] } }] }
// Category filtering (partial match)
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "finance" } }] }
// Category filtering (exact match)
filters: { AND: [{ user_id: "alice" }, { categories: { in: ["personal_information"] } }] }
```
---
## get() / getAll() -- Retrieve Memories
**Python:**
```python
# Single memory by ID
memory = client.get(memory_id="ea925981-...")
# All memories for a user
memories = client.get_all(filters={"user_id": "alice"})
# With date range
memories = client.get_all(
filters={"AND": [
{"user_id": "alex"},
{"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}}
]}
)
```
**TypeScript:**
```typescript
const memory = await client.get("ea925981-...");
const memories = await client.getAll({ filters: { user_id: "alice" } });
```
**Note:** `get_all` requires at least one of `user_id`, `agent_id`, `app_id`, or `run_id` in filters.
---
## update() -- Modify Memories
**Python:**
```python
client.update(memory_id="ea925981-...", text="Updated: vegan since 2024")
client.update(memory_id="ea925981-...", text="Updated", metadata={"verified": True})
```
**TypeScript:**
```typescript
await client.update("ea925981-...", { text: "Updated: vegan since 2024" });
```
---
## delete() / deleteAll() -- Remove Memories
**Python:**
```python
client.delete(memory_id="ea925981-...")
client.delete_all(user_id="alice") # Irreversible bulk delete
```
**TypeScript:**
```typescript
await client.delete("ea925981-...");
await client.deleteAll({ userId: "alice" });
```
---
## history() -- Track Changes
**Python:**
```python
history = client.history(memory_id="ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
**TypeScript:**
```typescript
const history = await client.history("ea925981-...");
```
---
## Batch Operations (TypeScript)
```typescript
// Batch update
await client.batchUpdate([
{ memoryId: "uuid-1", text: "Updated text" },
{ memoryId: "uuid-2", text: "Another updated text" },
]);
// Batch delete
await client.batchDelete(["uuid-1", "uuid-2", "uuid-3"]);
```
---
## Additional Methods
```python
# List all users/agents/sessions with memories
users = client.users()
# Delete a user/agent entity
client.delete_users(user_id="alice")
# Submit feedback on a memory
client.feedback(memory_id="...", feedback="POSITIVE", feedback_reason="Accurate extraction")
# Export memories
export = client.create_memory_export(filters={"AND": [{"user_id": "alice"}]})
data = client.get_memory_export(memory_export_id=export["id"])
```
---
## Common Pitfalls
1. **Entity cross-filtering fails silently** -- `AND` with `user_id` + `agent_id` returns empty. Use `OR`.
2. **SQL operators rejected** -- use `gte`, `lt`, etc. Not `>=`, `<`.
3. **Metadata filtering is limited** -- only top-level keys with `eq`, `contains`, `ne`.
4. **Wildcard `*` excludes null** -- only matches non-null values.
5. **Default threshold is 0.1** -- increase for stricter matching.
6. **Async processing** -- memories process asynchronously. Wait 2-3s after `add()` before searching.
## Naming Conventions
Python uses `snake_case` everywhere (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) and top-level parameters (`userId`, `topK`, `pageSize`), but filter keys use `snake_case` (`user_id`, `agent_id`).
---
## v2 to v3 Migration
### Breaking Changes in v3
**1. Entity IDs in search() and getAll()**
v3 requires entity IDs (`user_id`, `agent_id`, `run_id`) inside `filters` instead of as top-level parameters:
```python
# v2 (deprecated)
client.search("query", user_id="alice")
client.get_all(user_id="alice")
# v3
client.search("query", filters={"user_id": "alice"})
client.get_all(filters={"user_id": "alice"})
```
```typescript
// v2 (deprecated)
await client.search("query", { user_id: "alice" });
await client.getAll({ user_id: "alice" });
// v3
await client.search("query", { filters: { user_id: "alice" } });
await client.getAll({ filters: { user_id: "alice" } });
```
**2. TypeScript Parameter Naming**
v3 TypeScript uses camelCase for all parameters:
| v2 | v3 |
|----|-----|
| `user_id` | `userId` |
| `agent_id` | `agentId` |
| `run_id` | `runId` |
| `top_k` | `topK` |
| `page_size` | `pageSize` |
**3. Default Values Changed**
| Parameter | v2 Default | v3 Default |
|-----------|------------|------------|
| `threshold` | 0.3 | 0.1 |
| `rerank` | (not specified) | `false` |
**4. Removed Parameters**
The following parameters are no longer supported:
| Parameter | Status |
|-----------|--------|
| `enable_graph` | Removed from add/search/getAll |
| `keyword_search` | Removed from search |
| `filter_memories` | Removed |
| `immutable` | Removed from add |
| `expiration_date` | Removed from add |
| `includes` | Removed from add |
| `excludes` | Removed from add |
| `async_mode` | Removed from add |
@@ -0,0 +1,720 @@
# Mem0 Use Cases & Examples
Real-world implementation patterns for Mem0 Platform. Each use case includes complete, runnable code in both Python and TypeScript.
## Table of Contents
- [Personalized AI Companion](#1-personalized-ai-companion)
- [Customer Support with Categories](#2-customer-support-with-categories)
- [Healthcare Coach](#3-healthcare-coach)
- [Content Creation Workflow](#4-content-creation-workflow)
- [Multi-Agent / Multi-Tenant](#5-multi-agent--multi-tenant)
- [Personalized Search](#6-personalized-search)
- [Email Intelligence](#7-email-intelligence)
- [Common Patterns Across Use Cases](#common-patterns-across-use-cases)
---
## 1. Personalized AI Companion
A fitness coach that remembers goals, preferences, and progress across sessions. Mem0 persists context across app restarts — no session state needed.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
# 2. Generate response with memory context
system_prompt = f"""You are Ray, a personal fitness coach.
Use these known facts about the user to personalize your response:
{context if context else 'No prior context yet.'}"""
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
# Usage
chat("I want to run a marathon in under 4 hours", user_id="max")
# Next day, app restarted:
chat("What should I focus on today?", user_id="max")
# Ray remembers the sub-4 marathon goal
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function chat(userInput: string, userId: string): Promise<string> {
// 1. Retrieve relevant memories
const memories = await mem0.search(userInput, { filters: { user_id: userId } });
const context = memories.results
?.map((m: any) => `- ${m.memory}`)
.join('\n') || 'No prior context yet.';
// 2. Generate response with memory context
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `You are Ray, a personal fitness coach.\nUser context:\n${context}` },
{ role: 'user', content: userInput },
],
});
const reply = response.choices[0].message.content!;
// 3. Store interaction
await mem0.add(
[{ role: 'user', content: userInput }, { role: 'assistant', content: reply }],
{ userId: userId }
);
return reply;
}
```
### Key Benefits
- Context persists across app restarts — no session management needed
- Memories are automatically deduplicated and updated
- Works with any LLM provider (OpenAI, Anthropic, etc.)
**Best for:** Fitness coaches, tutors, therapists — any assistant that needs to remember goals across sessions.
---
## 2. Customer Support with Categories
Auto-categorize support data so teams retrieve the right facts fast. Uses custom categories for structured retrieval.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# 1. Define categories at the project level (one-time setup)
custom_categories = [
{"support_tickets": "Customer issues and resolutions"},
{"account_info": "Account details and preferences"},
{"billing": "Payment history and billing questions"},
{"product_feedback": "Feature requests and feedback"},
]
client.project.update(custom_categories=custom_categories)
# 2. Store interactions — auto-classified into categories
def log_support_interaction(user_id: str, message: str, priority: str = "normal"):
client.add(
[{"role": "user", "content": message}],
user_id=user_id,
metadata={"priority": priority, "source": "support_chat"}
)
# 3. Retrieve by category
def get_billing_issues(user_id: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"categories": {"in": ["billing"]}}
]
}
)
def search_support_history(user_id: str, query: str):
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"categories": {"contains": "support_tickets"}}
]
},
top_k=5
)
# Usage
log_support_interaction("maria", "I was charged twice for last month's subscription", priority="high")
log_support_interaction("maria", "The dashboard is loading slowly on mobile")
billing = get_billing_issues("maria") # Returns only billing-related memories
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
// Setup categories (one-time)
await client.updateProject({
custom_categories: [
{ support_tickets: 'Customer issues and resolutions' },
{ billing: 'Payment history and billing questions' },
{ product_feedback: 'Feature requests and feedback' },
],
});
async function logInteraction(userId: string, message: string, priority = 'normal') {
await client.add(
[{ role: 'user', content: message }],
{ userId: userId, metadata: { priority, source: 'support_chat' } }
);
}
async function getBillingIssues(userId: string) {
return client.getAll({
filters: { AND: [{ user_id: userId }, { categories: { in: ['billing'] } }] },
});
}
```
### Key Benefits
- Automatic categorization — no manual tagging
- Filter by category for structured retrieval
- Metadata (`priority`, `source`) enables multi-dimensional queries
**Best for:** Help desks, SaaS support, e-commerce — structured retrieval by category eliminates manual scanning.
---
## 3. Healthcare Coach
Guide patients with an assistant that remembers medical history. Uses high `threshold` for confident retrieval in safety-critical contexts.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def save_patient_info(user_id: str, information: str):
mem0.add(
[{"role": "user", "content": information}],
user_id=user_id,
run_id="healthcare_session",
metadata={"type": "patient_information"}
)
def consult(user_id: str, question: str) -> str:
# High threshold for medical accuracy
memories = mem0.search(question, user_id=user_id, top_k=5, threshold=0.7)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"You are a health coach. Patient context:\n{context}"},
{"role": "user", "content": question},
]
)
reply = response.choices[0].message.content
# Store the interaction
mem0.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=user_id,
run_id="healthcare_session",
)
return reply
# Usage
save_patient_info("alex", "I'm allergic to penicillin and take metformin for type 2 diabetes")
consult("alex", "Can I take amoxicillin for my sore throat?")
# Remembers penicillin allergy — amoxicillin is a penicillin-type antibiotic
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function savePatientInfo(userId: string, info: string) {
await mem0.add(
[{ role: 'user', content: info }],
{ userId: userId, runId: 'healthcare_session', metadata: { type: 'patient_information' } }
);
}
async function consult(userId: string, question: string): Promise<string> {
const memories = await mem0.search(question, {
filters: { user_id: userId },
topK: 5,
threshold: 0.7,
});
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `You are a health coach. Patient context:\n${context}` },
{ role: 'user', content: question },
],
});
const reply = response.choices[0].message.content!;
await mem0.add(
[{ role: 'user', content: question }, { role: 'assistant', content: reply }],
{ userId: userId, runId: 'healthcare_session' }
);
return reply;
}
```
### Key Benefits
- High threshold (0.7) ensures only confident matches for safety-critical retrieval
- Session scoping via `run_id` groups related health interactions
- Metadata tagging separates patient info from conversation history
**Best for:** Telehealth, wellness apps, patient management — persistent health context across visits.
---
## 4. Content Creation Workflow
Store voice guidelines once and apply them across every draft. Uses `run_id` and `metadata` to scope writing preferences per session.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def store_writing_preferences(user_id: str, preferences: str):
mem0.add(
[{"role": "user", "content": preferences}],
user_id=user_id,
run_id="editing_session",
metadata={"type": "preferences", "category": "writing_style"}
)
def draft_content(user_id: str, topic: str) -> str:
# Retrieve writing preferences
prefs = mem0.search(
"writing style preferences",
filters={"AND": [{"user_id": user_id}, {"run_id": "editing_session"}]}
)
style_context = "\n".join([f"- {m['memory']}" for m in prefs.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"Write content matching these style preferences:\n{style_context}"},
{"role": "user", "content": f"Write a blog post about: {topic}"},
]
)
return response.choices[0].message.content
# Usage
store_writing_preferences("writer_01", "I prefer short sentences. Active voice. No jargon. Use analogies.")
draft_content("writer_01", "Why AI memory matters for chatbots")
# Drafts content matching the stored voice guidelines
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function storePreferences(userId: string, preferences: string) {
await mem0.add(
[{ role: 'user', content: preferences }],
{ userId: userId, runId: 'editing_session', metadata: { type: 'preferences' } }
);
}
async function draftContent(userId: string, topic: string): Promise<string> {
const prefs = await mem0.search('writing style preferences', {
filters: { AND: [{ user_id: userId }, { run_id: 'editing_session' }] },
});
const styleContext = prefs.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `Write content matching these preferences:\n${styleContext}` },
{ role: 'user', content: `Write a blog post about: ${topic}` },
],
});
return response.choices[0].message.content!;
}
```
### Key Benefits
- Voice consistency across all content without repeating guidelines
- Scoped sessions let you maintain different style profiles
- Preferences update automatically as you refine them
**Best for:** Marketing teams, technical writers, agencies — consistent voice across all content.
---
## 5. Multi-Agent / Multi-Tenant
Keep memories separate using `user_id`, `agent_id`, `app_id`, and `run_id` scoping. Critical for multi-agent workflows and multi-tenant apps.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# Store memories scoped to user + agent + session
def store_scoped_memory(messages: list, user_id: str, agent_id: str, run_id: str, app_id: str):
client.add(
messages,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
app_id=app_id
)
# Query within a specific scope
def search_user_session(query: str, user_id: str, app_id: str, run_id: str):
"""Search memories for a specific user within a specific session."""
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"app_id": app_id},
{"run_id": run_id}
]
}
)
def search_agent_knowledge(query: str, agent_id: str, app_id: str):
"""Search all memories an agent has across all users."""
return client.search(
query,
filters={
"AND": [
{"agent_id": agent_id},
{"app_id": app_id}
]
}
)
# Usage: Travel concierge app with multiple agents
store_scoped_memory(
[{"role": "user", "content": "I'm vegetarian and prefer window seats"}],
user_id="traveler_cam",
agent_id="travel_planner",
run_id="tokyo-2025",
app_id="concierge_app"
)
# User-scoped query: "What does Cam prefer?"
user_mems = search_user_session("dietary restrictions?", "traveler_cam", "concierge_app", "tokyo-2025")
# Agent-scoped query: "What do all travelers prefer?" (across users)
agent_mems = search_agent_knowledge("common dietary restrictions?", "travel_planner", "concierge_app")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeScopedMemory(
messages: Array<{ role: string; content: string }>,
userId: string, agentId: string, runId: string, appId: string
) {
await client.add(messages, {
userId: userId,
agentId: agentId,
runId: runId,
appId: appId,
});
}
async function searchUserSession(query: string, userId: string, appId: string, runId: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { app_id: appId }, { run_id: runId }] },
});
}
async function searchAgentKnowledge(query: string, agentId: string, appId: string) {
return client.search(query, {
filters: { AND: [{ agent_id: agentId }, { app_id: appId }] },
});
}
```
### Key Benefits
- Full isolation between users, agents, sessions, and apps
- Query at any scope level — user, agent, session, or app-wide
- No memory leakage between tenants
**Best for:** Multi-agent workflows, multi-tenant SaaS — proper isolation at every level.
---
## 6. Personalized Search
Blend real-time search results with personal context. Uses `custom_instructions` to infer preferences from queries.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
# One-time setup: configure Mem0 to infer from queries
mem0.project.update(
custom_instructions="""Infer user preferences and facts from their search queries.
Extract dietary preferences, location, interests, and purchase history."""
)
def personalized_search(user_id: str, query: str, search_results: list) -> str:
# Get user context from memory
memories = mem0.search(query, user_id=user_id, top_k=5)
user_context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-5-mini",
messages=[
{"role": "system", "content": f"Personalize search results using user context:\n{user_context}"},
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{search_results}"},
]
)
reply = response.choices[0].message.content
# Store the query to learn preferences over time
mem0.add(
[{"role": "user", "content": query}],
user_id=user_id
)
return reply
# Usage
personalized_search("user_42", "best restaurants nearby", ["Restaurant A", "Restaurant B"])
# Over time, Mem0 learns: "user prefers vegetarian, lives in Austin"
# Future searches are automatically personalized
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function personalizedSearch(userId: string, query: string, searchResults: string[]): Promise<string> {
const memories = await mem0.search(query, { filters: { user_id: userId }, topK: 5 });
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-5-mini',
messages: [
{ role: 'system', content: `Personalize results using user context:\n${context}` },
{ role: 'user', content: `Query: ${query}\nResults: ${searchResults.join(', ')}` },
],
});
const reply = response.choices[0].message.content!;
await mem0.add([{ role: 'user', content: query }], { userId: userId });
return reply;
}
```
### Key Benefits
- Learns preferences from queries automatically via `custom_instructions`
- Personalizes any search provider (Tavily, Google, Bing)
- Zero manual preference setup — improves over time
**Best for:** Personalized search engines, recommendation systems — search results tailored to individual users.
---
## 7. Email Intelligence
Capture, categorize, and recall inbox threads using persistent memories with rich metadata.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
def store_email(user_id: str, sender: str, subject: str, body: str, date: str):
client.add(
[{"role": "user", "content": f"Email from {sender}: {subject}\n\n{body}"}],
user_id=user_id,
metadata={"email_type": "incoming", "sender": sender, "subject": subject, "date": date}
)
def search_emails(user_id: str, query: str):
return client.search(
query,
filters={"AND": [{"user_id": user_id}, {"categories": {"contains": "email"}}]},
top_k=10
)
def get_emails_from_sender(user_id: str, sender: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"metadata": {"contains": sender}}
]
}
)
# Usage
store_email("alice", "bob@acme.com", "Q3 Budget Review", "Attached is the Q3 budget...", "2025-01-15")
store_email("alice", "carol@acme.com", "Sprint Planning", "Here are the priorities...", "2025-01-16")
results = search_emails("alice", "budget discussions")
sender_emails = get_emails_from_sender("alice", "bob@acme.com")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeEmail(userId: string, sender: string, subject: string, body: string, date: string) {
await client.add(
[{ role: 'user', content: `Email from ${sender}: ${subject}\n\n${body}` }],
{ userId: userId, metadata: { email_type: 'incoming', sender, subject, date } }
);
}
async function searchEmails(userId: string, query: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { categories: { contains: 'email' } }] },
topK: 10,
});
}
```
### Key Benefits
- Rich metadata enables multi-dimensional queries (sender, date, subject)
- Category filtering separates emails from other memory types
- Semantic search across all email content
**Best for:** Inbox management, email automation — searchable email memories with metadata filtering.
---
## Common Patterns Across Use Cases
### Pattern 1: Retrieve → Generate → Store
Every use case follows the same 3-step loop:
```python
# 1. Retrieve relevant context
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate with context
response = llm.generate(system_prompt=f"Context:\n{context}", user_input=user_input)
# 3. Store the interaction
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response}],
user_id=user_id
)
```
### Pattern 2: Scope with Entity Identifiers
Use `user_id`, `agent_id`, `app_id`, and `run_id` to isolate memories:
```python
# User-level: personal preferences
client.add(messages, user_id="alice")
# Session-level: conversation within one session
client.add(messages, user_id="alice", run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
```
### Pattern 3: Rich Metadata for Filtering
Attach structured metadata for multi-dimensional queries:
```python
# Store with metadata
client.add(messages, user_id="alice", metadata={"priority": "high", "source": "phone_call"})
# Filter by category + metadata
client.search("billing issues", filters={
"AND": [{"user_id": "alice"}, {"categories": {"contains": "billing"}}]
})
```
### Pattern 4: Custom Instructions for Domain-Specific Extraction
Control what Mem0 extracts from conversations:
```python
client.project.update(
custom_instructions="Extract medical conditions, medications, and allergies. Exclude billing info."
)
```
---
## More Examples
For 30+ cookbooks with complete working code: [docs.mem0.ai/cookbooks](https://docs.mem0.ai/cookbooks)
@@ -0,0 +1,181 @@
#!/usr/bin/env bun
export {};
const DOCS_BASE = "https://docs.mem0.ai";
const SEARCH_ENDPOINT = `${DOCS_BASE}/api/search`;
const LLMS_INDEX = `${DOCS_BASE}/llms.txt`;
const SECTION_MAP: Record<string, string[]> = {
platform: [
"/platform/overview",
"/platform/quickstart",
"/platform/features",
"/platform/features/graph-memory",
"/platform/features/selective-memory",
"/platform/features/custom-categories",
"/platform/features/v2-memory-filters",
"/platform/features/async-client",
"/platform/features/webhooks",
"/platform/features/multimodal-support",
],
api: [
"/api-reference/memory/add-memories",
"/api-reference/memory/v2-search-memories",
"/api-reference/memory/v2-get-memories",
"/api-reference/memory/get-memory",
"/api-reference/memory/update-memory",
"/api-reference/memory/delete-memory",
],
"open-source": [
"/open-source/overview",
"/open-source/python-quickstart",
"/open-source/node-quickstart",
"/open-source/features",
"/open-source/features/graph-memory",
"/open-source/features/rest-api",
"/open-source/configure-components",
],
openmemory: [
"/openmemory/overview",
"/openmemory/quickstart",
],
sdks: ["/sdks/python", "/sdks/js"],
integrations: ["/integrations"],
};
async function fetchUrl(url: string): Promise<string> {
try {
const res = await fetch(url, {
headers: { "User-Agent": "Mem0DocSearchAgent/1.0" },
signal: AbortSignal.timeout(15000),
});
if (!res.ok) return `HTTP Error ${res.status}: ${res.statusText}`;
return await res.text();
} catch (e: any) {
return `Error: ${e.message}`;
}
}
async function searchDocs(query: string, section?: string) {
const params = new URLSearchParams({ query });
try {
const result = await fetchUrl(`${SEARCH_ENDPOINT}?${params}`);
const data = JSON.parse(result);
if (data?.results) {
let results = data.results;
if (section && SECTION_MAP[section]) {
const paths = SECTION_MAP[section];
results = results.filter((r: any) =>
paths.some((p) => r.url?.startsWith(p)),
);
}
return { source: "mintlify_search", results };
}
} catch {}
const indexContent = await fetchUrl(LLMS_INDEX);
const queryLower = query.toLowerCase();
let matching = indexContent
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#") && l.toLowerCase().includes(queryLower));
if (section && SECTION_MAP[section]) {
const paths = SECTION_MAP[section];
matching = matching.filter((u) => paths.some((p) => u.includes(p)));
}
return {
source: "llms_txt_index",
query,
matching_urls: matching.slice(0, 20),
suggestion: "Fetch specific URLs for detailed content",
};
}
async function fetchPage(pagePath: string) {
const url = pagePath.startsWith("/") ? `${DOCS_BASE}${pagePath}` : pagePath;
const content = await fetchUrl(url);
return { url, content: content.slice(0, 10000), truncated: content.length > 10000 };
}
async function getIndex() {
const content = await fetchUrl(LLMS_INDEX);
const urls = content
.split("\n")
.map((l) => l.trim())
.filter((l) => l && !l.startsWith("#"));
return { total_pages: urls.length, urls, sections: Object.keys(SECTION_MAP) };
}
function listSection(section: string) {
if (!SECTION_MAP[section]) {
return { error: `Unknown section: ${section}`, available: Object.keys(SECTION_MAP) };
}
return { section, pages: SECTION_MAP[section].map((p) => `${DOCS_BASE}${p}`) };
}
function printResult(result: any) {
if (result.results) {
console.log(`Source: ${result.source ?? "unknown"}`);
for (const r of result.results) {
console.log(` - ${r.title ?? "N/A"}: ${r.url ?? "N/A"}`);
if (r.description) console.log(` ${r.description.slice(0, 200)}`);
}
} else if (result.matching_urls) {
console.log(`Source: ${result.source}`);
console.log(`Query: ${result.query}`);
for (const url of result.matching_urls) console.log(` - ${url}`);
if (result.suggestion) console.log(`\n${result.suggestion}`);
} else if (result.urls) {
console.log(`Total documentation pages: ${result.total_pages}`);
console.log(`Sections: ${result.sections.join(", ")}`);
for (const url of result.urls.slice(0, 30)) console.log(` - ${url}`);
if (result.total_pages > 30) console.log(` ... and ${result.total_pages - 30} more`);
} else if (result.pages) {
console.log(`Section: ${result.section}`);
for (const page of result.pages) console.log(` - ${page}`);
} else if (result.content) {
console.log(`URL: ${result.url}`);
if (result.truncated) console.log("[Content truncated to 10000 chars]");
console.log(result.content);
} else if (result.error) {
console.log(`Error: ${result.error}`);
if (result.available) console.log(`Available sections: ${result.available.join(", ")}`);
} else {
console.log(JSON.stringify(result, null, 2));
}
}
const args = process.argv.slice(2);
const flags: Record<string, string | boolean> = {};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--query" && args[i + 1]) flags.query = args[++i];
else if (args[i] === "--page" && args[i + 1]) flags.page = args[++i];
else if (args[i] === "--section" && args[i + 1]) flags.section = args[++i];
else if (args[i] === "--index") flags.index = true;
else if (args[i] === "--json") flags.json = true;
}
let result: any;
if (flags.index) {
result = await getIndex();
} else if (flags.section && !flags.query) {
result = listSection(flags.section as string);
} else if (flags.page) {
result = await fetchPage(flags.page as string);
} else if (flags.query) {
result = await searchDocs(flags.query as string, flags.section as string | undefined);
} else {
console.log("Usage:");
console.log(" bun scripts/mem0_doc_search.ts --query \"topic\"");
console.log(" bun scripts/mem0_doc_search.ts --page \"/platform/features/graph-memory\"");
console.log(" bun scripts/mem0_doc_search.ts --index");
console.log(" bun scripts/mem0_doc_search.ts --section platform");
process.exit(1);
}
if (flags.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printResult(result);
}
@@ -0,0 +1,63 @@
---
name: memory-reviewer
description: Reviews stored memory quality by detecting duplicates, contradictions, and stale entries with actionable recommendations. Use when search results seem conflicting, before running dream consolidation, or for periodic memory hygiene audits.
---
# Memory Reviewer
Audits memory quality for the active project. Finds duplicates, contradictions, and low-confidence entries.
## When to use
- User asks "check my memories", "memory quality", "any duplicates?"
- User runs `/mem0:memory-reviewer` directly
- After a session with 5+ memory writes (suggest proactively)
- After `/mem0:health --deep` identifies issues
## Steps
1. **Fetch all memories** for active project via `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Paginate if needed — cap at 200 memories.
2. **Group by `metadata.type`**. Common types: `decision`, `convention`, `anti_pattern`, `task_learning`, `project_profile`, `user_preference`, `session_state`.
3. **Scan each group for issues:**
| Issue | Detection method |
|---|---|
| **Near-duplicates** | >60% noun overlap within same type. Compare memory text after stripping stop words. |
| **Contradictions** | Opposing facts about same topic (e.g., "use PostgreSQL" vs "use MySQL" for same component) |
| **Low-confidence** | `metadata.confidence < 0.3` |
| **Missing type** | No `metadata.type` set |
| **Stale** | `created_at` older than 180 days with no updates |
4. **Output compact summary:**
```
memory-reviewer: project=<id> total=<N>
duplicates: <N> found
contradictions: <N> found
low_confidence: <N> found
untagged: <N> found
stale: <N> found
```
5. **If issues found**, list them with memory IDs:
```
Issues:
[duplicate] "<memory_a>" ≈ "<memory_b>" [mem0:<id_a>, mem0:<id_b>]
[contradiction] "<memory_x>" vs "<memory_y>" [mem0:<id_x>, mem0:<id_y>]
[low_conf] "<memory_z>" (confidence: 0.1) [mem0:<id_z>]
```
6. **Suggest action**: "Run `/mem0:dream` to consolidate duplicates and resolve contradictions."
## Constraints
- **Read-only** — never modify or delete memories (that's `/mem0:dream`'s job)
- **Max 200 memories** per scan
- Report findings, let user decide on action
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,181 @@
---
name: onboard
description: Sets up mem0 for a new project including API key configuration, MCP authentication, project file import, and coding categories. Use on first run in a new project, when API key needs updating, or to re-run initial setup after configuration changes.
---
# Mem0 Onboarding Wizard
Run this wizard to set up the mem0 plugin for the current project. Complete in ~60 seconds.
**IMPORTANT: Execute steps strictly in order (0 → 1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead. Complete one step fully before starting the next.**
## Step 0: Skip dependency install
The plugin lists `mem0ai` as a declared dependency — no install step is needed. Proceed directly to Step 1.
## Step 1: Set up API key
Check if the API key is available:
```bash
[ -n "${MEM0_API_KEY:-}" ] && echo "SET" || echo "NOT_SET"
```
IMPORTANT: Never run `echo $MEM0_API_KEY` — that prints the secret in plaintext to the conversation log.
### If API key IS set (output is "SET")
Print: `- API key found.` and proceed to Step 2.
### If API key is NOT set (output is "NOT_SET")
Guide the user through API key setup. Show this message:
```
Step 1: Setting up API key.
- API key not found. Let's set it up.
1. Get your API key from https://app.mem0.ai/dashboard/api-keys
2. Choose ONE method:
Option A — Shell profile:
echo 'export MEM0_API_KEY="m0-your-key-here"' >> ~/.zshrc
source ~/.zshrc
Option B — OpenCode environment config:
Add MEM0_API_KEY to your OpenCode environment settings
so it is available in all sessions.
3. Verify:
[ -n "${MEM0_API_KEY:-}" ] && echo "SET" || echo "NOT_SET"
```
After the user confirms, re-run the verify command. If NOT_SET, repeat. If SET, proceed to Step 2.
## Step 2: MCP server connection
First, check if MCP tools are already available using ToolSearch with query `"mem0 search_memories"`. The exact tool name varies by install method (may be `mcp__mem0__search_memories` or `mcp__plugin_mem0_mem0__search_memories`).
**If MCP tools ARE found:** Print `- MCP already connected.` and proceed to Step 3.
**If MCP tools are NOT found:**
The MCP server authenticates using the `MEM0_API_KEY` set in Step 1. No OAuth or browser login is needed.
1. Verify the API key is set (re-run the Step 1 check)
2. Check the plugin is installed and the MCP server for mem0 is listed in OpenCode's MCP configuration
3. If the server shows an error, ask the user to restart OpenCode and run `/mem0:onboard` again
4. If all checks pass but tools are still missing: "Restart OpenCode and run `/mem0:onboard` again."
**STOP here** — do not proceed without MCP tools.
## Step 3: Verify connectivity and show identity
Call `search_memories` with `query="project setup"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1` to verify connectivity.
Print:
```
- Connected
user: <user_id>
project: <project_id>
branch: <branch>
```
If the search fails, troubleshoot the API key and MCP connection.
## Step 4: Import project files
Check for common project context files in the working directory root. Read any that exist and store their key facts as memories using `add_memory`.
### 4a: Detect project files
```bash
for f in CLAUDE.md AGENTS.md .cursorrules .windsurfrules mem0.md .mem0.md; do
[ -f "$f" ] && echo "FOUND: $f ($(wc -c < "$f") bytes)"
done || true
```
If no files found, print `- No project files found. Skipping import.` and proceed to Step 5.
### 4b: Read and import via MCP
For each file found in 4a:
1. Read the file contents with the Read tool.
2. Extract the key facts, conventions, and decisions documented in the file. Do not import the entire file verbatim — summarize meaningful chunks.
3. Call `add_memory` for each meaningful chunk with:
- `data`: the extracted fact or convention
- `user_id`: the active user id
- `app_id`: the active project id
- `metadata`: `{"source": "<filename>", "type": "project_context"}`
If a file is very large (over 4000 bytes), split it into logical sections (one `add_memory` call per section).
### 4c: Report to user
After processing all files, print a user-friendly summary:
```
- Importing project files into mem0... done.
<N> file(s) read, <M> memories stored.
These are available as context for future sessions.
```
If `add_memory` calls fail, print:
```
- Project file import failed. Check API key and MCP connection, then retry with: /mem0:onboard
```
## Step 5: Verify coding categories
Coding categories are now configured automatically in the background when the plugin starts. This step only verifies they are set up.
Check if the categories are already configured by searching for a project_profile memory:
Call `search_memories` with `query="coding categories project profile"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1`.
If a project_profile memory is found, print:
```
- Coding categories already configured (17 categories, auto-installed).
```
If no project_profile memory is found, store one as a fallback. Call `add_memory` once with:
- `data`: a plain-text description of the project profile and the list of active coding categories:
```
Project profile for <project_id>.
Active coding categories: architecture_decisions, api_design, data_models,
algorithms, dependencies, environment_setup, testing_strategy, debugging_notes,
performance, security, deployment, code_conventions, error_handling,
refactoring_history, integrations, onboarding, project_meta.
```
- `user_id`: the active user id
- `app_id`: the active project id
- `metadata`: `{"type": "project_profile", "source": "onboard"}`
After the `add_memory` call succeeds, print:
```
- Coding categories installed (17 categories).
```
## Step 6: Summary
Print a summary:
```
- Onboarding complete.
user_id: <user_id>
project_id: <project_id> (app_id)
files: <N> found, <M> memories stored
categories: <N installed | already installed | skipped by user>
Memory is now active for this project. Start working — mem0 will
automatically search relevant context and capture learnings.
Run /mem0:tour to see what mem0 already knows about this project.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,56 @@
---
name: peek
description: Searches memories and displays compact one-liner results, or looks up a specific memory by ID. Use for quick memory lookups, checking if a decision was recorded, resolving [mem0:id] citations, or browsing memories without full category detail.
---
# Mem0 Peek
Quick search with compact output. Lighter than `/mem0:tour`.
## Execution
### Step 1: Parse query
The user provides a search query: `/mem0:peek auth middleware`
If no query provided, ask: "What should I search for?"
**Memory ID detection:** If the query matches any of these patterns, treat it as a
direct memory ID lookup instead of a search:
- Bare hex: `^[a-f0-9]{8}$` (short ID) or `^[a-f0-9]{8}-[a-f0-9-]+$` (full UUID)
- Citation ref: `[mem0:<hex>]` — extract the hex portion
When an ID is detected:
1. Call `get_memory(<id>)` directly (if short ID, try as prefix of full UUID)
2. If found, skip to Step 3 and display the single result
3. If not found, fall through to search using the ID as query text
### Step 2: Search
Run 2 parallel `search_memories` calls:
1. Broad: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
### Step 3: Display
Deduplicate by ID, then show compact results:
```
## mem0 peek: "<query>" (<N> results)
1. [decision] Auth module uses JWT with RS256 keys (2025-05-15) [mem0:a3f8b2c1]
2. [anti_pattern] Don't use symmetric HS256 — leaked in env (2025-05-10) [mem0:7e2d9f4a]
3. [convention] All middleware in src/middleware/ (2025-05-08) [mem0:c4d5e6f7]
```
Format: `<number>. [<type>] <content, 80 chars> (<date>) [mem0:<short_id>]`
If no results:
```
No memories matching "<query>" for project <project_id>.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,71 @@
---
name: pin
description: Pins or unpins a memory to protect it from pruning during dream consolidation. Use when a memory is critical and must never be removed, such as architecture decisions, security constraints, or immutable team conventions.
---
# Mem0 Pin
Pin a memory to mark it as high-priority and protect from pruning.
## Execution
### Step 1: Find the memory
The user provides either a search query or memory ID.
**If memory ID:**
- Call `get_memory` with the ID.
**If search query:**
- Call `search_memories` with the query, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=5`.
- Show numbered list with content previews.
- Ask: "Which memory to pin? Enter a number."
### Step 2: Read current content
Call `get_memory` with the selected memory ID. Store:
- `original_text` — the memory's text content
- `original_metadata` — the existing `metadata` dict
### Step 3: Pin it
The MCP `update_memory` tool only accepts `memory_id`, `text`, and `source` — it
does not accept a `metadata` parameter. To pin, append a pin marker to the text:
```python
pinned_text = "[PINNED] " + original_text if not original_text.startswith("[PINNED]") else original_text
update_memory(memory_id=<selected_id>, text=pinned_text)
```
**For new memories** (user wants to pin text that isn't stored yet):
1. Call `add_memory` with:
- `text="[PINNED] <the user's text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"pinned": true, "type": "decision", "confidence": 1.0}`
- `infer=False`
2. The response contains `event_id`. Call `get_event_status(event_id=<event_id>)` once to retrieve the memory ID, then confirm.
### Step 4: Confirm
```
Pinned: "<memory content, first 80 chars>"
Memory ID: <id>
```
Append `...` only if content exceeds 80 characters.
### Unpin
If the user says "unpin":
1. Call `get_memory` to read current content.
2. Remove the pin marker from the text:
```python
unpinned_text = original_text.removeprefix("[PINNED] ")
update_memory(memory_id=<id>, text=unpinned_text)
```
3. Print: `Unpinned: "<content>..."`
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,61 @@
---
name: remember
description: Stores a memory verbatim from user input with appropriate type classification and metadata. Use when the user says remember this, save this, store this, note that, or explicitly asks to record a decision, preference, convention, or learning.
---
# Mem0 Remember
Store a fact or learning directly into mem0.
## Execution
### Step 1: Extract the content
The user provides the content as an argument: `/mem0:remember <text>`
If no text was provided, ask: "What should I remember?"
### Step 2: Classify the memory
Based on the content, pick the best `metadata.type`:
| Content signal | Type |
|---|---|
| "we decided...", "always use...", "never..." | `decision` |
| "X doesn't work because...", "don't try..." | `anti_pattern` |
| "I prefer...", "use X instead of Y" | `user_preference` |
| "the convention is...", "we always..." | `convention` |
| "learned that...", "figured out..." | `task_learning` |
| setup, env, tooling, config | `environmental` |
| anything else | `task_learning` |
### Step 3: Store
Call `add_memory` with:
- `text="<the user's text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "<classified_type>", "branch": "<active_branch>", "confidence": 1.0, "source": "remember_command"}`
- `infer=False`
`infer=False` because the user stated the fact explicitly — no extraction needed.
`confidence=1.0` because the user explicitly asked to store this.
### Step 4: Confirm
The `add_memory` response returns `event_id` (not `memory_id`) because writes are async.
Call `get_event_status(event_id=<event_id>)` once.
- If status is `SUCCEEDED`: print the memory ID from the result.
- If status is `PENDING` or `processing`: print with the event ID as fallback.
```
Remembered as <type>: "<content, first 80 chars>"
Memory ID: <id from event status>
```
Append `...` only if content was truncated (longer than 80 chars).
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,135 @@
---
name: stats
description: Displays memory usage statistics for the current session and project including counts by category, age distribution, and API latency. Use when checking how many memories exist, reviewing session activity, or auditing memory distribution across categories.
---
# Mem0 Stats
Show session and lifetime memory statistics.
## Execution
### Step 1: Gather session context
Read the session identity from env vars set by the plugin's shell.env hook:
- `MEM0_USER_ID` (falls back to `$USER` if unset)
- `MEM0_APP_ID` — the active project identifier
- `MEM0_SESSION_ID` — current session identifier
- `MEM0_BRANCH` — current git branch
If `MEM0_USER_ID` is unset, use `$USER`. If `MEM0_APP_ID` is unset, note "No project configured" and stop.
### Step 2: Fetch total memory count
Call `get_memories` MCP tool to get the total count for this project:
`filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `page_size=1`
Read the `count` field from the response — this is the total number of memories for the project regardless of page size.
### Step 3: Fetch memories for category breakdown
Call `get_memories` MCP tool to retrieve memories for grouping:
`filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `page_size=200`
Group each memory by:
1. `categories[0]` (platform-assigned) — primary grouping
2. `metadata.type` (agent-assigned) — secondary if `categories` is empty or absent
3. `created_at` date — for age analysis
**Category normalization:** Merge `auto_capture` and `uncategorized` into a single `uncategorized` row. Do NOT show `auto_capture` as its own row.
Also run a `search_memories` MCP tool call with `query="project"`, `filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}]}`, `top_k=1` to measure round-trip latency. Note the time before and after the MCP call — do NOT attempt raw HTTP calls to the API.
### Step 4: Display
Print a minimal plain-text dashboard. No markdown formatting — OpenCode TUI renders text verbatim.
Example output shape:
```
mem0 stats
Session (<first 12 chars of MEM0_SESSION_ID>) branch: main
Project: my-project — 55 memories — API: 84ms
Category Count
-------------------- -----
decision 24
convention 15
anti_pattern 6
task_learning 5
user_preference 3
session_state 2
Age — oldest: 2026-02-15 newest: 2026-05-23
< 7 days: 5 | 7-30d: 12 | 30-90d: 10 | > 90d: 8
Identity — user: kartik project: my-project branch: main
```
**Display rules:**
- Use plain text with spaces to align columns — no markdown tables, no | pipes, no ** bold, no ## headers
- Category section: sort by count descending, omit categories with 0 memories
- Age: single line with pipe-separated buckets, computed from `created_at`
- Session line: show MEM0_SESSION_ID (first 12 chars) and MEM0_BRANCH if available; skip the line entirely if both are unset
- If only 1-2 total memories, skip the category table — just show the count
- Keep everything compact — no decorative borders or filler
## Weekly digest mode
When invoked with `--weekly` (e.g., `/mem0:stats --weekly`), append a weekly
activity digest after the standard stats dashboard.
### W1: Fetch recent memories
Call `search_memories` in parallel with time-scoped queries:
1. `query="decisions made this week"`, `filters={"AND": [{"user_id": "<MEM0_USER_ID>"}, {"app_id": "<MEM0_APP_ID>"}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `top_k=20`
2. `query="bugs errors fixes"`, same time filter, `top_k=20`
3. `query="patterns conventions learnings"`, same time filter, `top_k=20`
### W2: Analyze
Merge by ID. Group into "New this week" by `categories[0]` or `metadata.type`.
Calculate: memories added last 7 days, most active categories, most active day.
### W3: Display
Append after the standard stats in plain text (no markdown):
```
This week (May 16 - May 23)
+12 memories — most active: Wednesday (5)
Category New
-------------- ---
decision 5
task_learning 4
bug_fix 3
Highlights
- <2-3 sentence summary of most important decisions/learnings this week>
```
### W4: Write digest file
Write to `~/.mem0/weekly-digest.txt` (overwrite). Append one line to
`~/.mem0/digest-history.log`:
```
<YYYY-MM-DD> | <MEM0_APP_ID> | +<new_count> memories | top: <top_category>
```
### W5: Empty state
If no new memories in 7 days, output:
```
No new memories in the past week. Total: <N> memories in <MEM0_APP_ID>.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,107 @@
---
name: switch-project
description: Overrides the auto-detected project scope to read and write memories under a different project ID, or enables global search to access all memories across all users and projects. Use when working across multiple projects, accessing memories from another repo, enabling team-wide memory access, or when auto-detection resolves to the wrong project.
---
# Mem0 Switch Project
Override the automatic project_id detection for the current directory, or enable global search mode.
## Usage
- `/mem0:switch-project <project-name>` — switch to a specific project scope
- `/mem0:switch-project --global` — enable global search (all memories, all users, all projects)
- `/mem0:switch-project --no-global` — disable global search and return to per-project scoping
## Execution
### If `--global` flag is provided:
1. Set `global_search: true` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = True
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search enabled')
"
```
2. Print:
```
Global search enabled.
Searches now return all memories across all users and projects.
Writes still use the current user_id and app_id.
Restart the session for the change to take effect.
```
### If `--no-global` flag is provided:
1. Set `global_search: false` in `~/.mem0/settings.json` using the Bash tool:
```bash
python3 -c "
import json, os
settings_file = os.path.expanduser('~/.mem0/settings.json')
settings = {}
if os.path.isfile(settings_file):
with open(settings_file) as f:
settings = json.load(f)
settings['global_search'] = False
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print('Global search disabled')
"
```
2. Print:
```
Global search disabled.
Searches now return only memories scoped to the current project.
Restart the session for the change to take effect.
```
### If a project name is provided (no flags):
1. If no project name was given, ask: "What project_id should this directory use?"
2. Write the mapping to `~/.mem0/project_map.json` using the Bash tool:
```bash
python3 -c "
import json, os
map_file = os.path.expanduser('~/.mem0/project_map.json')
mapping = {}
if os.path.isfile(map_file):
with open(map_file) as f:
mapping = json.load(f)
mapping[os.getcwd()] = '<PROJECT_NAME>'
os.makedirs(os.path.dirname(map_file), exist_ok=True)
with open(map_file, 'w') as f:
json.dump(mapping, f, indent=2)
print(f'Mapped {os.getcwd()} -> <PROJECT_NAME>')
"
```
(Replace `<PROJECT_NAME>` with the user's chosen project name.)
3. Verify by searching for existing memories:
- Call `search_memories` with `query="project"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<PROJECT_NAME>"}]}`, `top_k=1`
4. Print:
```
Switched to project <PROJECT_NAME>.
<N> memories found for this project.
Note: This override persists across sessions for this directory.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim — markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,156 @@
---
name: tour
description: Browses all stored memories grouped by category with full content display. Use when reviewing all project memories, exploring stored knowledge, onboarding to a project, or getting an overview of captured decisions, conventions, and learnings.
---
# Mem0 Project Tour
Show the user what mem0 has stored for the current project.
## Cross-project mode
When invoked with `--all-projects` (e.g., `/mem0:tour --all-projects` or
`/mem0:tour --all-projects auth middleware`), search across ALL projects:
1. Call `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}]}`, `page_size=200` — **no `app_id` filter**.
2. If a search query was also provided, run `search_memories` with `query=<query>`,
`filters={"AND": [{"user_id": "<active_user_id>"}]}`, `top_k=20` — again no `app_id`.
3. Group results by `app_id` first, then by category within each project.
4. Display:
```
## <app_id_1> (<N> memories) ← current
**Architecture Decisions** — <memory content>
...
## <app_id_2> (<N> memories)
...
<N> memories across <M> projects
```
5. Mark the current project with `← (current)` in the heading.
If `--all-projects` is NOT present, use the standard single-project flow below.
## Peek mode (compact search)
When `/mem0:tour` receives a search query argument (e.g., `/mem0:tour auth middleware`)
WITHOUT `--all-projects`, run in **peek mode** — compact one-liner results:
1. Run 2 parallel `search_memories` calls:
- Broad: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
2. Deduplicate by ID, display compact results:
```
## mem0 search: "<query>" (<N> results)
1. [decision] Auth module uses JWT with RS256 keys (2025-05-15) [mem0:a3f8b2c1]
2. [anti_pattern] Don't use symmetric HS256 — leaked in env (2025-05-10) [mem0:7e2d9f4a]
3. [convention] All middleware in src/middleware/ (2025-05-08) [mem0:c4d5e6f7]
```
Format: `<number>. [<type>] <content, 80 chars> (<date>) [mem0:<short_id>]`
3. If no results: `No memories matching "<query>" for project <project_id>.`
If no query argument and no `--all-projects` flag, use the full tour flow below.
## Execution
### Step 1: Fetch ALL memories for this project
Call `get_memories` to fetch all memories for this project:
`filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=100`
### Step 2: Run supplementary semantic searches
In parallel, run these `search_memories` calls to get relevance-ranked results for key topics:
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- `query="bugs errors failures anti-patterns"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- `query="project setup tooling conventions preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
**Do NOT filter by `metadata.type` in these calls.** The platform auto-assigns `categories` — filtering on `metadata.type` misses memories that were auto-categorized but don't have an explicit `metadata.type`.
### Step 3: Merge and group
Merge all results by memory ID (deduplicate). For each memory, determine its group using this priority:
1. **Platform `categories` field** (array on each memory, auto-assigned by Mem0). Use the first category value.
2. **`metadata.type` field** (if present, set explicitly by hooks/agent). Use as fallback if no `categories`.
3. **"other"** bucket for memories with neither.
Map category names to display names:
| Platform category / metadata.type | Display name |
|---|---|
| `architecture decisions`, `architecture_decisions`, `decision` | Architecture Decisions |
| `anti patterns`, `anti_patterns`, `anti_pattern` | Anti-Patterns |
| `task learnings`, `task_learnings`, `task_learning` | Task Learnings |
| `coding conventions`, `coding_conventions`, `convention` | Coding Conventions |
| `user preferences`, `user_preferences`, `user_preference` | User Preferences |
| `project profile`, `project_profile` | Project Profile |
| `tooling setup`, `tooling_setup`, `environmental` | Tooling & Setup |
| `technology`, `professional_details` | Tooling & Setup |
| `session_state` | Session State |
| `compact_summary` | Compact Summaries |
| anything else | Other |
### Step 4: Display results
Sort groups by descending memory count. Display in compact tabular format:
First show the category summary table:
```
mem0 tour
Session (ses_abc123) branch: main
Project: my-project - 349 memories
Category Count
-----------------------------------------
tooling_setup 119
bug_fixes 78
architecture_decisions 32
task_learnings 14
...
```
Then for each category (sorted by count descending), show memories as numbered one-liners. Truncate each memory to 100 chars max:
```
tooling_setup (119)
1. User requires that no git commit or push be performed without explicit permission...
2. OpenCode plugins are loaded from ~/.config/opencode/plugins/ for global installation...
3. Assistant determined that the symlink method for loading the Mem0 plugin was failing...
... and 116 more
bug_fixes (78)
1. Fixed getAll filter format from flat object to AND-wrapped array for mem0ai TS SDK v3...
2. Root cause of user_id mismatch: plugin derived kartik.labhshetwar from git email...
... and 76 more
```
Show top 5 memories per category by recency. If a group has more than 5, note `... and <N> more`.
Skip empty groups entirely.
### Step 5: Print totals
```
<N> memories across <M> categories
project: <project_id> branch: <active_branch>
Identity - user: <user_id> project: <project_id> branch: <branch>
```
### Step 6: Empty state
If zero memories found for this project, print:
```
No memories stored yet for project <project_id>.
Run /mem0-onboard to import project files, or start working - mem0 captures learnings automatically.
```
## Output formatting
IMPORTANT: Do NOT use markdown in your output. OpenCode TUI renders text verbatim - markdown like **bold**, ## headers, and | table | syntax appears as raw characters. Use plain text with indentation for structure. Use dashes for lists. Use spaces to align columns instead of markdown tables.
@@ -0,0 +1,14 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@mem0/opencode-plugin"],
"mcp": {
"mem0": {
"type": "remote",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token {env:MEM0_API_KEY}"
},
"oauth": false
}
}
}
@@ -0,0 +1,71 @@
{
"name": "@mem0/opencode-plugin",
"version": "0.1.3",
"type": "module",
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"bin": {
"mem0-opencode": "./cli.ts"
},
"publishConfig": {
"access": "public"
},
"license": "Apache-2.0",
"keywords": [
"opencode",
"opencode-plugin",
"mem0",
"memory",
"mcp",
"ai-memory",
"persistent-memory"
],
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/mem0-plugin/.opencode-plugin"
},
"files": [
"dist",
"cli.ts",
"opencode.json",
"LICENSE",
"opencode-skills"
],
"scripts": {
"build": "bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext] && tsc --emitDeclarationOnly --outDir dist --declaration",
"dev": "bun build opencode-mem0.ts --outdir dist --target bun --format esm --entry-naming index.[ext] --watch",
"type-check": "tsc --noEmit",
"prepack": "bun run build",
"postpack": ""
},
"opencode": {
"type": "plugin",
"hooks": [
"chat.message",
"tool.execute.before",
"tool.execute.after",
"experimental.chat.messages.transform",
"experimental.session.compacting",
"shell.env"
]
},
"dependencies": {
"@opencode-ai/plugin": "^1.0.162",
"mem0ai": "^3.0.5"
},
"devDependencies": {
"bun-types": ">=1.3.14",
"typescript": "^5.7.3"
},
"peerDependencies": {
"bun": ">=1.0.0"
}
}
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationDir": "dist",
"outDir": "dist",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"lib": ["ESNext"],
"types": ["bun-types"],
"noEmit": false,
"emitDeclarationOnly": true
},
"include": ["**/*"],
"exclude": ["node_modules", "dist"]
}
+370
View File
@@ -0,0 +1,370 @@
# Changelog
All notable changes to the Mem0 plugin will be documented in this file.
## 0.2.9 — File-context injection, session summaries & activity timeline
### Added
- **File-context injection (`PreToolUse/Read` hook):** Before Claude reads a file, the new `on_file_read.sh` hook searches mem0 for memories that reference that file path and injects a compact timeline of prior work as `additionalContext`. Gives Claude "I've seen this file before and here's what I remember" context automatically. Gates on file size (>= 1,500 bytes), 5-second hard timeout, silent skip on any failure. Applies to Claude Code, Codex, and Cursor (`on_file_read_cursor.sh`).
- **Stop hook session summary (`on_stop.sh`):** On session end, parses the transcript JSONL, extracts the last assistant message and files touched, builds a structured prompt, and stores it via the mem0 API with `infer=True` — letting the platform's backend AI extract structured facts (request, decisions, learnings, next steps). Memories are stored as `metadata.type=session_summary` with 90-day expiry. Guards: skips subagent sessions (`agent_id` present), dedup via marker file, always exits 0. Applies to Claude Code, Codex, and Cursor (`on_stop_cursor.sh`).
- **SessionStart activity timeline (`session_timeline.py`):** On startup (when project has existing memories), fetches the 10 most recent memories and renders a compact timeline with type icons, age indicators, and short text below the existing banner. Shows recent decisions, bug fixes, and session summaries at a glance. 5-second timeout — if the API is slow, the timeline silently skips while the banner still displays.
- **`scripts/file_context.py`:** Core Python module for file-context injection. Searches mem0 cloud API with both relative and absolute file paths, deduplicates results, formats as compact timeline with type icons and memory age.
- **`scripts/capture_session_summary.py`:** Core Python module for Stop hook. Reads transcript JSONL (tail 3,000 lines), extracts last assistant message, extracts file paths from tool_input fields, strips system tags and `<private>` blocks, stores via mem0 API.
- **`scripts/session_timeline.py`:** Core Python module for SessionStart timeline. Fetches recent memories from mem0 API, formats with type icons, relative age, and short text.
### Changed
- **`on_session_start.sh`:** On startup (when memories > 0), calls `session_timeline.py` to inject a compact recent activity timeline below the existing banner and rubric instructions.
- **`hooks/hooks.json`:** Added `PreToolUse` matcher for `Read` (5s timeout) and `Stop` hook (30s timeout).
- **`hooks/codex-hooks.json`:** Added `PreToolUse` matcher for `Read` (5s timeout) and `Stop` hook (30s timeout).
- **`hooks/cursor-hooks.json`:** Added `preToolUse` matcher for `Read` (5s timeout) and `stop` hook (30s timeout).
## 0.2.8 — Automatic coding categories & global search
### Added
- **Global search mode (`global_search` setting):** New `global_search` toggle in `~/.mem0/settings.json` (default: `false`). When enabled, `search_memories` and `get_memories` calls use `{"OR": [{"user_id": "*"}]}` instead of the per-user per-project `AND` filter — returning all memories across all users and all `app_id` scopes in the platform project. Writes (`add_memory`) still tag with the current `user_id` and `app_id`. Solves the team-shared-memory use case where multiple team members need access to all memories regardless of which repo or user created them. Works on Claude Code, Cursor, and Codex.
- **`/mem0:switch-project --global` / `--no-global`:** Enables or disables global search via the switch-project skill. Persists to `~/.mem0/settings.json`. No manual config editing needed.
- **Session banner scope indicator:** Banner shows `scope=global` when global search is active instead of `project=<app_id>`.
- **Global-aware memory count:** Session start memory count query uses the global filter when `global_search` is enabled.
- **Background coding-category setup (`scripts/auto_setup_categories.py`):** The coding-focused category taxonomy (17 categories tuned for development work) is now installed automatically in the background on session start — the same way `auto_import.py` imports `CLAUDE.md`/`AGENTS.md`. Users are no longer asked to configure it during onboarding. Mirrors the auto-import design: resolves the API key, holds a lock file (`~/.mem0/categories_setup.lock`), reuses the proven `setup_coding_categories.py` taxonomy + `project.update` path via the plugin venv, logs to stderr only, and always exits 0 so it can never block a session.
- **Per-account state gating (`~/.mem0/categories_setup.json`):** Keyed by a hash of the API key → a hash of the taxonomy. Categories are scoped to the mem0 project tied to the API key (not the local repo), so setup runs once per account and skips all network calls thereafter — re-applying only if the taxonomy itself changes.
- **`tests/test_auto_setup_categories.py`:** Covers fingerprint determinism/sensitivity, state-file load/save/gating, and idempotent apply via an injected fake client (no SDK, no network).
### Changed
- **`on_session_start.sh`:** Spawns `auto_setup_categories.py` in the background on `startup` (alongside `auto_import.py`), preferring the venv python so the SDK is available. Covers Claude Code, Cursor, and Codex, which all route SessionStart through this script.
- **`/mem0:onboard` Step 5 is no longer interactive:** Removed the `Install coding categories? [Y/n]` prompt. Categories now configure automatically in the background; the onboarding step only verifies status and applies them if the background run hasn't finished yet — mirroring how Step 4 (project-file import) already works.
- **Session-start "new project" hint** now notes that coding categories install automatically in the background.
## 0.1.0 — Antigravity
### Added
- **Antigravity plugin** (`.antigravity/`): Restructured to follow the same shared-infrastructure pattern as Claude Code, Cursor, and Codex. Self-contained plugin directory with `plugin.json`, `mcp_config.json`, `hooks/hooks.json` (own file), `scripts/` (symlink → `../scripts/`), and `skills/` (symlink → `../skills/`). Installable via `agy plugin install .antigravity` or `npx degit mem0ai/mem0/mem0-plugin/.antigravity ~/.gemini/config/plugins/mem0`. Uses `contextFileName: "AGENTS.md"` per Antigravity convention.
- **Codex hooks parity:** Added missing `PreToolUse` Write/Edit/MultiEdit block and `PreCompact` hook to Codex hooks config, bringing it to full parity with Claude Code.
### Changed
- **Antigravity plugin directory:** Renamed `.antigravity-plugin/` → `.antigravity/` to match the naming convention of `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`. Plugin is now self-contained so `agy plugin install` and `npx degit` both work.
- **Antigravity hooks:** Hooks now live directly in `.antigravity/hooks/hooks.json` as a standalone file — no indirection.
- **Antigravity install command:** Updated from `npx degit mem0ai/mem0/mem0-plugin` to `npx degit mem0ai/mem0/mem0-plugin/.antigravity`. Added `agy plugin install` as alternative for local clones.
### Removed
- **Stale root-level files:** Deleted `mem0-plugin/hooks.json`, `mem0-plugin/mcp_config.json`, `mem0-plugin/plugin.json` — leftover artifacts from before plugin restructuring into per-editor subdirectories. Nothing referenced them; install commands point to `.antigravity/`.
## 0.2.7
### Fixed
- **First-install auth failure (closes #4876):** Removed `authorizationUrl` from `.mcp.json`. When both a static `Authorization` header and `authorizationUrl` were present, Claude Code preferred the OAuth flow, which failed on reconnect — leaving new users stuck with only `authenticate`/`complete_authentication` stub tools. Authentication now uses the `MEM0_API_KEY` header exclusively; no browser OAuth flow is triggered.
- **Onboarding skill removed OAuth step:** `/mem0:onboard` Step 2 no longer guides users through a browser-based OAuth login. The MCP server authenticates via the API key set in Step 1.
- **Removed `claude plugin configure mem0` references:** This CLI command does not exist. The `userConfig` mechanism works through the plugin enable UI prompt — Claude Code prompts for the API key when the plugin is first enabled and stores it securely in the system keychain. Updated session start banner, onboarding skill, identity script comments, and manual testing guide.
## 0.2.6
### Fixed
- **Memory count zero / stats and tour showing 0 memories:** The `run_id: "*"` wildcard filter — added in the initial v0.2.6 fix — returns 0 on both the v3 list endpoint (`/v3/memories/`) and search endpoint (`/v3/memories/search/`) when memories were written without a `run_id` (which is all memories since v0.2.6 stopped setting `run_id` on `add_memory`). Removed `run_id: "*"` from: `on_session_start.sh` count queries, `enforce_metadata_defaults.sh` hook injection (was injecting into every `search_memories` and `get_memories` call), `_search.py` search payload, `/mem0:stats` and `/mem0:tour` skill instructions. All read paths now use simple `user_id` + `app_id` filters without `run_id`, matching how v0.2.3 worked.
- **`add_memory` no longer sets `run_id`:** Session tracking moved from top-level `run_id` (which creates a separate API partition) to `metadata.session_id`. New memories land in the default partition and are visible to all queries.
- **`enforce_metadata_defaults.sh` no longer injects `run_id`:** The hook was appending `{"run_id": "*"}` to every `search_memories` and `get_memories` filter, which broke both endpoints. Removed entirely — identity injection (`user_id`/`app_id`) still works.
- **`_search.py` simplified:** Removed `run_id: "*"` from search payload. Uses plain `user_id` + `app_id` filters.
- **`auto_import.py` delete endpoint 404:** Stale chunk deletion used `DELETE /v3/memories/{id}/` which returns 404 (v3 is ADD-only). Changed to `DELETE /v1/memories/{id}/`.
- **Banner count accurate:** `on_session_start.sh` count query uses `user_id` + `app_id` filters without `run_id`. Shows total count only (removed noisy auto-import breakdown).
- **`quickstart.md` wrong add endpoint:** cURL example used `POST /v1/memories/` (v1 add is removed). Fixed to `POST /v3/memories/add/`.
- **Session stats always 0:** PostToolUse hooks never fire for plugin MCP tools (confirmed via debug logs — only SessionStart, UserPromptSubmit, PreToolUse, and Stop fire). Moved session stats tracking (`session_stats.py add/search`) into `enforce_metadata_defaults.sh` (PreToolUse), which does fire on every MCP tool call.
- **Periodic nudge never firing:** Message count file used session UUID in filename (`/tmp/mem0_msg_count_${SESSION_ID}`) which was cleared on session start. Changed to `$USER`-keyed filename, matching the session stats file convention.
- **`/mem0:stats` session query returning 0:** Stats skill attempted API queries with `run_id` and `metadata.session_id` filters that return empty results. Session stats now come exclusively from the local stats file (which is accurate now that PreToolUse tracks adds).
- **Auto-capture stops working mid-session:** After the initial rubric injection (first message), subsequent messages got zero context from the UserPromptSubmit hook. The banner instruction to "proactively store learnings" fades as conversation grows and Claude forgets. Two-pronged fix: (1) **Direct API auto-capture hook** (`auto_capture.py`): every 3rd message, `on_user_prompt.sh` spawns a background Python script that reads the last 3 exchanges from the transcript JSONL and sends them directly to `POST /v3/memories/add/` with `infer=True`. No reliance on Claude calling `add_memory`. (2) **Proportional prompt nudge** as fallback: starting from 3rd message, if Claude has stored fewer than 1 memory per 3 messages, a brief "store learnings via add_memory" directive is injected.
- **Desktop app: API key not found:** Claude Code Desktop does not inherit shell environment variables — only `PATH` is read from shell profiles. Users who set `export MEM0_API_KEY=m0-...` in `~/.zshrc` or `~/.bashrc` got "Setup Required" on Desktop while CLI worked fine. Added grep-based shell profile extraction as a 4th fallback in both `_identity.sh` (bash) and `_identity.py` (Python). Scans `~/.zshrc`, `~/.bashrc`, `~/.zprofile`, `~/.bash_profile`, `~/.profile` for `MEM0_API_KEY=` assignments. Skips variable references (`$OTHER_VAR`), commented-out lines, and strips quotes/inline comments.
- **Desktop app: zero memories added over multi-day usage:** Agent never proactively called `add_memory` — only `search_memories` and `get_all`. Root cause: session banner instruction was passive ("before finishing a session, store learnings") and easily ignored. No mechanism existed to re-prompt the agent mid-session. Fixed with a periodic nudge in `on_user_prompt.sh`: every 5th substantial message, the hook checks `session_stats` for add count; if fewer than 2 memories stored, injects a directive into Claude's context via `additionalContext` telling it to store learnings immediately. Counter resets on session start.
- **Setup Required banner missing Desktop instructions:** Updated no-API-key banner with Desktop-specific setup paths: `claude plugin configure mem0`, Desktop app environment editor (Settings > Environment), and CLI `export` as fallback.
### Removed
- **Stop hook (all 3 editors):** Removed from `hooks.json`, `cursor-hooks.json`, `codex-hooks.json`. Deleted `on_stop.sh`, `on_stop_cursor.sh`, `on_stop_codex.sh`, `stop_hook_check.py`. The Stop hook could not reliably feed context back to Claude (command-type hooks' `reason` field is user-facing only, not injected into Claude's context). Auto-capture handled by PreCompact hook instead.
- **SessionEnd hook:** Removed from `hooks.json`. Deleted `on_session_end.sh`. Redundant with PreCompact auto-capture.
- **5 redundant hook scripts:** `on_git_commit_capture.sh` (fired on every Bash command containing "git"), `on_post_commit.sh` (fired on every Bash command), `on_task_completed.sh`, `on_post_compact.sh`, `on_subagent_stop.sh`. These were already removed from Claude's `hooks.json` in v0.2.5 but script files remained on disk. Also removed `on_post_commit.sh` references from `cursor-hooks.json` and `codex-hooks.json`.
- **Dead settings:** Removed `output_style`, `skip_tools`, `capture_tools` from `load_settings.py` defaults. The `output-styles/` directory and `on_tool_failure.sh` script were already deleted.
- **`test_on_file_read.py`:** Removed test file for deleted `on_file_read.sh` hook.
### Changed
- **`/mem0:stats` lifetime query:** Single `get_memories` call with `user_id` + `app_id` filters (no `run_id`).
- **`/mem0:tour` full fetch:** Single `get_memories` call with `user_id` + `app_id` filters (no `run_id`).
- **API key resolution order (4 fallbacks):** `MEM0_API_KEY` env var > `CLAUDE_PLUGIN_OPTION_API_KEY` (plugin configure) > `CLAUDE_PLUGIN_OPTION_MEM0_API_KEY` (legacy userConfig) > shell profile extraction. Applies to both `_identity.sh` and `_identity.py`.
- **Session start banner:** Proactive memory instruction changed from passive "before finishing a session" to active "proactively store learnings incrementally as work progresses. Do NOT wait until the session ends."
- **Message counter on session start:** `on_session_start.sh` now resets `/tmp/mem0_msg_count_*` files to ensure nudge counter starts fresh each session.
## 0.2.5
### Fixed
- **PostToolUse field name: `tool_output` → `tool_response`:** All three PostToolUse scripts (`on_bash_output.sh`, `on_post_commit.sh`, `on_post_tool_use.sh`) were reading `.tool_output` from stdin JSON — a field that never existed in the Claude Code hooks spec. The correct field is `.tool_response` (confirmed via official docs at code.claude.com/docs/en/hooks). This was silently `null` on every invocation, meaning bash error detection and post-commit checks never actually fired.
- **Stop hook invalid `hookSpecificOutput`:** `on_stop.sh` returned `hookSpecificOutput` with `hookEventName: "Stop"` — but `Stop` is not a valid `hookEventName` discriminant. Claude Code rejected the JSON with "Hook JSON output validation failed". Replaced with spec-compliant `{ decision: "block", reason: "..." }`.
- **SessionStart banner invisible:** Switched from JSON `hookSpecificOutput.additionalContext` (discrete/hidden system reminder) back to raw text `cat <<BANNER` (shown directly in transcript). Per official docs, raw stdout from SessionStart hooks is visible context; `additionalContext` is not.
- **`Write|Edit` matcher missing `MultiEdit`:** `block_memory_write.sh` could be bypassed via `MultiEdit` tool. Matcher now `Write|Edit|MultiEdit`.
- **PreToolUse MCP matcher coverage gap:** `enforce_metadata_defaults.sh` not triggered for `get_memory` or `update_memory`. Added 4 tool name variants.
- **`capture_compact_summary.py` never called:** `on_session_start.sh` compact branch never spawned it. Post-compaction summaries were not stored. Added background spawn.
- **Unguarded variables under `set -u` in `on_session_start.sh`:** Bare `${MEM0_RESOLVED_USER_ID}` etc. without `:-` fallbacks. Script died silently if `_identity.sh` failed to source. All references now use `${VAR:-default}`.
- **`enforce_metadata_defaults.sh` silent failure on jq error:** Final `jq -n --argjson` had no error guard under `set -euo pipefail`. Added `|| true`.
- **Single-quote injection in `on_git_commit_capture.sh`:** Shell variables interpolated via `'$VAR'` inside `python3 -c`. Filenames with `'` broke Python syntax. Replaced with `os.environ.get()`.
- **Identity injection on `add_memory` (`enforce_metadata_defaults.sh`):** Hook now sources `_identity.sh` and injects `user_id` and `app_id` as top-level params when the agent omits them. Prevents orphaned memories with null scoping that were invisible to filtered queries. Root cause of onboarding writes landing with `user_id=null, app_id=null`.
- **Identity injection on `search_memories` and `get_memories`:** Hook now intercepts these tools and injects `user_id`/`app_id` into `filters.AND[]` when missing. Handles three filter states: no filters (creates from scratch), flat filters (converts to AND format), existing AND array (appends missing clauses). Prevents MCP server from auto-injecting wrong identity.
- **Identity injection on `delete_all_memories`:** Hook injects top-level `user_id`/`app_id` to prevent accidental cross-scope deletion.
- **`/mem0:export` incorrect `get_memories` call:** Was passing `user_id`/`app_id` as top-level params; MCP tool only accepts them inside `filters`. Changed to `filters={"AND": [{"user_id": "..."}, {"app_id": "..."}]}`.
- **`/mem0:stats` same `get_memories` issue:** Fixed both lifetime and session stat queries to use `filters` instead of top-level identity params.
- **`/mem0:pin` passes `metadata` to `update_memory`:** MCP `update_memory` tool only accepts `memory_id`, `text`, `source` — no `metadata` param. Pin/unpin now uses `[PINNED]` text prefix marker instead. Also added explicit `user_id`/`app_id` to `add_memory` for new pinned memories.
- **`/mem0:health --deep` ambiguous `get_memories` call:** Clarified identity goes in `filters`, not as top-level params.
- **`/mem0:memory-reviewer` same ambiguity:** Explicit `filters={"AND": [...]}` for `get_memories`.
- **`/mem0:dream` stale artifact:** Removed `(item 15)` from Step 4 heading.
- **`/mem0:dream` contradiction resolution no-op:** `update_memory` on loser with its own text did nothing. Changed to `delete_memory(memory_id=<loser_id>)` to actually remove the losing memory.
- **`/mem0:health` Check 3 `search_memories` top-level `user_id`:** Removed top-level `user_id` param; identity only in `filters.AND[]`. Changed `limit` to `top_k`.
- **`/mem0:health` Check 4 `add_memory` missing `infer=False`:** Health probe wasted LLM tokens on extraction. Added `infer=False`. Also fixed: was expecting `memory_id` in response but v3 returns `event_id`. Now uses `get_event_status` to get memory ID for cleanup.
- **`/mem0:tour` `get_memories` top-level identity:** Both standard and cross-project modes passed `user_id`/`app_id` as top-level params. Moved to `filters.AND[]`.
- **`/mem0:onboard` `search_memories` top-level `user_id`:** Removed extra top-level `user_id` param from connectivity check.
- **`/mem0:context-loader` incomplete filter table:** Filter examples showed only `metadata.type` without `user_id`/`app_id`. Now shows full `AND` filter structure.
- **7 skills used `limit` instead of `top_k` for `search_memories`:** MCP tool param is `top_k`, not `limit`. Fixed in: health, onboard, tour (3 places), switch-project, stats (weekly mode + latency probe).
- **`/mem0:stats` latency probe missing `filters`:** `search_memories` call had no identity filters. Added `user_id`/`app_id` in `filters.AND[]`.
### Added
- **`stop_hook_check.py`:** Pure-stdlib transcript analyzer for the Stop hook. Reads last 500 lines of transcript JSONL, parses tool calls, file modifications, and git commands. Returns `{"should_block": bool, "context": "..."}`. Trivial sessions (< 3 tool calls, no file edits) skip capture entirely.
- **Checklist for `/mem0:dream`:** 6-step progress tracker per Claude skill best practices for complex multi-step workflows.
- **Checklist for `/mem0:onboard`:** 7-step progress tracker for onboarding wizard.
- **Expanded hook matcher (all 3 configs):** `enforce_metadata_defaults.sh` now triggers on `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, and `delete_all_memories` (12 tool name variants covering both MCP naming conventions).
### Changed
- **Stop hook uses MCP-driven capture:** When meaningful work detected and no memories stored, returns `decision: "block"` asking Claude to call `add_memory` via MCP. One-shot flag prevents infinite loops. REST API capture runs in background as fallback.
- **SessionStart banner uses raw text stdout:** Replaced JSON `additionalContext` with `cat <<BANNER` for reliable display per official hooks spec.
- **`enforce_metadata_defaults.sh` rewritten:** Handler-based dispatch for 4 tool types. `add_memory` gets top-level identity + metadata defaults. `search_memories`/`get_memories` get filter identity injection. `delete_all_memories` gets top-level identity. Never overrides explicitly-passed identity.
## 0.2.4
### Fixed
- **PostToolUse matcher mismatch (`hooks.json:81`):** Changed from `mcp__mem0__` to `mcp__mem0__|mcp__plugin_mem0_mem0__`. Root cause of session stats recording nothing — 146 consecutive "no memory operations" entries. Unblocks `/mem0:stats` session line, stop-hook report, and `session_stats.py` tracking.
- **File-read hook noise (`on_file_read.sh`):** Replaced bare-filename semantic search with `metadata.files` filter + score threshold (≥ 0.4). Falls back to basename search if metadata filter returns nothing. Eliminates irrelevant context injection on every Read.
- **`/mem0:pin` uses v3-removed `immutable=True`:** Removed Step 3a entirely. Standardized on `metadata.pinned: true` (which `dream/SKILL.md` already respects during pruning). Fixed unconditional `...` appended to short pin confirmations.
- **`/mem0:list-projects` undercounts (implicit null scoping):** Dual-query approach — runs both null-scoped and app-scoped `get_memories` calls, merges by ID. Handles legacy `metadata.project_id` and `metadata.project` fields.
- **`/mem0:peek` free-text search on memory IDs:** Detects bare hex IDs (`^[a-f0-9]{8}$`) and `[mem0:<hex>]` citation refs, routes to `get_memory` direct lookup instead of semantic search.
- **Dead `PostToolUseFailure` hook block:** Removed entirely — this hook event does not exist in Claude Code.
### Added
- **Session ID capture (`on_session_start.sh`):** Extracts `session_id` from Claude Code stdin JSON, persists to `/tmp/mem0_session_id_$USER`. Falls back to timestamp-based ID. Enables `run_id`-based session scoping.
- **`run_id` injection (`enforce_metadata_defaults.sh`):** Reads session ID from temp file, injects as `run_id` into every `add_memory` call. Tags all memories with session identity for entity-scoped filtering.
- **`min_score`, `metadata_filters`, `rerank`, `threshold` params (`_search.py`):** `search_memories()` now accepts `min_score: float` to filter low-relevance results, `metadata_filters: dict` for field-level filtering, `rerank: bool` for managed reranker, and `threshold: float` (default 0.3, up from platform default 0.1) for server-side relevance gating. Existing callers unaffected (new params have defaults).
- **Rerank on tour/peek:** `/mem0:tour` and `/mem0:peek` search calls now pass `rerank=true` for better result ordering (+150–200ms latency, significantly improved precision).
- **`/mem0:stats` session query via `run_id`:** Queries memories by `run_id` filter for API-backed session counts. Cross-checks against local stats file. Shows truncated session ID in output.
### Removed
- **`/mem0:protocol` skill:** Routing table fully superseded by individual skill descriptions (auto-trigger). Operational guidelines (search patterns, metadata rules) covered by `enforce_metadata_defaults.sh` hook and individual skill bodies.
### Changed
- **All 17 skill descriptions:** Rewritten per Claude skill best practices — each now includes what the skill does AND when to trigger it, with specific keywords for auto-discovery. Average length 200–270 chars (under 1024 max). Third person, action verbs.
- **Onboarding auto-trigger (`on_session_start.sh`):** Replaced 3-state marker-file logic with memory-count detection. New project (0 memories) → prompts Claude to invoke `/mem0:onboard`. No marker files, no OAuth state. Simplified no-API-key path to single inactive banner.
## 0.2.3
### Added
- **Read hook (`on_file_read.sh`):** `PreToolUse(Read)` hook that searches mem0 for memories tagged with the file being opened and injects them as context. Skips non-code files, lockfiles, and `node_modules/`. Deduplicates repeated reads within a session.
- **Shared search module (`_search.py`):** Wraps `POST /v3/memories/search/` into a reusable `search_memories()` call with `format_results_for_context()` for consistent `[type] content [mem0:id]` display. Used by all pre-fetch hooks.
- **User settings (`load_settings.py`):** Loads config from `~/.mem0/settings.json` with 9 keys: `auto_save`, `auto_search`, `search_limit`, `retention_session_days`, `confidence_threshold`, `output_style`, `debug`, `skip_tools`, `capture_tools`. Supports `init` subcommand for first-run creation.
- **`/mem0:forget` skill:** Standalone skill for deleting memories by search query or UUID with confirmation. Supports undo-last-N via `session_stats.py peek`.
- **`/mem0:peek` skill:** Compact quick-search with one-liner output. Runs 2 parallel searches (broad + `decision`-filtered), deduplicates by ID.
- **`/mem0:memory-reviewer` skill:** Read-only memory quality audit. Scans for near-duplicates (>60% noun overlap), contradictions, low-confidence entries, untagged memories, and stale entries (>180 days). Refers to `/mem0:dream` for remediation.
- **`/mem0:context-loader` skill:** Pre-fetch agent that runs 2–4 parallel `search_memories` calls across query angles and type filters, deduplicates, outputs up to 10 memories. Silent on empty results.
- **Compact memory output style (`output-styles/compact-memory.md`):** `[<type>] <content, max 80 chars> [mem0:<short_id>]` format used by `peek`, `tour`, and inline search display.
- **`userConfig` in `.claude-plugin/plugin.json`:** `api_key` field (`sensitive: true`) enables key storage via `claude plugin configure mem0`.
- **OAuth support:** `authorizationUrl` added to `.mcp.json` for OAuth flow alongside token-based auth.
- **Tests:** `test_search.py`, `test_rubric_dedup.py`, `test_on_file_read.py`.
### Changed
- **Skills renamed:** All `mem0-*` prefixed skill directories renamed to shorter forms (e.g., `mem0-dream` → `dream`, `mem0-mcp` → `protocol`, `mem0-tour` → `tour`). 12 skills renamed total.
- **Pre-fetch on bash errors:** `on_bash_output.sh` now performs actual `search_memories` calls via `_search.py` instead of emitting search query templates as text.
- **Session-resume pre-fetch:** `on_user_prompt.sh` detects resume phrases ("where did we leave off", "continue from where") and pre-fetches `session_state` + `decision` memories.
- **Remember-intent routing:** `on_user_prompt.sh` detects save phrases ("remember this", "don't forget that") and routes to `/mem0:remember` skill instead of raw `add_memory`.
- **Rubric deduplication:** Search guidance rubric injected only on first user prompt per session via flag file.
- **Namespace-agnostic tool matching:** `on_post_tool_use.sh` and `on_tool_failure.sh` now use wildcard suffix matching (`*__add_memory`) instead of hardcoded `mcp__mem0__` prefix.
- **API key resolution:** `_identity.sh`/`_identity.py` now check `CLAUDE_PLUGIN_OPTION_API_KEY` (from `userConfig`) before legacy `CLAUDE_PLUGIN_OPTION_MEM0_API_KEY`.
- **Session start:** Three-state no-key handling (first run → auto-onboard, OAuth mode → proceed, neither → inactive banner). Banner now shows `auth=api_key|oauth`.
- **Hook timeouts:** `on_user_prompt.sh` and `on_bash_output.sh` raised from 5s to 12s.
- **Compact prompts:** `on_task_completed.sh` and `on_stop.sh` replaced multi-step checklists with single-line directives (0–2 durable facts max).
- **`/mem0:dream`:** Removed `--forget` (now standalone `forget` skill) and `--schedule` flags.
- **`/mem0:health`:** Removed `--fix` auto-fix mode; output condensed to `PASS/FAIL CheckName Detail` one-liners.
- **`/mem0:protocol` (was `mem0-mcp`):** Added 14-entry natural-language-to-skill routing table. _(Removed in 0.2.4 — superseded by skill descriptions.)_
- **CLI config fallback removed:** `_identity.sh`/`_identity.py` no longer read `~/.mem0/config.json`; API key resolution is env-var-only.
- **`auto_import.py`:** Added content-hash deduplication to skip files with identical content within a single import run.
- **All skill descriptions:** Shortened to concise one-liners.
### Fixed
- **Namespace-agnostic tool name parsing in `on_tool_failure.sh`:** `${TOOL_NAME##*__}` correctly strips any MCP namespace prefix, not just `mcp__mem0__`.
- **`_identity.py`/`_identity.sh`:** `CLAUDE_PLUGIN_OPTION_API_KEY` env var was not checked, causing key-not-found when using `userConfig`.
- **Three previously failing tests** (`test_on_file_read.py`, `test_rubric_dedup.py`, `test_write_path.py`) fixed after hook changes.
### Removed
- **`/mem0:dream --forget` flag:** Extracted to standalone `forget` skill.
- **`/mem0:dream --schedule` flag:** Use Claude Code's built-in `/schedule` command instead.
- **`/mem0:health --fix` flag:** Auto-fix mode removed entirely; use `/mem0:dream` for remediation.
- **CLI dependency for key management:** All `~/.mem0/config.json` fallback code removed.
## 0.2.2
### Fixed
- **Duplicate memory writes on compaction:** `on_pre_compact.py` was running as both primary and backup capture path. Now reads `session_stats` and skips if agent already stored 2+ memories. `on_stop.sh` and `on_session_end.sh` were both calling `on_pre_compact.py`, creating double captures — removed redundant calls.
- **Verbose session-state blobs:** `on_pre_compact.py` `build_content()` was producing 5000+ char structured markdown. Now produces minimal 2-line summary (`Working on:`, `Files touched:`), relying on `infer=True` for extraction.
- **Pre-compaction prompt rewrite:** Instruction changed from "store a single large `session_state` blob with `infer=False`" to "store 0–3 durable facts per session (15–50 words each), one per `add_memory` call, tagged by category."
- **False-positive error detection:** `on_bash_output.sh` and `on_user_prompt.sh` error grep pattern was too broad, triggering on routine test/linter output. Now uses two-tier approach: high-signal patterns (`Traceback`, `panic:`, `FATAL:`) fire always; lower-signal (`Error:`, `Exception:`) require 2+ occurrences.
- **Background `on_pre_commit.py` on every git commit:** `on_git_commit_capture.sh` was storing `commit_context` memories regardless of usefulness. Background call removed; hook now only performs memory search. `on_pre_commit.py` deleted.
- **Post-commit prompt always firing:** `on_post_commit.sh` now gated behind `settings.commit_prompts: true` in `mem0.md` (defaults to off).
- **Project ID lost after folder rename:** `_project.py` only looked up `project_map.json` by CWD path. Added remote-hash fallback: hashes `git remote.origin.url` to a 16-char key, self-heals map on hit.
- **Subagent skip list hardcoded:** `on_subagent_stop.sh` now reads `settings.subagent_skip` from `mem0.md` instead of hardcoded `Explore|Plan`.
- **Auto-import missed project-root files:** `auto_import.py` only searched CWD. Added `_git_root()` helper to also search git root when invoked from subdirectory.
- **Concurrent `ensure_deps.sh` install race:** Multiple parallel sessions could corrupt the venv. Added lock directory with 60s spin-wait and `.install-failed` sentinel.
- **Telemetry `distinct_id` used MD5:** Changed to SHA-256 (truncated 32 chars).
- **Telemetry caller props could override system props:** Moved system fields after spread so they always win.
- **Plugin version hardcoded in `telemetry.py`:** Replaced with `_load_plugin_version()` reading from `plugin.json`.
- **Onboard marker race:** Marker now created in `on_session_start.sh` when prompt is first displayed, not after skill completes.
### Changed
- **`capture_compact_summary.py`:** `infer` changed from `False` to `True` so platform can extract structured facts.
- **Chunking utilities extracted to `_chunking.py`:** `split_by_headers`, `split_by_hr_or_headers`, `filter_and_truncate` moved from `import_competing_tools.py` to shared module.
- **`auto_import.py` now chunks Markdown files:** `.md` files split by `## ` headers before import instead of single blob.
- **`enforce_metadata_defaults.sh`:** New `PreToolUse` hook on `add_memory` for all three editors. Injects default metadata (`confidence: 0.7`, `source: "auto_capture"`, `type: "task_learning"`) when agent omits them.
- **`mem0.md` Settings section parsing:** `parse_mem0_config.py` now parses `Settings` section; added `--key <dotted.path>` CLI argument for programmatic lookup.
- **Project config display condensed:** Session start shows compact summary line instead of raw JSON dump.
### Added
- `scripts/_chunking.py` — shared content-chunking utilities.
- `scripts/enforce_metadata_defaults.sh` — metadata defaults injection hook.
- `parse_mem0_config.py --key` — CLI accessor for individual `mem0.md` config keys.
- **Native `MEMORY.md` detection:** `on_session_start.sh` detects Claude Code auto-memory and prompts to disable or run `/mem0:import`.
- **`/mem0:list-projects` skill:** Discovers all project `app_id` scopes by paginating `get_memories` without an `app_id` filter.
- **`/mem0:tour` cross-project mode** (`--all-projects`) and peek mode (query argument).
- **`/mem0:stats` weekly digest mode** (`--weekly`).
- **`/mem0:dream --forget` mode:** Search-confirm-delete flow with undo-last-write.
- **`/mem0:import --tools` flag:** Import from competing AI tool configs.
- `conftest.py` — `_clean_project_map` autouse fixture preventing cross-test pollution.
### Removed
- `scripts/on_pre_commit.py` — source of unwanted background writes on every commit.
- **`/mem0:digest` skill** — merged into `/mem0:stats --weekly`.
- **`/mem0:forget` skill** — merged into `/mem0:dream --forget`.
- **`/mem0:import-tools` skill** — merged into `/mem0:import --tools`.
- **`/mem0:peek` skill** — merged into `/mem0:tour <query>`.
- `tests/test_pre_commit.py` — tests for deleted script.
## 0.2.1
### Added
- **11 new skills:** `/mem0:dream` (memory consolidation), `/mem0:export` (portable YAML-frontmatter export), `/mem0:import` (re-import exported memories), `/mem0:import-tools` (import from Cursor/Copilot/Cline/Continue configs), `/mem0:forget` (delete with confirmation), `/mem0:health` (diagnostic check — API key, MCP connectivity, read/write), `/mem0:peek` (compact quick-search), `/mem0:pin` (mark memory as high-priority), `/mem0:remember` (quick verbatim store with auto-classification), `/mem0:stats` (session + lifetime statistics), `/mem0:digest` (weekly memory summary).
- **8 new hook scripts:** `on_bash_output.sh` (scan bash output for errors, surface `anti_pattern`/`bug_fix` memories), `on_git_commit_capture.sh` (detect git commit/merge/rebase, search relevant memories), `on_post_commit.sh` (prompt to save commit learnings), `on_post_compact.sh` (recovery prompt to reload context after compaction), `on_session_end.sh` (last-chance transcript capture with dedup marker), `on_subagent_stop.sh` (remind to capture learnings from non-Explore/Plan subagents), `on_tool_failure.sh` (classify MCP failures as auth/rate-limit/network, suggest recovery), `on_pre_commit.py` (capture staged changes via REST API).
- **PostHog telemetry (`telemetry.py`):** Anonymous, fire-and-forget, 10% sampled. Uses stdlib `urllib` (no SDK dependency). Sends event type, platform, plugin version, anonymized identity. Never sends memory content or API keys. Opt-out via `MEM0_TELEMETRY=false`.
- **10 new memory categories in `setup_coding_categories.py`:** `dependency_decisions`, `performance_findings`, `security_constraints`, `testing_patterns`, `data_model`, `api_contracts`, `deployment_runbook`, `team_norms`, `domain_glossary`, `experiment_results`.
- **Dependency management (`ensure_deps.sh`):** Installs `mem0ai` into persistent venv at `${CLAUDE_PLUGIN_DATA}/venv`. Skips re-install if `requirements.txt` hash unchanged. Runs on `Setup(init|maintenance)` hook and every `SessionStart`.
- **Competing tool importer (`import_competing_tools.py`):** Parses and uploads configs from `.cursorrules`, `.github/copilot-instructions.md`, `memory-bank/`, `.continue/rules.md`. Splits by Markdown headers/horizontal rules.
- **Export file parser (`parse_export_file.py`):** Parses YAML-frontmatter Markdown format into JSON array.
- **Config parser (`parse_mem0_config.py`):** Reads `mem0.md` and extracts `Retention` section into category-to-days mapping.
- **Auto-onboarding:** `on_session_start.sh` detects first-time projects and triggers `/mem0:onboard` automatically.
- **`mem0.md` config loading:** Session start parses `mem0.md` and injects project config into context.
- **Inactive API key banner:** Shows `Mem0 Inactive` with `api_key=NOT_SET` instead of silently exiting.
- **`requirements.txt`:** Declares `mem0ai` as plugin's Python dependency.
- **Tests:** `test_coding_categories.py`, `test_import_competing_tools.py`, `test_parse_export_file.py`, `test_parse_mem0_config.py`, `test_pre_commit.py`, `test_session_stats.py`, `test_telemetry.py`, `test_write_path.py`.
### Changed
- **API scoping: `metadata.project_id` → `app_id`:** All scripts and skills now pass `project_id` as top-level `app_id` parameter instead of inside `metadata`. Affects `auto_import.py`, `capture_compact_summary.py`, `on_pre_compact.py`, all stop hooks, all skill instructions.
- **API endpoint: v1 → v3:** `auto_import.py`, `capture_compact_summary.py`, `on_pre_compact.py` now call `/v3/memories/add/`.
- **API key resolution:** `_identity.py` and `_identity.sh` now check `CLAUDE_PLUGIN_OPTION_MEM0_API_KEY` (Claude Code `userConfig` env var) as fallback after `MEM0_API_KEY`.
- **`session_stats.py`:** Added per-category counters, rolling list of up to 50 recent memory IDs, `peek` subcommand for non-destructive stat reading.
- **`setup_coding_categories.py`:** Now imports `mem0ai` from managed venv via `sys.path` injection.
- **`on_stop_cursor.sh`:** Added `loop_count` guard to prevent re-entry loops.
- **`on_stop.sh`:** Removed `set -e` so session-end reminder always emits even if `session_stats.py` fails. Added dedup marker (`~/.mem0/.captured_<session_id>`).
- **`on_user_prompt.sh`:** Detects source file paths in prompt and adds `metadata.files contains` search filter. Handles no-API-key case gracefully.
- **`/mem0:tour`:** Replaced 7 parallel type-filtered `search_memories` calls with single `get_memories(app_id=...)` + 3 broad searches (type filtering misses auto-categorized memories).
- **`/mem0:onboard`:** Added SDK install step, changed MCP check to use ToolSearch, writes onboard marker to prevent re-triggering.
- **`/mem0:mcp`:** Added 16-row category-to-query routing table, inline citations requirement, `branch` in metadata requirement.
### Fixed
- **`auto_import.py`, `capture_compact_summary.py`, `on_pre_compact.py`:** Were reading `MEM0_API_KEY` directly; now use `resolve_api_key()` so `CLAUDE_PLUGIN_OPTION_MEM0_API_KEY` is honored.
- **`on_pre_compact.py`:** `resolve_project_id()` and `resolve_branch()` now receive `cwd` from hook input instead of `os.getcwd()`, fixing incorrect identification when hook fires in different directory.
- **`on_stop_codex.sh`, `on_stop_cursor.sh`:** Missing `_identity.sh` source call; API key resolution via `userConfig` fallback was broken.
- **`on_post_tool_use.sh`:** Input field corrected from `tool_result` to `tool_output` to match hook JSON schema.
## 0.2.0
### Added
- **Project-scoped memories:** Deterministic `project_id` from git remote (`_project.sh` / `_project.py`). Memories are now isolated per-repo via `metadata.project_id` on every `add_memory` and `search_memories` call. Same repo cloned twice → same `project_id`.
- **Branch-aware tagging:** `metadata.branch` stamped on session-state and compact-summary memories. Enables branch-scoped recall (e.g. "what was I doing on feature/auth-rewrite?").
- **Auto-import of project files:** SessionStart detects CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md — hashes them (SHA-256), imports changed files as `project_profile` memories. Idempotent across sessions.
- **Active identity banner:** SessionStart now prints `user=X | project=Y | branch=Z | memories=N` instead of a silent bootstrap.
- **Session-end report:** Stop hook prints `Session: wrote N memories, retrieved M. Categories touched: ...` and appends to `~/.mem0/session-log.md`.
- **`/mem0:onboard` skill:** Post-install wizard — verifies API key, detects and imports project files, installs coding categories, prints setup summary. 30 seconds to value.
- **`/mem0:tour` skill:** Shows all memories for the current project grouped by category. Proof-of-value demo.
- **`/mem0:switch-project` skill:** Manual `project_id` override for monorepos and non-git directories. Persists to `~/.mem0/project_map.json`.
- **Session stats tracker** (`session_stats.py`): Tracks memory adds/searches per session for the end-of-session report.
### Changed
- All hooks and `mem0-mcp/SKILL.md` now include `project_id` in every filter and metadata example.
- SessionStart banner replaces the previous "## Mem0 Identity" block with a compact one-liner.
- `on_pre_compact.py` and `capture_compact_summary.py` now include `project_id` and `branch` in stored metadata.
## 0.1.3
### Fixed
- **user_id resolution no longer derives from `MEM0_API_KEY`.** v0.1.2 changed the resolver to fall back to `"mem0-" + sha256(MEM0_API_KEY)[:12]` ahead of `$USER`, which silently moved every existing user to a new bucket on update. Memories written under the previous `$USER` value became unreachable from the plugin. Resolution is now back to `MEM0_USER_ID` → `$USER` → `"default"`.
- Dropped the "regardless of which machine you're on" line from the SessionStart bootstrap, since cross-machine consolidation now requires setting `MEM0_USER_ID` explicitly.
### Notes for users upgrading from 0.1.2
- The `~/.mem0/identity.json` cache file is no longer read or written. Safe to delete.
- If you wrote memories during the v0.1.2 window, they live under `mem0-<sha256(api_key)[:12]>`. To recover: temporarily `export MEM0_USER_ID=mem0-<hash>`, search/export, then unset.
- Want a single bucket across machines (the original goal of #5076)? Set `MEM0_USER_ID` explicitly in your shell profile. The plugin will not auto-derive one.
## 0.1.2
### Added
- Deterministic `user_id` resolver (`_identity.sh` / `_identity.py`) — **reverted in 0.1.3, see above.**
- SessionStart-compact handler (`capture_compact_summary.py`) that stores the post-compaction summary as a memory with `metadata.type=compact_summary`.
- Coding-taxonomy setup script (`setup_coding_categories.py`) — one-shot `project.update(custom_categories=[...])` for `architecture_decisions`, `anti_patterns`, `task_learnings`, `tooling_setup`, `bug_fixes`, `coding_conventions`, `user_preferences`.
- Opt-in hook logging via `MEM0_DEBUG=1` → `~/.mem0/hooks.log`.
- `mem0-mcp` skill replacing the Claude-Code-specific `mem0-codex` skill.
### Fixed
- `session_id` now written to memory metadata (`on_pre_compact.py`).
- SessionStart bootstrap exits silently when `MEM0_API_KEY` is unset.
- `block_memory_write.sh` regex tightened to `MEMORY.md` / `.claude/memory/*` — no longer blocks `docs/memory/*.md`.
- Removed duplicate PreCompact write path (kept agent-driven, dropped the parallel Python REST entry from `hooks.json` / `cursor-hooks.json`).
- Hook-side captures (`session_state`, `compact_summary`) now set `expiration_date = today + 90 days`.
## 0.1.1
- Cursor plugin fully functional (`#4547`).
- Codex plugin support and integration docs (`#4665`).
- Codex lifecycle hooks via opt-in installer (`#4917`).
- Removed invalid keys from Claude plugin config (`#4821`).
## 0.1.0
- Initial release: Mem0 plugin for Claude Code and Cursor (`#4518`).
+305
View File
@@ -0,0 +1,305 @@
# Mem0 Plugin for Claude Code, Claude Cowork, Cursor, Codex, OpenCode & Antigravity
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), **Cursor**, **Codex**, **OpenCode**, and **Antigravity**.
## Quick path for agents
If you're an AI agent setting up Mem0 autonomously (no human dashboard signup available), run:
```bash
mem0 init --agent --json
```
This mints an evaluation API key in <5 seconds — no email, no OTP, no browser. The key is written to your config and exported as `MEM0_API_KEY` for the current shell. Use it immediately against the Mem0 Platform or the MCP server. The human owner can later run `mem0 init --email <their-email>` to claim ownership: memories transfer, the same API key keeps working, and the agent isn't disrupted.
Humans setting up Mem0 by hand should continue with Step 1 below.
## Step 1: Set your API key
> **You must complete this step before installing the plugin.**
1. Sign up at [app.mem0.ai](https://app.mem0.ai?utm_source=oss&utm_medium=mem0-plugin-readme) if you haven't already
2. Go to [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=mem0-plugin-readme)
3. Click **Create API Key** and copy the key (starts with `m0-`)
4. Set the key using **one** of these methods:
**CLI** — add to your shell profile:
```bash
# For zsh (default on macOS)
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
source ~/.zshrc
# For bash
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
source ~/.bashrc
```
**Desktop app** — use the local environment editor:
Click the environment dropdown next to the prompt box → hover over **Local** → click the **gear icon** → add `MEM0_API_KEY` with your key. Values are stored encrypted on your machine.
> **Note:** The Desktop app does not inherit custom environment variables from shell profiles — it only reads `PATH`. You must use the local environment editor for Desktop.
5. Confirm it's set:
```bash
echo $MEM0_API_KEY
# Should print: m0-your-api-key
```
## Step 2: Install the plugin
Choose one of the options below. All require `MEM0_API_KEY` to be set first (see above).
### Claude Code (CLI) / Claude Cowork (Desktop)
Claude Code and Claude Cowork share the same plugin system.
**CLI:**
```
/plugin marketplace add mem0ai/mem0
/plugin install mem0@mem0-plugins
```
**Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
This installs the full plugin including the MCP server, lifecycle hooks (automatic memory capture), and the Mem0 SDK skill.
### Codex
**Option A — Direct MCP** (fastest, MCP only):
Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
```toml
[mcp_servers.mem0]
url = "https://mcp.mem0.ai/mcp"
bearer_token_env_var = "MEM0_API_KEY"
```
Export `MEM0_API_KEY` in your shell and restart Codex. `codex mcp add` only supports stdio servers, so HTTP servers like Mem0's must be added via `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
**Option B — Sideload the plugin** (full experience: MCP + skills + opt-in hooks):
Clone the repo and register the bundled marketplace with one CLI call:
```bash
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
codex plugin marketplace add ~/codex-plugins/mem0-source
```
This points Codex at the repo's `.agents/plugins/marketplace.json`, which references `integrations/mem0-plugin/` as the local source. Restart Codex, run `/plugins`, and install **Mem0** from the **Mem0 Plugins** marketplace.
> **Don't combine with Option A.** The plugin manifest auto-registers `mem0` as an MCP server via `integrations/mem0-plugin/.codex-mcp.json` — adding a manual `[mcp_servers.mem0]` block would duplicate the registration.
**Optional — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`) ([docs](https://developers.openai.com/codex/hooks)). Run the bundled installer once to merge Mem0's entries:
```bash
python3 ~/codex-plugins/mem0-source/integrations/mem0-plugin/scripts/install_codex_hooks.py
```
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into your clone:
| Event | What it does |
|-------|--------------|
| `SessionStart` | Loads prior memories as bootstrap context |
| `UserPromptSubmit` | Injects relevant memories into the prompt |
| `Stop` | Reminds the agent to persist learnings at turn end |
Re-running the installer is idempotent (replaces the Mem0 entries rather than duplicating) and preserves any other hooks you have. To remove: `python3 .../install_codex_hooks.py --uninstall`. If you move or delete the clone directory, re-run the installer from the new location — the hooks file stores absolute paths.
Codex hooks also require the `codex_hooks` feature flag in `~/.codex/config.toml`:
```toml
[features]
codex_hooks = true
```
The installer prints a reminder if the flag isn't set. Restart Codex after editing the config.
**Managing the plugin:**
```bash
codex plugin marketplace upgrade # pull latest plugin versions
codex plugin marketplace remove mem0-plugins # unregister the marketplace
```
### Cursor
> **Already have `mem0` configured as an MCP server?** Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
**Option A — One-click deeplink** (installs MCP server only):
[Install Mem0 MCP in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=mem0&config=eyJtY3BTZXJ2ZXJzIjp7Im1lbTAiOnsidXJsIjoiaHR0cHM6Ly9tY3AubWVtMC5haS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiVG9rZW4gJHtlbnY6TUVNMF9BUElfS0VZfSJ9fX19)
**Option B — Manual configuration** (MCP server only):
Add the following to your `.cursor/mcp.json`:
```json
{
"mcpServers": {
"mem0": {
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${env:MEM0_API_KEY}"
}
}
}
}
```
**Option C — Cursor Marketplace** (full plugin with hooks and skills):
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks and the Mem0 SDK skill.
### OpenCode
```bash
bunx @mem0/opencode-plugin@latest install
```
Or via OpenCode's built-in CLI: `opencode plugin @mem0/opencode-plugin`
Then add the MCP server to your `opencode.json` (project or global at `~/.config/opencode/opencode.json`):
```json
{
"mcp": {
"mem0": {
"type": "remote",
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token {env:MEM0_API_KEY}"
},
"oauth": false
}
}
}
```
Restart OpenCode. The plugin installs hooks and skills automatically. Drop the `plugin` install if you only want MCP.
See [OpenCode integration docs](https://docs.mem0.ai/integrations/opencode) for full details.
### Antigravity (Google)
**Option A — degit** (recommended):
```bash
# Install the plugin (MCP server, hooks, scripts)
npx degit mem0ai/mem0/integrations/mem0-plugin ~/.gemini/config/plugins/mem0
```
This installs the MCP server, lifecycle hooks, and shared scripts.
See [Antigravity integration docs](https://docs.mem0.ai/integrations/antigravity) for full details.
## Post-Installation: Run `/mem0:onboard`
After installing, start a new session and run:
```
/mem0:onboard
```
This runs the setup wizard which:
1. Verifies your API key and MCP connection
2. Detects and imports project files (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`)
3. Installs coding-optimized memory categories
4. Shows your identity (user ID, project scope, branch)
The onboarding is idempotent — safe to re-run anytime. On first session in a new project (0 memories), Claude is prompted to run it automatically.
## Verify it works
After onboarding, confirm everything is connected:
1. Run `/mem0:health` to check connectivity
2. Run `/mem0:stats` to see memory counts
3. Try `/mem0:remember "we use TypeScript"` then `/mem0:tour` to see it stored
## Available Skills
The plugin includes 17 skills accessible via `/mem0:` commands:
| Command | Description |
|---------|-------------|
| `/mem0:remember` | Store a memory verbatim — decisions, preferences, conventions |
| `/mem0:tour` | Browse all memories grouped by category |
| `/mem0:peek` | Quick search with compact one-liner results |
| `/mem0:stats` | Session and project memory statistics |
| `/mem0:dream` | Consolidate memories — merge duplicates, resolve contradictions |
| `/mem0:pin` | Protect critical memories from pruning |
| `/mem0:forget` | Delete memories by search or ID |
| `/mem0:health` | Diagnose connectivity, API key, and read/write |
| `/mem0:export` | Export memories to portable Markdown |
| `/mem0:import` | Import memories from export file or MEMORY.md |
| `/mem0:list-projects` | List all projects with stored memories |
| `/mem0:switch-project` | Override auto-detected project scope |
| `/mem0:memory-reviewer` | Audit memory quality — duplicates, contradictions, stale |
| `/mem0:context-loader` | Pre-load relevant memories for current task |
## What's included
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex (Sideload) | Codex (Direct MCP) | OpenCode (Full) | OpenCode (MCP) | Antigravity |
|-----------|:--------------------:|:--------------------:|:------------------------:|:----------------:|:------------------:|:---------------:|:--------------:|:-----------:|
| MCP Server | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
| Lifecycle Hooks | Yes | Yes | No | Opt-in | No | Yes | No | Yes |
| Mem0 SDK Skill | Yes | Yes | No | Yes | No | Yes | No | Yes |
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
- **Lifecycle Hooks** — Automatic memory capture at key points. Claude Code, Cursor, OpenCode, and Antigravity wire hooks natively when the full plugin is installed. Codex hooks are opt-in via a one-time installer (`scripts/install_codex_hooks.py`).
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
## Updating the plugin
When the plugin updates (new version pulled from the marketplace, or a fresh local install), the MCP server connection in your existing session is left holding a stale handle and stops responding. **Restart your client to reconnect:**
- **Claude Code:** run `/restart` in the prompt, or close and reopen the CLI.
- **Cursor:** quit and relaunch.
- **Codex:** restart the editor session.
- **OpenCode:** restart the session.
- **Antigravity:** restart the session.
Your `MEM0_API_KEY` doesn't need to be re-entered — the auth header is re-read from your environment on the new session. The plugin's MCP config uses `${MEM0_API_KEY}` interpolation at session start, not at install time, so as long as the env var is set persistently (in your shell profile or `~/.claude/settings.json` `env` block), reconnection is automatic on restart.
If reconnection still fails after a restart, check that `MEM0_API_KEY` is reachable in the new shell (`echo $MEM0_API_KEY`) and confirm you're using a key that starts with `m0-` (from https://app.mem0.ai/dashboard/api-keys, not a legacy token).
## Coding-tuned categories (automatic)
mem0 auto-tags every memory with one or more `categories` from a project-level list. The default list is consumer-oriented (`food`, `hobbies`, `music` …) — useful for chat assistants, less so for code. **The plugin installs a coding-focused taxonomy automatically in the background on session start** — no prompt, no manual step. New memories then auto-tag against 17 development-oriented categories: `architecture_decisions`, `anti_patterns`, `task_learnings`, `tooling_setup`, `bug_fixes`, `coding_conventions`, `user_preferences`, `dependency_decisions`, `performance_findings`, `security_constraints`, `testing_patterns`, `data_model`, `api_contracts`, `deployment_runbook`, `team_norms`, `domain_glossary`, `experiment_results`.
The background setup is idempotent and runs once per account (cached in `~/.mem0/categories_setup.json`); it re-applies only if the taxonomy itself changes. To preview the taxonomy or force a refresh manually:
```bash
# Dry-run -- prints current vs proposed, no changes:
python integrations/mem0-plugin/scripts/setup_coding_categories.py
# Write explicitly:
python integrations/mem0-plugin/scripts/setup_coding_categories.py --apply
```
Requires the `mem0ai` Python SDK (`pip install mem0ai`) and `MEM0_API_KEY` set. `project.update(custom_categories=[...])` always replaces the full list.
## MCP Tools
Once installed, the following tools are available:
| Tool | Description |
|------|-------------|
| `add_memory` | Save text or conversation history for a user/agent |
| `search_memories` | Semantic search across memories with filters |
| `get_memories` | List memories with filters and pagination |
| `get_memory` | Retrieve a specific memory by ID |
| `update_memory` | Overwrite a memory's text by ID |
| `delete_memory` | Delete a single memory by ID |
| `delete_all_memories` | Bulk delete all memories in scope |
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
## License
Apache-2.0
+104
View File
@@ -0,0 +1,104 @@
{
"hooks": {
"SessionStart": [
{
"matcher": "*",
"hooks": [
{
"name": "mem0-ensure-deps",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/ensure_deps.sh 2>/dev/null || true",
"timeout": 60
},
{
"name": "mem0-session-start",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_session_start.sh 2>/dev/null || true"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"name": "mem0-user-prompt",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_user_prompt.sh 2>/dev/null || true",
"timeout": 8
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"name": "mem0-block-memory-write",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/block_memory_write.sh"
}
]
},
{
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"hooks": [
{
"name": "mem0-enforce-metadata",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/enforce_metadata_defaults.sh",
"timeout": 3
}
]
},
{
"matcher": "Read",
"hooks": [
{
"name": "mem0-file-context",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_file_read.sh",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"name": "mem0-session-summary",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_stop.sh",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"hooks": [
{
"name": "mem0-post-tool",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_post_tool_use.sh",
"timeout": 3
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"name": "mem0-bash-output",
"type": "command",
"command": "ANTIGRAVITY_PLUGIN_ROOT=${extensionPath} CLAUDE_PLUGIN_ROOT=${extensionPath} bash ${extensionPath}/scripts/on_bash_output.sh",
"timeout": 5
}
]
}
]
}
}
@@ -0,0 +1,104 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/block_memory_write.sh",
"timeout": 3
}
]
},
{
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"timeout": 3
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_file_read.sh",
"timeout": 5
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume|compact",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_session_start.sh",
"statusMessage": "Loading mem0 context..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_user_prompt.sh",
"statusMessage": "Checking memory relevance...",
"timeout": 12
}
]
}
],
"PostToolUse": [
{
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
"timeout": 3
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_bash_output.sh",
"timeout": 12
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_stop.sh",
"timeout": 30
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "${PLUGIN_ROOT}/scripts/on_pre_compact.sh",
"statusMessage": "Preparing pre-compaction summary..."
}
]
}
]
}
}
@@ -0,0 +1,56 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_session_start_cursor.sh",
"matcher": "startup|resume|compact"
}
],
"preToolUse": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write_cursor.sh",
"matcher": "Write|Edit|MultiEdit"
},
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"timeout": 3
},
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_file_read_cursor.sh",
"matcher": "Read",
"timeout": 5
}
],
"postToolUse": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_post_tool_use_cursor.sh",
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"timeout": 3
},
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_bash_output.sh",
"matcher": "Bash",
"timeout": 12
}
],
"stop": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop_cursor.sh",
"timeout": 30
}
],
"preCompact": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact_cursor.sh"
}
],
"beforeSubmitPrompt": [
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt_cursor.sh",
"timeout": 12
}
]
}
}
+126
View File
@@ -0,0 +1,126 @@
{
"hooks": {
"Setup": [
{
"matcher": "init|maintenance",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh",
"statusMessage": "Installing mem0 SDK...",
"timeout": 120
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "diff -q \"${CLAUDE_PLUGIN_ROOT}/requirements.txt\" \"${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}/requirements.txt\" >/dev/null 2>&1 || \"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh\"",
"statusMessage": "Installing mem0 SDK...",
"timeout": 60
}
]
},
{
"matcher": "startup|resume|compact",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_session_start.sh",
"statusMessage": "Loading mem0 context..."
}
]
}
],
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block_memory_write.sh"
}
]
},
{
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"timeout": 3
}
]
},
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_file_read.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
"timeout": 3
}
]
},
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_bash_output.sh",
"timeout": 5
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_stop.sh",
"timeout": 30
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.sh",
"statusMessage": "Preparing pre-compaction summary..."
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
"statusMessage": "Checking memory relevance...",
"timeout": 8
}
]
}
]
}
}
+19
View File
@@ -0,0 +1,19 @@
<svg width="307" height="307" viewBox="0 0 307 307" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M162.496 25.3505C165.003 25.3505 167.453 24.6071 169.538 23.2144C171.622 21.8216 173.247 19.8419 174.206 17.5258C175.165 15.2097 175.416 12.6612 174.927 10.2024C174.438 7.74365 173.231 5.48516 171.458 3.71249C169.686 1.93983 167.427 0.73263 164.968 0.243552C162.51 -0.245525 159.961 0.00550576 157.645 0.964866C155.329 1.92423 153.349 3.54885 151.956 5.63328C150.564 7.71772 149.82 10.1683 149.82 12.6753C149.818 14.3404 150.145 15.9895 150.781 17.5283C151.417 19.0671 152.351 20.4653 153.528 21.6427C154.706 22.8201 156.104 23.7537 157.643 24.39C159.181 25.0262 160.83 25.3526 162.496 25.3505Z" fill="white"/>
<path d="M69.3342 56.559C71.1066 54.7862 72.3135 52.5277 72.8024 50.069C73.2913 47.6103 73.0401 45.0619 72.0807 42.7459C71.1213 40.43 69.4967 38.4505 67.4123 37.0579C65.3279 35.6652 62.8774 34.9219 60.3706 34.9219C57.8637 34.9219 55.4132 35.6652 53.3288 37.0579C51.2444 38.4505 49.6198 40.43 48.6604 42.7459C47.701 45.0619 47.4498 47.6103 47.9387 50.069C48.4276 52.5277 49.6345 54.7862 51.4069 56.559C52.5839 57.7363 53.9813 58.6701 55.5193 59.3073C57.0573 59.9444 58.7058 60.2724 60.3706 60.2724C62.0353 60.2724 63.6838 59.9444 65.2218 59.3073C66.7598 58.6701 68.1572 57.7363 69.3342 56.559Z" fill="white"/>
<path d="M25.3505 144.504C25.3505 141.997 24.6071 139.547 23.2143 137.462C21.8216 135.378 19.842 133.753 17.5259 132.794C15.2098 131.835 12.6612 131.584 10.2024 132.073C7.74368 132.562 5.48513 133.769 3.71247 135.542C1.9398 137.314 0.732655 139.573 0.243578 142.032C-0.2455 144.49 0.00543354 147.039 0.964793 149.355C1.92415 151.671 3.54877 153.651 5.63321 155.044C7.71764 156.436 10.1683 157.18 12.6752 157.18C16.0369 157.18 19.261 155.844 21.638 153.467C24.0151 151.09 25.3505 147.866 25.3505 144.504Z" fill="white"/>
<path d="M56.5589 237.749C54.7862 235.976 52.5277 234.769 50.069 234.28C47.6103 233.792 45.0619 234.043 42.7459 235.002C40.43 235.962 38.4505 237.586 37.0579 239.671C35.6652 241.755 34.9219 244.206 34.9219 246.712C34.9219 249.219 35.6652 251.67 37.0579 253.754C38.4505 255.838 40.43 257.463 42.7459 258.423C45.0619 259.382 47.6103 259.633 50.069 259.144C52.5277 258.655 54.7862 257.448 56.5589 255.676C57.7362 254.499 58.6701 253.102 59.3073 251.564C59.9444 250.026 60.2724 248.377 60.2724 246.712C60.2724 245.048 59.9444 243.399 59.3073 241.861C58.6701 240.323 57.7362 238.926 56.5589 237.749Z" fill="white"/>
<path d="M144.488 281.648C141.981 281.648 139.53 282.392 137.446 283.785C135.361 285.177 133.737 287.157 132.777 289.473C131.818 291.789 131.567 294.338 132.056 296.797C132.545 299.255 133.752 301.514 135.525 303.286C137.298 305.059 139.556 306.266 142.015 306.755C144.474 307.244 147.022 306.993 149.338 306.034C151.655 305.075 153.634 303.45 155.027 301.366C156.42 299.281 157.163 296.831 157.163 294.324C157.159 290.963 155.822 287.742 153.446 285.366C151.07 282.989 147.848 281.653 144.488 281.648Z" fill="white"/>
<path d="M237.751 250.487C235.978 252.26 234.771 254.518 234.282 256.977C233.794 259.435 234.045 261.984 235.004 264.3C235.964 266.616 237.588 268.595 239.673 269.988C241.757 271.381 244.207 272.124 246.714 272.124C249.221 272.124 251.672 271.381 253.756 269.988C255.84 268.595 257.465 266.616 258.424 264.3C259.384 261.984 259.635 259.435 259.146 256.977C258.657 254.518 257.45 252.26 255.678 250.487C254.501 249.31 253.104 248.376 251.566 247.739C250.028 247.101 248.379 246.773 246.714 246.773C245.05 246.773 243.401 247.101 241.863 247.739C240.325 248.376 238.928 249.31 237.751 250.487Z" fill="white"/>
<path d="M281.648 162.512C281.648 165.019 282.392 167.469 283.785 169.554C285.177 171.638 287.157 173.263 289.473 174.222C291.789 175.181 294.338 175.432 296.797 174.943C299.255 174.454 301.514 173.247 303.286 171.474C305.059 169.702 306.266 167.443 306.755 164.984C307.244 162.526 306.993 159.977 306.034 157.661C305.075 155.345 303.45 153.365 301.366 151.973C299.281 150.58 296.831 149.836 294.324 149.836C290.962 149.836 287.738 151.172 285.361 153.549C282.984 155.926 281.648 159.15 281.648 162.512Z" fill="white"/>
<path d="M250.471 69.3303C252.244 71.1027 254.503 72.3097 256.961 72.7985C259.42 73.2874 261.968 73.0363 264.284 72.0768C266.6 71.1174 268.58 69.4928 269.972 67.4084C271.365 65.324 272.108 62.8735 272.108 60.3667C272.108 57.8599 271.365 55.4093 269.972 53.3249C268.58 51.2406 266.6 49.616 264.284 48.6565C261.968 47.6971 259.42 47.4459 256.961 47.9348C254.503 48.4236 252.244 49.6306 250.471 51.403C249.294 52.58 248.36 53.9775 247.723 55.5155C247.086 57.0535 246.758 58.7019 246.758 60.3667C246.758 62.0314 247.086 63.6799 247.723 65.2179C248.36 66.7559 249.294 68.1533 250.471 69.3303Z" fill="white"/>
<path d="M184.782 60.8054C180.168 63.4713 178.3 69.0427 177.63 74.3267C177.047 78.9358 175.033 83.2457 171.87 86.6488C168.707 90.052 164.556 92.3766 160.002 93.2951C155.448 94.2136 150.721 93.6796 146.487 91.7683C142.252 89.857 138.724 86.6649 136.401 82.642C134.077 78.6192 133.075 73.9684 133.535 69.3455C133.995 64.7226 135.895 60.3607 138.966 56.8748C142.037 53.389 146.125 50.9549 150.653 49.9159C155.181 48.8768 159.921 49.2852 164.204 51.0834C169.121 53.1428 174.884 54.2762 179.514 51.6422C184.143 49.0082 185.995 43.4049 186.665 38.1208C187.245 33.5107 189.257 29.1987 192.418 25.7931C195.579 22.3876 199.73 20.0603 204.284 19.1394C208.838 18.2185 213.567 18.7505 217.803 20.6605C222.039 22.5704 225.568 25.7618 227.893 29.7846C230.218 33.8075 231.222 38.4587 230.763 43.0824C230.303 47.7062 228.404 52.0691 225.333 55.5559C222.262 59.0426 218.173 61.4773 213.645 62.5165C209.116 63.5557 204.375 63.147 200.091 61.3481C195.174 59.3048 189.411 58.1554 184.782 60.8054Z" fill="white"/>
<path d="M110.073 65.8178C108.7 70.9742 111.318 76.2422 114.575 80.4567C117.417 84.1261 119.036 88.595 119.204 93.2335C119.372 97.872 118.08 102.446 115.51 106.311C112.941 110.177 109.223 113.138 104.881 114.778C100.538 116.419 95.7912 116.655 91.3077 115.454C86.8242 114.253 82.8306 111.675 79.8898 108.084C76.9489 104.493 75.2091 100.07 74.9155 95.4379C74.6219 90.8057 75.7894 86.1981 78.2533 82.2645C80.7173 78.331 84.3534 75.2698 88.6494 73.5124C93.5822 71.485 98.4991 68.2444 99.8241 63.0881C101.149 57.9317 98.579 52.6637 95.3224 48.4493C92.4827 44.7781 90.8665 40.3083 90.7018 35.6699C90.537 31.0315 91.8319 26.4583 94.4039 22.5949C96.976 18.7314 100.695 15.7724 105.038 14.1349C109.381 12.4974 114.128 12.2639 118.611 13.4673C123.094 14.6708 127.085 17.2505 130.024 20.8429C132.963 24.4354 134.7 28.8594 134.991 33.4916C135.283 38.1237 134.113 42.7305 131.647 46.6627C129.182 50.5948 125.544 53.6541 121.248 55.4095C116.363 57.4209 111.462 60.6775 110.073 65.8178Z" fill="white"/>
<path d="M60.7892 122.218C63.4552 126.831 69.0425 128.699 74.3265 129.37C78.9361 129.955 83.2455 131.973 86.6471 135.138C90.0487 138.304 92.3707 142.457 93.2857 147.013C94.2006 151.569 93.6625 156.296 91.747 160.53C89.8314 164.763 86.6353 168.288 82.6093 170.608C78.5833 172.928 73.9305 173.926 69.3073 173.46C64.6841 172.995 60.3236 171.09 56.841 168.014C53.3583 164.938 50.9292 160.846 49.8962 156.316C48.8631 151.785 49.2783 147.045 51.0832 142.763C53.1426 137.846 54.2759 132.083 51.6419 127.454C49.0079 122.824 43.4046 120.973 38.1047 120.302C33.4951 119.717 29.1856 117.699 25.7841 114.533C22.3825 111.368 20.0604 107.214 19.1454 102.659C18.2304 98.1032 18.7687 93.3752 20.6842 89.1418C22.5997 84.9084 25.7959 81.3832 29.8219 79.0632C33.8479 76.7433 38.5006 75.7457 43.1238 76.2113C47.7471 76.6768 52.1075 78.582 55.5902 81.658C59.0728 84.7341 61.502 88.8258 62.535 93.3561C63.568 97.8865 63.1528 102.627 61.3479 106.908C59.2886 111.825 58.1552 117.588 60.7892 122.218Z" fill="white"/>
<path d="M65.8204 196.93C70.9767 198.303 76.2287 195.685 80.4592 192.428C84.1286 189.586 88.5975 187.967 93.236 187.799C97.8745 187.631 102.449 188.923 106.314 191.493C110.179 194.062 113.141 197.78 114.781 202.122C116.421 206.464 116.657 211.212 115.457 215.695C114.256 220.179 111.678 224.172 108.087 227.113C104.496 230.054 100.073 231.794 95.4404 232.087C90.8082 232.381 86.2006 231.214 82.2671 228.75C78.3335 226.286 75.2723 222.649 73.5149 218.353C71.4875 213.421 68.231 208.504 63.0906 207.179C57.9503 205.854 52.6662 208.424 48.4518 211.681C44.7804 214.528 40.308 216.151 35.6652 216.32C31.0224 216.49 26.4435 215.199 22.5738 212.628C18.7042 210.057 15.7391 206.336 14.0968 201.99C12.4544 197.644 12.2176 192.892 13.4197 188.404C14.6218 183.917 17.2021 179.919 20.7969 176.976C24.3917 174.033 28.8195 172.293 33.4562 172C38.0929 171.708 42.7045 172.878 46.6407 175.345C50.577 177.813 53.6394 181.454 55.3961 185.755C57.4235 190.656 60.6641 195.541 65.8204 196.93Z" fill="white"/>
<path d="M122.205 246.21C126.818 243.544 128.686 237.956 129.373 232.672C129.96 228.068 131.978 223.763 135.142 220.366C138.306 216.969 142.456 214.651 147.008 213.738C151.559 212.825 156.283 213.364 160.512 215.278C164.741 217.192 168.263 220.385 170.58 224.408C172.898 228.43 173.895 233.078 173.43 237.697C172.966 242.316 171.064 246.673 167.991 250.153C164.919 253.633 160.832 256.061 156.306 257.095C151.781 258.129 147.045 257.717 142.766 255.916C137.833 253.856 132.07 252.723 127.457 255.357C122.843 257.991 120.96 263.594 120.289 268.894C119.7 273.498 117.681 277.8 114.517 281.196C111.353 284.591 107.204 286.908 102.653 287.821C98.1027 288.733 93.3808 288.194 89.1525 286.281C84.9243 284.367 81.4031 281.175 79.085 277.154C76.767 273.134 75.7689 268.487 76.2316 263.869C76.6942 259.251 78.5942 254.895 81.6638 251.414C84.7334 247.933 88.8179 245.503 93.3417 244.466C97.8655 243.429 102.601 243.838 106.88 245.635C111.828 247.694 117.591 248.876 122.205 246.21Z" fill="white"/>
<path d="M196.915 241.18C198.304 236.024 195.686 230.756 192.414 226.542C189.567 222.87 187.944 218.398 187.774 213.755C187.604 209.112 188.896 204.533 191.467 200.664C194.038 196.794 197.759 193.829 202.104 192.187C206.45 190.544 211.202 190.307 215.69 191.509C220.178 192.712 224.175 195.292 227.118 198.887C230.061 202.481 231.802 206.909 232.094 211.546C232.387 216.183 231.217 220.794 228.749 224.731C226.281 228.667 222.64 231.729 218.339 233.486C213.406 235.513 208.505 238.77 207.164 243.91C205.823 249.051 208.393 254.335 211.666 258.549C214.513 262.22 216.136 266.693 216.306 271.335C216.476 275.978 215.184 280.557 212.613 284.427C210.042 288.297 206.321 291.262 201.975 292.904C197.629 294.546 192.877 294.783 188.39 293.581C183.902 292.379 179.905 289.799 176.962 286.204C174.019 282.609 172.278 278.181 171.985 273.545C171.693 268.908 172.863 264.296 175.331 260.36C177.799 256.424 181.44 253.361 185.741 251.605C190.658 249.577 195.543 246.337 196.915 241.18Z" fill="white"/>
<path d="M246.195 184.797C243.529 180.184 237.957 178.316 232.673 177.629C228.069 177.045 223.764 175.03 220.365 171.869C216.967 168.708 214.646 164.56 213.729 160.01C212.813 155.46 213.348 150.737 215.258 146.507C217.168 142.277 220.357 138.753 224.376 136.431C228.395 134.11 233.041 133.108 237.66 133.567C242.279 134.026 246.637 135.923 250.12 138.991C253.604 142.058 256.037 146.141 257.077 150.664C258.117 155.188 257.711 159.923 255.917 164.204C253.857 169.137 252.724 174.9 255.342 179.513C257.96 184.127 263.595 186.01 268.879 186.681C273.484 187.267 277.789 189.283 281.186 192.446C284.584 195.608 286.904 199.757 287.819 204.308C288.733 208.859 288.197 213.582 286.285 217.811C284.372 222.041 281.181 225.564 277.16 227.884C273.14 230.203 268.492 231.203 263.874 230.741C259.255 230.279 254.897 228.379 251.416 225.31C247.934 222.24 245.503 218.155 244.466 213.63C243.429 209.106 243.838 204.37 245.636 200.09C247.695 195.173 248.861 189.411 246.195 184.797Z" fill="white"/>
<path d="M241.18 110.07C236.024 108.697 230.756 111.315 226.542 114.588C222.87 117.435 218.398 119.058 213.755 119.228C209.112 119.398 204.533 118.106 200.664 115.535C196.794 112.964 193.829 109.243 192.187 104.897C190.544 100.551 190.307 95.7994 191.509 91.3117C192.712 86.824 195.292 82.8268 198.887 79.8837C202.481 76.9405 206.909 75.2 211.546 74.9073C216.183 74.6147 220.794 75.7849 224.731 78.2527C228.667 80.7206 231.729 84.3617 233.486 88.6627C235.513 93.5955 238.754 98.4964 243.91 99.8374C249.066 101.178 254.335 98.6082 258.549 95.3356C262.22 92.4959 266.69 90.8798 271.328 90.7151C275.967 90.5503 280.54 91.8452 284.403 94.4172C288.267 96.9892 291.226 100.709 292.863 105.052C294.501 109.394 294.734 114.142 293.531 118.624C292.327 123.107 289.748 127.099 286.155 130.037C282.563 132.976 278.139 134.714 273.507 135.005C268.875 135.296 264.268 134.126 260.336 131.661C256.403 129.195 253.344 125.557 251.589 121.261C249.577 116.36 246.321 111.459 241.18 110.07Z" fill="white"/>
<path d="M153.491 191.533C174.501 191.533 191.533 174.501 191.533 153.491C191.533 132.482 174.501 115.45 153.491 115.45C132.481 115.45 115.449 132.482 115.449 153.491C115.449 174.501 132.481 191.533 153.491 191.533Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+10
View File
@@ -0,0 +1,10 @@
{
"mcpServers": {
"mem0": {
"serverUrl": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${MEM0_API_KEY}"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"id": "mem0",
"name": "mem0",
"version": "0.1.1",
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
"author": { "name": "Mem0", "email": "support@mem0.ai" },
"publisher": "mem0ai",
"homepage": "https://mem0.ai",
"repository": "https://github.com/mem0ai/mem0",
"license": "Apache-2.0",
"keywords": ["memory", "persistence", "personalization", "mcp", "semantic-search"],
"contextFileName": "AGENTS.md"
}
@@ -0,0 +1 @@
mem0ai
@@ -0,0 +1,75 @@
"""Shared content-chunking utilities for mem0-plugin import scripts."""
from __future__ import annotations
MIN_CHUNK_CHARS = 50
MAX_CHUNK_CHARS = 10_000
def split_by_headers(content: str, header_prefix: str = "## ") -> list[str]:
"""Split content by Markdown header lines (e.g. '## ').
The header line is included at the start of each chunk.
Returns a list of non-empty chunk strings.
"""
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
if line.startswith(header_prefix) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def split_by_hr_or_headers(content: str) -> list[str]:
"""Split content by '---' horizontal rules or '## ' headers.
Used for .continue/rules.md which may use either convention.
"""
import re
# Split on lines that are exactly "---" or start with "## "
chunks: list[str] = []
current_lines: list[str] = []
for line in content.splitlines(keepends=True):
is_hr = re.match(r"^---\s*$", line)
is_h2 = line.startswith("## ")
if (is_hr or is_h2) and current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
current_lines = [] if is_hr else [line]
else:
current_lines.append(line)
if current_lines:
chunk = "".join(current_lines).strip()
if chunk:
chunks.append(chunk)
return chunks
def filter_and_truncate(chunks: list[str]) -> list[str]:
"""Filter out chunks shorter than MIN_CHUNK_CHARS, truncate long chunks."""
result: list[str] = []
for chunk in chunks:
if len(chunk) < MIN_CHUNK_CHARS:
continue
if len(chunk) > MAX_CHUNK_CHARS:
chunk = chunk[:MAX_CHUNK_CHARS]
result.append(chunk)
return result
@@ -0,0 +1,51 @@
"""Shared formatting helpers for mem0 plugin hooks.
Constants and utilities used by file_context.py, session_timeline.py,
and any future hook that displays memories.
"""
from __future__ import annotations
TYPE_ICONS = {
"decision": "⚖️",
"anti_pattern": "\U0001f534",
"bug_fix": "\U0001f534",
"convention": "\U0001f504",
"task_learning": "\U0001f535",
"user_preference": "\U0001f7e3",
"session_summary": "\U0001f4cb",
"session_state": "\U0001f4cb",
"project_profile": "\U0001f4d6",
"compact_summary": "\U0001f4cb",
"auto_capture": "✅",
"environmental": "🌐",
"health_check": "🩺",
}
def format_age(memory: dict) -> str:
"""Format how long ago a memory was created, e.g. '2h ago', '3d ago'."""
created = memory.get("created_at", "")
if not created:
return ""
try:
from datetime import datetime, timezone
if created.endswith("Z"):
created = created[:-1] + "+00:00"
dt = datetime.fromisoformat(created)
now = datetime.now(timezone.utc)
delta = now - dt
seconds = int(delta.total_seconds())
if seconds < 3600:
return f"{seconds // 60}m ago"
if seconds < 86400:
return f"{seconds // 3600}h ago"
days = seconds // 86400
if days == 1:
return "1d ago"
if days < 30:
return f"{days}d ago"
return f"{days // 30}mo ago"
except Exception:
return ""
@@ -0,0 +1,104 @@
"""Resolve mem0 identity: API key, user_id, and settings.
API key resolution (first non-empty wins):
1. MEM0_API_KEY env var (explicit / shell profile)
2. CLAUDE_PLUGIN_OPTION_API_KEY (injected by Claude Code userConfig)
3. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (legacy userConfig)
4. Extract from shell profile files (~/.zshrc, ~/.bashrc, etc.)
Desktop app doesn't inherit shell env — this covers users who
set MEM0_API_KEY in their profile but use the Desktop app.
User ID resolution:
1. MEM0_USER_ID env var (explicit override)
2. $USER, else "default"
Settings resolution:
~/.mem0/settings.json (user-editable, falls back to defaults)
"""
from __future__ import annotations
import os
import re
from pathlib import Path
def _extract_key_from_shell_profiles() -> str:
"""Extract MEM0_API_KEY from shell profile files.
The Desktop app only reads PATH from shell profiles — env vars like
MEM0_API_KEY are not inherited. This handles the common
``export MEM0_API_KEY=...`` pattern without sourcing the full profile.
"""
profiles = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".profile"]
pattern = re.compile(r'^\s*(?:export\s+)?MEM0_API_KEY=(.+)$')
for name in profiles:
path = Path.home() / name
if not path.is_file():
continue
try:
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
m = pattern.match(line)
if not m:
continue
value = m.group(1).strip()
value = re.sub(r'#.*$', '', value).strip()
value = value.strip("\"'")
if value and not value.startswith("$"):
return value
except OSError:
continue
return ""
def resolve_api_key() -> str:
key = os.environ.get("MEM0_API_KEY", "").strip()
if key:
return key
key = os.environ.get("CLAUDE_PLUGIN_OPTION_API_KEY", "").strip()
if key:
return key
key = os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "").strip()
if key:
return key
key = _extract_key_from_shell_profiles()
if key:
return key
return ""
def resolve_user_id() -> str:
explicit = os.environ.get("MEM0_USER_ID", "").strip()
if explicit:
return explicit
return os.environ.get("USER") or "default"
def resolve_config() -> dict:
"""Resolve settings from ~/.mem0/settings.json (primary) with env var overrides."""
try:
from load_settings import load_settings
return load_settings()
except ImportError:
return {
"auto_save": True,
"auto_search": True,
"search_limit": 10,
"retention_session_days": 90,
"confidence_threshold": 0.3,
"debug": False,
}
try:
from _project import resolve_branch, resolve_project_id, save_project_mapping
except ImportError:
def resolve_project_id(cwd: str | None = None) -> str:
return os.path.basename(cwd or os.getcwd())
def resolve_branch(cwd: str | None = None) -> str:
return "unknown"
def save_project_mapping(cwd: str, project_id: str) -> None:
pass
@@ -0,0 +1,87 @@
# Source this file. Sets MEM0_API_KEY, MEM0_RESOLVED_USER_ID, and settings.
#
# API key resolution (first non-empty wins):
# 1. MEM0_API_KEY env var (explicit / shell profile)
# 2. CLAUDE_PLUGIN_OPTION_API_KEY (injected by Claude Code userConfig)
# 3. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (legacy userConfig)
# 4. Extract from shell profile files (~/.zshrc, ~/.bashrc, etc.)
# Desktop app doesn't inherit shell env — this fallback covers users
# who set MEM0_API_KEY in their profile but use the Desktop app.
#
# Settings: ~/.mem0/settings.json (user-editable, falls back to defaults)
_SCRIPT_DIR="$( cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd )"
# Resolve API key: env var > userConfig > shell profile extraction
if [ -z "${MEM0_API_KEY:-}" ] && [ -n "${CLAUDE_PLUGIN_OPTION_API_KEY:-}" ]; then
MEM0_API_KEY="$CLAUDE_PLUGIN_OPTION_API_KEY"
export MEM0_API_KEY
fi
if [ -z "${MEM0_API_KEY:-}" ] && [ -n "${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-}" ]; then
MEM0_API_KEY="$CLAUDE_PLUGIN_OPTION_MEM0_API_KEY"
export MEM0_API_KEY
fi
# Fallback: extract MEM0_API_KEY from shell profile files.
# The Desktop app only reads PATH from shell profiles — env vars like
# MEM0_API_KEY are not inherited. This grep-based extraction handles
# the common `export MEM0_API_KEY=...` pattern without sourcing the
# full profile (which could have side effects).
if [ -z "${MEM0_API_KEY:-}" ]; then
for _profile in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.zprofile" "$HOME/.bash_profile" "$HOME/.profile"; do
if [ -f "$_profile" ]; then
_extracted=$(grep -E '^\s*(export\s+)?MEM0_API_KEY=' "$_profile" 2>/dev/null \
| tail -1 \
| sed 's/^[^=]*=//' \
| sed "s/^[\"']//;s/[\"']$//" \
| sed 's/#.*//' \
| tr -d '[:space:]')
# Only use literal values — skip variable references like ${OTHER_VAR}
if [ -n "$_extracted" ] && [ "${_extracted#\$}" = "$_extracted" ]; then
MEM0_API_KEY="$_extracted"
export MEM0_API_KEY
break
fi
fi
done
fi
_mem0_resolve_identity() {
if [ -n "${MEM0_USER_ID:-}" ]; then
printf '%s' "$MEM0_USER_ID"
return
fi
printf '%s' "${USER:-default}"
}
MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)"
export MEM0_RESOLVED_USER_ID
_MEM0_IDENTITY_ANNOTATION=""
if [ -n "${MEM0_USER_ID:-}" ] && [ "$MEM0_USER_ID" != "${USER:-default}" ]; then
_MEM0_IDENTITY_ANNOTATION=" (override; default: ${USER:-default})"
fi
export _MEM0_IDENTITY_ANNOTATION
# Load settings from ~/.mem0/settings.json
if command -v python3 >/dev/null 2>&1; then
_SETTINGS_JSON=$(PYTHONPATH="$_SCRIPT_DIR" python3 -c "from load_settings import load_settings; import json; print(json.dumps(load_settings()))" 2>/dev/null || echo "{}")
MEM0_AUTO_SAVE=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(str(json.load(sys.stdin).get('auto_save',True)).lower())" 2>/dev/null || echo "true")
MEM0_AUTO_SEARCH=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(str(json.load(sys.stdin).get('auto_search',True)).lower())" 2>/dev/null || echo "true")
MEM0_SEARCH_LIMIT=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('search_limit',10))" 2>/dev/null || echo "10")
MEM0_RETENTION_SESSION_DAYS=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('retention_session_days',90))" 2>/dev/null || echo "90")
MEM0_CONFIDENCE_THRESHOLD=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('confidence_threshold',0.3))" 2>/dev/null || echo "0.3")
MEM0_GLOBAL_SEARCH=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(str(json.load(sys.stdin).get('global_search',False)).lower())" 2>/dev/null || echo "false")
MEM0_DEBUG=$(echo "$_SETTINGS_JSON" | python3 -c "import sys,json; print(str(json.load(sys.stdin).get('debug',False)).lower())" 2>/dev/null || echo "false")
else
MEM0_AUTO_SAVE="true"
MEM0_AUTO_SEARCH="true"
MEM0_SEARCH_LIMIT="10"
MEM0_RETENTION_SESSION_DAYS="90"
MEM0_CONFIDENCE_THRESHOLD="0.3"
MEM0_GLOBAL_SEARCH="false"
MEM0_DEBUG="false"
fi
export MEM0_AUTO_SAVE MEM0_AUTO_SEARCH MEM0_SEARCH_LIMIT MEM0_RETENTION_SESSION_DAYS MEM0_CONFIDENCE_THRESHOLD MEM0_GLOBAL_SEARCH MEM0_DEBUG
# Also resolve project context
. "$_SCRIPT_DIR/_project.sh"
@@ -0,0 +1,175 @@
"""Resolve mem0 project_id and branch.
Resolution priority (project_id):
1. MEM0_PROJECT_ID env var (explicit override)
2. ~/.mem0/project_map.json lookup by cwd
2b. ~/.mem0/project_map.json lookup by remote hash (self-healing fallback)
3. Git remote slug: strip protocol/prefix, strip .git, replace / and : with -
e.g. git@github.com:mem0ai/mem0.git -> mem0ai-mem0
4. Fallback: basename of cwd
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import subprocess
def resolve_project_id(cwd: str | None = None) -> str:
if cwd is None:
cwd = os.getcwd()
# 1. Explicit override
explicit = os.environ.get("MEM0_PROJECT_ID", "").strip()
if explicit:
return explicit
# 2. project_map.json lookup
map_path = os.path.expanduser("~/.mem0/project_map.json")
if os.path.isfile(map_path):
try:
with open(map_path) as f:
project_map = json.load(f)
mapped = project_map.get(cwd, "").strip()
if mapped:
return mapped
# 2b. Remote hash fallback (self-healing when folder is moved/renamed)
remote_key = _remote_hash_key(cwd)
if remote_key:
mapped = project_map.get(remote_key, "").strip()
if mapped:
# Self-heal: write the new CWD key so future lookups are fast
project_map[cwd] = mapped
try:
with open(map_path, "w") as f:
json.dump(project_map, f, indent=2)
except OSError:
pass
return mapped
except (OSError, json.JSONDecodeError, AttributeError):
pass
# 3. Git remote slug
try:
result = subprocess.run(
["git", "remote", "get-url", "origin"],
capture_output=True,
text=True,
check=True,
cwd=cwd,
)
remote_url = result.stdout.strip()
if remote_url:
slug = _remote_url_to_slug(remote_url)
if slug:
return slug
except (subprocess.CalledProcessError, OSError):
pass
# 4. Fallback: basename of cwd
return os.path.basename(cwd) or "unknown"
def resolve_branch(cwd: str | None = None) -> str:
if cwd is None:
cwd = os.getcwd()
try:
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True,
text=True,
check=True,
cwd=cwd,
)
branch = result.stdout.strip()
return branch if branch else "unknown"
except (subprocess.CalledProcessError, OSError):
return "unknown"
def save_project_mapping(cwd: str, project_id: str) -> None:
"""Write cwd -> project_id (and remote hash key -> project_id) into ~/.mem0/project_map.json."""
mem0_dir = os.path.expanduser("~/.mem0")
os.makedirs(mem0_dir, exist_ok=True)
map_path = os.path.join(mem0_dir, "project_map.json")
project_map: dict[str, str] = {}
if os.path.isfile(map_path):
try:
with open(map_path) as f:
project_map = json.load(f)
except (OSError, json.JSONDecodeError):
project_map = {}
project_map[cwd] = project_id
# Also write the remote hash key so the mapping survives folder moves/renames
remote_key = _remote_hash_key(cwd)
if remote_key:
project_map[remote_key] = project_id
with open(map_path, "w") as f:
json.dump(project_map, f, indent=2)
def _remote_hash_key(cwd: str | None = None) -> str:
"""Return a stable key derived from the git remote URL.
Runs ``git config --get remote.origin.url`` in *cwd* and returns a string
of the form ``remote:<sha256(url)[:16]>``. Returns an empty string when
the directory is not a git repo or has no remote configured.
"""
if cwd is None:
cwd = os.getcwd()
try:
result = subprocess.run(
["git", "config", "--get", "remote.origin.url"],
capture_output=True,
text=True,
check=True,
cwd=cwd,
)
url = result.stdout.strip()
if not url:
return ""
digest = hashlib.sha256(url.encode()).hexdigest()[:16]
return f"remote:{digest}"
except (subprocess.CalledProcessError, OSError):
return ""
def _remote_url_to_slug(url: str) -> str:
"""Convert a git remote URL to a deterministic slug.
Handles:
- HTTPS: https://github.com/owner/repo.git
- SSH: git@github.com:owner/repo.git
- SSH: git@github.com-alias:owner/repo.git (custom host aliases)
- ssh://: ssh://git@github.com/owner/repo.git
- git://: git://github.com/owner/repo.git
"""
slug = url.strip()
# Strip .git suffix
if slug.endswith(".git"):
slug = slug[:-4]
# Strip protocol prefixes
for prefix in ("https://", "http://", "ssh://", "git://"):
if slug.startswith(prefix):
slug = slug[len(prefix):]
break
else:
# Handle git@ style (no protocol prefix matched)
slug = re.sub(r"^git@", "", slug)
# Replace the first colon (SSH host:path separator) with /
slug = slug.replace(":", "/", 1)
# Split on / and take last two components (owner, repo)
parts = [p for p in slug.split("/") if p]
if len(parts) >= 2:
owner, repo = parts[-2], parts[-1]
slug = f"{owner}-{repo}"
elif parts:
slug = parts[-1]
else:
return ""
# Replace any remaining / and : with -
slug = slug.replace("/", "-").replace(":", "-")
return slug
+77
View File
@@ -0,0 +1,77 @@
# Source this file. Sets MEM0_PROJECT_ID and MEM0_BRANCH.
#
# Resolution priority (project_id):
# 1. MEM0_PROJECT_ID env var (explicit override)
# 2. ~/.mem0/project_map.json lookup by $PWD (requires jq)
# 3. Git remote slug: strip protocol/prefix, strip .git, replace / and : with -
# e.g. git@github.com:mem0ai/mem0.git -> mem0ai-mem0
# 4. Fallback: basename of $PWD
#
# Branch resolution:
# git branch --show-current, fallback "unknown"
_mem0_resolve_project_id() {
# 1. Explicit override
if [ -n "${MEM0_PROJECT_ID:-}" ]; then
printf '%s' "$MEM0_PROJECT_ID"
return
fi
# 2. project_map.json lookup by $PWD
_mem0_map="$HOME/.mem0/project_map.json"
if [ -f "$_mem0_map" ] && command -v jq >/dev/null 2>&1; then
_mem0_mapped=$(jq -r --arg cwd "$PWD" '.[$cwd] // empty' "$_mem0_map" 2>/dev/null)
if [ -n "$_mem0_mapped" ]; then
printf '%s' "$_mem0_mapped"
return
fi
fi
# 3. Git remote slug
_mem0_remote_url=$(git remote get-url origin 2>/dev/null)
if [ -n "$_mem0_remote_url" ]; then
_mem0_slug="$_mem0_remote_url"
# Strip .git suffix
_mem0_slug="${_mem0_slug%.git}"
# Strip protocol prefixes
_mem0_slug="${_mem0_slug#https://}"
_mem0_slug="${_mem0_slug#http://}"
_mem0_slug="${_mem0_slug#ssh://}"
_mem0_slug="${_mem0_slug#git://}"
_mem0_slug="${_mem0_slug#git@}"
# Replace first colon (SSH host:path separator) with /
# shellcheck disable=SC2039
_mem0_slug="${_mem0_slug/://}"
# Keep only the last two path components (owner/repo)
_mem0_owner=$(printf '%s' "$_mem0_slug" | awk -F'/' '{print $(NF-1)}')
_mem0_repo=$(printf '%s' "$_mem0_slug" | awk -F'/' '{print $NF}')
_mem0_slug="${_mem0_owner}-${_mem0_repo}"
# Replace any remaining / and : with -
# shellcheck disable=SC2039
_mem0_slug="${_mem0_slug//\//-}"
# shellcheck disable=SC2039
_mem0_slug="${_mem0_slug//:/-}"
if [ -n "$_mem0_slug" ]; then
printf '%s' "$_mem0_slug"
_MEM0_PERSIST_CWD="$PWD" _MEM0_PERSIST_SLUG="$_mem0_slug" python3 -c "
import os, sys
sys.path.insert(0, '$(dirname "${BASH_SOURCE[0]:-$0}")')
from _project import save_project_mapping
save_project_mapping(os.environ['_MEM0_PERSIST_CWD'], os.environ['_MEM0_PERSIST_SLUG'])
" 2>/dev/null || true
return
fi
fi
# 4. Fallback: basename of $PWD
printf '%s' "$(basename "$PWD")"
}
_mem0_resolve_branch() {
git branch --show-current 2>/dev/null || printf 'unknown'
}
MEM0_PROJECT_ID="$(_mem0_resolve_project_id)"
MEM0_BRANCH="$(_mem0_resolve_branch)"
export MEM0_PROJECT_ID
export MEM0_BRANCH
@@ -0,0 +1,84 @@
"""Shared mem0 search API helper.
Wraps POST /v3/memories/search/ into a single function call.
All pre-fetch hooks use this instead of duplicating urllib boilerplate.
"""
from __future__ import annotations
import json
import urllib.request
SEARCH_URL = "https://api.mem0.ai/v3/memories/search/"
SEARCH_TIMEOUT = 5
def _do_search(api_key: str, payload: dict) -> list[dict]:
body = json.dumps(payload).encode()
req = urllib.request.Request(
SEARCH_URL,
data=body,
headers={"Authorization": f"Token {api_key}", "Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=SEARCH_TIMEOUT) as r:
data = json.loads(r.read())
return data if isinstance(data, list) else data.get("results", [])
def search_memories(
api_key: str,
user_id: str,
project_id: str,
query: str,
metadata_type: str | None = None,
metadata_filters: dict | None = None,
top_k: int = 3,
min_score: float = 0.0,
rerank: bool = False,
threshold: float = 0.3,
global_search: bool = False,
) -> list[dict]:
if not api_key:
return []
if global_search:
filters: dict = {"OR": [{"user_id": "*"}]}
else:
base_clauses: list[dict] = [{"user_id": user_id}, {"app_id": project_id}]
if metadata_type:
base_clauses.append({"metadata": {"type": metadata_type}})
if metadata_filters:
for key, value in metadata_filters.items():
base_clauses.append({"metadata": {key: value}})
filters = {"AND": base_clauses}
base_payload: dict = {"query": query, "top_k": top_k, "threshold": threshold}
if rerank:
base_payload["rerank"] = True
try:
payload = {**base_payload, "filters": filters}
results = _do_search(api_key, payload)[:top_k]
if min_score > 0:
results = [m for m in results if m.get("score", 0) >= min_score]
return results
except Exception:
return []
def format_results_for_context(
memories: list[dict],
heading: str = "Relevant memories",
) -> str:
if not memories:
return ""
lines = [f"### {heading}", ""]
for m in memories:
mid = m.get("id", "?")[:8]
text = m.get("memory", "")[:200]
cat = (m.get("metadata") or {}).get("type", "unknown")
lines.append(f"- [{cat}] {text} [mem0:{mid}]")
lines.append("")
return "\n".join(lines)
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
"""Auto-capture recent conversation exchanges into mem0.
Runs in the background from UserPromptSubmit hook (every 3rd message).
Reads the last few exchanges from the transcript, sends them to the
mem0 API with infer=True so the platform extracts facts automatically.
Input: env vars (MEM0_API_KEY, MEM0_RESOLVED_USER_ID, MEM0_PROJECT_ID, etc.)
argv[1] = transcript_path
Output: stderr logs only (exit 0 always — must not block)
"""
from __future__ import annotations
import json
import logging
import os
import sys
import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-auto-capture")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-auto-capture] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_fh = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_fh.setFormatter(logging.Formatter("[mem0-auto-capture] %(asctime)s %(message)s"))
log.addHandler(_fh)
except OSError:
pass
API_URL = "https://api.mem0.ai"
TAIL_LINES = 200
MAX_CONTENT_CHARS = 8000
MIN_CONTENT_CHARS = 100
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def extract_recent_exchanges(lines: list[str], max_exchanges: int = 3) -> list[dict]:
"""Extract the last N user+assistant message pairs from the transcript JSONL."""
messages = []
for line in lines:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("isCompactSummary"):
continue
msg = entry.get("message", {})
role = msg.get("role", "")
if role not in ("user", "assistant"):
continue
content = msg.get("content", "")
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
content = "\n".join(parts).strip()
if not content or len(content) < 20:
continue
# Skip tool-call-only assistant messages
if role == "assistant" and content.startswith("{"):
continue
messages.append({"role": role, "content": content[:2000]})
# Take last N exchanges (pairs of user+assistant)
if not messages:
return []
result = messages[-(max_exchanges * 2):]
return result
def store_exchange(api_key: str, messages: list[dict], user_id: str,
project_id: str, branch: str, session_id: str) -> bool:
metadata = {
"type": "auto_capture",
"source": "auto_capture",
"confidence": 0.7,
}
if branch:
metadata["branch"] = branch
if session_id:
metadata["session_id"] = session_id
body = {
"messages": messages,
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
"infer": True,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
result = json.loads(resp.read())
log.info("Auto-captured: event_id=%s status=%s",
result.get("event_id", "?"), result.get("status", "?"))
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def main():
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping")
return
if len(sys.argv) < 2:
log.debug("No transcript_path argument")
return
transcript_path = sys.argv[1]
if not transcript_path or not os.path.isfile(transcript_path):
log.debug("Transcript not found: %s", transcript_path)
return
user_id = resolve_user_id()
project_id = resolve_project_id()
branch = resolve_branch()
session_id = ""
sid_file = f"/tmp/mem0_session_id_{os.environ.get('USER', 'default')}"
if os.path.isfile(sid_file):
try:
with open(sid_file) as f:
session_id = f.read().strip()
except OSError:
pass
lines = tail_lines(transcript_path, TAIL_LINES)
if not lines:
log.debug("Transcript empty")
return
messages = extract_recent_exchanges(lines, max_exchanges=4)
if not messages:
log.debug("No substantial exchanges found")
return
total_chars = sum(len(m["content"]) for m in messages)
if total_chars < MIN_CONTENT_CHARS:
log.debug("Exchanges too short (%d chars), skipping", total_chars)
return
log.info("Auto-capturing %d messages (%d chars)", len(messages), total_chars)
if store_exchange(api_key, messages, user_id, project_id, branch, session_id):
try:
import session_stats
session_stats.record_add("auto_capture")
except Exception:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
@@ -0,0 +1,374 @@
#!/usr/bin/env python3
"""Auto-import declarative project files into mem0.
Runs in the background from the SessionStart hook (startup only).
Imports CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md
into mem0 as project profile memories, skipping unchanged files via
SHA-256 hashing.
Input: MEM0_CWD env var (optional, defaults to os.getcwd())
Output: stderr logs only (exit 0 always — must not block)
"""
from __future__ import annotations
import hashlib
import json
import logging
import os
import sys
import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _chunking import filter_and_truncate, split_by_headers
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id, save_project_mapping
log = logging.getLogger("mem0-auto-import")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-auto-import] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-auto-import] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_FILE_SIZE = 100_000 # skip files over 100 KB
TARGET_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules", "mem0.md"]
HASH_STORE = os.path.expanduser("~/.mem0/file_hashes.json")
LOCK_FILE = os.path.expanduser("~/.mem0/auto_import.lock")
def _acquire_lock() -> bool:
"""Try to acquire a file lock. Returns False if another instance is running."""
try:
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
os.close(fd)
return True
except FileExistsError:
try:
mtime = os.path.getmtime(LOCK_FILE)
import time
if time.time() - mtime > 120:
os.unlink(LOCK_FILE)
return _acquire_lock()
except OSError:
pass
return False
def _release_lock() -> None:
try:
os.unlink(LOCK_FILE)
except OSError:
pass
def _git_root(cwd: str) -> str:
"""Return the git repo root, or empty string if not in a repo."""
import subprocess
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=cwd, capture_output=True, text=True, timeout=5,
)
if result.returncode == 0:
return result.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
pass
return ""
def sha256_file(path: str) -> str:
"""Return the hex SHA-256 digest of a file."""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def load_hashes() -> dict[str, str]:
"""Load the hash store from disk; return empty dict on any error."""
if not os.path.isfile(HASH_STORE):
return {}
try:
with open(HASH_STORE) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return {}
def save_hashes(hashes: dict[str, str]) -> None:
"""Persist the hash store to disk."""
mem0_dir = os.path.expanduser("~/.mem0")
os.makedirs(mem0_dir, exist_ok=True)
try:
with open(HASH_STORE, "w") as f:
json.dump(hashes, f, indent=2)
except OSError as e:
log.warning("Could not save hash store: %s", e)
def already_imported(api_key: str, user_id: str, project_id: str, filename: str) -> bool:
body = json.dumps({
"query": filename,
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 10,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
results = data if isinstance(data, list) else data.get("results", [])
for result in results:
meta = result.get("metadata", {}) if isinstance(result, dict) else {}
file_field = meta.get("file", "")
if file_field == filename or file_field.startswith(f"{filename}["):
return True
return False
except Exception:
return False
def _delete_stale_chunks(api_key: str, user_id: str, project_id: str, filename: str) -> int:
"""Find and delete existing chunks for a file before re-import. Returns count deleted."""
body = json.dumps({
"query": filename,
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 20,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
ids_to_delete = []
try:
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
results = data if isinstance(data, list) else data.get("results", [])
for result in results:
if not isinstance(result, dict):
continue
meta = result.get("metadata", {})
file_field = meta.get("file", "")
if file_field == filename or file_field.startswith(f"{filename}["):
mid = result.get("id")
if mid:
ids_to_delete.append(mid)
except Exception as e:
log.warning("Failed to search for stale chunks of %s: %s", filename, e)
return 0
deleted = 0
for mid in ids_to_delete:
try:
del_req = urllib.request.Request(
f"{API_URL}/v1/memories/{mid}/",
headers={"Authorization": f"Token {api_key}"},
method="DELETE",
)
with urllib.request.urlopen(del_req, timeout=10):
deleted += 1
except Exception as e:
log.warning("Failed to delete stale chunk %s: %s", mid, e)
if deleted:
log.info("Deleted %d stale chunk(s) for %s before re-import", deleted, filename)
return deleted
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str, branch: str = "") -> bool:
"""POST a project profile memory to the Mem0 REST API."""
metadata = {
"type": "project_profile",
"file": filename,
"source": "auto-import",
}
if branch:
metadata["branch"] = branch
body = {
"messages": [
{
"role": "user",
"content": f"## Project Profile: {filename}\n\nProject: {project_id}\n\n{content}",
}
],
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
"infer": False,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Imported %s (project=%s)", filename, project_id)
return True
log.warning("API returned status %d for %s", resp.status, filename)
return False
except urllib.error.URLError as e:
log.warning("API call failed for %s: %s", filename, e)
return False
def main() -> None:
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping auto-import")
return
cwd = os.environ.get("MEM0_CWD", "").strip() or os.getcwd()
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
save_project_mapping(cwd, project_id)
git_root = _git_root(cwd)
search_dirs = [cwd]
if git_root and os.path.realpath(git_root) != os.path.realpath(cwd):
search_dirs.append(git_root)
log.debug("Auto-import started: cwd=%s git_root=%s project=%s user=%s branch=%s", cwd, git_root or "(none)", project_id, user_id, branch)
hashes = load_hashes()
updated = False
seen_content_hashes: set[str] = set()
for filename in TARGET_FILES:
filepath = ""
for search_dir in search_dirs:
candidate = os.path.join(search_dir, filename)
if os.path.isfile(candidate):
filepath = candidate
break
if not filepath:
log.debug("Not found, skipping: %s", filename)
continue
filepath = os.path.realpath(filepath)
try:
file_size = os.path.getsize(filepath)
except OSError:
log.debug("Cannot stat %s, skipping", filename)
continue
if file_size > MAX_FILE_SIZE:
log.debug("Skipping %s: size %d exceeds %d bytes", filename, file_size, MAX_FILE_SIZE)
continue
try:
current_hash = sha256_file(filepath)
except OSError as e:
log.debug("Cannot hash %s: %s", filename, e)
continue
if current_hash in seen_content_hashes:
log.debug("Duplicate content, skipping: %s (same as earlier file)", filename)
continue
seen_content_hashes.add(current_hash)
hash_key = f"{project_id}:{branch}:{filename}" if branch else f"{project_id}:{filename}"
if hashes.get(hash_key) == current_hash:
if already_imported(api_key, user_id, project_id, filename):
log.debug("Unchanged and still in mem0, skipping: %s", filename)
continue
log.info("Hash matches but memories missing server-side, re-importing: %s", filename)
elif already_imported(api_key, user_id, project_id, filename):
log.debug("Already in mem0, updating hash store: %s", filename)
hashes[hash_key] = current_hash
updated = True
continue
try:
with open(filepath, encoding="utf-8", errors="replace") as f:
content = f.read()
except OSError as e:
log.debug("Cannot read %s: %s", filename, e)
continue
_delete_stale_chunks(api_key, user_id, project_id, filename)
is_markdown = filename.endswith(".md")
if is_markdown:
chunks = filter_and_truncate(split_by_headers(content))
else:
chunks = filter_and_truncate([content])
if not chunks:
chunks = [content[:10000]]
success = True
for i, chunk in enumerate(chunks):
chunk_name = f"{filename}[{i+1}/{len(chunks)}]" if len(chunks) > 1 else filename
if not post_memory(api_key, chunk, user_id, chunk_name, project_id, branch):
success = False
if success:
hashes[hash_key] = current_hash
updated = True
if updated:
save_hashes(hashes)
else:
log.debug("No files imported this run")
if __name__ == "__main__":
if not _acquire_lock():
log.debug("Another auto_import instance is running — skipping")
sys.exit(0)
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
finally:
_release_lock()
sys.exit(0)
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Auto-configure mem0's coding-category taxonomy in the background.
Runs from the SessionStart hook (startup only), exactly like auto_import.py.
mem0 auto-tags every memory with `categories`; by default that list is
consumer-oriented (food, hobbies, ...), which is useless for code. This script
replaces it with the coding-focused taxonomy defined in
``setup_coding_categories.CODING_CATEGORIES`` so search and retrieval are tuned
for development work — without the user ever being asked during onboarding.
Design (mirrors auto_import.py):
- Resolve the API key; do nothing if it is absent.
- Gate on a state file (``~/.mem0/categories_setup.json``) keyed by a hash of
the API key -> a hash of the taxonomy. Categories are scoped to the mem0
*project* tied to the API key (NOT to the local repo), so this only needs to
run once per account, and re-runs only if the taxonomy itself changes.
- Hold a lock file so concurrent sessions don't race.
- Reuse the proven SDK path (``client.project.update``) via the plugin venv.
- Always exit 0; log to stderr only. Must never block a session.
Run with no arguments (background) or in the foreground for onboarding to print
a parseable status line.
Requires MEM0_API_KEY (or CLAUDE_PLUGIN_OPTION_API_KEY) and the mem0ai SDK,
which ensure_deps.sh installs into ${CLAUDE_PLUGIN_DATA}/venv on session start.
"""
from __future__ import annotations
import hashlib
import json
import logging
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Importing setup_coding_categories also injects the plugin venv's site-packages
# onto sys.path (its module-level bootstrap), so ``from mem0 import MemoryClient``
# works even when this script is run with the system python3.
from _identity import resolve_api_key # noqa: E402
from setup_coding_categories import CODING_CATEGORIES, _categories_match # noqa: E402
log = logging.getLogger("mem0-auto-categories")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-auto-categories] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-auto-categories] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
STATE_FILE = os.path.expanduser("~/.mem0/categories_setup.json")
LOCK_FILE = os.path.expanduser("~/.mem0/categories_setup.lock")
# --------------------------------------------------------------------------- #
# Fingerprints #
# --------------------------------------------------------------------------- #
def categories_fingerprint(categories: list = CODING_CATEGORIES) -> str:
"""Stable, order-independent 16-hex digest of the category taxonomy.
Reordering the categories yields the same fingerprint; adding, removing, or
editing a category changes it (so the taxonomy re-applies on upgrade).
"""
pairs = sorted(
(str(key), str(value))
for entry in categories
if isinstance(entry, dict)
for key, value in entry.items()
)
payload = json.dumps(pairs, sort_keys=True, ensure_ascii=False)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
def apikey_fingerprint(api_key: str) -> str:
"""Opaque 16-hex digest of the API key. Never stores the key itself."""
return hashlib.sha256(api_key.encode("utf-8")).hexdigest()[:16]
# --------------------------------------------------------------------------- #
# State file #
# --------------------------------------------------------------------------- #
def load_state(path: str = STATE_FILE) -> dict:
"""Load the apikey-fingerprint -> categories-fingerprint map; {} on any error."""
if not os.path.isfile(path):
return {}
try:
with open(path) as f:
data = json.load(f)
return data if isinstance(data, dict) else {}
except (OSError, json.JSONDecodeError):
return {}
def save_state(state: dict, path: str = STATE_FILE) -> None:
"""Persist the state map, creating the parent directory if needed."""
parent = os.path.dirname(path)
if parent:
try:
os.makedirs(parent, exist_ok=True)
except OSError as e:
log.warning("Could not create state dir: %s", e)
return
try:
with open(path, "w") as f:
json.dump(state, f, indent=2)
except OSError as e:
log.warning("Could not save categories state: %s", e)
def is_applied(state: dict, key_fp: str, cat_fp: str) -> bool:
"""True only when this API key has already had this exact taxonomy applied."""
return state.get(key_fp) == cat_fp
# --------------------------------------------------------------------------- #
# SDK interaction (client injected for testability) #
# --------------------------------------------------------------------------- #
def make_client():
"""Construct a MemoryClient. Imported lazily so this module loads without the SDK."""
from mem0 import MemoryClient
return MemoryClient()
def fetch_current_categories(client) -> list | None:
"""Return the project's current custom_categories, or None if unavailable."""
current = client.project.get(fields=["custom_categories"])
if isinstance(current, dict):
return current.get("custom_categories")
return None
def apply_categories(client, proposed: list = CODING_CATEGORIES) -> str:
"""Install the coding taxonomy if it isn't already in place.
Returns "already-configured" when the project already matches (no write), or
"applied" after a successful ``project.update``. Raises on API failure.
"""
current = fetch_current_categories(client)
if _categories_match(current, proposed):
return "already-configured"
client.project.update(custom_categories=proposed)
return "applied"
# --------------------------------------------------------------------------- #
# Lock (mirrors auto_import.py) #
# --------------------------------------------------------------------------- #
def _acquire_lock() -> bool:
"""Try to acquire a file lock. Returns False if another instance is running."""
try:
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
os.close(fd)
return True
except FileExistsError:
try:
if time.time() - os.path.getmtime(LOCK_FILE) > 120:
os.unlink(LOCK_FILE)
return _acquire_lock()
except OSError:
pass
return False
def _release_lock() -> None:
try:
os.unlink(LOCK_FILE)
except OSError:
pass
# --------------------------------------------------------------------------- #
# Entry point #
# --------------------------------------------------------------------------- #
def main() -> None:
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping coding-categories setup")
return
key_fp = apikey_fingerprint(api_key)
cat_fp = categories_fingerprint()
state = load_state()
if is_applied(state, key_fp, cat_fp):
log.debug("Coding categories already configured for this account (cached); skipping")
return
os.environ["MEM0_API_KEY"] = api_key
try:
client = make_client()
except ImportError:
log.debug("mem0ai SDK not ready yet (venv installing?); will retry next session")
return
except Exception as e:
log.warning("Could not initialise MemoryClient: %s", e)
return
try:
result = apply_categories(client)
except Exception as e:
log.warning("Could not configure coding categories: %s", e)
return
state[key_fp] = cat_fp
save_state(state)
if result == "applied":
log.info("Applied %d coding categories", len(CODING_CATEGORIES))
else:
log.info("Coding categories already configured")
if __name__ == "__main__":
if not _acquire_lock():
log.debug("Another auto_setup_categories instance is running — skipping")
sys.exit(0)
try:
main()
except Exception as e: # never block a session
log.error("Unexpected error: %s", e)
finally:
_release_lock()
sys.exit(0)
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Hook: PreToolUse (matcher: Write|Edit)
#
# Blocks writes to MEMORY.md and auto-memory files, redirecting Claude
# to use the mem0 MCP add_memory tool instead.
#
# Input: JSON on stdin with tool_name, tool_input
# Output: stderr message (exit 2 = block)
#
# Exit codes:
# 0 = allow the tool call
# 2 = block the tool call (stderr is shown to Claude as feedback)
set -euo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
exit 0
fi
case "$FILE_PATH" in
*/.claude/*/MEMORY.md|*/.claude/memory/*)
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
exit 2
;;
*)
exit 0
;;
esac
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Hook: preToolUse (Cursor) — blocks writes to MEMORY.md
#
# Cursor variant of block_memory_write.sh. Returns JSON:
# {"permission":"deny","agent_message":"..."} on block,
# {"permission":"allow"} on pass.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
jq -cn '{permission:"allow"}'
exit 0
fi
case "$FILE_PATH" in
*/MEMORY.md|*/.claude/memory/*|*/.cursor/memory/*)
jq -cn --arg msg "Do not write to $FILE_PATH. Use the mem0 MCP add_memory tool instead to persist memories." \
'{permission:"deny", agent_message:$msg}'
exit 0
;;
*)
jq -cn '{permission:"allow"}'
exit 0
;;
esac
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""Capture the post-compaction summary into mem0.
PreCompact hooks fire BEFORE the summary is generated, so they can't
store the actual compact-summary text. This script runs at
SessionStart with source=compact, reads the transcript, finds the
most recent entry flagged isCompactSummary=true, and stores it as a
memory tagged metadata.type=compact_summary.
Input: JSON on stdin with transcript_path, session_id, source
Output: stderr logs only (exit 0 always -- must not block)
Spawned in the background by on_session_start.sh; the user-facing
bootstrap text continues without waiting on the network.
"""
from __future__ import annotations
import json
import logging
import os
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-compact-summary")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-compact-summary] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 2000
MAX_SUMMARY_CHARS = 50000
# Compact summaries describe a single session's state -- stale after a quarter.
COMPACT_SUMMARY_EXPIRY_DAYS = 90
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def find_compact_summary(lines: list[str]) -> str:
"""Walk transcript backwards, return text content of the most recent
entry flagged isCompactSummary=true. Empty string if none found."""
for line in reversed(lines):
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if not entry.get("isCompactSummary"):
continue
message = entry.get("message", {})
content = message.get("content", [])
if isinstance(content, str):
return content[:MAX_SUMMARY_CHARS]
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
return "\n".join(parts).strip()[:MAX_SUMMARY_CHARS]
return ""
def store_summary(api_key: str, summary: str, user_id: str, session_id: str, project_id: str = "", branch: str = "") -> bool:
expires = (date.today() + timedelta(days=COMPACT_SUMMARY_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "compact_summary",
"source": "session-start-compact",
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
body = {
"messages": [{"role": "user", "content": summary}],
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
"infer": True,
"expiration_date": expires,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Compact summary stored")
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def main():
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
return
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
log.debug("No valid JSON on stdin")
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
log.debug("No transcript_path provided")
return
session_id = hook_input.get("session_id", "")
cwd = hook_input.get("cwd") or None
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
summary = find_compact_summary(lines)
if not summary:
log.debug("No isCompactSummary entry found")
return
if len(summary.strip()) < 100:
log.debug("Compact summary too short (%d chars) — skipping", len(summary.strip()))
return
marker_dir = os.path.expanduser("~/.mem0")
marker_file = os.path.join(marker_dir, f"compact_captured_{session_id}")
if session_id and os.path.isfile(marker_file):
log.info("Compact summary already captured for session %s — skipping", session_id)
return
log.info("Capturing compact summary (%d chars)", len(summary))
if store_summary(api_key, summary, user_id, session_id, project_id, branch):
if session_id:
try:
os.makedirs(marker_dir, exist_ok=True)
with open(marker_file, "w") as f:
f.write("")
except OSError:
pass
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
@@ -0,0 +1,264 @@
#!/usr/bin/env python3
"""Capture a structured session summary on Stop hook.
Runs on every Stop (end of each assistant turn). Each invocation reads
the transcript JSONL, extracts the latest assistant message and all
files touched so far, then stores via mem0 API with infer=True. Uses
run_id=session_id to scope infer dedup to the session, so the final
stored summary reflects the most recent turn — not just the first.
Input: JSON on stdin with transcript_path, session_id, cwd, agent_id
Output: stderr logs only (exit 0 always — must not block)
"""
from __future__ import annotations
import json
import logging
import os
import re
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-session-summary")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-session-summary] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_fh = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_fh.setFormatter(logging.Formatter("[mem0-session-summary] %(asctime)s %(message)s"))
log.addHandler(_fh)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 3000
MAX_SUMMARY_CHARS = 50000
SUMMARY_EXPIRY_DAYS = 90
SYSTEM_TAG_RE = re.compile(
r"<(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>"
r".*?"
r"</(?:system-reminder|private|claude-mem-context|persisted-output|system_instruction)>",
re.DOTALL,
)
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def extract_last_assistant_message(lines: list[str]) -> str:
"""Walk transcript backwards, return text content of the last assistant message."""
for line in reversed(lines):
line = line.strip()
if not line:
continue
if '"type":"assistant"' not in line and '"type": "assistant"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("type") != "assistant":
continue
message = entry.get("message", {})
content = message.get("content", [])
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for block in content:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
return "\n".join(parts).strip()
return ""
def extract_files_touched(lines: list[str]) -> list[str]:
"""Extract unique file paths from tool_use content blocks in transcript."""
files = set()
file_ext_re = re.compile(
r"[a-zA-Z0-9_./-]+\.(?:py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html)"
)
for line in lines:
line = line.strip()
if not line:
continue
if '"tool_use"' not in line and '"file_path"' not in line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
content = entry.get("message", {}).get("content", [])
if not isinstance(content, list):
continue
for block in content:
if not isinstance(block, dict) or block.get("type") != "tool_use":
continue
inp = block.get("input", {})
if not isinstance(inp, dict):
continue
fp = inp.get("file_path", "")
if fp:
files.add(fp)
command = inp.get("command", "")
if command:
for match in file_ext_re.findall(command):
files.add(match)
return sorted(files)[:20]
def strip_tags(text: str) -> str:
return SYSTEM_TAG_RE.sub("", text).strip()
def build_summary_prompt(assistant_msg: str, files: list[str]) -> str:
"""Build a structured prompt that helps mem0's AI extract a good summary."""
files_section = ""
if files:
file_list = ", ".join(files[:10])
files_section = f"\n\nFiles touched during this session: {file_list}"
return (
f"Session summary — store the following as a structured session summary.\n\n"
f"What the assistant accomplished in this session:\n"
f"{assistant_msg[:MAX_SUMMARY_CHARS]}"
f"{files_section}\n\n"
f"Extract and remember: what was requested, what was investigated, "
f"key decisions made, what was completed, and what needs to happen next."
)
def store_summary(
api_key: str,
summary_prompt: str,
user_id: str,
session_id: str,
project_id: str,
branch: str,
files: list[str],
) -> bool:
expires = (date.today() + timedelta(days=SUMMARY_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "session_summary",
"source": "stop-hook",
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
if files:
metadata["files_touched"] = json.dumps(files[:20])
body = {
"messages": [{"role": "user", "content": summary_prompt}],
"user_id": user_id,
"app_id": project_id,
"run_id": session_id,
"metadata": metadata,
"infer": True,
"expiration_date": expires,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Session summary stored")
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def main():
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping")
return
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
log.debug("No valid JSON on stdin")
return
# Guard: skip subagent sessions (only root sessions get summaries)
agent_id = hook_input.get("agent_id", "")
if agent_id:
log.debug("Subagent session (agent_id=%s), skipping", agent_id)
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
log.debug("No transcript_path provided")
return
session_id = hook_input.get("session_id", "")
cwd = hook_input.get("cwd") or None
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
assistant_msg = extract_last_assistant_message(lines)
if not assistant_msg or len(assistant_msg.strip()) < 100:
log.debug("Assistant message too short (%d chars) — skipping", len(assistant_msg.strip()))
return
assistant_msg = strip_tags(assistant_msg)
files = extract_files_touched(lines)
summary_prompt = build_summary_prompt(assistant_msg, files)
log.info("Capturing session summary (%d chars, %d files)", len(assistant_msg), len(files))
store_summary(api_key, summary_prompt, user_id, session_id, project_id, branch, files)
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
@@ -0,0 +1,218 @@
#!/usr/bin/env bash
# PreToolUse hook for mem0 MCP tools.
# Injects identity (user_id, app_id) and metadata defaults when the agent
# omits them. Uses the hookSpecificOutput.updatedInput contract to modify
# tool call parameters before execution.
#
# Handles:
# add_memory — top-level user_id, app_id, metadata defaults
# search_memories — user_id/app_id into filters.AND[]
# get_memories — user_id/app_id into filters.AND[]
# delete_all_memories — top-level user_id, app_id
#
# Hook contract:
# exit 0 = allow. If stdout contains {"hookSpecificOutput": {"updatedInput": ...}},
# the updatedInput replaces the tool's input parameters.
# exit 2 = block (stderr shown as rejection reason).
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
# Determine which handler to use based on tool name
HANDLER=""
case "$TOOL_NAME" in
mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory)
HANDLER="add_memory" ;;
mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories)
HANDLER="search_memories" ;;
mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories)
HANDLER="get_memories" ;;
mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories)
HANDLER="delete_all" ;;
*) exit 0 ;;
esac
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // "{}"' 2>/dev/null)
_PATCH_OUT="/tmp/mem0_enforce_$$"
trap 'rm -f "$_PATCH_OUT"' EXIT
_MEM0_TOOL_INPUT="$TOOL_INPUT" \
_MEM0_USER_ID="${MEM0_RESOLVED_USER_ID:-}" \
_MEM0_APP_ID="${MEM0_PROJECT_ID:-}" \
_MEM0_GLOBAL_SEARCH="${MEM0_GLOBAL_SEARCH:-false}" \
_MEM0_HANDLER="$HANDLER" \
python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
import json, os, sys
raw = os.environ.get("_MEM0_TOOL_INPUT", "{}")
try:
inp = json.loads(raw)
except Exception:
sys.exit(0)
handler = os.environ.get("_MEM0_HANDLER", "")
resolved_uid = os.environ.get("_MEM0_USER_ID", "")
resolved_aid = os.environ.get("_MEM0_APP_ID", "")
global_search = os.environ.get("_MEM0_GLOBAL_SEARCH", "false") == "true"
changed = False
def inject_top_level_identity(inp, uid, aid):
"""Inject user_id/app_id as top-level params (for add_memory, delete_all)."""
changed = False
if uid and not inp.get("user_id"):
inp["user_id"] = uid
changed = True
if aid and not inp.get("app_id"):
inp["app_id"] = aid
changed = True
return changed
def inject_filter_identity(inp, uid, aid):
"""Inject user_id/app_id into filters.AND[] (for search/get_memories)."""
changed = False
if not uid and not aid:
return False
filters = inp.get("filters")
if filters is None:
# No filters at all — create from scratch
and_clauses = []
if uid:
and_clauses.append({"user_id": uid})
if aid:
and_clauses.append({"app_id": aid})
inp["filters"] = {"AND": and_clauses}
return True
if not isinstance(filters, dict):
return False
# Check if filters already contain user_id/app_id
and_clauses = filters.get("AND")
if and_clauses is None:
# Filters exist but no AND — could be flat like {"user_id": "x"}
has_uid = "user_id" in filters
has_aid = "app_id" in filters
if has_uid and has_aid:
return False
# Convert flat filters to AND format and add missing identity
existing = []
for k, v in list(filters.items()):
existing.append({k: v})
if uid and not has_uid:
existing.append({"user_id": uid})
changed = True
if aid and not has_aid:
existing.append({"app_id": aid})
changed = True
if changed:
inp["filters"] = {"AND": existing}
return changed
if not isinstance(and_clauses, list):
return False
# AND array exists — check for existing user_id/app_id
has_uid = any("user_id" in c for c in and_clauses if isinstance(c, dict))
has_aid = any("app_id" in c for c in and_clauses if isinstance(c, dict))
if uid and not has_uid:
and_clauses.append({"user_id": uid})
changed = True
if aid and not has_aid:
and_clauses.append({"app_id": aid})
changed = True
return changed
if handler == "add_memory":
changed = inject_top_level_identity(inp, resolved_uid, resolved_aid)
meta = inp.get("metadata") or {}
if "confidence" not in meta:
meta["confidence"] = 0.7
changed = True
if "files" not in meta:
meta["files"] = ["*"]
changed = True
if "source" not in meta:
meta["source"] = "auto_capture"
changed = True
if "type" not in meta:
meta["type"] = "task_learning"
changed = True
if meta.get("confidence", 0) >= 1.0 and "infer" not in inp:
inp["infer"] = False
changed = True
# Track session in metadata instead of run_id.
# run_id creates a separate entity partition in the v3 API,
# making memories invisible to search/get_memories calls
# that don't include a run_id filter.
if "session_id" not in meta:
sid = os.environ.get("MEM0_SESSION_ID", "")
if not sid:
session_file = "/tmp/mem0_session_id_" + os.environ.get("USER", "default")
if os.path.isfile(session_file):
try:
with open(session_file) as f:
sid = f.read().strip()
except OSError:
pass
if sid:
meta["session_id"] = sid
changed = True
if changed:
inp["metadata"] = meta
elif handler in ("search_memories", "get_memories"):
if global_search:
inp["filters"] = {"OR": [{"user_id": "*"}]}
changed = True
else:
changed = inject_filter_identity(inp, resolved_uid, resolved_aid)
elif handler == "delete_all":
changed = inject_top_level_identity(inp, resolved_uid, resolved_aid)
if changed:
print(json.dumps(inp))
PYEOF
PATCHED=$(cat "$_PATCH_OUT" 2>/dev/null)
rm -f "$_PATCH_OUT"
if [ -n "$PATCHED" ] && echo "$PATCHED" | jq empty 2>/dev/null; then
jq -n --argjson updated "$PATCHED" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": $updated
}
}' 2>/dev/null || true
fi
# Track session stats here because PostToolUse hooks don't fire for plugin MCP tools.
case "$HANDLER" in
add_memory)
_CAT=$(echo "$TOOL_INPUT" | jq -r '.metadata.type // .metadata.category // ""' 2>/dev/null || echo "")
python3 "$SCRIPT_DIR/session_stats.py" add "$_CAT" 2>/dev/null &
;;
search_memories|get_memories)
python3 "$SCRIPT_DIR/session_stats.py" search 2>/dev/null &
;;
esac
exit 0
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Install mem0ai SDK into a persistent venv inside CLAUDE_PLUGIN_DATA.
# Runs on SessionStart; skips if requirements.txt hasn't changed.
set -euo pipefail
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
DATA_DIR="${CLAUDE_PLUGIN_DATA:-${HOME}/.mem0/plugin-data}"
VENV_DIR="${DATA_DIR}/venv"
REQ_SRC="${PLUGIN_ROOT}/requirements.txt"
REQ_STAMP="${DATA_DIR}/requirements.txt"
mkdir -p "${DATA_DIR}"
LOCKDIR="${DATA_DIR}/.install-lock"
needs_install=false
if [ ! -f "${VENV_DIR}/bin/python3" ]; then
needs_install=true
elif ! diff -q "${REQ_SRC}" "${REQ_STAMP}" >/dev/null 2>&1; then
needs_install=true
fi
if [ "${needs_install}" = "true" ]; then
if mkdir "${LOCKDIR}" 2>/dev/null; then
# We acquired the lock — proceed with installation
trap 'rmdir "${LOCKDIR}" 2>/dev/null || true' EXIT
python3 -m venv "${VENV_DIR}" 2>/dev/null || python -m venv "${VENV_DIR}"
"${VENV_DIR}/bin/pip" install --quiet --upgrade pip >/dev/null 2>&1 || true
if "${VENV_DIR}/bin/pip" install --quiet -r "${REQ_SRC}" 2>/dev/null; then
cp "${REQ_SRC}" "${REQ_STAMP}"
rm -f "${DATA_DIR}/.install-failed"
else
rm -f "${REQ_STAMP}"
touch "${DATA_DIR}/.install-failed"
echo "mem0 plugin: failed to install Python dependencies" >&2
exit 0
fi
else
# Another process holds the lock — wait up to 60s for it to finish
for i in $(seq 1 60); do
[ ! -d "${LOCKDIR}" ] && break
sleep 1
done
# Check if the other process's install failed
if [ -f "${DATA_DIR}/.install-failed" ]; then
echo "mem0 plugin: dependency installation failed (by another session)" >&2
fi
fi
fi
@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""File-context injection for PreToolUse/Read hook.
When Claude is about to read a file, this script searches mem0 for
memories that reference that file path and returns a compact timeline
of prior work. This gives Claude context like "last time you fixed a
null pointer here" before it reads the file.
Modeled after claude-mem's file-context handler but adapted for mem0's
cloud API architecture.
Input: file_path (positional arg), env vars for identity
Output: JSON to stdout with hookSpecificOutput.additionalContext
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _formatting import TYPE_ICONS, format_age
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_project_id
from _search import search_memories
FILE_READ_GATE_MIN_BYTES = 1500
MAX_RESULTS = 5
SEARCH_TIMEOUT = 5
def gate_file(file_path: str, cwd: str) -> str | None:
"""Return the resolved absolute path if the file passes gating, else None."""
if not file_path:
return None
p = Path(file_path)
if not p.is_absolute():
p = Path(cwd) / p
try:
p = p.resolve()
if not p.is_file():
return None
if p.stat().st_size < FILE_READ_GATE_MIN_BYTES:
return None
return str(p)
except OSError:
return None
def relative_path(abs_path: str, cwd: str) -> str:
try:
return os.path.relpath(abs_path, cwd)
except ValueError:
return abs_path
def format_timeline(memories: list[dict], file_path: str) -> str:
"""Format memories into a compact timeline for context injection."""
if not memories:
return ""
rel = file_path
lines = [
f"Prior work on `{rel}` — {len(memories)} memories found.",
"Need details? Use `search_memories` with the memory ID.",
"",
]
for m in memories:
mid = m.get("id", "?")[:8]
text = (m.get("memory", "") or "")[:150].replace("\n", " ").strip()
meta = m.get("metadata") or {}
cat = meta.get("type", "unknown")
icon = TYPE_ICONS.get(cat, "❓")
age = format_age(m)
age_str = f" ({age})" if age else ""
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
return "\n".join(lines)
def search_file_context(
api_key: str, user_id: str, project_id: str, file_path: str, cwd: str
) -> str:
"""Search mem0 for memories related to a file path."""
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
rel = relative_path(file_path, cwd)
basename = os.path.basename(file_path)
query = f"{rel} {basename}" if rel != basename else rel
results = search_memories(
api_key, user_id, project_id, query,
top_k=MAX_RESULTS, threshold=0.3,
global_search=global_search,
)
results = results[:MAX_RESULTS]
return format_timeline(results, rel)
def main():
if len(sys.argv) < 2:
sys.exit(0)
file_path = sys.argv[1]
cwd = sys.argv[2] if len(sys.argv) > 2 else os.getcwd()
api_key = resolve_api_key()
if not api_key:
sys.exit(0)
resolved = gate_file(file_path, cwd)
if not resolved:
sys.exit(0)
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
timeline = search_file_context(api_key, user_id, project_id, resolved, cwd)
if not timeline:
sys.exit(0)
print(timeline, end="")
if __name__ == "__main__":
try:
main()
except Exception:
pass
sys.exit(0)
@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""Import memories from competing AI tool configuration files into mem0.
Sub-commands (via sys.argv[1]):
cursorrules [--path .cursorrules]
copilot [--path .github/copilot-instructions.md]
cline [--path memory-bank/]
continue [--path .continue/rules.md]
Each sub-command reads configuration files from competing tools,
splits them into chunks, and POSTs each chunk to the mem0 API as a
project_profile memory.
Output: progress messages to stdout, errors to stderr
Exit: 0 always
"""
from __future__ import annotations
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _chunking import (
filter_and_truncate,
split_by_headers,
split_by_hr_or_headers,
)
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
API_URL = "https://api.mem0.ai"
HASH_STORE = os.path.expanduser("~/.mem0/import_hashes.json")
def _load_hashes() -> dict[str, str]:
if not os.path.isfile(HASH_STORE):
return {}
try:
with open(HASH_STORE) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return {}
def _save_hashes(hashes: dict[str, str]) -> None:
os.makedirs(os.path.dirname(HASH_STORE), exist_ok=True)
try:
with open(HASH_STORE, "w") as f:
json.dump(hashes, f, indent=2)
except OSError:
pass
def _content_hash(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
# API helpers
# ---------------------------------------------------------------------------
def post_memory(api_key: str, content: str, user_id: str, project_id: str, branch: str, source: str) -> bool:
"""POST a single memory chunk to the mem0 API."""
metadata: dict = {
"type": "project_profile",
"source": source,
}
if branch:
metadata["branch"] = branch
body = {
"messages": [{"role": "user", "content": content}],
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
"infer": False,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=20) as resp:
return resp.status in (200, 201)
except urllib.error.URLError as e:
print(f" [warn] API call failed: {e}", file=sys.stderr)
return False
def import_chunks(chunks: list[str], api_key: str, user_id: str, project_id: str, branch: str, source: str, hash_key: str = "") -> int:
"""Import a list of content chunks; return number of successful imports.
Skips import if content hash matches a previous run for the same hash_key."""
if hash_key:
combined = "\n".join(chunks)
current_hash = _content_hash(combined)
hashes = _load_hashes()
if hashes.get(hash_key) == current_hash:
print(f"Already imported (unchanged) -- skipping: {hash_key}")
return 0
else:
current_hash = ""
hashes = {}
success = 0
for chunk in chunks:
if post_memory(api_key, chunk, user_id, project_id, branch, source):
success += 1
if success > 0 and hash_key and current_hash:
hashes[hash_key] = current_hash
_save_hashes(hashes)
return success
# ---------------------------------------------------------------------------
# Sub-command implementations
# ---------------------------------------------------------------------------
def _parse_path_arg(args: list[str], flag: str, default: str) -> str:
"""Extract --path <value> from args list, falling back to default."""
for i, arg in enumerate(args):
if arg == flag and i + 1 < len(args):
return args[i + 1]
if arg.startswith(f"{flag}="):
return arg[len(flag) + 1:]
return default
def cmd_cursorrules(args: list[str]) -> None:
path = _parse_path_arg(args, "--path", ".cursorrules")
source = "cursor-import"
api_key = resolve_api_key()
user_id = resolve_user_id()
project_id = resolve_project_id()
branch = resolve_branch()
if not api_key:
print("Error: MEM0_API_KEY not set", file=sys.stderr)
return
if not os.path.isfile(path):
print(f"File not found: {path}", file=sys.stderr)
return
with open(path, encoding="utf-8", errors="replace") as f:
content = f.read()
raw_chunks = split_by_headers(content, "## ")
# Fall back to treating the whole file as one chunk if no headers found
if not raw_chunks:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
def cmd_copilot(args: list[str]) -> None:
path = _parse_path_arg(args, "--path", ".github/copilot-instructions.md")
source = "copilot-import"
api_key = resolve_api_key()
user_id = resolve_user_id()
project_id = resolve_project_id()
branch = resolve_branch()
if not api_key:
print("Error: MEM0_API_KEY not set", file=sys.stderr)
return
if not os.path.isfile(path):
print(f"File not found: {path}", file=sys.stderr)
return
with open(path, encoding="utf-8", errors="replace") as f:
content = f.read()
raw_chunks = split_by_headers(content, "## ")
if not raw_chunks:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
def cmd_cline(args: list[str]) -> None:
dir_path = _parse_path_arg(args, "--path", "memory-bank/")
source = "cline-import"
api_key = resolve_api_key()
user_id = resolve_user_id()
project_id = resolve_project_id()
branch = resolve_branch()
if not api_key:
print("Error: MEM0_API_KEY not set", file=sys.stderr)
return
if not os.path.isdir(dir_path):
print(f"Directory not found: {dir_path}", file=sys.stderr)
return
md_files = sorted(
f for f in os.listdir(dir_path) if f.endswith(".md")
)
if not md_files:
print(f"No .md files found in {dir_path}", file=sys.stderr)
return
total = 0
for filename in md_files:
filepath = os.path.join(dir_path, filename)
with open(filepath, encoding="utf-8", errors="replace") as f:
content = f.read().strip()
if not content:
continue
chunks = filter_and_truncate([content])
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{filepath}")
total += n
print(f"Imported {total} memories from {source} ({dir_path})")
def cmd_continue(args: list[str]) -> None:
path = _parse_path_arg(args, "--path", ".continue/rules.md")
source = "continue-import"
api_key = resolve_api_key()
user_id = resolve_user_id()
project_id = resolve_project_id()
branch = resolve_branch()
if not api_key:
print("Error: MEM0_API_KEY not set", file=sys.stderr)
return
if not os.path.isfile(path):
print(f"File not found: {path}", file=sys.stderr)
return
with open(path, encoding="utf-8", errors="replace") as f:
content = f.read()
raw_chunks = split_by_hr_or_headers(content)
if not raw_chunks:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
COMMANDS = {
"cursorrules": cmd_cursorrules,
"copilot": cmd_copilot,
"cline": cmd_cline,
"continue": cmd_continue,
}
def main() -> None:
if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS:
available = ", ".join(COMMANDS.keys())
print("Usage: import_competing_tools.py <subcommand> [--path <path>]", file=sys.stderr)
print(f"Subcommands: {available}", file=sys.stderr)
sys.exit(0)
subcommand = sys.argv[1]
remaining_args = sys.argv[2:]
COMMANDS[subcommand](remaining_args)
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
sys.exit(0)
+162
View File
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Install Mem0 lifecycle hooks into ~/.codex/hooks.json.
Codex discovers hooks only at ~/.codex/hooks.json or <repo>/.codex/hooks.json,
and has no plugin-host mechanism for auto-wiring hooks from an installed
plugin. This installer reads the template at hooks/codex-hooks.json, rewrites
the ${PLUGIN_ROOT} placeholder to the absolute install path of this
plugin, then merges the entries into ~/.codex/hooks.json.
Re-running is idempotent: existing Mem0 entries (identified by the plugin
directory name in the command string) are removed before fresh entries are
added, so upgrades don't leave duplicates.
Usage:
python3 install_codex_hooks.py # install or update
python3 install_codex_hooks.py --uninstall # remove Mem0 entries
After installing, Codex requires the hooks feature flag in ~/.codex/config.toml:
[features]
codex_hooks = true
"""
from __future__ import annotations
import argparse
import json
import platform
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
PLUGIN_ROOT = SCRIPT_DIR.parent
CODEX_DIR = Path.home() / ".codex"
HOOKS_FILE = CODEX_DIR / "hooks.json"
CONFIG_FILE = CODEX_DIR / "config.toml"
TEMPLATE_FILE = PLUGIN_ROOT / "hooks" / "codex-hooks.json"
# Substring we look for when identifying entries this installer owns.
# Matches the plugin directory name, which stays stable across install paths.
OWNER_MARKER = "mem0-plugin"
def load_template() -> dict:
raw = TEMPLATE_FILE.read_text()
raw = raw.replace("${PLUGIN_ROOT}", str(PLUGIN_ROOT))
return json.loads(raw)
def load_existing() -> dict:
if not HOOKS_FILE.exists():
return {"hooks": {}}
try:
return json.loads(HOOKS_FILE.read_text())
except (json.JSONDecodeError, OSError) as e:
print(f"error: failed to read {HOOKS_FILE}: {e}", file=sys.stderr)
sys.exit(1)
def is_owned_entry(entry: dict) -> bool:
for hook in entry.get("hooks", []):
if OWNER_MARKER in hook.get("command", ""):
return True
return False
def strip_owned_entries(config: dict) -> dict:
hooks = config.get("hooks", {}) or {}
for event in list(hooks.keys()):
hooks[event] = [e for e in hooks[event] if not is_owned_entry(e)]
if not hooks[event]:
del hooks[event]
config["hooks"] = hooks
return config
def merge_template(config: dict, template: dict) -> dict:
hooks = config.setdefault("hooks", {})
for event, entries in template.get("hooks", {}).items():
hooks.setdefault(event, []).extend(entries)
return config
def write_config(config: dict) -> None:
CODEX_DIR.mkdir(parents=True, exist_ok=True)
HOOKS_FILE.write_text(json.dumps(config, indent=2) + "\n")
def feature_flag_enabled() -> bool:
if not CONFIG_FILE.exists():
return False
content = CONFIG_FILE.read_text()
for line in content.splitlines():
stripped = line.split("#", 1)[0].strip().replace(" ", "")
if stripped == "codex_hooks=true":
return True
return False
def print_feature_flag_hint() -> None:
print()
print("Codex hooks feature flag is not enabled.")
print(f"Add this to {CONFIG_FILE}:")
print()
print(" [features]")
print(" codex_hooks = true")
print()
print("Then restart Codex.")
def main() -> int:
parser = argparse.ArgumentParser(description="Install or remove Mem0 Codex hooks.")
parser.add_argument(
"--uninstall",
action="store_true",
help="Remove Mem0 entries from ~/.codex/hooks.json and exit.",
)
args = parser.parse_args()
config = load_existing()
if args.uninstall:
config = strip_owned_entries(config)
write_config(config)
print(f"Removed Mem0 hooks from {HOOKS_FILE}")
return 0
# Codex lifecycle hooks register .sh paths directly in ~/.codex/hooks.json.
# On native Windows .sh has no default handler, so Codex spawning a hook
# triggers "Open With" dialogs (one OpenWith.exe per event). See #5243.
if platform.system() == "Windows":
print(
"Codex lifecycle hooks register .sh scripts directly, which Windows\n"
"cannot execute without a bash interpreter on PATH. Re-run this\n"
"installer from WSL or Git Bash, or use Mem0 via MCP / Direct tools\n",
file=sys.stderr,
)
return 2
if not TEMPLATE_FILE.exists():
print(f"error: template not found at {TEMPLATE_FILE}", file=sys.stderr)
return 1
template = load_template()
config = strip_owned_entries(config)
config = merge_template(config, template)
write_config(config)
print(f"Installed Mem0 hooks into {HOOKS_FILE}")
print(f"Plugin path: {PLUGIN_ROOT}")
print("Events: PreToolUse, SessionStart, UserPromptSubmit, PostToolUse")
if not feature_flag_enabled():
print_feature_flag_hint()
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,50 @@
"""Load plugin settings from ~/.mem0/settings.json.
Settings file is user-editable. Missing file or keys fall back to defaults.
"""
from __future__ import annotations
import json
from pathlib import Path
SETTINGS_PATH = Path.home() / ".mem0" / "settings.json"
DEFAULTS = {
"auto_save": True,
"auto_search": True,
"search_limit": 10,
"retention_session_days": 90,
"confidence_threshold": 0.3,
"global_search": False,
"debug": False,
}
def load_settings() -> dict:
settings = dict(DEFAULTS)
if SETTINGS_PATH.exists():
try:
with open(SETTINGS_PATH) as f:
user = json.load(f)
settings.update({k: v for k, v in user.items() if k in DEFAULTS})
except (json.JSONDecodeError, OSError):
pass
return settings
def create_default_settings() -> None:
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
if not SETTINGS_PATH.exists():
with open(SETTINGS_PATH, "w") as f:
json.dump(DEFAULTS, f, indent=2)
f.write("\n")
if __name__ == "__main__":
import sys
if len(sys.argv) > 1 and sys.argv[1] == "init":
create_default_settings()
print(f"Created {SETTINGS_PATH}")
else:
print(json.dumps(load_settings()))
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
# Hook: PostToolUse (matcher: Bash)
#
# Scans bash command output for stack traces and error patterns.
# When found, injects a search rubric telling the agent to check mem0
# for prior occurrences of the same error.
#
# This complements on_user_prompt.sh (which catches errors in the user's
# typed message). This hook catches errors in COMMAND OUTPUT — e.g.,
# when `npm test` or `python script.py` fails with a traceback.
#
# Input: JSON on stdin with tool_name, tool_input, tool_response
# Output: Context injected into Claude's next response (exit 0)
set -uo pipefail
INPUT=$(cat)
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_response // ""' 2>/dev/null || echo "")
# Skip short output (< 50 chars unlikely to contain a real stack trace)
if [ ${#TOOL_RESULT} -lt 50 ]; then
exit 0
fi
# Skip git operations — not useful for error detection
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null || echo "")
case "$COMMAND" in
*"git commit"*|*"git merge"*|*"git rebase"*)
exit 0
;;
esac
# Detect stack traces and error patterns in command output
HAS_ERROR=""
if echo "$TOOL_RESULT" | grep -qE '(Traceback \(most recent call last\)|panic: |FATAL:|error\[E[0-9]+\])'; then
HAS_ERROR="true"
elif [ "$(echo "$TOOL_RESULT" | grep -cE '(Error:|Exception:)')" -ge 2 ]; then
HAS_ERROR="true"
fi
if [ -z "$HAS_ERROR" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
# Extract error class/message (first matching line)
ERROR_LINE=$(echo "$TOOL_RESULT" | grep -iE '(Error:|Exception:|panic:|FAIL:|fatal:)' | head -1 | sed 's/^[[:space:]]*//' | cut -c1-120)
# Extract file paths from stack trace frames
TRACE_FILES=$(echo "$TOOL_RESULT" | grep -oE '([a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh))(:[0-9]+)?' | head -5 | sort -u)
# Build file list for display
FILE_DISPLAY=""
if [ -n "$TRACE_FILES" ]; then
FILE_DISPLAY=$(echo "$TRACE_FILES" | sed 's/^/ - /')
fi
USER_ID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
# Extract query (first 80 chars of error line)
ERROR_QUERY=$(echo "$ERROR_LINE" | cut -c1-80)
# Telemetry (fire regardless of API key)
python3 "$SCRIPT_DIR/telemetry.py" bash_error --error_detected 2>/dev/null &
# No API key — skip output entirely
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
# Pre-fetch memories: anti_pattern and bug_fix searches
RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_QUERY="$ERROR_QUERY" MEM0_SEARCH_USER="$USER_ID" \
MEM0_API_KEY="${MEM0_API_KEY}" MEM0_PROJECT_ID="${MEM0_PROJECT_ID:-unknown}" \
python3 -c "
import os, sys
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
from _search import search_memories, format_results_for_context
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
project_id = os.environ.get('MEM0_PROJECT_ID', 'unknown')
query = os.environ.get('MEM0_SEARCH_QUERY', '')
r1 = search_memories(api_key, user_id, project_id, query, metadata_type='anti_pattern', top_k=3)
r2 = search_memories(api_key, user_id, project_id, query, metadata_type='bug_fix', top_k=3)
seen = set()
combined = []
for m in r1 + r2:
mid = m.get('id', '')
if mid not in seen:
seen.add(mid)
combined.append(m)
print(format_results_for_context(combined, heading='Prior error memories'), end='')
" 2>/dev/null || echo "")
# Build context string for JSON output
CTX="Error detected in command output\n\n"
CTX="${CTX}\`${COMMAND}\` produced an error:\n> ${ERROR_LINE}\n"
if [ -n "$FILE_DISPLAY" ]; then
CTX="${CTX}\nFiles in stack trace:\n${FILE_DISPLAY}\n"
fi
if [ -n "$RESULTS" ]; then
CTX="${CTX}\n${RESULTS}\n"
fi
CTX="${CTX}\nResolved errors are stored as anti_pattern or bug_fix memories for future reference."
jq -cn --arg ctx "$CTX" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}' 2>/dev/null || true
exit 0
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Hook: PreToolUse (matcher: Read)
#
# Injects prior work context before Claude reads a file. Searches mem0
# for memories referencing the file path and returns a compact timeline.
#
# Modeled after claude-mem's file-context handler, adapted for mem0 cloud API.
#
# Input: JSON on stdin with tool_name, tool_input (file_path), cwd
# Output: JSON with hookSpecificOutput.additionalContext + permissionDecision
#
# Must never block the Read — silent exit on any failure.
set -uo pipefail
INPUT=$(cat)
# Extract file path from tool_input
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Resolve API key (covers Desktop app users who set it in shell profile)
if [ -z "${MEM0_API_KEY:-}" ]; then
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
# Call the Python worker — it handles gating (file size, existence)
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
if [ -z "$TIMELINE" ]; then
exit 0
fi
# Return context injection with permissionDecision: allow
jq -cn --arg ctx "$TIMELINE" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: $ctx,
permissionDecision: "allow"
}
}' 2>/dev/null || true
exit 0
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Hook: preToolUse (matcher: Read) — Cursor variant
#
# Same as on_file_read.sh but uses CURSOR_PLUGIN_ROOT for path resolution
# and sources Cursor-specific identity.
set -uo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""' 2>/dev/null || echo "")
if [ -z "$FILE_PATH" ]; then
exit 0
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CWD=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
TIMELINE=$(python3 "$SCRIPT_DIR/file_context.py" "$FILE_PATH" "$CWD" 2>/dev/null || echo "")
if [ -z "$TIMELINE" ]; then
exit 0
fi
jq -cn --arg ctx "$TIMELINE" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
additionalContext: $ctx,
permissionDecision: "allow"
}
}' 2>/dev/null || true
exit 0
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Hook: PostToolUse — track mem0 MCP tool usage for session stats
#
# Fires after any tool call. We only care about mem0 MCP tools:
# mcp__mem0__add_memory → record an add
# mcp__mem0__search_memories → record a search
#
# Input: JSON on stdin with tool_name, tool_input, tool_response
# Output: none (exit 0, non-blocking)
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "")
case "$TOOL_NAME" in
*__add_memory)
CATEGORY=$(echo "$INPUT" | jq -r '.tool_input.metadata.type // .tool_input.metadata.category // ""' 2>/dev/null || echo "")
python3 "$SCRIPT_DIR/session_stats.py" add "$CATEGORY" 2>/dev/null || true
python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=add_memory 2>/dev/null &
;;
*__search_memories|*__get_memories)
python3 "$SCRIPT_DIR/session_stats.py" search 2>/dev/null || true
python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=search_memories 2>/dev/null &
;;
*__delete_memory)
python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=delete_memory 2>/dev/null &
;;
*__update_memory)
python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=update_memory 2>/dev/null &
;;
esac
exit 0
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Hook: postToolUse (Cursor) — track mem0 MCP tool usage for session stats
#
# Wraps on_post_tool_use.sh. Cursor expects JSON output but PostToolUse
# has no meaningful return value, so we output {} after tracking.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Run the shared tracker (output is ignored)
"$SCRIPT_DIR/on_post_tool_use.sh" 2>/dev/null || true
echo '{}'
exit 0
+314
View File
@@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""Capture session state via the Mem0 REST API.
Safety net for PreCompact and Stop hooks — reads the transcript JSONL,
extracts structured session state, and stores it in Mem0 directly.
Used by:
- PreCompact hook: Tags with "pre-compaction" (context about to be lost)
- Stop hook: Tags with "session-end" (session ending, Claude can't respond)
Input: JSON on stdin with transcript_path, session_id, cwd
Output: stderr logs only (exit 0 always — must not block)
"""
from __future__ import annotations
import json
import logging
import os
import sys
import urllib.error
import urllib.request
from datetime import date, timedelta
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
log = logging.getLogger("mem0-capture")
log.setLevel(logging.DEBUG)
_handler = logging.StreamHandler(sys.stderr)
_handler.setFormatter(logging.Formatter("[mem0-capture] %(message)s"))
log.addHandler(_handler)
if os.environ.get("MEM0_DEBUG"):
_log_dir = os.path.expanduser("~/.mem0")
try:
os.makedirs(_log_dir, exist_ok=True)
_file_handler = logging.FileHandler(os.path.join(_log_dir, "hooks.log"))
_file_handler.setFormatter(logging.Formatter("[mem0-capture] %(asctime)s %(message)s"))
log.addHandler(_file_handler)
except OSError:
pass
API_URL = "https://api.mem0.ai"
MAX_TAIL_LINES = 500
MAX_USER_MESSAGES = 30
MAX_BASH_COMMANDS = 20
MAX_ASSISTANT_TEXT = 10000
# session_state captures churn fast (active codebase, files in flight). Past
# ~3 months they're stale noise. Durable facts (decisions, conventions) are
# stored separately by the agent without an expiration_date.
SESSION_STATE_EXPIRY_DAYS = 90
def tail_lines(filepath: str, n: int) -> list[str]:
"""Read last n lines of a file efficiently."""
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def parse_transcript(lines: list[str]) -> dict:
"""Parse transcript JSONL lines and extract session state."""
user_messages: list[str] = []
files_modified: set[str] = set()
bash_commands: list[str] = []
last_assistant_text = ""
for line in lines:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
entry_type = entry.get("type")
if entry_type not in ("user", "assistant"):
continue
if entry.get("isSidechain"):
continue
message = entry.get("message", {})
content_blocks = message.get("content", [])
if entry_type == "user":
parts = []
if isinstance(content_blocks, str):
parts.append(content_blocks)
elif isinstance(content_blocks, list):
for block in content_blocks:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
text = "\n".join(parts).strip()
if text and len(text) > 10 and not text.startswith("<"):
user_messages.append(text)
elif entry_type == "assistant":
for block in content_blocks:
if not isinstance(block, dict):
continue
if block.get("type") == "text":
text = block.get("text", "").strip()
if text:
last_assistant_text = text
if block.get("type") == "tool_use":
tool_name = block.get("name", "")
tool_input = block.get("input", {})
if tool_name in ("Write", "Edit"):
fp = tool_input.get("file_path", "")
if fp:
files_modified.add(fp)
elif tool_name == "Bash":
cmd = tool_input.get("command", "")
if cmd:
bash_commands.append(cmd)
return {
"user_messages": user_messages[-MAX_USER_MESSAGES:],
"files_modified": sorted(files_modified),
"bash_commands": bash_commands[-MAX_BASH_COMMANDS:],
"last_assistant_text": last_assistant_text[:MAX_ASSISTANT_TEXT],
}
def build_content(state: dict, source: str) -> str:
"""Build minimal context — only what's needed to resume work.
This is a FALLBACK safety net, not the primary capture path.
The agent handles rich memory storage via on_pre_compact.sh prompts.
This script only fires when the agent didn't store enough on its own.
Keep it short — mem0 infer=True will extract structured facts.
"""
parts = []
if state["files_modified"]:
parts.append(f"Files touched: {', '.join(state['files_modified'][:15])}")
if state["bash_commands"]:
git_cmds = [c for c in state["bash_commands"] if "git " in c]
if git_cmds:
parts.append(f"Git operations: {len(git_cmds)}")
return "\n".join(parts)
def store_memory(api_key: str, content: str, user_id: str, source: str, session_id: str = "", project_id: str = "", branch: str = "") -> bool:
"""Store session state as a memory via the Mem0 REST API."""
expires = (date.today() + timedelta(days=SESSION_STATE_EXPIRY_DAYS)).isoformat()
metadata = {
"type": "session_state",
"source": source,
"session_id": session_id,
}
if branch:
metadata["branch"] = branch
body = {
"messages": [
{"role": "user", "content": content}
],
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
"expiration_date": expires,
"infer": True,
}
data = json.dumps(body).encode("utf-8")
req = urllib.request.Request(
f"{API_URL}/v3/memories/add/",
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Token {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
if resp.status in (200, 201):
log.info("Session state stored successfully")
return True
log.warning("API returned status %d", resp.status)
return False
except urllib.error.URLError as e:
log.warning("API call failed: %s", e)
return False
def format_status(state: dict, source: str, stored: bool, skipped_reason: str = "") -> str:
"""Build a clean, readable status line for terminal display."""
files_count = len(state.get("files_modified", []))
git_cmds = [c for c in state.get("bash_commands", []) if "git " in c]
user_msgs = len(state.get("user_messages", []))
parts = []
if files_count:
parts.append(f"{files_count} file{'s' if files_count != 1 else ''} touched")
if git_cmds:
parts.append(f"{len(git_cmds)} git op{'s' if len(git_cmds) != 1 else ''}")
if user_msgs:
parts.append(f"{user_msgs} exchange{'s' if user_msgs != 1 else ''}")
activity = ", ".join(parts) if parts else "minimal activity"
if source == "pre-compaction":
icon = "✨" # ✨
label = "Pre-compaction snapshot"
else:
icon = "\U0001f4be" # 💾
label = "Session-end snapshot"
if skipped_reason:
return f"{icon} Mem0 {label} — {activity} — {skipped_reason}"
elif stored:
return f"{icon} Mem0 {label} — {activity} — saved to mem0"
else:
return f"{icon} Mem0 {label} — {activity} — nothing to capture"
def main():
source = "pre-compaction"
show_status = False
for arg in sys.argv[1:]:
if arg.startswith("--source="):
source = arg.split("=", 1)[1]
elif arg == "--status":
show_status = True
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
if show_status:
print("✨ Mem0 — no API key, skipping capture")
return
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
log.debug("No valid JSON on stdin")
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
log.debug("No transcript_path provided")
return
session_id = hook_input.get("session_id", "")
cwd = hook_input.get("cwd") or None
user_id = resolve_user_id()
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
state = parse_transcript(lines)
# Skip if agent already stored memories this session — avoid duplicate writes.
stats_file = f"/tmp/mem0_session_stats_{os.environ.get('USER', 'default')}.json"
try:
with open(stats_file) as f:
stats = json.load(f)
if stats.get("adds", 0) >= 1:
log.info("Agent stored %d memories this session — skipping fallback", stats["adds"])
if show_status:
print(format_status(state, source, False, f"agent already stored {stats['adds']} memor{'ies' if stats['adds'] != 1 else 'y'}"))
return
except (OSError, json.JSONDecodeError):
pass
if not state["files_modified"]:
log.debug("No files modified — skipping fallback capture")
if show_status:
print(format_status(state, source, False))
return
content = build_content(state, source)
if not content.strip():
log.debug("No content to store")
if show_status:
print(format_status(state, source, False))
return
log.info("Fallback capture: %d files modified", len(state["files_modified"]))
stored = store_memory(api_key, content, user_id, source, session_id, project_id, branch)
if show_status:
print(format_status(state, source, stored))
if __name__ == "__main__":
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
sys.exit(0)
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Hook: PreCompact
#
# Fires BEFORE context compaction. Captures session state via REST API
# in the background. Runs silently — no output to user.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
python3 "$SCRIPT_DIR/telemetry.py" pre_compact 2>/dev/null &
# Capture in background, no stdout
_TMP="/tmp/mem0_precompact_input_$$.json"
printf '%s' "$INPUT" > "$_TMP" 2>/dev/null
(python3 "$SCRIPT_DIR/on_pre_compact.py" --source=pre-compaction < "$_TMP" 2>/dev/null; rm -f "$_TMP") &
exit 0
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Hook: preCompact (Cursor)
#
# Wraps on_pre_compact.sh and converts plain-text output to Cursor's
# expected JSON format: {"user_message":"<text>"}
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEXT=$("$SCRIPT_DIR/on_pre_compact.sh" 2>/dev/null || echo "")
if [ -z "$TEXT" ]; then
echo '{}'
exit 0
fi
jq -cn --arg msg "$TEXT" '{user_message:$msg}'
exit 0
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
if [ "$SOURCE" = "startup" ]; then
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
rm -f /tmp/mem0_recent_reads_${USER}_* 2>/dev/null || true
fi
PYTHONPATH="$SCRIPT_DIR" python3 "$SCRIPT_DIR/load_settings.py" init 2>/dev/null || true
rm -f "/tmp/mem0_rubric_injected_${USER}" 2>/dev/null || true
rm -f /tmp/mem0_rubric_* 2>/dev/null || true
rm -f "/tmp/mem0_msg_count_${USER:-default}" 2>/dev/null || true
MEM0_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
if [ -z "$MEM0_SESSION_ID" ]; then
MEM0_SESSION_ID="ses_$(date +%s)_$$"
fi
printf '%s' "$MEM0_SESSION_ID" > "/tmp/mem0_session_id_${USER}"
export MEM0_SESSION_ID
# Persist identity to Claude's env so Bash tool calls, MCP config, and other hooks see them
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export MEM0_SESSION_ID=\"$MEM0_SESSION_ID\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_RESOLVED_USER_ID=\"${MEM0_RESOLVED_USER_ID:-$USER}\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_PROJECT_ID=\"${MEM0_PROJECT_ID:-unknown}\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_BRANCH=\"${MEM0_BRANCH:-unknown}\"" >> "$CLAUDE_ENV_FILE"
if [ -n "${MEM0_API_KEY:-}" ]; then
echo "export MEM0_API_KEY=\"$MEM0_API_KEY\"" >> "$CLAUDE_ENV_FILE"
fi
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
_UID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
_PID="${MEM0_PROJECT_ID:-unknown}"
_BR="${MEM0_BRANCH:-unknown}"
cat <<BANNER
## Mem0 — Setup Required
\`user=${_UID} | project=${_PID} | branch=${_BR} | auth=none\`
IMPORTANT: In your FIRST response, display this exact status line as your opening line:
\`\`\`
Mem0 — Setup Required | user=${_UID} | project=${_PID} | branch=${_BR} | auth=none
\`\`\`
MEM0_API_KEY is not set. To configure:
- **Reinstall the plugin**: Uninstall and reinstall — Claude Code will prompt for your API key during setup (stored securely in keychain)
- **Desktop app**: Click the environment dropdown next to the prompt box → hover over **Local** → click the **gear icon** → add \`MEM0_API_KEY=m0-...\`
- **CLI**: Add \`export MEM0_API_KEY=m0-...\` to your shell profile (~/.zshrc or ~/.bashrc)
- Get a key at https://app.mem0.ai/dashboard/api-keys
Then invoke the \`mem0:onboard\` skill to complete setup.
BANNER
exit 0
fi
_DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}"
_INSTALL_WARN=""
if [ -f "${_DATA_DIR}/.install-failed" ]; then
_INSTALL_WARN="mem0 SDK installation failed. Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
fi
MEM0_COUNT="?"
if command -v python3 >/dev/null 2>&1; then
MEM0_COUNT=$(python3 -c "
import json, os, urllib.request, urllib.error
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
app_id = os.environ.get('MEM0_PROJECT_ID', '')
global_search = os.environ.get('MEM0_GLOBAL_SEARCH', 'false') == 'true'
def get_count(filters):
body = json.dumps({'filters': filters}).encode()
req = urllib.request.Request(
'https://api.mem0.ai/v3/memories/?page=1&page_size=1',
headers={'Authorization': f'Token {api_key}', 'Content-Type': 'application/json'},
data=body, method='POST',
)
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
if isinstance(data, dict) and 'count' in data:
return data['count']
if isinstance(data, list):
return len(data)
return 0
try:
if global_search:
filters = {'OR': [{'user_id': '*'}]}
else:
filters = {'AND': [{'user_id': user_id}, {'app_id': app_id}]}
total = get_count(filters)
print(total)
except Exception:
print('?')
" 2>/dev/null || echo "?")
fi
_UID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
_ANN="${_MEM0_IDENTITY_ANNOTATION:-}"
_PID="${MEM0_PROJECT_ID:-unknown}"
_BR="${MEM0_BRANCH:-unknown}"
_GS="${MEM0_GLOBAL_SEARCH:-false}"
if [ "$_GS" = "true" ]; then
_SCOPE_LABEL="scope=global"
_SCOPE_INSTR="Global search is ON — searches return all memories across all users and projects. Writes still use user_id: \`${_UID}\`, app_id: \`${_PID}\`."
else
_SCOPE_LABEL="project=${_PID}"
_SCOPE_INSTR="Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:
- user_id: \`${_UID}\`
- app_id: \`${_PID}\` (project scope — passed as top-level \`app_id\`, NOT in metadata)"
fi
cat <<BANNER
## Mem0 Active
\`user=${_UID}${_ANN} | ${_SCOPE_LABEL} | branch=${_BR} | memories=${MEM0_COUNT}\`
IMPORTANT: In your FIRST response, display this exact status line as your opening line:
\`\`\`
Mem0 Active | user=${_UID}${_ANN} | ${_SCOPE_LABEL} | branch=${_BR} | memories=${MEM0_COUNT}
\`\`\`
${_SCOPE_INSTR}
After completing any task, decision, or meaningful exchange, proactively store learnings via \`add_memory\`. Do NOT wait until the session ends — store memories incrementally as work progresses. Focus on: decisions made, bugs fixed, patterns discovered, user preferences, or task outcomes. Aim for 1–3 memories per substantial interaction.
BANNER
if [ -n "$_INSTALL_WARN" ]; then
echo "$_INSTALL_WARN"
fi
MEM0_CWD_RESOLVED=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
if command -v python3 >/dev/null 2>&1; then
MEM0_PROJECT_CONFIG=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --full "$MEM0_CWD_RESOLVED" 2>/dev/null || echo "{}")
if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
_CONFIG_KEYS=$(echo "$MEM0_PROJECT_CONFIG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "?")
echo "mem0.md loaded (${_CONFIG_KEYS} sections configured)."
fi
fi
if [ "$SOURCE" = "startup" ]; then
if [ "$MEM0_COUNT" = "0" ]; then
echo "New project with 0 memories. Invoke the mem0:onboard skill to import project files. Coding categories install automatically in the background."
else
echo "Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type."
# Inject compact recent activity timeline (non-blocking, 5s timeout)
# Use perl alarm as portable timeout (macOS lacks GNU timeout)
_TIMELINE=$(MEM0_CWD="$MEM0_CWD_RESOLVED" perl -e 'alarm 5; exec @ARGV' python3 "$SCRIPT_DIR/session_timeline.py" 2>/dev/null || echo "")
if [ -n "$_TIMELINE" ]; then
echo ""
echo "$_TIMELINE"
fi
fi
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
_MEMORY_MD="$HOME/.claude/projects/${_PROJ_KEY}/memory/MEMORY.md"
if [ -f "$_MEMORY_MD" ] && [ -s "$_MEMORY_MD" ]; then
echo "Native MEMORY.md detected at ${_MEMORY_MD}. Add autoMemoryEnabled:false to settings.json or run /mem0:import."
fi
MEM0_CWD="$MEM0_CWD_RESOLVED" \
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
# Configure the coding-category taxonomy in the background (idempotent, never blocks).
# Prefer the venv python since this path needs the mem0ai SDK.
_VENV_PY="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}/venv/bin/python3"
if [ -x "$_VENV_PY" ]; then
MEM0_CWD="$MEM0_CWD_RESOLVED" "$_VENV_PY" "$SCRIPT_DIR/auto_setup_categories.py" 2>/dev/null &
else
MEM0_CWD="$MEM0_CWD_RESOLVED" python3 "$SCRIPT_DIR/auto_setup_categories.py" 2>/dev/null &
fi
elif [ "$SOURCE" = "resume" ]; then
echo "Session resumed. Search mem0 for session_state and decision memories to pick up where you left off. Run 2 parallel searches."
elif [ "$SOURCE" = "compact" ]; then
echo "Context compacted. Search mem0 for session_state and decision memories to recover context. Run 2 parallel searches."
if [ "${MEM0_AUTO_SAVE:-true}" != "false" ]; then
printf '%s' "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
fi
fi
python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null &
exit 0
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Hook: sessionStart (Cursor)
#
# Wraps on_session_start.sh and converts plain-text output to Cursor's
# expected JSON format: {"additional_context":"<text>"}
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEXT=$("$SCRIPT_DIR/on_session_start.sh" 2>/dev/null || echo "")
if [ -z "$TEXT" ]; then
echo '{}'
exit 0
fi
jq -cn --arg ctx "$TEXT" '{additional_context:$ctx}'
exit 0
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Hook: Stop
#
# Captures a structured session summary when a Claude Code session ends.
# Parses the transcript, extracts the last assistant message and files
# touched, then stores via mem0 API with infer=True for AI extraction.
#
# Guards:
# - Skips subagent sessions (agent_id present)
# - Skips if no API key
# - Skips if no transcript_path
# - Dedup via marker file
#
# Input: JSON on stdin with transcript_path, session_id, agent_id, cwd
# Output: Nothing to stdout (background capture). Always exits 0.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
# Guard: skip subagent sessions
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
if [ -n "$AGENT_ID" ]; then
exit 0
fi
# Resolve identity if needed
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
# Honor auto_save=false in ~/.mem0/settings.json
if [ "${MEM0_AUTO_SAVE:-true}" = "false" ]; then
exit 0
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ -z "$TRANSCRIPT_PATH" ]; then
exit 0
fi
# Run capture in the background — fires every turn now, so avoid blocking
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null &
# Telemetry
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
exit 0
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Hook: stop — Cursor variant
#
# Same as on_stop.sh but uses CURSOR_PLUGIN_ROOT and Cursor-specific
# identity resolution.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
AGENT_ID=$(echo "$INPUT" | jq -r '.agent_id // ""' 2>/dev/null || echo "")
if [ -n "$AGENT_ID" ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ -z "$TRANSCRIPT_PATH" ]; then
exit 0
fi
echo "$INPUT" | python3 "$SCRIPT_DIR/capture_session_summary.py" 2>/dev/null || true
python3 "$SCRIPT_DIR/telemetry.py" session_stop 2>/dev/null &
exit 0
+201
View File
@@ -0,0 +1,201 @@
#!/usr/bin/env bash
# Hook: UserPromptSubmit
#
# Fires on every user message. Instead of pre-searching mem0 with the
# raw prompt, this injects a decision rubric telling the agent when
# and how to search itself. The agent has more context than this
# script does -- let it decide.
#
# Input: JSON on stdin (prompt, session_id, cwd, transcript_path)
# Output: Decision rubric injected into Claude's context (exit 0)
# Intentionally omit -e so the script always exits 0 even if jq fails --
# must never block the user's prompt.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
fi
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
# Acknowledgements and short replies don't warrant memory context
if [ ${#PROMPT} -lt 20 ]; then
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
# Rubric dedup: only inject full rubric once per session.
# Key on session ID to avoid cross-session interference.
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
if [ -z "$SESSION_ID" ]; then
_SID_FILE="/tmp/mem0_session_id_${USER:-default}"
[ -f "$_SID_FILE" ] && SESSION_ID=$(cat "$_SID_FILE" 2>/dev/null) || true
fi
if [ -z "$SESSION_ID" ]; then
SESSION_ID="default_${USER:-unknown}"
fi
RUBRIC_DIR="${MEM0_RUBRIC_DIR:-/tmp}"
RUBRIC_FLAG="$RUBRIC_DIR/mem0_rubric_${SESSION_ID}"
RUBRIC_ALREADY_SHOWN=""
if [ -f "$RUBRIC_FLAG" ]; then
RUBRIC_ALREADY_SHOWN="true"
fi
# Track message count for periodic memory-save nudges.
# Every 5th substantial message, remind the agent to store learnings.
MSG_COUNT_FILE="/tmp/mem0_msg_count_${USER:-default}"
MSG_COUNT=0
if [ -f "$MSG_COUNT_FILE" ]; then
MSG_COUNT=$(cat "$MSG_COUNT_FILE" 2>/dev/null || echo "0")
fi
MSG_COUNT=$((MSG_COUNT + 1))
printf '%s' "$MSG_COUNT" > "$MSG_COUNT_FILE" 2>/dev/null || true
NEEDS_SAVE_NUDGE=""
if [ $((MSG_COUNT % 5)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ]; then
NEEDS_SAVE_NUDGE="true"
fi
# Detect stack traces and error patterns in the prompt (no API needed)
HAS_ERROR=""
if echo "$PROMPT" | grep -qE '(Traceback|panic:)'; then
HAS_ERROR="true"
elif echo "$PROMPT" | grep -qE '^\s*fatal: '; then
HAS_ERROR="true"
elif [ "$(echo "$PROMPT" | grep -cE '(Error:|Exception:|FAIL:)')" -ge 2 ]; then
HAS_ERROR="true"
fi
# Detect file paths in the prompt (no API needed)
FILE_PATHS=$(echo "$PROMPT" | grep -oE '([a-zA-Z0-9_./-]+\.(py|ts|tsx|js|jsx|rs|go|rb|java|sh|yaml|yml|json|toml|md|sql|css|html))\b' 2>/dev/null | head -5 || echo "")
# Detect session-resume patterns
HAS_RESUME=""
if echo "$PROMPT" | grep -qiE '(where (did )?(we|I) (leave|left) off|continue (from )?(where|last)|what were we (working|doing)|pick up where|resume (from |where)|what.s the (current|latest) (state|status)|catch me up|where are we)'; then
HAS_RESUME="true"
fi
# Detect explicit memory-save intent
HAS_REMEMBER=""
if echo "$PROMPT" | grep -qiE '(remember (this|that)|save (this|that) (fact|info|memory|note)|store (this|that)|don.t forget (this|that)|keep (this|that) in (mind|memory))'; then
HAS_REMEMBER="true"
fi
# Telemetry (background, fire-and-forget)
_TELEM_ARGS=""
[ -n "$HAS_ERROR" ] && _TELEM_ARGS="$_TELEM_ARGS --error_detected"
[ -n "$FILE_PATHS" ] && _TELEM_ARGS="$_TELEM_ARGS --file_paths_detected"
[ -n "$HAS_RESUME" ] && _TELEM_ARGS="$_TELEM_ARGS --resume_detected"
[ -n "$HAS_REMEMBER" ] && _TELEM_ARGS="$_TELEM_ARGS --remember_detected"
python3 "$SCRIPT_DIR/telemetry.py" user_prompt $_TELEM_ARGS 2>/dev/null &
# No API key — emit detections only, skip search rubric
if [ -z "${MEM0_API_KEY:-}" ]; then
_PROMPT_CTX=""
if [ -n "$HAS_ERROR" ]; then
_PROMPT_CTX="Error detected in prompt. Set MEM0_API_KEY to search past debugging context."
fi
if [ -n "$FILE_PATHS" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
fi
if [ -n "$_PROMPT_CTX" ]; then
jq -cn --arg ctx "$_PROMPT_CTX" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
fi
exit 0
fi
USER_ID="$MEM0_RESOLVED_USER_ID"
_PROMPT_CTX=""
if [ -n "$HAS_RESUME" ]; then
RESUME_RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" python3 -c "
import os, sys
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
from _search import search_memories, format_results_for_context
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
project_id = os.environ.get('MEM0_PROJECT_ID', 'unknown')
state = search_memories(api_key, user_id, project_id, 'session state current task', metadata_type='session_state', top_k=3)
decisions = search_memories(api_key, user_id, project_id, 'recent decisions and learnings', metadata_type='decision', top_k=3)
all_r = state + decisions
seen = set()
unique = []
for m in all_r:
mid = m.get('id', '')
if mid not in seen:
seen.add(mid)
unique.append(m)
if unique:
print(format_results_for_context(unique, heading='Session context recovered from mem0'))
print()
print('These memories provide context for resuming work.')
else:
print('No session state found in mem0.')
" 2>/dev/null || echo "")
if [ -n "$RESUME_RESULTS" ]; then
_PROMPT_CTX="${RESUME_RESULTS}"
fi
fi
if [ -n "$HAS_REMEMBER" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Remember intent detected. The /mem0:remember skill auto-classifies, sets confidence=1.0, and stores verbatim."
fi
if [ -z "$RUBRIC_ALREADY_SHOWN" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id."
touch "$RUBRIC_FLAG" 2>/dev/null || true
fi
if [ -n "$HAS_ERROR" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Error detected in prompt. Prior occurrences are available in mem0 via anti_pattern and task_learning type filters."
fi
if [ -n "$FILE_PATHS" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
fi
# Auto-capture: directly call mem0 API in background every 3rd message.
# At MSG_COUNT=3 the 3rd response isn't in the transcript yet (hook fires
# before Claude responds), so we capture 4 exchanges instead of 3. The
# overlapping window ensures the next batch (MSG_COUNT=6) picks up the
# exchange that was incomplete in the previous batch.
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null || echo "")
if [ "${MEM0_AUTO_SAVE:-true}" != "false" ] && [ $((MSG_COUNT % 3)) -eq 0 ] && [ "$MSG_COUNT" -gt 0 ] && [ -n "$TRANSCRIPT_PATH" ]; then
python3 "$SCRIPT_DIR/auto_capture.py" "$TRANSCRIPT_PATH" 2>/dev/null &
fi
# Prompt-based nudge as fallback when auto-capture hasn't run yet.
_ADDS=0
_STATS_FILE="/tmp/mem0_session_stats_${USER:-default}.json"
if [ -f "$_STATS_FILE" ]; then
_ADDS=$(python3 -c "import json; print(json.load(open('$_STATS_FILE')).get('adds',0))" 2>/dev/null || echo "0")
fi
if [ "$MSG_COUNT" -ge 3 ] && [ "$_ADDS" -lt "$((MSG_COUNT / 3))" ]; then
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}After responding, store any new decisions, learnings, or preferences from this exchange via add_memory. Keep it to 1 sentence per memory."
fi
if [ -n "$_PROMPT_CTX" ]; then
jq -cn --arg ctx "$_PROMPT_CTX" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
fi
exit 0
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Hook: beforeSubmitPrompt (Cursor)
#
# Wraps on_user_prompt.sh and converts plain-text output to Cursor's
# expected JSON format: {"continue":true,"user_message":"<text>"}
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEXT=$("$SCRIPT_DIR/on_user_prompt.sh" 2>/dev/null || echo "")
if [ -z "$TEXT" ]; then
jq -cn '{continue:true}'
exit 0
fi
jq -cn --arg msg "$TEXT" '{continue:true, user_message:$msg}'
exit 0
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Parse a mem0 export file and output JSON.
Input: path to a mem0-export-*.md file (sys.argv[1])
Output: JSON array of memory records to stdout
Exit: 0 always
Each block in the file is delimited by lines containing exactly "---".
Blocks have a YAML-like frontmatter section (key: value lines) followed
by a blank line and the memory content text.
Example block format:
---
id: abc123
created_at: 2024-01-01T00:00:00Z
type: task_learnings
confidence: 0.9
branch: main
files: src/foo.py, src/bar.py
categories: coding_conventions, task_learnings
---
The actual memory content text goes here.
"""
from __future__ import annotations
import json
import re
import sys
def parse_blocks(content: str) -> list[dict]:
"""Split content on '---' boundaries and parse each block.
Returns a list of dicts with keys:
id, type, confidence, branch, files (list), categories (list), content (str)
Blocks with empty content are skipped.
Missing optional fields default to "" (scalar) or [] (list fields).
"""
# Normalise line endings
content = content.replace("\r\n", "\n").replace("\r", "\n")
# Split on lines that are exactly "---"
raw_blocks = re.split(r"(?m)^---\s*$", content)
# After splitting on "---", the structure for each memory is:
# raw_blocks[0] = preamble (before first ---, typically empty)
# raw_blocks[1] = frontmatter for block 1
# raw_blocks[2] = content for block 1
# raw_blocks[3] = frontmatter for block 2
# raw_blocks[4] = content for block 2
# ...
# So frontmatter blocks are at odd indices (1, 3, 5, ...) and
# content blocks at even indices (2, 4, 6, ...).
results: list[dict] = []
# Pair up frontmatter + content starting at index 1
i = 1
while i < len(raw_blocks):
frontmatter_raw = raw_blocks[i]
content_raw = raw_blocks[i + 1] if i + 1 < len(raw_blocks) else ""
# Parse the frontmatter key-value pairs
fm = _parse_frontmatter(frontmatter_raw)
# Strip leading/trailing whitespace from content
memory_content = content_raw.strip()
# Skip blocks with empty content
if not memory_content:
i += 2
continue
record = {
"id": fm.get("id", ""),
"type": fm.get("type", ""),
"confidence": fm.get("confidence", ""),
"branch": fm.get("branch", ""),
"files": _parse_list_field(fm.get("files", "")),
"categories": _parse_list_field(fm.get("categories", "")),
"content": memory_content,
}
# Include created_at if present
if "created_at" in fm:
record["created_at"] = fm["created_at"]
results.append(record)
i += 2
return results
def _parse_frontmatter(text: str) -> dict[str, str]:
"""Parse simple 'key: value' lines from frontmatter text.
Only the first colon is used as the delimiter — values may contain colons.
Lines not matching 'key: value' are ignored.
"""
result: dict[str, str] = {}
for line in text.splitlines():
line = line.strip()
if not line:
continue
match = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$", line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
result[key] = value
return result
def _parse_list_field(value: str) -> list[str]:
"""Split a comma-separated value into a list, stripping whitespace.
Returns [] for empty/whitespace-only input.
"""
if not value or not value.strip():
return []
return [item.strip() for item in value.split(",") if item.strip()]
def main() -> None:
if len(sys.argv) < 2:
print("Usage: parse_export_file.py <path-to-export-file>", file=sys.stderr)
print("[]")
sys.exit(0)
filepath = sys.argv[1]
try:
if filepath == "-":
content = sys.stdin.read()
else:
with open(filepath, encoding="utf-8", errors="replace") as f:
content = f.read()
except OSError as e:
print(f"Error reading file: {e}", file=sys.stderr)
print("[]")
sys.exit(0)
records = parse_blocks(content)
print(json.dumps(records, ensure_ascii=False, indent=2))
sys.exit(0)
if __name__ == "__main__":
main()
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Parse mem0.md project configuration file.
Reads the optional ``mem0.md`` file in a project directory and extracts
retention policies from a ``## Retention`` section.
Retention format (inside the section):
<category>: <N>d — keep for N days
<category>: forever — never prune (returned as None)
Usage (CLI):
python3 parse_mem0_config.py [<cwd>]
Prints a JSON object mapping category names to day counts (int) or null
(forever) on stdout. Prints ``{}`` when no mem0.md or no ## Retention
section is found.
"""
from __future__ import annotations
import json
import os
import re
import sys
def find_mem0_config(cwd: str) -> str | None:
"""Look for ``mem0.md`` in *cwd*.
Returns the absolute path to ``mem0.md`` if found, else ``None``.
"""
candidate = os.path.join(cwd, "mem0.md")
return candidate if os.path.isfile(candidate) else None
def parse_retention(content: str) -> dict[str, int | None]:
"""Parse the ``## Retention`` section of *content*.
Scans for a heading that matches ``## Retention`` (case-insensitive),
then reads lines until the next ``##``-level heading or end of string.
Each non-blank, non-comment line inside the section is expected to be::
<category>: <N>d → days=N (int)
<category>: forever → days=None
Malformed lines are silently skipped.
Args:
content: Full text of a mem0.md file.
Returns:
Dict mapping category name (str) to day count (int) or ``None``
(forever). Empty dict when no ``## Retention`` section is found.
"""
# Find the ## Retention section (allow any amount of trailing whitespace /
# extra words, but the heading must start with "## Retention").
section_match = re.search(
r"^##\s+Retention[^\n]*\n(.*?)(?=^##\s|\Z)",
content,
flags=re.MULTILINE | re.DOTALL | re.IGNORECASE,
)
if not section_match:
return {}
section_text = section_match.group(1)
policies: dict[str, int | None] = {}
for line in section_text.splitlines():
# Strip comments and whitespace
line = re.sub(r"#.*$", "", line).strip()
if not line:
continue
# Match "<category>: <value>"
line_match = re.match(r"^([^:]+):\s*(.+)$", line)
if not line_match:
continue
category = line_match.group(1).strip()
value = line_match.group(2).strip().lower()
if value == "forever":
policies[category] = None
else:
days_match = re.match(r"^(\d+)d$", value)
if days_match:
policies[category] = int(days_match.group(1))
# else: malformed value — skip silently
return policies
def parse_section_kv(content: str, heading: str) -> dict[str, str]:
"""Parse a key-value section from mem0.md.
Looks for ``## <heading>`` (case-insensitive) and reads ``key: value``
lines until the next ``##``-level heading or end of string.
"""
pattern = rf"^##\s+{re.escape(heading)}[^\n]*\n(.*?)(?=^##\s|\Z)"
match = re.search(pattern, content, flags=re.MULTILINE | re.DOTALL | re.IGNORECASE)
if not match:
return {}
result: dict[str, str] = {}
for line in match.group(1).splitlines():
line = re.sub(r"#.*$", "", line).strip()
if not line:
continue
m = re.match(r"^([^:]+):\s*(.+)$", line)
if m:
result[m.group(1).strip()] = m.group(2).strip()
return result
def parse_section_list(content: str, heading: str) -> list[str]:
"""Parse a list section from mem0.md.
Looks for ``## <heading>`` and reads ``- item`` or bare lines.
"""
pattern = rf"^##\s+{re.escape(heading)}[^\n]*\n(.*?)(?=^##\s|\Z)"
match = re.search(pattern, content, flags=re.MULTILINE | re.DOTALL | re.IGNORECASE)
if not match:
return []
items: list[str] = []
for line in match.group(1).splitlines():
line = re.sub(r"#.*$", "", line).strip()
line = re.sub(r"^[-*]\s+", "", line).strip()
if line:
items.append(line)
return items
def parse_ignore_patterns(content: str) -> list[str]:
"""Parse the ``## Ignore`` section of *content*.
Each non-blank line is a glob pattern (e.g., ``node_modules``, ``*.lock``).
Lines starting with ``#`` are comments and skipped.
"""
pattern = r"^##\s+Ignore[^\n]*\n(.*?)(?=^##\s|\Z)"
match = re.search(pattern, content, flags=re.MULTILINE | re.DOTALL | re.IGNORECASE)
if not match:
return []
patterns: list[str] = []
for line in match.group(1).splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
line = re.sub(r"^[-*]\s+", "", line).strip()
if line:
patterns.append(line)
return patterns
def load_full_config(cwd: str | None = None) -> dict:
"""Load all config sections from mem0.md.
Returns a dict with keys: retention, search, categories, identity,
ignore, project_id.
Each is populated only if the corresponding ``##`` section exists.
"""
if cwd is None:
cwd = os.getcwd()
config_path = find_mem0_config(cwd)
if config_path is None:
return {}
try:
with open(config_path, encoding="utf-8") as fh:
content = fh.read()
except OSError:
return {}
config: dict = {}
retention = parse_retention(content)
if retention:
config["retention"] = retention
search = parse_section_kv(content, "Search")
if search:
config["search"] = search
categories = parse_section_list(content, "Categories")
if categories:
config["categories"] = categories
config["default_categories"] = categories
identity = parse_section_kv(content, "Identity")
if identity:
config["identity"] = identity
if "project_id" in identity:
config["project_id"] = identity["project_id"]
ignore = parse_ignore_patterns(content)
if ignore:
config["ignore"] = ignore
settings = parse_section_kv(content, "Settings")
if settings:
config["settings"] = settings
return config
def load_retention_policies(cwd: str | None = None) -> dict[str, int | None]:
"""Load retention policies from the mem0.md in *cwd*.
Combines :func:`find_mem0_config` and :func:`parse_retention` into a
single convenience function.
"""
if cwd is None:
cwd = os.getcwd()
config_path = find_mem0_config(cwd)
if config_path is None:
return {}
try:
with open(config_path, encoding="utf-8") as fh:
content = fh.read()
except OSError:
return {}
return parse_retention(content)
def main() -> int:
"""CLI entry point.
With ``--full``, prints the complete config. Without it, prints only
retention policies (backward-compatible).
With ``--key <dotted.path>``, prints the scalar value at that path in the
full config (e.g. ``--key settings.commit_prompts``). Prints an empty
string when the key is absent. Exits 1 only on unexpected errors.
"""
full_mode = "--full" in sys.argv
# Extract --key <value>
key_path: str | None = None
raw_args = sys.argv[1:]
filtered_args: list[str] = []
i = 0
while i < len(raw_args):
if raw_args[i] == "--key" and i + 1 < len(raw_args):
key_path = raw_args[i + 1]
i += 2
elif raw_args[i].startswith("--key="):
key_path = raw_args[i][len("--key="):]
i += 1
elif raw_args[i].startswith("--"):
i += 1 # skip other flags like --full
else:
filtered_args.append(raw_args[i])
i += 1
cwd = filtered_args[0] if filtered_args else os.getcwd()
if key_path is not None:
config = load_full_config(cwd)
# Traverse dotted path
value: object = config
for part in key_path.split("."):
if not isinstance(value, dict):
value = None
break
value = value.get(part)
print(value if value is not None else "")
return 0
if full_mode:
config = load_full_config(cwd)
else:
config = load_retention_policies(cwd)
print(json.dumps(config))
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""Session stats tracker for mem0 plugin.
Tracks memory adds/searches per session.
Uses /tmp/mem0_session_stats_$USER.json (single file per user, reset on init).
Usage:
python session_stats.py init # reset for new session
python session_stats.py add <category> # record a memory write
python session_stats.py search # record a search
python session_stats.py report # print summary, clean up temp file
"""
from __future__ import annotations
import json
import os
import sys
from datetime import datetime
STATS_FILE = f"/tmp/mem0_session_stats_{os.environ.get('USER', 'default')}.json"
def _load() -> dict:
if os.path.isfile(STATS_FILE):
try:
with open(STATS_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
pass
return {
"adds": 0,
"searches": 0,
"categories": [],
"category_counts": {},
"started": datetime.now().isoformat(),
}
def _save(stats: dict) -> None:
with open(STATS_FILE, "w") as f:
json.dump(stats, f)
MAX_RECENT_IDS = 50
def init() -> None:
_save({
"adds": 0,
"searches": 0,
"categories": [],
"category_counts": {},
"recent_ids": [],
"started": datetime.now().isoformat(),
})
def record_add(category: str = "", memory_id: str = "") -> None:
stats = _load()
stats["adds"] = stats.get("adds", 0) + 1
if category:
if category not in stats.get("categories", []):
stats.setdefault("categories", []).append(category)
counts = stats.setdefault("category_counts", {})
counts[category] = counts.get(category, 0) + 1
if memory_id:
recent = stats.setdefault("recent_ids", [])
recent.append({"id": memory_id, "category": category, "ts": datetime.now().isoformat()})
if len(recent) > MAX_RECENT_IDS:
stats["recent_ids"] = recent[-MAX_RECENT_IDS:]
_save(stats)
def record_search() -> None:
stats = _load()
stats["searches"] = stats.get("searches", 0) + 1
_save(stats)
def peek() -> str:
"""Return current stats as JSON without clearing the file."""
stats = _load()
return json.dumps(stats)
def report() -> str:
stats = _load()
adds = stats.get("adds", 0)
searches = stats.get("searches", 0)
categories = stats.get("categories", [])
if adds == 0 and searches == 0:
return ""
parts = []
category_counts = stats.get("category_counts", {})
if category_counts:
breakdown = ", ".join(f"{c} {n}" for c, n in sorted(category_counts.items(), key=lambda x: -x[1]))
parts.append(f"Session: wrote {adds} memories ({breakdown}), retrieved {searches}")
else:
parts.append(f"Session: wrote {adds} memories, retrieved {searches}")
if categories:
parts.append(f"Categories touched: {', '.join(categories)}")
return ". ".join(parts) + "."
def main() -> int:
if len(sys.argv) < 2:
print("Usage: session_stats.py [init|add|search|report]", file=sys.stderr)
return 1
cmd = sys.argv[1]
if cmd == "init":
init()
elif cmd == "add":
category = sys.argv[2] if len(sys.argv) > 2 else ""
memory_id = sys.argv[3] if len(sys.argv) > 3 else ""
record_add(category, memory_id)
elif cmd == "search":
record_search()
elif cmd == "peek":
print(peek())
elif cmd == "report":
result = report()
if result:
print(result)
else:
print("Session: no memory operations.")
else:
print(f"Unknown command: {cmd}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Fetch recent memories and format a compact timeline for SessionStart.
Searches mem0 cloud API for the most recent memories in the project
and formats them as a compact activity timeline injected below the
existing SessionStart banner.
Input: env vars for identity (MEM0_API_KEY, MEM0_RESOLVED_USER_ID, etc.)
Output: Compact timeline text to stdout (empty if nothing found)
"""
from __future__ import annotations
import json
import os
import sys
import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _formatting import TYPE_ICONS, format_age
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_project_id
API_URL = "https://api.mem0.ai"
MAX_RECENT = 10
MAX_SUMMARIES = 3
FETCH_TIMEOUT = 5
def fetch_recent_memories(api_key: str, user_id: str, project_id: str) -> list[dict]:
"""Fetch the most recent memories for this project via GET list endpoint."""
global_search = os.environ.get("MEM0_GLOBAL_SEARCH", "false") == "true"
if global_search:
filters = {"OR": [{"user_id": "*"}]}
else:
filters = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
body = json.dumps({"filters": filters}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/?page=1&page_size={MAX_RECENT}",
data=body,
headers={
"Authorization": f"Token {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
result = json.loads(r.read())
if isinstance(result, dict) and "results" in result:
return result["results"][:MAX_RECENT]
if isinstance(result, list):
return result[:MAX_RECENT]
return []
except Exception:
return []
def format_timeline(memories: list[dict]) -> str:
"""Format memories into a compact recent activity timeline."""
if not memories:
return ""
lines = ["### Recent Activity", ""]
for m in memories:
mid = m.get("id", "?")[:8]
text = (m.get("memory", "") or "")[:120].replace("\n", " ").strip()
meta = m.get("metadata") or {}
cat = meta.get("type", "unknown")
icon = TYPE_ICONS.get(cat, "❓")
age = format_age(m)
age_str = f" ({age})" if age else ""
lines.append(f"- {icon} [{cat}]{age_str} {text} [mem0:{mid}]")
lines.append("")
lines.append("Search mem0 for details on any of these, or for past decisions and task learnings relevant to the current task.")
return "\n".join(lines)
def main():
api_key = resolve_api_key()
if not api_key:
return
user_id = resolve_user_id()
project_id = resolve_project_id(os.environ.get("MEM0_CWD"))
memories = fetch_recent_memories(api_key, user_id, project_id)
if not memories:
return
timeline = format_timeline(memories)
if timeline:
print(timeline, end="")
if __name__ == "__main__":
try:
main()
except Exception:
pass
sys.exit(0)
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Replace mem0's default category taxonomy with one tuned for coding workflows.
mem0 auto-tags every memory with one or more `categories`. By default the list
is consumer-oriented (food, hobbies, music, ...), which is meaningless for code.
This script replaces the project's category list with a coding-focused one.
Uses the mem0ai SDK (client.project.update). The SDK is installed into a
persistent venv at ${CLAUDE_PLUGIN_DATA}/venv by the ensure_deps.sh hook.
Usage:
python setup_coding_categories.py # dry-run: show current vs proposed
python setup_coding_categories.py --apply # actually call project.update()
Requires MEM0_API_KEY (or CLAUDE_PLUGIN_OPTION_MEM0_API_KEY).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
_script_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, _script_dir)
from _identity import resolve_api_key # noqa: E402
_plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", os.path.join(_script_dir, ".."))
_data_dir = os.environ.get("CLAUDE_PLUGIN_DATA", os.path.join(os.path.expanduser("~"), ".mem0", "plugin-data"))
_venv_site = os.path.join(_data_dir, "venv", "lib")
if os.path.isdir(_venv_site):
for d in sorted(os.listdir(_venv_site)):
sp = os.path.join(_venv_site, d, "site-packages")
if os.path.isdir(sp) and sp not in sys.path:
sys.path.insert(1, sp)
CODING_CATEGORIES = [
{
"architecture_decisions": (
"Design choices, system structure, technology selection, trade-offs evaluated, "
"and architectural patterns adopted in the project."
)
},
{
"anti_patterns": (
"Approaches that failed, debugging dead-ends, common mistakes to avoid, "
"and lessons learned from things that didn't work."
)
},
{
"task_learnings": (
"Strategies and approaches that succeeded for specific tasks, including tooling "
"tricks, workflow shortcuts, and effective problem-solving patterns."
)
},
{
"tooling_setup": (
"Development environment, build tools, dependencies, package managers, deploy "
"pipelines, and configuration steps for the project."
)
},
{
"bug_fixes": (
"Specific bug fixes with root cause analysis, the fix applied, and how the bug "
"was diagnosed -- useful for recognising similar issues later."
)
},
{
"coding_conventions": (
"Code style, naming patterns, file organisation, error-handling conventions, "
"and team agreements about how code is written in this project."
)
},
{
"user_preferences": (
"User's stated preferences for tools, libraries, languages, formatting, "
"and ways of working."
)
},
{
"dependency_decisions": (
"Why specific libraries, frameworks, or package versions were chosen or replaced, "
"including the alternatives considered and the reasoning behind the selection."
)
},
{
"performance_findings": (
"Profiling results, bottlenecks identified, optimisations applied, and measurable "
"improvements achieved -- useful for avoiding regressions and guiding future work."
)
},
{
"security_constraints": (
"Security requirements, authentication and authorisation rules, data-handling "
"constraints, compliance obligations, and known threat mitigations in effect."
)
},
{
"testing_patterns": (
"Test strategies, frameworks chosen, coverage targets, fixture patterns, mocking "
"approaches, and how the test suite is structured for this project."
)
},
{
"data_model": (
"Schema definitions, database column semantics, domain object relationships, "
"field constraints, and how data flows between storage and application layers."
)
},
{
"api_contracts": (
"API endpoint shapes, request and response schemas, authentication requirements, "
"versioning policy, and any breaking-change commitments or deprecation timelines."
)
},
{
"deployment_runbook": (
"How to build, release, deploy, and roll back the project. CI/CD pipeline steps, "
"environment-specific configuration, and on-call runbook entries."
)
},
{
"team_norms": (
"Team working agreements, PR review etiquette, branching strategy, on-call "
"rotation, and other social or process conventions the team has agreed on."
)
},
{
"domain_glossary": (
"Domain-specific terms, abbreviations, and acronyms with their precise meanings "
"in this project -- prevents misunderstandings across code, docs, and discussion."
)
},
{
"experiment_results": (
"Results from A/B tests, feature-flag experiments, spikes, or proof-of-concept "
"work -- what was tried, what was measured, and what conclusion was reached."
)
},
]
def _print_categories(label: str, cats):
print(f"=== {label} ===")
if cats:
print(json.dumps(cats, indent=2))
else:
print("(none / using mem0 defaults)")
print()
def _categories_match(current: list | None, proposed: list) -> bool:
"""Compare categories by key sets, tolerating order differences and extra API fields."""
if not current:
return False
current_keys = {k for d in current if isinstance(d, dict) for k in d}
proposed_keys = {k for d in proposed if isinstance(d, dict) for k in d}
if current_keys != proposed_keys:
return False
current_map = {k: v for d in current if isinstance(d, dict) for k, v in d.items()}
proposed_map = {k: v for d in proposed if isinstance(d, dict) for k, v in d.items()}
return all(
current_map.get(k, "").strip() == v.strip()
for k, v in proposed_map.items()
)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument(
"--apply",
action="store_true",
help="Actually call project.update(). Without this flag, runs in dry-run mode.",
)
args = ap.parse_args()
api_key = resolve_api_key()
if not api_key:
print("ERROR: MEM0_API_KEY is not set. Export it or configure it via plugin userConfig.", file=sys.stderr)
return 1
os.environ["MEM0_API_KEY"] = api_key
try:
from mem0 import MemoryClient
except ImportError:
print(
"ERROR: mem0ai SDK not found. The plugin's ensure_deps.sh hook should\n"
"install it automatically on session start. Try restarting Claude Code,\n"
"or run manually: pip install mem0ai",
file=sys.stderr,
)
return 1
try:
client = MemoryClient()
except Exception as e:
print(
f"ERROR initialising MemoryClient: {e}\n"
"Most commonly this is an invalid MEM0_API_KEY -- check the key at "
"https://app.mem0.ai/dashboard/api-keys",
file=sys.stderr,
)
return 1
try:
current = client.project.get(fields=["custom_categories"])
current_cats = current.get("custom_categories") if isinstance(current, dict) else None
except Exception as e:
print(f"ERROR fetching current categories: {e}", file=sys.stderr)
return 1
_print_categories("Current project categories", current_cats)
_print_categories("Proposed coding categories", CODING_CATEGORIES)
if not args.apply:
print("Dry-run only -- no changes made. Re-run with --apply to write.")
return 0
if _categories_match(current_cats, CODING_CATEGORIES):
print("Categories already match -- skipping update.")
return 0
print("Applying coding categories...")
try:
response = client.project.update(custom_categories=CODING_CATEGORIES)
except Exception as e:
print(f"ERROR applying update: {e}", file=sys.stderr)
return 1
print("Done.", response if response else "")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""Lightweight fire-and-forget telemetry for the mem0 plugin.
Sends anonymous usage events to PostHog using the same project key and
endpoint as the mem0 Python SDK and CLI. No posthog library dependency —
uses stdlib urllib directly (same pattern as cli/python telemetry_sender.py).
CLI usage (called from hooks as a background subprocess):
python3 telemetry.py <event_type> [--memory_count=N] [--categories_count=N]
[--error_detected] [--file_paths_detected]
[--source=<src>] [--tool=<name>]
Opt-out: set MEM0_TELEMETRY=false (or 0/no/off) to disable all telemetry.
Never sends: user content, memory content, API keys, raw user/project IDs.
Only sends: event type, platform, plugin version, anonymized hashes, counts.
"""
from __future__ import annotations
import hashlib
import json
import os
import platform
import sys
import urllib.error
import urllib.request
def _load_plugin_version() -> str:
try:
plugin_json = os.path.join(os.path.dirname(__file__), "..", ".claude-plugin", "plugin.json")
with open(plugin_json) as f:
return json.load(f).get("version", "unknown")
except (OSError, json.JSONDecodeError, KeyError):
return "unknown"
PLUGIN_VERSION = _load_plugin_version()
POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
REQUEST_TIMEOUT = 2
SAMPLE_RATE = 1.0
def _sha256(value: str) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _distinct_id() -> str:
"""Stable anonymous ID: SHA-256 of API key if available, else SHA-256 of username."""
api_key = os.environ.get("MEM0_API_KEY") or os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY") or ""
if api_key:
return hashlib.sha256(api_key.encode()).hexdigest()[:32]
user_id = os.environ.get("MEM0_RESOLVED_USER_ID") or os.environ.get("USER") or "unknown"
return _sha256(user_id)
def detect_platform() -> str:
if os.environ.get("PLUGIN_ROOT"):
return "codex"
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_PLUGIN_ROOT"):
return "claude-code"
if os.environ.get("CURSOR_PLUGIN_ROOT"):
return "cursor"
if os.environ.get("WINDSURF_PLUGIN_ROOT"):
return "windsurf"
return "plugin"
def is_enabled() -> bool:
return os.environ.get("MEM0_TELEMETRY", "true").lower() not in ("false", "0", "no", "off")
def build_posthog_payload(event_name: str, properties: dict | None = None) -> dict:
project_id = os.environ.get("MEM0_PROJECT_ID") or "unknown"
return {
"api_key": POSTHOG_API_KEY,
"distinct_id": _distinct_id(),
"event": event_name,
"properties": {
**(properties or {}),
"source": "plugin",
"platform": detect_platform(),
"plugin_version": PLUGIN_VERSION,
"project_hash": _sha256(project_id),
"os": sys.platform,
"os_version": platform.version(),
"sample_rate": SAMPLE_RATE,
"$process_person_profile": False,
"$lib": "posthog-python",
},
}
def send(payload: dict) -> None:
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
POSTHOG_HOST,
data=data,
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT):
pass
except Exception:
pass
def emit(event_type: str, properties: dict | None = None) -> None:
if not is_enabled():
return
send(build_posthog_payload(f"plugin.{event_type}", properties))
def main() -> int:
if not is_enabled():
return 0
if len(sys.argv) < 2:
return 1
event_type = sys.argv[1]
properties: dict = {}
for arg in sys.argv[2:]:
if arg.startswith("--memory_count="):
try:
properties["memory_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
elif arg.startswith("--categories_count="):
try:
properties["categories_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
elif arg == "--error_detected":
properties["error_detected"] = True
elif arg == "--file_paths_detected":
properties["file_paths_detected"] = True
elif arg.startswith("--source="):
properties["source_detail"] = arg.split("=", 1)[1]
elif arg.startswith("--tool="):
properties["tool"] = arg.split("=", 1)[1]
elif arg.startswith("--files_count="):
try:
properties["files_count"] = int(arg.split("=", 1)[1])
except ValueError:
pass
emit(event_type, properties)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,48 @@
---
name: context-loader
description: Searches and injects relevant memories into context before starting work on a task. Use when beginning a new task, switching context, or when project history, past decisions, or coding conventions need to be loaded.
---
# Context Loader
Pre-fetches relevant memories to prime context before working on a task.
## When to use
- Session start (invoke manually or auto-triggered by skill description matching)
- User starts work on a specific feature or file set
- Complex multi-step task begins
- User says "what do we know about X" or "context for X"
## Steps
1. **Extract topics** from current message/task. Identify: file paths, module names, feature areas, error patterns.
2. **Run 2-4 parallel `search_memories` calls** with different angles:
| Query angle | Filter | Purpose |
|---|---|---|
| Feature/module name | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}` | Architecture decisions |
| File paths mentioned | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "convention"}}]}` | Coding patterns |
| Error keywords (if any) | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "anti_pattern"}}]}` | Known pitfalls |
| Broad project context | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}` | Catch-all |
3. **Deduplicate** results by memory ID across all search responses.
4. **Output compact context block** (max 10 memories):
```
context-loader: loaded <N> memories for "<task summary>"
- [decision] <content> [mem0:<short_id>]
- [convention] <content> [mem0:<short_id>]
- [anti_pattern] <content> [mem0:<short_id>]
```
5. If **zero results**: output nothing. Don't announce empty context.
## Constraints
- **Read-only** — never modify or delete memories
- **Max 10 memories** returned (most relevant only)
- **Silent on empty** — only surfaces findings if relevant context exists
- Skip memories already visible in current session context
@@ -0,0 +1,232 @@
---
name: dream
description: Consolidates stored memories by merging duplicates, resolving contradictions, and pruning stale entries. Use when memory count is high, search results feel noisy or repetitive, or periodic cleanup is needed to maintain memory quality.
---
# Mem0 Dream — Memory Consolidation
This skill performs a memory consolidation pass: it fetches all project memories,
identifies near-duplicates, flags contradictions, and prunes stale entries based on
configured retention policies. All proposed changes are shown as a diff for user
approval before anything is modified.
**IMPORTANT: Execute steps strictly in order (1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead.**
## Step 1: Load Retention Policies
Determine the active retention policy by running the parser script. Use the
appropriate `PLUGIN_ROOT` variable for the current platform (`${CLAUDE_PLUGIN_ROOT}`,
`${CODEX_PLUGIN_ROOT}`, or `${CURSOR_PLUGIN_ROOT}`):
```bash
python3 "<PLUGIN_ROOT>/scripts/parse_mem0_config.py" "<cwd>"
```
Parse the JSON output (a dict of `category → days | null`). If the script fails
or returns `{}`, fall back to these built-in defaults:
| `metadata.type` | Default retention |
|---|---|
| `session_state` | 90 days |
| `compact_summary` | 90 days |
| all others | no pruning |
Store the resolved policies for use in Step 3.
---
## Step 2: Fetch ALL Project Memories
Call `get_memories` to retrieve every memory for the active project:
```python
get_memories(
filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]},
page_size=200,
)
```
If the response indicates more pages exist, paginate until all memories are fetched.
Collect the full list before proceeding. If zero memories are found, print:
```
No memories found for project <project_id>. Nothing to consolidate.
```
…and stop.
---
## Step 3: Analyze — Find Issues
Work entirely in-memory; do not modify anything yet.
Group memories by `metadata.type` (use `"unknown"` when the field is absent).
For each group, identify the following:
### 3a. Near-duplicate pairs (merge candidates)
Two memories are near-duplicates when they express the same fact or decision but
phrased differently (e.g., "Use PostgreSQL for auth" and "Auth DB is PostgreSQL").
Heuristics — two memories are near-duplicates if **all** of these hold:
- Similarity threshold: estimated cosine similarity > 0.9 (use noun/keyword overlap as proxy — if >60% of significant nouns overlap, treat as >0.9 similarity).
- Same `metadata.type`.
- Neither memory is pinned (`metadata.pinned != true`).
For each qualifying pair, draft a merged version that is more complete and specific
than either original.
### 3b. Contradictions
Two memories contradict when they assert opposing facts about the same topic
(e.g., "Deploy to ECS" vs. "Deploy to Vercel").
Identify the likely winner: the more recent memory with higher confidence wins.
Store both IDs and their content for user review.
### 3c. Prune candidates
A memory is a prune candidate when **any** of the following is true:
1. Its `metadata.type` has a retention policy and the memory is older than the
configured number of days (compare `created_at` to today).
2. Its confidence score is below 0.3 AND it contains no information unique to
this project (no file paths, identifiers, or domain-specific nouns).
**Always skip memories where `metadata.pinned == true`**, regardless of age or
confidence.
---
## Step 4: Print Diff Report
Print a structured diff to the terminal before making any changes. Use exactly
this format:
```
## dream — consolidation report
Merges (<N>):
[mem0:<id1>] + [mem0:<id2>] → "<merged content, 100 chars>"
Conflicts (<N>):
[mem0:<idA>] vs [mem0:<idB>] — "<topic>" [A/B/skip]
Prune (<N>):
[mem0:<id>] — <type>, <age>d old
Proposed: <N> merges, <N> prunes, <N> conflicts. Apply? [Y/n]
```
If there are zero items in any category, omit that section entirely.
If there are zero total proposals (no merges, no prunes, no conflicts), print:
```
Dream complete. No duplicate, contradictory, or stale memories found.
```
…and stop.
---
## Step 5: Wait for User Input and Apply
### 5a. Contradictions
For each `CONFLICT` pair in the report, wait for the user to type `A`, `B`, or
`skip` (case-insensitive). If they enter nothing (empty), treat as `skip`.
Record the winner for each pair before proceeding to the final apply confirmation.
### 5b. Final confirmation
After all conflict resolutions are collected, prompt:
```
Apply? [Y/n]
```
If the user types `n` or `no` (case-insensitive), print `Cancelled. No changes made.`
and stop.
If the user confirms (`Y`, `yes`, or empty / Enter), apply all changes in this order:
#### Merges
For each approved merge pair:
1. `delete_memory(<id1>)`
2. `delete_memory(<id2>)`
3. `add_memory` with:
- `text="<merged content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>` (top-level, not in metadata)
- `metadata={"type": "<original type>", "branch": "<active_branch>", "confidence": <higher of the two original scores>, "source": "mem0-dream"}`
- `infer=False`
#### Contradictions (resolved)
For each resolved conflict where the user chose A or B:
- Delete the loser (the non-chosen memory): `delete_memory(memory_id=<loser_id>)`
Contradictions where the user chose `skip` are left untouched.
#### Prunes
For each prune candidate:
- `delete_memory(<memory_id>)`
---
## Step 6: Print Summary
After all changes are applied, print:
```
Dream complete — merged: <N>, pruned: <N>, conflicts resolved: <N>, skipped: <N>
```
---
## Auto mode
When invoked with `--auto` (e.g., `/mem0:dream --auto`), run non-interactively:
- **Merges**: applied automatically (no contradiction, both are compatible).
- **Prunes**: applied automatically (age/confidence-based, no ambiguity).
- **Contradictions**: skipped — they require human judgment.
### Concurrency guard
Before doing any work, check for a lock file at `/tmp/mem0_dream_auto.lock`:
- If the lock file exists and is less than 10 minutes old, print `[mem0-dream --auto] Another run in progress — skipping.` and stop.
- Otherwise, create the lock file (write the current timestamp). Delete it when done (in all exit paths).
### Execution
In auto mode:
1. Load policies and fetch memories (Steps 1–3) as normal.
2. Apply merges and prunes silently without printing the diff or prompting.
3. Print a compact summary:
```
[mem0-dream --auto] project=<id> merged=<N> pruned=<N> conflicts_skipped=<N>
```
4. If contradictions were detected but skipped, check if a `mem0-dream-auto` reminder already exists before storing one:
- Search for existing reminders: `search_memories(query="mem0-dream contradictions manual review", filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}, {"metadata": {"source": "mem0-dream-auto"}}]}, top_k=1)`
- If a result exists with similarity > 0.9, skip storing the reminder (one already exists).
- If no match, store the reminder:
```python
add_memory(
text="mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively.",
user_id="<active_user_id>",
app_id="<active_project_id>",
metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": "<active_branch>"},
infer=False,
)
```
## See also
- `/mem0:forget` — targeted deletion of specific memories (search + confirm + delete)
- `/mem0:health --deep` — quick quality scan without applying changes
@@ -0,0 +1,76 @@
---
name: export
description: Exports all project memories to a portable Markdown file for backup or migration. Use when backing up memories, migrating to another project, sharing memory state with teammates, or archiving before cleanup.
---
# Mem0 Export
Export all memories for the current project to a portable Markdown file.
## Execution
### Step 1: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 2: Fetch all memories
Call `get_memories` with:
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=200`
If the response is paginated (i.e. the result contains a `next` cursor or the count equals `page_size`), continue fetching pages until all memories are retrieved.
### Step 3: Format each memory as a YAML-frontmatter block
For each memory record, produce a block in this exact format:
```
---
id: <memory.id>
created_at: <memory.created_at>
type: <memory.metadata.type or "">
confidence: <memory.metadata.confidence or "">
branch: <memory.metadata.branch or "">
files: <memory.metadata.files joined with ", " or "">
categories: <memory.categories joined with ", " or "">
---
<memory.memory or memory content string>
```
Notes:
- The `---` delimiters must be on their own lines with no extra whitespace.
- `files` and `categories` are written as comma-separated values on a single line.
- Leave a blank line after the content before the next `---` (for readability).
- If a field is missing or null, write an empty string (not "null").
### Step 4: Write the export file
Determine the output filename:
```
mem0-export-<project_id>-<YYYY-MM-DD>.md
```
Where `<YYYY-MM-DD>` is today's date in UTC.
Write all formatted blocks to this file using the Write tool (or equivalent). The file is written to the current working directory.
### Step 5: Print summary
```
Exported <N> memories to <filename>
```
Where `<N>` is the total number of memory blocks written.
## Error Handling
- If `get_memories` returns an error or zero memories, print:
```
No memories found for project <project_id>. Nothing exported.
```
- If the write fails, report the error to the user.
@@ -0,0 +1,71 @@
---
name: forget
description: Deletes memories by search query or memory ID with confirmation before removal. Use when removing outdated decisions, incorrect memories, sensitive data, or cleaning up after experiments. Also handles undo of recent additions.
---
# Mem0 Forget
Delete specific memories from mem0.
## Execution
### Step 1: Parse input
The user provides either:
- A search query: `/mem0:forget auth module decisions`
- A memory ID: `/mem0:forget <memory_id>`
If no argument, ask: "What should I forget? Provide a search query or memory ID."
### Step 2: Find memories
**If memory ID provided** (looks like a UUID or hex string):
- Call `get_memory` with the ID to verify it exists.
- Show: `Found: "<memory content first 120 chars>" (created <date>)`
**If search query provided:**
- Call `search_memories` with:
- `query=<user's query>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<project_id>"}]}`
- `top_k=10`
- Show numbered list:
```
Found <N> memories matching "<query>":
1. <content, 120 chars> (type: <type>, created: <date>) [ID: <short_id>]
2. ...
```
### Step 3: Confirm
Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'."
For a single memory ID, ask: "Delete this memory? [y/N]"
**Never delete without confirmation.** This is destructive.
### Step 4: Delete
For each confirmed memory, call `delete_memory` with the memory ID.
### Step 5: Report
```
Deleted <N> memories.
```
If any deletions failed, report which ones and why.
## Undo recent writes
If the user says "undo last N memories" or "undo last write":
1. Read session stats to get recently written memory IDs:
```bash
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts"
python3 "$SCRIPT_DIR/session_stats.py" peek
```
2. Parse the `recent_ids` array from the JSON output. Each entry has `id`, `category`, `ts`.
3. Show the last N entries (default 1) and ask for confirmation.
4. Delete confirmed entries via `delete_memory`.
If `recent_ids` is empty, tell the user: "No recent memory IDs tracked this session. Try `/mem0:tour` to browse recent memories, or `/mem0:forget <search query>` to find specific ones."
@@ -0,0 +1,160 @@
---
name: health
description: Diagnoses mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, add_memory errors occur, MCP connection drops, or to verify the plugin is working correctly.
---
# Mem0 Health Check
Run a diagnostic check on the mem0 plugin. Useful for troubleshooting.
## Execution
Run ALL checks, then display a single summary. Do not stop on the first failure.
### Check 1: API key
```bash
_KEY="${MEM0_API_KEY:-${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-}}"
[ -n "$_KEY" ] && echo "${_KEY:0:6}..." || echo "NOT_SET"
```
- If `NOT_SET`: FAIL — "No API key configured"
- If set: PASS — the command already prints only the first 6 chars
### Check 2: Identity resolution
Resolve identity using the plugin's own resolver scripts to match what hooks use:
```bash
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts"
source "$SCRIPT_DIR/_identity.sh" 2>/dev/null
echo "user_id=${MEM0_RESOLVED_USER_ID:-}"
echo "project_id=${MEM0_PROJECT_ID:-}"
echo "branch=${MEM0_BRANCH:-}"
```
If `CLAUDE_PLUGIN_ROOT` is not available, fall back to:
- `user_id`: from `MEM0_USER_ID` or `$USER`
- `project_id`: from `MEM0_PROJECT_ID` or check `~/.mem0/project_map.json` for `$PWD`
- `branch`: from `git branch --show-current`
PASS if all three are non-empty. WARN if any falls back to defaults.
### Check 3: MCP server connectivity
Call `search_memories` with:
- `query="health check"`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `top_k=1`
- If returns successfully (even empty): PASS
- If errors: FAIL — show the error message
### Check 4: Memory write capability
Call `add_memory` with:
- `text="Health check probe — safe to delete."`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "health_check", "probe": true}`
- `infer=False`
The response returns `event_id` (v3 writes are async). Call `get_event_status(event_id=<event_id>)` to check processing.
- If status is `SUCCEEDED`: PASS — extract the memory ID from the event result, then call `delete_memory` with that ID to clean up.
- If status is `PENDING` after 5 seconds: PASS (write accepted, processing delayed)
- If errors: FAIL — show the error.
### Check 5: Session stats tracker
Check if the session stats file exists and is readable:
```bash
STATS_FILE="/tmp/mem0_session_stats_${USER}.json"
if [ -f "$STATS_FILE" ] && python3 -c "import json; json.load(open('$STATS_FILE'))" 2>/dev/null; then
echo "OK"
else
echo "FAIL"
fi
```
This file is created by the SessionStart hook and updated by PostToolUse hooks throughout the session. If it doesn't exist, the session hooks may not have fired yet — try sending a message first, then recheck.
### Display
```
## mem0 health
PASS API Key m0-dVe...
PASS Identity user=kartik, project=mem0, branch=main
PASS MCP Connection 142ms
PASS Write/Read write + delete OK
PASS Session Tracker stats file active
All checks passed.
```
If any check fails, add a `## Troubleshooting` section with specific fix steps for each failure.
## Extended mode: Memory Quality Analysis
When invoked with `--deep` (e.g., `/mem0:health --deep`), run the standard 5 checks above **plus** a memory quality scan.
### Quality Check 1: Duplicates
Call `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Compare all pairs within the same `metadata.type` group for high textual overlap (shared nouns/keywords > 60%). Report:
```
Potential duplicates: <N> pairs
[mem0:<id1>] ≈ [mem0:<id2>] — both about "<shared topic>"
```
### Quality Check 2: Stale memories
Flag memories where:
- `metadata.type` is `session_state` or `compact_summary` AND older than 90 days
- `metadata.confidence` < 0.3 AND older than 30 days
```
Stale candidates: <N>
[mem0:<id>] — session_state, 142d old
```
### Quality Check 2b: Low-confidence memories
Flag memories where `metadata.confidence` < 0.5 (regardless of age). Report separately from stale:
```
Low-confidence memories: <N>
[mem0:<id>] — confidence=0.3, "<content preview>"
```
### Quality Check 3: Contradictions
Within each `metadata.type` group, flag pairs that assert opposing facts about the same topic. Use semantic judgment — look for negation patterns, conflicting tool/framework choices, or reversed decisions.
```
Possible contradictions: <N>
[mem0:<idA>] vs [mem0:<idB>] — conflicting on "<topic>"
```
### Quality Check 4: Orphan memories
Memories with no `metadata.type` set, or with `metadata.type` not in the 17 known coding categories. These were likely written without proper tagging.
```
Untagged/orphan memories: <N>
```
### Quality summary
```
## Memory Quality
Duplicates: <N> · Stale: <N> · Contradictions: <N> · Orphans: <N>
```
If all counts are 0: `Memory quality: clean.`
If any non-zero: append `Run /mem0:dream to fix.`
To fix issues found by `--deep`, run `/mem0:dream` for automated consolidation (merges, prunes, conflict resolution).
@@ -0,0 +1,177 @@
---
name: import
description: Imports memories from an exported Markdown file or MEMORY.md into the current project. Use when migrating from another project, restoring from backup, importing Claude Code native MEMORY.md content, or setting up a new project with existing knowledge.
---
# Mem0 Import
Import memories from a mem0 export file into the current project.
## Execution
### Step 1: Determine the export file to import
If the user provided a filename as an argument to `/mem0:import <filename>`, use that file.
Otherwise, list `.md` files in the current directory whose names contain `mem0-export`:
```bash
ls -1 *.md 2>/dev/null | grep mem0-export || echo "No export files found"
```
If multiple files are found, ask the user which one to import. If none are found, print:
```
No mem0-export files found in the current directory.
Run /mem0:export first, or provide the filename: /mem0:import <path-to-file>
```
### Step 2: Parse the export file
Determine the plugin root. Use the appropriate variable for the current platform:
- Claude Code: `${CLAUDE_PLUGIN_ROOT}`
- Codex: `${CODEX_PLUGIN_ROOT}`
- Cursor: `${CURSOR_PLUGIN_ROOT}`
Run the parser script to extract memory records as JSON:
```bash
python3 "<PLUGIN_ROOT>/scripts/parse_export_file.py" "<path-to-export-file>"
```
This outputs a JSON array where each element has:
- `id` — original memory ID (for reference only; a new ID will be assigned on import)
- `type` — metadata type
- `confidence` — metadata confidence value
- `branch` — metadata branch
- `files` — list of associated files
- `categories` — list of categories
- `content` — the memory text
If the script fails or outputs `[]`, print:
```
Failed to parse <filename> or file contains no valid memory blocks.
```
and stop.
### Step 3: Resolve identity
Determine the active identity:
- `user_id` from `MEM0_USER_ID` env var, else `$USER`, else `"default"`
- `project_id` (used as `app_id`) from `MEM0_PROJECT_ID` env var, or via the project resolver
### Step 4: Import each memory
For each record in the parsed JSON array, call `add_memory` with:
- `text="<record.content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={`
- `"type": "<record.type>"` (if non-empty)
- `"confidence": "<record.confidence>"` (if non-empty)
- `"branch": "<record.branch>"` (if non-empty)
- `"files": <record.files>` (the list, if non-empty)
- `"source": "import"`
- `}`
- `infer=False`
Notes:
- Do NOT pass the original `id` — the platform assigns a new ID.
- Skip records where `content` is empty (the parser already filters these, but be defensive).
- Continue importing even if individual records fail; track the count of successes.
### Step 5: Print results
```
Imported <N> memories into project <project_id>
```
Where `<N>` is the number of successfully imported memories.
If any failed:
```
Imported <N>/<total> memories into project <project_id> (<failed> failed)
```
## Importing from competing AI tools (`--tools`)
When invoked with `--tools` (e.g., `/mem0:import --tools`), detect and import
from competing AI tool configuration files:
### Supported tools
| Tool | File/directory |
|------|---------------|
| Cursor | `.cursorrules` |
| GitHub Copilot | `.github/copilot-instructions.md` |
| Cline | `memory-bank/` (directory of `.md` files) |
| Continue | `.continue/rules.md` |
### T1: Detect
```bash
test -f .cursorrules && echo "cursor: .cursorrules"
test -f .github/copilot-instructions.md && echo "copilot: .github/copilot-instructions.md"
test -d memory-bank/ && echo "cline: memory-bank/"
test -f .continue/rules.md && echo "continue: .continue/rules.md"
```
### T2: Ask user
List found files, ask which to import (numbers, comma-separated, or "all").
If none found:
```
No competing tool configuration files found.
Checked: .cursorrules, .github/copilot-instructions.md, memory-bank/, .continue/rules.md
```
### T3: Run import
For each selected tool:
```bash
python3 "<PLUGIN_ROOT>/scripts/import_competing_tools.py" <tool> --path <file>
```
Tools: `cursorrules`, `copilot`, `cline`, `continue`.
### T4: Report
```
Imported <N> memories into <project_id> (cursor: <N>, copilot: <N>)
```
Notes: `infer=False`, tagged `metadata.source=<tool>-import`, sections <50 chars
skipped, chunks >10k chars truncated, safe to re-run (deduplication handles it).
---
## Importing Claude Code's native MEMORY.md
When invoked with a path to Claude Code's native `MEMORY.md` file (typically
`~/.claude/projects/<proj-key>/memory/MEMORY.md`), or when `on_session_start.sh`
detects native auto-memory and the user chooses to import:
1. Read the file. It contains newline-separated memory entries (one fact per line,
sometimes with `- ` bullet prefix).
2. Split by non-empty lines. Each line becomes one memory.
3. Skip lines shorter than 20 characters or lines that are just headers (`#`).
4. For each line, call `add_memory` with:
- `text="<line>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "memory-md-import", "confidence": 0.8}`
- `infer=False`
5. Report: `Imported <N> memories from MEMORY.md into project <project_id>`
6. Suggest disabling native auto-memory:
```
To avoid duplicate memory systems, add to ~/.claude/settings.json:
"autoMemoryEnabled": false
```
This handles the cold-start gap when a user has been using Claude Code's native
memory and switches to mem0.
## Error Handling
- If the parser script is not found at `<PLUGIN_ROOT>/scripts/parse_export_file.py`, print an error and stop.
- If `add_memory` calls fail consistently (e.g. auth error), report the issue and stop early.

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