diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b137cb7f8..04b4be2cb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "mem0", "source": "./mem0-plugin", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.", - "version": "0.2.2" + "version": "0.2.3" } ] } diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index c884692b7..c0bc6ea43 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "mem0", "source": "./mem0-plugin", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.", - "version": "0.2.2" + "version": "0.2.3" } ] } diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx index 782e3d05b..5a1bafe4f 100644 --- a/docs/integrations/claude-code.mdx +++ b/docs/integrations/claude-code.mdx @@ -22,10 +22,25 @@ Before setting up Mem0 with Claude Code, ensure you have: 2. Claude Code CLI or Claude Cowork desktop app installed -3. Your API key exported in your shell: +3. Your API key added to your shell profile (persists across sessions): + + +```bash zsh +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc +source ~/.zshrc +``` + +```bash bash +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc +source ~/.bashrc +``` + + +Confirm it's set: ```bash -export MEM0_API_KEY="m0-your-api-key" +echo $MEM0_API_KEY +# Should print: m0-your-api-key ``` ## Installation @@ -149,9 +164,10 @@ You: Add refresh token rotation to the auth system. ## Troubleshooting -- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY` +- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`. If empty, add it to your shell profile (see Prerequisites) - **No tools appearing** — Restart your Claude Code session after installation -- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations. +- **Memories not being captured** — Ensure you installed via the plugin marketplace (Option A) for lifecycle hooks. MCP-only installs require manual memory operations +- **"Mem0 Inactive" banner every session** — Your API key isn't persisting. Add `export MEM0_API_KEY="m0-..."` to your `~/.zshrc` (or `~/.bashrc`) and run `source ~/.zshrc` diff --git a/docs/integrations/codex.mdx b/docs/integrations/codex.mdx index 0a8831250..85d24680d 100644 --- a/docs/integrations/codex.mdx +++ b/docs/integrations/codex.mdx @@ -22,12 +22,20 @@ Before setting up Mem0 with Codex, ensure you have: 2. OpenAI Codex access -3. Your API key exported in your shell: +3. Your API key added to your shell profile (persists across sessions): -```bash -export MEM0_API_KEY="m0-your-api-key" + +```bash zsh +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc +source ~/.zshrc ``` +```bash bash +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc +source ~/.bashrc +``` + + ## Installation ### Option A — Direct MCP (Recommended) diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx index 61e7a8dec..462dc424c 100644 --- a/docs/integrations/cursor.mdx +++ b/docs/integrations/cursor.mdx @@ -22,12 +22,20 @@ Before setting up Mem0 with Cursor, ensure you have: 2. Cursor installed ([cursor.com](https://cursor.com)) -3. Your API key exported in your shell: +3. Your API key added to your shell profile (persists across sessions): -```bash -export MEM0_API_KEY="m0-your-api-key" + +```bash zsh +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc +source ~/.zshrc ``` +```bash bash +echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc +source ~/.bashrc +``` + + Already have `mem0` configured as an MCP server in Cursor? Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools. diff --git a/mem0-plugin/.claude-plugin/plugin.json b/mem0-plugin/.claude-plugin/plugin.json index 9e638dd1c..a398e1758 100644 --- a/mem0-plugin/.claude-plugin/plugin.json +++ b/mem0-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mem0", - "version": "0.2.2", - "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.", + "version": "0.2.3", + "description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.", "author": { "name": "Mem0", "email": "support@mem0.ai" @@ -9,5 +9,14 @@ "homepage": "https://mem0.ai", "repository": "https://github.com/mem0ai/mem0", "license": "Apache-2.0", - "keywords": ["memory", "personalization", "mcp", "semantic-search"] + "keywords": ["memory", "personalization", "mcp", "semantic-search"], + "userConfig": { + "api_key": { + "type": "string", + "title": "Mem0 API Key", + "description": "Your Mem0 Platform API key. Get one at https://app.mem0.ai/dashboard/api-keys. Alternative: export MEM0_API_KEY in your shell profile.", + "sensitive": true, + "required": true + } + } } diff --git a/mem0-plugin/.codex-plugin/plugin.json b/mem0-plugin/.codex-plugin/plugin.json index bc29f76df..3d25dbbeb 100644 --- a/mem0-plugin/.codex-plugin/plugin.json +++ b/mem0-plugin/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mem0", - "version": "0.2.2", + "version": "0.2.3", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Codex workflows using the Mem0 Platform MCP server.", "author": { "name": "Mem0", diff --git a/mem0-plugin/.cursor-plugin/plugin.json b/mem0-plugin/.cursor-plugin/plugin.json index de4aab2d2..debbd32c9 100644 --- a/mem0-plugin/.cursor-plugin/plugin.json +++ b/mem0-plugin/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "mem0", - "version": "0.2.2", + "version": "0.2.3", "description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.", "author": { "name": "Mem0", diff --git a/mem0-plugin/.mcp.json b/mem0-plugin/.mcp.json index dd091bc0c..a99e7ba07 100644 --- a/mem0-plugin/.mcp.json +++ b/mem0-plugin/.mcp.json @@ -5,7 +5,8 @@ "url": "https://mcp.mem0.ai/mcp/", "headers": { "Authorization": "Token ${MEM0_API_KEY}" - } + }, + "authorizationUrl": "https://mcp.mem0.ai/authorize" } } } diff --git a/mem0-plugin/hooks/codex-hooks.json b/mem0-plugin/hooks/codex-hooks.json index 368120361..3422b1ffe 100644 --- a/mem0-plugin/hooks/codex-hooks.json +++ b/mem0-plugin/hooks/codex-hooks.json @@ -1,6 +1,16 @@ { "hooks": { "PreToolUse": [ + { + "matcher": "Read", + "hooks": [ + { + "type": "command", + "command": "${CODEX_PLUGIN_ROOT}/scripts/on_file_read.sh", + "timeout": 8 + } + ] + }, { "matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory", "hooks": [ @@ -31,7 +41,7 @@ "type": "command", "command": "${CODEX_PLUGIN_ROOT}/scripts/on_user_prompt.sh", "statusMessage": "Checking memory relevance...", - "timeout": 5 + "timeout": 12 } ] } @@ -58,7 +68,7 @@ { "type": "command", "command": "${CODEX_PLUGIN_ROOT}/scripts/on_bash_output.sh", - "timeout": 5 + "timeout": 12 } ] } diff --git a/mem0-plugin/hooks/cursor-hooks.json b/mem0-plugin/hooks/cursor-hooks.json index 13ccd456c..c17cc6537 100644 --- a/mem0-plugin/hooks/cursor-hooks.json +++ b/mem0-plugin/hooks/cursor-hooks.json @@ -11,6 +11,11 @@ "command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write_cursor.sh", "matcher": "Write|Edit" }, + { + "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_file_read.sh", + "matcher": "Read", + "timeout": 8 + }, { "command": "${CURSOR_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh", "matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory", @@ -31,7 +36,7 @@ { "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_bash_output.sh", "matcher": "Bash", - "timeout": 5 + "timeout": 12 } ], "preCompact": [ @@ -48,7 +53,7 @@ "beforeSubmitPrompt": [ { "command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt_cursor.sh", - "timeout": 5 + "timeout": 12 } ] } diff --git a/mem0-plugin/hooks/hooks.json b/mem0-plugin/hooks/hooks.json index db424633f..6571d3c4b 100644 --- a/mem0-plugin/hooks/hooks.json +++ b/mem0-plugin/hooks/hooks.json @@ -55,6 +55,16 @@ } ] }, + { + "matcher": "Read", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_file_read.sh", + "timeout": 8 + } + ] + }, { "matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory", "hooks": [ @@ -88,7 +98,7 @@ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_bash_output.sh", - "timeout": 5 + "timeout": 12 } ] } @@ -122,7 +132,7 @@ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.sh", "statusMessage": "Checking memory relevance...", - "timeout": 5 + "timeout": 12 } ] } diff --git a/mem0-plugin/output-styles/compact-memory.md b/mem0-plugin/output-styles/compact-memory.md new file mode 100644 index 000000000..aeaf655e4 --- /dev/null +++ b/mem0-plugin/output-styles/compact-memory.md @@ -0,0 +1,37 @@ +--- +name: compact-memory +description: Terse one-liner memory display — type, content, and ID. +--- + +# Compact Memory Output Style + +When this style is active, all memory-related output uses this one-liner format: + +``` +[] [mem0:] +``` + +## Examples + +``` +[decision] Auth module uses JWT with RS256 signing keys [mem0:a3f8b2c1] +[convention] All API routes live in src/routes/ with kebab-case filenames [mem0:7e2d9f4a] +[anti_pattern] Don't use raw SQL — always go through the ORM layer [mem0:c4d5e6f7] +[task_learning] Redis cache TTL should be 300s for user sessions [mem0:d8e9f0a1] +``` + +## Rules + +- Type in brackets, lowercase, from `metadata.type` +- Content truncated at 80 chars with no trailing ellipsis +- Short ID = first 8 chars of memory ID +- One memory per line, no extra formatting +- No headers or separators between memories unless grouped +- When grouped by type, use a blank line between groups + +## When to use + +- `/mem0:peek` results +- `/mem0:tour` memory listings +- Search results from `search_memories` +- Any context where memories are displayed inline diff --git a/mem0-plugin/scripts/_identity.py b/mem0-plugin/scripts/_identity.py index 16ae7e0ac..59f7ec498 100644 --- a/mem0-plugin/scripts/_identity.py +++ b/mem0-plugin/scripts/_identity.py @@ -1,12 +1,16 @@ -"""Resolve mem0 identity: API key and user_id. +"""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_MEM0_API_KEY (set by Claude Code userConfig) + 2. CLAUDE_PLUGIN_OPTION_API_KEY (set by `claude plugin configure mem0`) + 3. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (legacy userConfig) 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 @@ -18,7 +22,13 @@ def resolve_api_key() -> str: key = os.environ.get("MEM0_API_KEY", "").strip() if key: return key - return os.environ.get("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", "").strip() + 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 + return "" def resolve_user_id() -> str: @@ -28,6 +38,25 @@ def resolve_user_id() -> str: 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, + "output_style": "compact", + "debug": False, + "skip_tools": ["Read", "Glob", "Grep"], + "capture_tools": ["Edit", "Write", "Bash"], + } + + try: from _project import resolve_branch, resolve_project_id, save_project_mapping except ImportError: diff --git a/mem0-plugin/scripts/_identity.sh b/mem0-plugin/scripts/_identity.sh index ac69caa52..66199dbb1 100644 --- a/mem0-plugin/scripts/_identity.sh +++ b/mem0-plugin/scripts/_identity.sh @@ -1,14 +1,19 @@ -# Source this file. Sets MEM0_API_KEY and MEM0_RESOLVED_USER_ID. +# 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_MEM0_API_KEY (set by Claude Code userConfig) +# 2. CLAUDE_PLUGIN_OPTION_API_KEY (set by `claude plugin configure mem0`) +# 3. CLAUDE_PLUGIN_OPTION_MEM0_API_KEY (legacy userConfig) # -# User ID resolution: -# 1. MEM0_USER_ID env var (explicit override) -# 2. $USER, else "default" +# Settings: ~/.mem0/settings.json (user-editable, falls back to defaults) -# Resolve API key from userConfig fallback +_SCRIPT_DIR="$( cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd )" + +# Resolve API key: env var > userConfig +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 @@ -25,5 +30,24 @@ _mem0_resolve_identity() { MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)" export MEM0_RESOLVED_USER_ID +# 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_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_DEBUG="false" +fi +export MEM0_AUTO_SAVE MEM0_AUTO_SEARCH MEM0_SEARCH_LIMIT MEM0_RETENTION_SESSION_DAYS MEM0_CONFIDENCE_THRESHOLD MEM0_DEBUG + # Also resolve project context -. "$( cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd )/_project.sh" +. "$_SCRIPT_DIR/_project.sh" diff --git a/mem0-plugin/scripts/_search.py b/mem0-plugin/scripts/_search.py new file mode 100644 index 000000000..51d54f5f6 --- /dev/null +++ b/mem0-plugin/scripts/_search.py @@ -0,0 +1,61 @@ +"""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 search_memories( + api_key: str, + user_id: str, + project_id: str, + query: str, + metadata_type: str | None = None, + top_k: int = 3, +) -> list[dict]: + if not api_key: + return [] + + filters: dict = {"AND": [{"user_id": user_id}, {"app_id": project_id}]} + if metadata_type: + filters["AND"].append({"metadata": {"type": metadata_type}}) + + body = json.dumps({"query": query, "filters": filters, "top_k": top_k}).encode() + req = urllib.request.Request( + SEARCH_URL, + data=body, + headers={"Authorization": f"Token {api_key}", "Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=SEARCH_TIMEOUT) as r: + data = json.loads(r.read()) + if isinstance(data, list): + return data + return data.get("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) diff --git a/mem0-plugin/scripts/auto_import.py b/mem0-plugin/scripts/auto_import.py index 3f4be30a2..8245f0e4f 100644 --- a/mem0-plugin/scripts/auto_import.py +++ b/mem0-plugin/scripts/auto_import.py @@ -158,6 +158,7 @@ def main() -> None: hashes = load_hashes() updated = False + seen_content_hashes: set[str] = set() for filename in TARGET_FILES: filepath = "" @@ -187,6 +188,11 @@ def main() -> None: 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}:{filename}" if hashes.get(hash_key) == current_hash: log.debug("Unchanged, skipping: %s", filename) diff --git a/mem0-plugin/scripts/enforce_metadata_defaults.sh b/mem0-plugin/scripts/enforce_metadata_defaults.sh index 1e0ba4c19..4556f1c1e 100755 --- a/mem0-plugin/scripts/enforce_metadata_defaults.sh +++ b/mem0-plugin/scripts/enforce_metadata_defaults.sh @@ -46,6 +46,11 @@ if 'type' not in meta: meta['type'] = 'task_learning' changed = True +# If confidence is 1.0 (user explicitly stated), ensure infer=False +if meta.get('confidence', 0) >= 1.0 and 'infer' not in inp: + inp['infer'] = False + changed = True + if changed: inp['metadata'] = meta print(json.dumps(inp)) diff --git a/mem0-plugin/scripts/load_settings.py b/mem0-plugin/scripts/load_settings.py new file mode 100644 index 000000000..e77c81224 --- /dev/null +++ b/mem0-plugin/scripts/load_settings.py @@ -0,0 +1,52 @@ +"""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, + "output_style": "compact", + "debug": False, + "skip_tools": ["Read", "Glob", "Grep"], + "capture_tools": ["Edit", "Write", "Bash"], +} + + +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())) diff --git a/mem0-plugin/scripts/on_bash_output.sh b/mem0-plugin/scripts/on_bash_output.sh index a79a705b1..61f0654d1 100755 --- a/mem0-plugin/scripts/on_bash_output.sh +++ b/mem0-plugin/scripts/on_bash_output.sh @@ -46,10 +46,6 @@ fi SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" . "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true -if [ -z "${MEM0_API_KEY:-}" ]; then - exit 0 -fi - # 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) @@ -62,45 +58,60 @@ if [ -n "$TRACE_FILES" ]; then FILE_DISPLAY=$(echo "$TRACE_FILES" | sed 's/^/ - /') fi -USER_ID="$MEM0_RESOLVED_USER_ID" +USER_ID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}" -cat < $ERROR_LINE - -EOF - -if [ -n "$FILE_DISPLAY" ]; then - cat </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 "") + +# Output error header +printf '\n## Error detected in command output\n\n' +printf '`%s` produced an error:\n> %s\n\n' "$COMMAND" "$ERROR_LINE" + +if [ -n "$FILE_DISPLAY" ]; then + printf '**Files in stack trace:**\n%s\n\n' "$FILE_DISPLAY" +fi + +if [ -n "$RESULTS" ]; then + printf '%s\n' "$RESULTS" +else + printf 'No prior memories found for this error.\n\n' +fi + +printf 'If you solve this, store the fix as an `anti_pattern` or `bug_fix` memory for next time.\n' + exit 0 diff --git a/mem0-plugin/scripts/on_file_read.sh b/mem0-plugin/scripts/on_file_read.sh new file mode 100755 index 000000000..de6e0526c --- /dev/null +++ b/mem0-plugin/scripts/on_file_read.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Hook: PreToolUse (matcher: Read) +# +# When Claude reads a file, searches mem0 for memories tagged with that +# file path. Injects results as additionalContext if found. + +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 + +# Skip non-code files (images, binaries, lockfiles, etc.) +case "$FILE_PATH" in + *.png|*.jpg|*.jpeg|*.gif|*.svg|*.ico|*.woff|*.woff2|*.ttf|*.eot) exit 0 ;; + *.lock|*.sum|*.min.js|*.min.css|*.map) exit 0 ;; + *node_modules/*|*.git/*|*__pycache__/*) exit 0 ;; +esac + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true + +# Skip if no API key (checked after _identity.sh resolves CLAUDE_PLUGIN_OPTION_MEM0_API_KEY) +if [ -z "${MEM0_API_KEY:-}" ]; then + exit 0 +fi + +# Skip repeated reads: track last 10 files in a temp file. +# Use full FILE_PATH (not basename) to avoid false dedup. +RECENT_FILE="/tmp/mem0_recent_reads_${USER}" +if [ -f "$RECENT_FILE" ] && grep -qxF "$FILE_PATH" "$RECENT_FILE" 2>/dev/null; then + exit 0 +fi +echo "$FILE_PATH" >> "$RECENT_FILE" 2>/dev/null || true +tail -10 "$RECENT_FILE" > "$RECENT_FILE.tmp" 2>/dev/null && mv "$RECENT_FILE.tmp" "$RECENT_FILE" 2>/dev/null || true + +USER_ID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}" +PROJECT_ID="${MEM0_PROJECT_ID:-unknown}" +BASENAME=$(basename "$FILE_PATH") + +# SECURITY: pass data via env vars, never interpolate into python3 -c +CONTEXT=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" MEM0_SEARCH_PROJECT="$PROJECT_ID" MEM0_SEARCH_QUERY="$BASENAME" 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_SEARCH_PROJECT', 'unknown') +filename = os.environ.get('MEM0_SEARCH_QUERY', '') + +results = search_memories(api_key, user_id, project_id, filename, top_k=3) +if results: + print(format_results_for_context(results, heading=f'mem0 context for {filename}')) +" 2>/dev/null || true) + +if [ -n "$CONTEXT" ]; then + jq -nc --arg ctx "$CONTEXT" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}' +fi + +exit 0 diff --git a/mem0-plugin/scripts/on_post_tool_use.sh b/mem0-plugin/scripts/on_post_tool_use.sh index 0f1d88275..2d4883dca 100755 --- a/mem0-plugin/scripts/on_post_tool_use.sh +++ b/mem0-plugin/scripts/on_post_tool_use.sh @@ -16,19 +16,19 @@ INPUT=$(cat) TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "") case "$TOOL_NAME" in - mcp__mem0__add_memory) + *__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 & ;; - mcp__mem0__search_memories|mcp__mem0__get_memories) + *__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 & ;; - mcp__mem0__delete_memory) + *__delete_memory) python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=delete_memory 2>/dev/null & ;; - mcp__mem0__update_memory) + *__update_memory) python3 "$SCRIPT_DIR/telemetry.py" tool_use --tool=update_memory 2>/dev/null & ;; esac diff --git a/mem0-plugin/scripts/on_session_start.sh b/mem0-plugin/scripts/on_session_start.sh index 29f8fbdac..d44cc2f6c 100755 --- a/mem0-plugin/scripts/on_session_start.sh +++ b/mem0-plugin/scripts/on_session_start.sh @@ -19,26 +19,71 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=_identity.sh . "$SCRIPT_DIR/_identity.sh" -# Initialize session stats tracker +# Initialize session stats tracker and settings file python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true +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_recent_reads_${USER}" 2>/dev/null || true INPUT=$(cat) SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup") -# Skip bootstrap if no API key — still show banner but with "no key" status. +MEM0_AUTH_MODE="api_key" if [ -z "${MEM0_API_KEY:-}" ]; then - echo "## Mem0 Inactive" - echo "" - echo "\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | api_key=NOT_SET\`" - echo "" - echo "IMPORTANT: In your FIRST response to the user, you MUST display the status banner exactly as shown below (copy-paste it as your opening line before any other output):" - echo "" - echo '```' - echo "Mem0 Inactive | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | api_key=NOT_SET" - echo '```' - echo "" - echo "Set MEM0_API_KEY to enable persistent memory. Get a key at https://app.mem0.ai/dashboard/api-keys" - exit 0 + _SAFE_PID=$(printf '%s' "${MEM0_PROJECT_ID:-unknown}" | tr '/:' '--') + _ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}" + + if [ ! -f "$_ONBOARD_MARKER" ] && [ "${SOURCE:-startup}" = "startup" ]; then + # State 1: First run — trigger onboarding + cat </dev/null 2>&1; then +if [ "$MEM0_AUTH_MODE" = "api_key" ] && 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', '') @@ -83,20 +129,22 @@ fi # uses the same user_id the hooks resolved. Without this, the agent's # search_memories/add_memory MCP calls may bind to a different bucket # than what the hooks write to. -echo "## Mem0 Active" -echo "" -echo "\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT\`" -echo "" -echo "IMPORTANT: In your FIRST response to the user, you MUST display the identity banner exactly as shown below (copy-paste it as your opening line before any other output):" -echo "" -echo '```' -echo "Mem0 Active | user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT" -echo '```' -echo "" -echo "Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:" -echo "- user_id: \`$MEM0_RESOLVED_USER_ID\`" -echo "- app_id: \`$MEM0_PROJECT_ID\` (project scope — passed as top-level \`app_id\`, NOT in metadata)" -echo "" +cat </dev/null || true - touch "$_ONBOARD_MARKER" cat <<'EOF' -## Mem0 First Run — Automatic Onboarding - -This is the first time mem0 is running for this project. You MUST run the onboarding wizard NOW before doing anything else. - -Execute the `/mem0:onboard` skill immediately. This will: -1. Verify the API key -2. Detect and import project files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) -3. Install coding categories optimized for development -4. Show a summary of what was set up - -Do NOT skip this. Do NOT ask the user if they want to onboard. Just run `/mem0:onboard` now. +IMPORTANT: This is the first run for this project. You MUST invoke the `mem0:onboard` skill now using the Skill tool BEFORE responding to the user. Do not just suggest it — actually call it. This runs the setup wizard (API check, file import, coding categories). EOF else cat <<'EOF' -## Mem0 Session Bootstrap - -You have access to persistent memory via the mem0 MCP tools. Before doing anything else: - -1. Call `search_memories` with a query related to the current project or user request to load relevant context. -2. Review the returned memories to understand what has been learned in prior sessions. -3. If appropriate, call `get_memories` to browse all stored memories for this user. - -IMPORTANT: Do NOT skip this step. Always bootstrap context first. +Search mem0 for recent decisions and task learnings before responding to the user's first message. Run 2 parallel searches: one for `decision` type, one for `task_learning` type. EOF fi @@ -172,26 +200,12 @@ MEMEOF elif [ "$SOURCE" = "resume" ]; then cat <<'EOF' -## Mem0 Session Resumed - -This is a resumed session. Your prior context is already loaded. Before continuing: - -1. Call `search_memories` with a query related to the current task to refresh relevant memories. -2. If significant time has passed, search for recent project-wide updates. - -Continue where you left off. +Session resumed. Search mem0 for `session_state` and `decision` memories to pick up where you left off. Run 2 parallel searches. EOF elif [ "$SOURCE" = "compact" ]; then cat <<'EOF' -## Mem0 Post-Compaction Recovery - -Context was just compacted. Reload your context from mem0. - -1. Call `search_memories` to reload context, layering up to three angles: - - `metadata.type=session_state` -- the pre-compaction summary you wrote before compaction - - `metadata.type=decision` / `anti_pattern` -- specific facts you stored during the session -2. Continue working from the recovered context. +Context compacted. Search mem0 for `session_state` and `decision` memories to recover context. Run 2 parallel searches. EOF fi diff --git a/mem0-plugin/scripts/on_stop.sh b/mem0-plugin/scripts/on_stop.sh index 656170fce..de18ebff3 100755 --- a/mem0-plugin/scripts/on_stop.sh +++ b/mem0-plugin/scripts/on_stop.sh @@ -47,19 +47,7 @@ if [ -n "$REPORT" ]; then fi cat <<'EOF' -Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool: - -1. Were any significant decisions made? -> Store with metadata `{"type": "decision"}` -2. Were any new patterns or strategies discovered? -> Store with metadata `{"type": "task_learning"}` -3. Did any approach fail? -> Store with metadata `{"type": "anti_pattern"}` -4. Did you learn anything about the user's preferences? -> Store with metadata `{"type": "user_preference"}` -5. Were there environment/setup discoveries? -> Store with metadata `{"type": "environmental"}` - -Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners. - -If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings. - -Always include `app_id` (the active project_id from SessionStart) as a top-level parameter in every `add_memory` call. +Store 0-2 durable facts from this turn via `add_memory` — only decisions, anti-patterns, or conventions that would help a future agent. Skip if nothing new was learned. EOF SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "") diff --git a/mem0-plugin/scripts/on_task_completed.sh b/mem0-plugin/scripts/on_task_completed.sh index 2d8eef903..8608a9b94 100755 --- a/mem0-plugin/scripts/on_task_completed.sh +++ b/mem0-plugin/scripts/on_task_completed.sh @@ -25,16 +25,7 @@ _PROJECT="${MEM0_PROJECT_ID:-unknown}" cat < Store with metadata \`{"type": "task_learning"}\` -2. Were there failed approaches before finding the solution? -> Store with metadata \`{"type": "anti_pattern"}\` -3. Were there architectural decisions? -> Store with metadata \`{"type": "decision"}\` -4. Any new conventions or patterns established? -> Store with metadata \`{"type": "convention"}\` - -Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples. -Only store genuinely useful learnings — skip if the task was trivial. -Include \`app_id\` = \`"$_PROJECT"\` as a top-level parameter in every \`add_memory\` call (not in metadata). +Store 0-2 key learnings via \`add_memory\` with \`app_id="$_PROJECT"\`. Use types: \`decision\`, \`task_learning\`, \`anti_pattern\`, or \`convention\`. Skip if trivial. EOF # Telemetry (background, fire-and-forget) diff --git a/mem0-plugin/scripts/on_tool_failure.sh b/mem0-plugin/scripts/on_tool_failure.sh index 7a2b9fa55..eb40991bb 100755 --- a/mem0-plugin/scripts/on_tool_failure.sh +++ b/mem0-plugin/scripts/on_tool_failure.sh @@ -20,8 +20,9 @@ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null || echo "") TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_error // ""' 2>/dev/null || echo "") TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null || echo "{}") -# Extract the short tool name (strip mcp__mem0__ prefix) -SHORT_NAME="${TOOL_NAME#mcp__mem0__}" +# Extract the short tool name (strip any mcp prefix variant) +SHORT_NAME="${TOOL_NAME##*__}" +[ "$SHORT_NAME" = "$TOOL_NAME" ] && SHORT_NAME="${TOOL_NAME#mcp__mem0__}" # Log failure to persistent file for debugging mkdir -p "$HOME/.mem0" 2>/dev/null || true diff --git a/mem0-plugin/scripts/on_user_prompt.sh b/mem0-plugin/scripts/on_user_prompt.sh index e3e4f5a2a..a9098ff6c 100755 --- a/mem0-plugin/scripts/on_user_prompt.sh +++ b/mem0-plugin/scripts/on_user_prompt.sh @@ -29,6 +29,20 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # shellcheck source=_identity.sh . "$SCRIPT_DIR/_identity.sh" +# Rubric dedup: only inject full rubric once per session. +# Key on session ID (from stdin JSON) to avoid cross-session interference. +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "") +RUBRIC_DIR="${MEM0_RUBRIC_DIR:-/tmp}" +if [ -n "$SESSION_ID" ]; then + RUBRIC_FLAG="$RUBRIC_DIR/mem0_rubric_${SESSION_ID}" +else + RUBRIC_FLAG="$RUBRIC_DIR/mem0_rubric_injected_${USER}" +fi +RUBRIC_ALREADY_SHOWN="" +if [ -f "$RUBRIC_FLAG" ]; then + RUBRIC_ALREADY_SHOWN="true" +fi + # Detect stack traces and error patterns in the prompt (no API needed) HAS_ERROR="" if echo "$PROMPT" | grep -qE '(Traceback|panic:)'; then @@ -42,10 +56,24 @@ 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 @@ -60,33 +88,62 @@ if [ -z "${MEM0_API_KEY:-}" ]; then fi USER_ID="$MEM0_RESOLVED_USER_ID" -cat </dev/null || echo "") + + if [ -n "$RESUME_RESULTS" ]; then + echo "" + echo "$RESUME_RESULTS" + fi +fi + +if [ -n "$HAS_REMEMBER" ]; then + cat <<'REMEMBER_EOF' + +**Remember intent detected.** Use `/mem0:remember` (not raw `add_memory`) — it auto-classifies, sets confidence=1.0, and stores verbatim. +REMEMBER_EOF +fi + +if [ -z "$RUBRIC_ALREADY_SHOWN" ]; then + cat </dev/null || true +fi if [ -n "$HAS_ERROR" ]; then cat <"}}]})\` -- Also run a broader text search without the files filter as fallback: -- \`search_memories(query="", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}]})\` EOF fi -cat <"}}\`. Skip recency for durable facts (conventions, decisions). -- Empty results are normal -- proceed without context. -EOF - exit 0 diff --git a/mem0-plugin/skills/context-loader/SKILL.md b/mem0-plugin/skills/context-loader/SKILL.md new file mode 100644 index 000000000..ef1ce220e --- /dev/null +++ b/mem0-plugin/skills/context-loader/SKILL.md @@ -0,0 +1,50 @@ +--- +name: context-loader +description: Pre-load relevant memories for current task +--- + +# Context Loader + +Pre-fetches relevant memories to prime context before working on a task. + +## When to use + +- Session start (auto-triggered by `on_session_start.sh` for onboarded projects) +- 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 | `{"metadata": {"type": "decision"}}` | Architecture decisions | + | File paths mentioned | `{"metadata": {"type": "convention"}}` | Coding patterns | + | Error keywords (if any) | `{"metadata": {"type": "anti_pattern"}}` | Known pitfalls | + | Broad project context | no metadata filter | Catch-all | + + All calls must include `user_id` and `app_id` filters. + +3. **Deduplicate** results by memory ID across all search responses. + +4. **Output compact context block** (max 10 memories): + +``` +context-loader: loaded memories for "" + - [decision] [mem0:] + - [convention] [mem0:] + - [anti_pattern] [mem0:] +``` + +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 diff --git a/mem0-plugin/skills/dream/SKILL.md b/mem0-plugin/skills/dream/SKILL.md new file mode 100644 index 000000000..c0938ae53 --- /dev/null +++ b/mem0-plugin/skills/dream/SKILL.md @@ -0,0 +1,224 @@ +--- +name: dream +description: Consolidate memories — merge duplicates, resolve contradictions, prune stale +--- + +# 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. + +--- + +## 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 "/scripts/parse_mem0_config.py" "" +``` + +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": ""}, {"app_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 . 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 (item 15) + +Print a structured diff to the terminal before making any changes. Use exactly +this format: + +``` +## dream — consolidation report + +Merges (): + [mem0:] + [mem0:] → "" + +Conflicts (): + [mem0:] vs [mem0:] — "" [A/B/skip] + +Prune (): + [mem0:] — , d old + +Proposed: merges, prunes, 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()` +2. `delete_memory()` +3. `add_memory` with: + - `messages=[{"role": "user", "content": ""}]` + - `user_id=` + - `app_id=` (top-level, not in metadata) + - `metadata={"type": "", "branch": "", "confidence": , "source": "mem0-dream"}` + - `infer=False` + +#### Contradictions (resolved) + +For each resolved conflict where the user chose A or B: +- Identify the loser (the non-chosen memory). +- First call `get_memory()` to read its current text content. +- Then call `update_memory(, text=)` to preserve the text while updating it. +- **Important:** `update_memory` requires the `text` parameter. A metadata-only call may error or wipe the content. Always read first, then update with the original text. + +Contradictions where the user chose `skip` are left untouched. + +#### Prunes + +For each prune candidate: +- `delete_memory()` + +--- + +## Step 6: Print Summary + +After all changes are applied, print: + +``` +Dream complete — merged: , pruned: , conflicts resolved: , skipped: +``` + +--- + +## 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. + +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= merged= pruned= conflicts_skipped= + ``` +4. If contradictions were detected but skipped, store a reminder memory: + ```python + add_memory( + messages=[{"role": "user", "content": "mem0-dream detected contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively."}], + user_id="", + app_id="", + metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": ""}, + infer=False, + ) + ``` + +## See also + +- `/mem0:forget` — targeted deletion of specific memories (search + confirm + delete) +- `/mem0:health --deep` — quick quality scan without applying changes diff --git a/mem0-plugin/skills/mem0-export/SKILL.md b/mem0-plugin/skills/export/SKILL.md similarity index 86% rename from mem0-plugin/skills/mem0-export/SKILL.md rename to mem0-plugin/skills/export/SKILL.md index f94611fa0..e19b0db81 100644 --- a/mem0-plugin/skills/mem0-export/SKILL.md +++ b/mem0-plugin/skills/export/SKILL.md @@ -1,10 +1,6 @@ --- -name: mem0-export -description: > - Export all memories for the current project to a local Markdown file. - Each memory is written as a YAML-frontmatter block that can be re-imported later. - TRIGGER: user runs /mem0:export, or asks "export memories", "backup memories", - "download my memories", "save memories to file". +name: export +description: Export project memories to a portable Markdown file --- # Mem0 Export diff --git a/mem0-plugin/skills/forget/SKILL.md b/mem0-plugin/skills/forget/SKILL.md new file mode 100644 index 000000000..bde9b0f29 --- /dev/null +++ b/mem0-plugin/skills/forget/SKILL.md @@ -0,0 +1,71 @@ +--- +name: forget +description: Delete memories by search or ID — confirms before deleting +--- + +# 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 ` + +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: "" (created )` + +**If search query provided:** +- Call `search_memories` with: + - `query=` + - `filters={"AND": [{"user_id": ""}, {"app_id": ""}]}` + - `top_k=10` +- Show numbered list: + ``` + Found memories matching "": + 1. (type: , created: ) [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 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 ` to find specific ones." diff --git a/mem0-plugin/skills/mem0-health/SKILL.md b/mem0-plugin/skills/health/SKILL.md similarity index 59% rename from mem0-plugin/skills/mem0-health/SKILL.md rename to mem0-plugin/skills/health/SKILL.md index 2b30b900c..8d984e0b6 100644 --- a/mem0-plugin/skills/mem0-health/SKILL.md +++ b/mem0-plugin/skills/health/SKILL.md @@ -1,10 +1,6 @@ --- -name: mem0-health -description: > - Diagnostic health check for the mem0 plugin. Verifies API key, MCP server - connectivity, identity resolution, and memory read/write capability. - TRIGGER: user runs /mem0:health, or asks "is mem0 working", "mem0 health", - "check mem0 connection", "debug mem0". +name: health +description: Diagnose mem0 connection, API key, and memory read/write --- # Mem0 Health Check @@ -70,26 +66,22 @@ This file is created by the SessionStart hook and updated by PostToolUse hooks t ### Display ``` -## mem0 Health Check +## mem0 health -| Check | Status | Detail | -|--------------------|--------|-------------------------------| -| API Key | PASS | m0-dVe... | -| Identity | PASS | user=kartik, project=mem0 | -| MCP Connectivity | PASS | 142ms round-trip | -| Memory Write/Read | PASS | write + delete OK | -| Session Tracker | PASS | stats file active | +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. mem0 is healthy. +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`) or `--fix` (e.g., `/mem0:health --fix`), run the standard 5 checks above **plus** a memory quality scan. - -`--fix` implies `--deep` and automatically applies safe fixes after showing the analysis (see bottom of this section). +When invoked with `--deep` (e.g., `/mem0:health --deep`), run the standard 5 checks above **plus** a memory quality scan. ### Quality Check 1: Duplicates @@ -141,31 +133,11 @@ Untagged/orphan memories: ``` ## Memory Quality -| Metric | Count | Action | -|----------------|-------|---------------------------------| -| Duplicates | | Run /mem0:dream to merge | -| Stale | | Run /mem0:dream to prune | -| Contradictions | | Run /mem0:dream to resolve | -| Orphans | | Consider retagging via MCP | + +Duplicates: · Stale: · Contradictions: · Orphans: ``` -If all counts are 0: `Memory quality: clean. No duplicates, stale entries, or contradictions found.` +If all counts are 0: `Memory quality: clean.` +If any non-zero: append `Run /mem0:dream to fix.` -### Auto-fix mode (`--fix`) - -When `--fix` is passed, apply these safe fixes automatically after displaying the quality summary: - -1. **Orphans:** For each untagged memory, infer a `metadata.type` from content and call `update_memory` to set it. If inference is uncertain, skip. -2. **Stale `session_state`/`compact_summary` > 90d:** Delete them via `delete_memory`. These are ephemeral by design. -3. **Duplicates:** Do NOT auto-merge — print "Run `/mem0:dream` to merge duplicates" instead. -4. **Contradictions:** Do NOT auto-resolve — print "Run `/mem0:dream` to resolve contradictions" instead. -5. **Low-confidence < 0.3 AND > 30d old:** Delete them via `delete_memory`. - -Print a summary of actions taken: - -``` -## Auto-fix Results - Deleted: stale, low-confidence - Retagged: orphans - Skipped: duplicates (use /mem0:dream), contradictions (use /mem0:dream) -``` +To fix issues found by `--deep`, run `/mem0:dream` for automated consolidation (merges, prunes, conflict resolution). diff --git a/mem0-plugin/skills/mem0-import/SKILL.md b/mem0-plugin/skills/import/SKILL.md similarity index 92% rename from mem0-plugin/skills/mem0-import/SKILL.md rename to mem0-plugin/skills/import/SKILL.md index 34d9cdd59..1c843cc0a 100644 --- a/mem0-plugin/skills/mem0-import/SKILL.md +++ b/mem0-plugin/skills/import/SKILL.md @@ -1,11 +1,6 @@ --- -name: mem0-import -description: > - Import memories from a mem0 export file back into the current project. - Reads a YAML-frontmatter Markdown file produced by /mem0:export and - adds each memory block to mem0. - TRIGGER: user runs /mem0:import, or asks "import memories", "restore memories", - "load memories from file", "reimport backup". +name: import +description: Import memories from an export file into this project --- # Mem0 Import @@ -142,10 +137,7 @@ Tools: `cursorrules`, `copilot`, `cline`, `continue`. ### T4: Report ``` -Import complete. - Cursor: memories - Copilot: memories - Total: memories imported into project +Imported memories into (cursor: , copilot: ) ``` Notes: `infer=False`, tagged `metadata.source=-import`, sections <50 chars diff --git a/mem0-plugin/skills/list-projects/SKILL.md b/mem0-plugin/skills/list-projects/SKILL.md new file mode 100644 index 000000000..01ca996f0 --- /dev/null +++ b/mem0-plugin/skills/list-projects/SKILL.md @@ -0,0 +1,54 @@ +--- +name: list-projects +description: List all projects that have stored memories +--- + +# 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 and extracting distinct `app_id` values. + +Call `get_memories` with: +- `filters={"user_id": ""}` +- `page_size=200` + +Do NOT pass `app_id` — we want memories across ALL projects. + +If the response indicates more pages, paginate until all are fetched (up to 1000 +memories max to avoid excessive API calls). + +### Step 2: Extract distinct projects + +For each memory, read the `app_id` field (may also appear as `metadata.app_id` +on older memories). Collect distinct values. + +For each project, count: +- Total memories +- Most recent `created_at` date +- Top 3 `metadata.type` values by frequency + +### Step 3: Display + +``` +## mem0 projects + + memories (last: ) ← current + memories (last: ) + + projects, 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. +``` diff --git a/mem0-plugin/skills/mem0-dream/SKILL.md b/mem0-plugin/skills/mem0-dream/SKILL.md deleted file mode 100644 index d0a8bccc2..000000000 --- a/mem0-plugin/skills/mem0-dream/SKILL.md +++ /dev/null @@ -1,396 +0,0 @@ ---- -name: mem0-dream -description: > - Memory consolidation pass. Fetches all project memories, finds near-duplicates, - merges them, flags contradictions, prunes stale entries per retention policy. - Outputs a diff for user approval before applying changes. - TRIGGER: user runs /mem0:dream, or asks "consolidate memories", "clean up memories", - "merge duplicate memories", "run dream". ---- - -# 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. - ---- - -## 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 "/scripts/parse_mem0_config.py" "" -``` - -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( - user_id="", - app_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 . 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 (item 15) - -Print a structured diff to the terminal before making any changes. Use exactly -this format: - -``` -## Dream — Memory Consolidation Report - -### Merge proposals ( pairs) -MERGE [mem0:] + [mem0:] → NEW - - Original 1: "" - - Original 2: "" - - Merged: "" - -### Contradictions ( pairs) -CONFLICT [mem0:] vs [mem0:] - - A: "" (, confidence: ) - - B: "" (, confidence: ) - Which is current? [A/B/skip] - -### Prune candidates ( memories) -PRUNE [mem0:] — , d old (policy: d) - ---- -Proposed: merges, prunes, 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()` -2. `delete_memory()` -3. `add_memory` with: - - `messages=[{"role": "user", "content": ""}]` - - `user_id=` - - `app_id=` (top-level, not in metadata) - - `metadata={"type": "", "branch": "", "confidence": , "source": "mem0-dream"}` - - `infer=False` - -#### Contradictions (resolved) - -For each resolved conflict where the user chose A or B: -- Identify the loser (the non-chosen memory). -- First call `get_memory()` to read its current text content. -- Then call `update_memory(, data=)` to preserve the text while updating it. -- **Important:** `update_memory` requires the `data` (text) parameter. A metadata-only call may error or wipe the content. Always read first, then update with the original text. - -Contradictions where the user chose `skip` are left untouched. - -#### Prunes - -For each prune candidate: -- `delete_memory()` - ---- - -## Step 6: Print Summary - -After all changes are applied, print: - -``` -Dream complete. - Merged: pairs → new memories - Pruned: memories deleted - Flagged: contradictions resolved, skipped -``` - ---- - -## 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. - -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= merged= pruned= conflicts_skipped= - ``` -4. If contradictions were detected but skipped, store a reminder memory: - ```python - add_memory( - messages=[{"role": "user", "content": "mem0-dream detected contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively."}], - user_id="", - app_id="", - metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": ""}, - infer=False, - ) - ``` - -## Forget mode (targeted deletion) - -When invoked with `--forget` (e.g., `/mem0:dream --forget auth module decisions` -or `/mem0:dream --forget `), skip consolidation and go straight to -search-confirm-delete: - -### F1: Parse input - -The argument after `--forget` is either: -- A search query: `/mem0:dream --forget auth module decisions` -- A memory ID: `/mem0:dream --forget ` - -If no argument after `--forget`, ask: "What should I forget? Provide a search query or memory ID." - -### F2: 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: "" (created )` - -**If search query provided:** -- Call `search_memories` with: - - `query=` - - `filters={"AND": [{"user_id": ""}, {"app_id": ""}]}` - - `limit=10` -- Show numbered list: - ``` - Found memories matching "": - 1. (type: , created: ) [ID: ] - 2. ... - ``` - -### F3: Confirm - -Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'." -For a single memory ID: "Delete this memory? [y/N]" - -**Never delete without confirmation.** - -### F4: Delete and report - -Call `delete_memory` for each confirmed entry. Report: `Deleted memories.` - -### Undo recent writes - -If the user says "undo last N memories" or "undo last write": -1. Run `python3 "$SCRIPT_DIR/session_stats.py" peek` to get `recent_ids`. -2. Show last N entries, ask for confirmation. -3. Delete confirmed entries via `delete_memory`. - ---- - -## Scheduling recurring dreams - -When invoked with `--schedule` (e.g., `/mem0:dream --schedule weekly`), register a -cloud routine via Claude Code's built-in `/schedule` command so the dream runs -automatically without any local cron or launchd setup. - -### Step S1: Parse schedule frequency - -Accept natural-language frequency after `--schedule`: - -| User input | Cron equivalent | Description | -|---|---|---| -| `weekly` or `--schedule weekly` | Every Sunday 3:00 AM local | Default weekly consolidation | -| `daily` | Every day 3:00 AM local | For high-volume projects | -| `biweekly` | Every other Sunday 3:00 AM local | Lower frequency option | -| Custom (e.g., `"every Monday 9am"`) | Pass verbatim to `/schedule` | Let Claude Code resolve it | - -### Step S2: Create the routine - -Use Claude Code's `/schedule` command to create a cloud routine. The routine runs -`/mem0:dream --auto` on the specified schedule against the current repository: - -``` -/schedule /mem0:dream --auto -``` - -For example: -- `/schedule weekly /mem0:dream --auto` — runs every week -- `/schedule daily at 3am /mem0:dream --auto` — runs every day at 3 AM -- `/schedule every Monday 9am /mem0:dream --auto` — runs every Monday at 9 AM - -The `/schedule` command handles all the cloud infrastructure: repository cloning, -environment setup, and cron scheduling. The routine runs as a full Claude Code -cloud session with access to the mem0 MCP tools. - -### Step S3: Confirm to user - -After the routine is created, print: - -``` -Dream scheduled: -Routine name: mem0-dream- -Next run: - -Manage at: https://claude.ai/code/routines -Edit: /schedule list → /schedule update -Cancel: /schedule list → delete the routine -``` - -### Managing scheduled dreams - -| Action | Command | -|---|---| -| List all routines | `/schedule list` | -| Run dream now | `/schedule run` (select the dream routine) | -| Change frequency | `/schedule update` (select the dream routine) | -| Pause | Toggle off at claude.ai/code/routines | -| Delete | Delete at claude.ai/code/routines or `/schedule update` | - -### Fallback for non-cloud users - -If `/schedule` is unavailable (API key auth, no claude.ai subscription), fall back -to local options: - -1. **macOS launchd plist** — generate and install: - ```bash - cat > ~/Library/LaunchAgents/com.mem0.dream.plist << 'PLIST' - - - - - Labelcom.mem0.dream - ProgramArguments - - claude - -p - /mem0:dream --auto - --allowedTools - mcp__mem0__* - - StartCalendarInterval - - Weekday0 - Hour3 - Minute0 - - StandardOutPath/tmp/mem0-dream.log - StandardErrorPath/tmp/mem0-dream.err - WorkingDirectoryPROJECT_DIR - - - PLIST - launchctl load ~/Library/LaunchAgents/com.mem0.dream.plist - ``` - Replace `PROJECT_DIR` with the actual project path. - -2. **Linux cron** — add entry: - ```bash - (crontab -l 2>/dev/null; echo "0 3 * * 0 cd PROJECT_DIR && claude -p '/mem0:dream --auto' >> /tmp/mem0-dream.log 2>&1") | crontab - - ``` - -Print which method was used and how to verify: -``` -Dream scheduled (local: launchd/cron): weekly Sundays 3am -Verify: launchctl list | grep mem0 # macOS - crontab -l | grep mem0 # Linux -``` diff --git a/mem0-plugin/skills/mem0-list-projects/SKILL.md b/mem0-plugin/skills/mem0-list-projects/SKILL.md deleted file mode 100644 index d5dfd6746..000000000 --- a/mem0-plugin/skills/mem0-list-projects/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: mem0-list-projects -description: > - List all project scopes (app_ids) that have stored memories for the current - user. Essential for /mem0:switch-project discoverability and cross-project - workflows. - TRIGGER: user runs /mem0:list-projects, or asks "what projects does mem0 know", - "list my mem0 projects", "show all projects", "which app_ids exist". ---- - -# 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 and extracting distinct `app_id` values. - -Call `get_memories` with: -- `user_id=` -- `page_size=200` - -Do NOT pass `app_id` — we want memories across ALL projects. - -If the response indicates more pages, paginate until all are fetched (up to 1000 -memories max to avoid excessive API calls). - -### Step 2: Extract distinct projects - -For each memory, read the `app_id` field (may also appear as `metadata.app_id` -on older memories). Collect distinct values. - -For each project, count: -- Total memories -- Most recent `created_at` date -- Top 3 `metadata.type` values by frequency - -### Step 3: Display - -``` -## mem0 Projects for - -| Project | Memories | Last Active | Top Categories | -|---------|----------|-------------|---------------| -| | | | decision, convention, anti_pattern | -| | | | task_learning, environmental | -| ... | | | | - -Active project: ← (current) -Total: projects, total memories -``` - -Mark the current project with `← (current)`. - -### Step 4: Empty state - -If zero memories found: -``` -No projects found for user . -Run /mem0:onboard in a project directory to get started. -``` - -### Step 5: Suggest next actions - -``` -Switch project: /mem0:switch-project -Search across all: /mem0:tour --all-projects -``` diff --git a/mem0-plugin/skills/mem0-onboard/SKILL.md b/mem0-plugin/skills/mem0-onboard/SKILL.md deleted file mode 100644 index e05ab8b64..000000000 --- a/mem0-plugin/skills/mem0-onboard/SKILL.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -name: mem0-onboard -description: > - Post-install onboarding wizard for the mem0 plugin. - Detects CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules, mem0.md - and offers to import them. Installs coding categories. Shows active identity. - TRIGGER: user runs /mem0:onboard, or mentions "setup mem0", "configure mem0 plugin". ---- - -# Mem0 Onboarding Wizard - -Run this wizard to set up the mem0 plugin for the current project. Complete in ~30 seconds. - -## Step 0: Ensure mem0ai SDK is installed - -The plugin installs the `mem0ai` Python SDK automatically on session start via a venv in `${CLAUDE_PLUGIN_DATA}/venv`. If Step 4 (categories) fails with an import error, run: - -```bash -"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh" -``` - -This is silent and idempotent — safe to run anytime. - -## Step 1: Verify API key and MCP connection - -Check that mem0 MCP tools are available. Use 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 **any mem0 search tool found**: Proceed to Step 2. The API key is working. -- If **NOT found**: The MCP server failed to connect. Tell the user: - 1. "MCP server not connected. Make sure `MEM0_API_KEY` is exported in your shell." - 2. Show: `export MEM0_API_KEY="m0-your-key-here"` then restart Claude Code. - 3. If they need a key: https://app.mem0.ai/dashboard/api-keys or `mem0 init --agent --json` - 4. **STOP here.** Do not proceed — all other steps need MCP tools. - -## Step 2: Show identity - -Report the active identity to the user: -- Call `search_memories` with `query="project setup"`, `user_id=`, `filters={"AND": [{"user_id": ""}, {"app_id": ""}]}`, `limit=1` to verify connectivity. -- Print: `Connected. user=, project=, branch=` -- If the search fails, troubleshoot the API key. - -## Step 3: Detect and import project files - -Check for these files in the project root: -1. `CLAUDE.md` -2. `AGENTS.md` -3. `.cursorrules` -4. `.windsurfrules` -5. `mem0.md` - -For each file found, ask the user: "Found `` ( bytes). Import into mem0? [Y/n]" - -If user says yes (or default): -- Read the file content -- Call `add_memory` with: - - `messages=[{"role": "user", "content": "## Project Profile: \n\nProject: \n\n"}]` - - `user_id=` - - `app_id=` - - `metadata={"type": "project_profile", "file": "", "source": "onboard", "branch": ""}` - - `infer=False` - -## Step 4: Install coding categories - -Ask: "Install coding categories optimized for development workflows? [Y/n]" - -If yes, run the setup script using the plugin's venv python: - -```bash -VENV_PY="${CLAUDE_PLUGIN_DATA}/venv/bin/python3" -if [ -x "${VENV_PY}" ]; then - "${VENV_PY}" "${CLAUDE_PLUGIN_ROOT}/scripts/setup_coding_categories.py" --apply -else - python3 "${CLAUDE_PLUGIN_ROOT}/scripts/setup_coding_categories.py" --apply -fi -``` - -If the script fails with "mem0ai SDK not found", run the dependency installer first: -```bash -"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh" -``` -Then retry the categories script. - -## Step 5: Mark project as onboarded - -The marker file is already created by `on_session_start.sh` on first display of the onboard prompt. Touch it here for idempotency: - -```bash -_SAFE_PID=$(printf '%s' "" | tr '/:' '--') -mkdir -p ~/.mem0 && touch ~/.mem0/.onboarded_${_SAFE_PID} -``` - -This is silent — no user-facing output needed. - -## Step 6: Summary - -Print a summary: -``` -Onboarding complete. - user_id: - project_id: (app_id) - imported: files - categories: - -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. -``` diff --git a/mem0-plugin/skills/mem0-pin/SKILL.md b/mem0-plugin/skills/mem0-pin/SKILL.md deleted file mode 100644 index ade371bad..000000000 --- a/mem0-plugin/skills/mem0-pin/SKILL.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -name: mem0-pin -description: > - Pin important memories so they surface prominently and are protected from - consolidation. New pins use immutable: true (SDK v2.1.33+) to prevent - deduplication from modifying or removing the memory. Existing memories get - metadata.pinned: true as a search-time filter; full consolidation protection - requires delete + re-add. TRIGGER: user runs /mem0:pin , or - says "pin this memory", "mark as important", "always remember this". ---- - -# Mem0 Pin - -Pin a memory to mark it as high-priority. - -## 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, `user_id`, `app_id`, `limit=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 `content` (text) field -- `original_metadata` — the existing `metadata` dict - -This is required because `update_memory` replaces the full memory — a metadata-only call would wipe the text content. - -### Step 3: Pin it - -There are two sub-cases depending on whether the user wants to pin an **existing** memory or create a **new** pinned memory from scratch. - -#### 3a: Pinning a new memory (preferred path for full protection) - -Use `add_memory` with `immutable: true` as a top-level parameter. The `immutable` flag is a first-class platform parameter (added in SDK v2.1.33) that prevents the memory from being modified or removed by consolidation/deduplication. `metadata.pinned: true` is also included as a search-time filter aid. - -```python -add_memory( - messages=[{"role": "user", "content": ""}], - user_id=, - immutable=True, - metadata={"pinned": True}, -) -``` - -#### 3b: Pinning an existing memory - -Call `update_memory` with: -- `memory_id=` -- `data=` (preserve the existing content) -- `metadata=` merge `original_metadata` with `{"pinned": true}` - -```python -updated_meta = {**original_metadata, "pinned": True} -update_memory(memory_id=, data=, metadata=updated_meta) -``` - -**Important:** `update_memory` requires the `data` (text) parameter. Passing only metadata may error or wipe content. Always read first, then update with the full text and explicit metadata. - -**Consolidation-protection limitation:** `update_memory` cannot set the `immutable` flag on an existing memory. If the user needs full consolidation protection for an existing memory, they must delete it and re-add it using `add_memory` with `immutable: true` (path 3a above). In the confirm step (Step 4), note this limitation if the user pinned an existing memory via `update_memory`. - -### Step 4: Confirm - -For new pins created via `add_memory` with `immutable: true`: -``` -Pinned: "..." -Memory ID: -Pinned with consolidation protection (immutable). This memory will not be modified or removed by deduplication. -``` - -For existing memories pinned via `update_memory`: -``` -Pinned: "..." -Memory ID: -Pinned memories surface first when relevant to a search. -Note: consolidation protection (immutable flag) requires delete + re-add. Say "re-pin this with full protection" to do that. -``` - -### Unpin - -If the user says "unpin" or `/mem0:unpin`: -1. Call `get_memory` to read current content and metadata. -2. Set `metadata.pinned = false` explicitly: - ```python - updated_meta = {**original_metadata, "pinned": False} - update_memory(memory_id=, data=, metadata=updated_meta) - ``` -3. Print: `Unpinned: "..."` diff --git a/mem0-plugin/skills/mem0-stats/SKILL.md b/mem0-plugin/skills/mem0-stats/SKILL.md deleted file mode 100644 index 5f9215f4a..000000000 --- a/mem0-plugin/skills/mem0-stats/SKILL.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -name: mem0-stats -description: > - Show memory statistics for the current session and project lifetime. - Combines local session counters with API-fetched totals. - TRIGGER: user runs /mem0:stats, or asks "how many memories", "mem0 stats", - "memory usage", "show memory count". ---- - -# Mem0 Stats - -Show session and lifetime memory statistics. - -## Execution - -### Step 1: Gather session stats - -Run the session stats reporter: - -```bash -SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts" -python3 "$SCRIPT_DIR/session_stats.py" peek 2>/dev/null || echo "{}" -``` - -The `peek` command returns JSON without clearing the stats file (unlike `report`). - -If the script returns empty or errors, note "No session data available" and continue. - -### Step 2: Fetch lifetime stats from API - -Call `get_memories` with: -- `user_id=` -- `app_id=` -- `page_size=100` - -Count the returned memories. Group them by: -1. `categories[0]` (platform-assigned) — primary grouping -2. `metadata.type` (agent-assigned) — secondary if no categories -3. `created_at` date — for age analysis - -Also run a `search_memories` call with `query="project"`, `limit=1` to measure round-trip latency (time the call). - -### Step 3: Display - -Print a compact dashboard with an ASCII histogram for category distribution: - -``` -## mem0 Stats - -### This Session - Memories written: - Searches run: - Categories touched: - -### Project Lifetime () - Total memories: - - By category: - decision ████████████████ 24 - convention ██████████░░░░░░ 15 - anti_pattern ████░░░░░░░░░░░░ 6 - task_learning ███░░░░░░░░░░░░░ 5 - user_preference ██░░░░░░░░░░░░░░ 3 - session_state █░░░░░░░░░░░░░░░ 2 - - By age: - < 7 days ████████████████ 5 - 7–30 days ██████████░░░░░░ 12 - 30–90 days ████░░░░░░░░░░░░ 10 - > 90 days ██░░░░░░░░░░░░░░ 8 - - By access count: - Never accessed ████████████████ 18 - 1–5 accesses ████████░░░░░░░░ 10 - 6–20 accesses ████░░░░░░░░░░░░ 4 - 20+ accesses █░░░░░░░░░░░░░░░ 3 - - Oldest memory: - Newest memory: - -### Health - API latency: ms - User: - Project: - Branch: -``` - -**Histogram rules:** -- Max bar width: 16 characters. Scale all bars relative to the highest count. -- Use `█` for filled and `░` for empty. Right-align the count number. -- Sort categories by count descending. Omit categories with 0 memories. -- If only 1-2 categories exist, still show the histogram — it provides visual context. -- **Age buckets:** Compute from `created_at`. Buckets: <7d, 7–30d, 30–90d, >90d. -- **Access count buckets:** Read `metadata.access_count` (default 0 if absent). Buckets: 0, 1–5, 6–20, 20+. - -Skip any section with zero data. - -## 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": ""}, {"app_id": ""}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `limit=20` -2. `query="bugs errors fixes"`, same time filter, `limit=20` -3. `query="patterns conventions learnings"`, same time filter, `limit=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: - -``` -### Weekly Digest ( to ) - -New Memories This Week: - : - - () - - ... - -Activity Pattern - Most active day: ( memories) - Categories touched: - -Highlights - <2-3 sentence summary of most important decisions/learnings this week> -``` - -### W4: Write digest file - -Write to `~/.mem0/weekly-digest.md` (overwrite). Append one-line to -`~/.mem0/digest-history.log`: -``` - | | + memories | top: -``` - -### W5: Empty state - -If no new memories in 7 days: -``` -No new memories in the past week for . -Total project memories: . -``` diff --git a/mem0-plugin/skills/mem0/SKILL.md b/mem0-plugin/skills/mem0/SKILL.md index e5ee18068..abc48852e 100644 --- a/mem0-plugin/skills/mem0/SKILL.md +++ b/mem0-plugin/skills/mem0/SKILL.md @@ -1,17 +1,6 @@ --- name: mem0 -description: > - Mem0 Platform SDK for adding persistent memory to AI applications. - TRIGGER when: user mentions "mem0", "MemoryClient", "memory layer", - "remember user preferences", "persistent context", "personalization", - or needs to add long-term memory to chatbots, agents, or AI apps. - Covers Python SDK (mem0ai), TypeScript SDK (mem0ai), and framework integrations - (LangChain, CrewAI, OpenAI Agents SDK, Pipecat, LlamaIndex, AutoGen, LangGraph). - Also covers the open-source self-hosted Memory class. - This is the DEFAULT mem0 skill for ambiguous queries. - DO NOT TRIGGER when: user asks about CLI commands, terminal usage, or shell - scripts (use mem0-cli), or Vercel AI SDK / @mem0/vercel-ai-provider / createMem0 - (use mem0-vercel-ai-sdk). +description: Mem0 SDK reference — Python, TypeScript, and integrations license: Apache-2.0 metadata: author: mem0ai @@ -25,7 +14,6 @@ compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm i > **Skill Graph:** This skill is part of the Mem0 skill graph: > - **mem0** (this skill) -- Platform Client SDK + OSS (Python + TypeScript) -> - **[mem0-cli](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli)** -- Command-line interface > - **[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. @@ -46,7 +34,7 @@ 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`?** Run `mem0 init --agent --json` (after `pip install mem0-cli` or `npm install -g @mem0/cli`) to mint an evaluation key without email or dashboard. The human can claim later with `mem0 init --email `. +> **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 @@ -134,20 +122,27 @@ def chat(user_input: str, user_id: str) -> str: ## Common edge cases -- **Search returns empty:** Memories process asynchronously. Wait 2-3s after `add()` 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. Use `OR` instead, or query separately. -- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. Stick to one mode. -- **Wrong import:** Always use `from mem0 import MemoryClient` (or `AsyncMemoryClient` for async). Do not use `from mem0 import Memory`. -- **v3 defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`. Adjust as needed for your use case. +- **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. -## v2 Compatibility +## v3 API (Current) -If you're using SDK v2.x, note these differences: -- **Entity IDs:** Pass `user_id` as top-level kwarg to `search()` instead of inside `filters` -- **Defaults:** `top_k=100`, no threshold, `rerank=True` -- **Graph memory:** Available via `enable_graph=True` +Mem0 v3 uses single-pass extraction, entity linking, and multi-signal retrieval. -See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details. +**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 @@ -189,5 +184,4 @@ Load these on demand for deeper detail: | Skill | When to use | Link | |-------|-------------|------| -| mem0-cli | Terminal commands, scripting, CI/CD, agent tool loops | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli) | | mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) | diff --git a/mem0-plugin/skills/mem0/references/features.md b/mem0-plugin/skills/mem0/references/features.md index b5d5e73ea..66875ee75 100644 --- a/mem0-plugin/skills/mem0/references/features.md +++ b/mem0-plugin/skills/mem0/references/features.md @@ -69,7 +69,7 @@ If you were using `enable_graph=True` in v2: - 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/oss-v2-to-v3) for details. +See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for details. --- diff --git a/mem0-plugin/skills/memory-reviewer/SKILL.md b/mem0-plugin/skills/memory-reviewer/SKILL.md new file mode 100644 index 000000000..c820afe46 --- /dev/null +++ b/mem0-plugin/skills/memory-reviewer/SKILL.md @@ -0,0 +1,59 @@ +--- +name: memory-reviewer +description: Review memory quality — duplicates, contradictions, stale entries +--- + +# 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 `user_id` and `app_id`. 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= total= + duplicates: found + contradictions: found + low_confidence: found + untagged: found + stale: found +``` + +5. **If issues found**, list them with memory IDs: + +``` +Issues: + [duplicate] "" ≈ "" [mem0:, mem0:] + [contradiction] "" vs "" [mem0:, mem0:] + [low_conf] "" (confidence: 0.1) [mem0:] +``` + +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 diff --git a/mem0-plugin/skills/onboard/SKILL.md b/mem0-plugin/skills/onboard/SKILL.md new file mode 100644 index 000000000..be647bb45 --- /dev/null +++ b/mem0-plugin/skills/onboard/SKILL.md @@ -0,0 +1,156 @@ +--- +name: onboard +description: Set up mem0 for this project — API key setup, config, categories, identity +--- + +# Mem0 Onboarding Wizard + +Run this wizard to set up the mem0 plugin for the current project. Complete in ~60 seconds. + +## Step 0: Ensure mem0ai SDK is installed + +The plugin installs the `mem0ai` Python SDK automatically on session start via a venv in `${CLAUDE_PLUGIN_DATA}/venv`. If Step 5 (categories) fails with an import error, run: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh" +``` + +This is silent and idempotent — safe to run anytime. + +## Step 1: Set up API key + +Check if `MEM0_API_KEY` is already set by running: + +```bash +echo $MEM0_API_KEY +``` + +### If API key IS set (non-empty output) + +Print: `- API key found.` and proceed to Step 2. + +### If API key is NOT set (empty output) + +Guide the user through API key setup. Show this message and walk them through it: + +``` +Step 1: Setting up API key. + +- MEM0_API_KEY not found. Let's set it up. + + 1. Get your API key from https://app.mem0.ai/dashboard/api-keys + (or run: mem0 init --agent --json) + + 2. Add to shell profile: + echo 'export MEM0_API_KEY="m0-your-key-here"' >> ~/.zshrc + source ~/.zshrc + + 3. Verify: + echo $MEM0_API_KEY + # Should print your key +``` + +After the user confirms they've set the key, verify it by running `echo $MEM0_API_KEY`. If still empty, repeat the instructions. If set, proceed to Step 2. + +## Step 2: MCP OAuth login + +Now authenticate the MCP server connection. Tell the user: + +``` +Step 2: MCP OAuth login. + + 1. Type /mcp in Claude Code + 2. A browser window will open for authentication at mcp.mem0.ai + 3. Log in with your mem0 account + 4. Return here after authenticating in your browser +``` + +After the user completes OAuth, verify MCP tools are 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 found: Print `- MCP connected.` and proceed to Step 3. +- If NOT found: Troubleshoot before giving up: + 1. Check plugin is installed: run `/plugins` and confirm `mem0` appears + 2. Ask if the browser auth completed successfully + 3. Look for `mcp.mem0.ai` in the MCP server list via `/mcp` + 4. If all checks fail: "Restart Claude Code 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"`, `user_id=`, `filters={"AND": [{"user_id": ""}, {"app_id": ""}]}`, `limit=1` to verify connectivity. + +Print: +``` +- Connected + user: + project: + branch: +``` + +If the search fails, troubleshoot the API key and MCP connection. + +## Step 4: Detect and import project files + +Check for these files in the project root: +1. `CLAUDE.md` +2. `AGENTS.md` +3. `.cursorrules` +4. `.windsurfrules` +5. `mem0.md` + +For each file found, ask the user: "Found `` ( bytes). Import into mem0? [Y/n]" + +If user says yes (or default): +- Read the file content +- Call `add_memory` with: + - `messages=[{"role": "user", "content": "## Project Profile: \n\nProject: \n\n"}]` + - `user_id=` + - `app_id=` + - `metadata={"type": "project_profile", "file": "", "source": "onboard", "branch": ""}` + - `infer=False` + +## Step 5: Install coding categories + +Ask: "Install coding categories optimized for development workflows? [Y/n]" + +If yes, run the setup script using the plugin's venv python: + +```bash +VENV_PY="${CLAUDE_PLUGIN_DATA}/venv/bin/python3" +if [ -x "${VENV_PY}" ]; then + "${VENV_PY}" "${CLAUDE_PLUGIN_ROOT}/scripts/setup_coding_categories.py" --apply +else + python3 "${CLAUDE_PLUGIN_ROOT}/scripts/setup_coding_categories.py" --apply +fi +``` + +If the script fails with "mem0ai SDK not found", run the dependency installer first: +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh" +``` +Then retry the categories script. + +## Step 6: Mark project as onboarded + +```bash +_SAFE_PID=$(printf '%s' "" | tr '/:' '--') +mkdir -p ~/.mem0 && touch ~/.mem0/.onboarded_${_SAFE_PID} +``` + +This is silent — no user-facing output needed. + +## Step 7: Summary + +Print a summary: +``` +- Onboarding complete. + user_id: + project_id: (app_id) + imported: files + categories: + +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. +``` diff --git a/mem0-plugin/skills/peek/SKILL.md b/mem0-plugin/skills/peek/SKILL.md new file mode 100644 index 000000000..49215e1cf --- /dev/null +++ b/mem0-plugin/skills/peek/SKILL.md @@ -0,0 +1,42 @@ +--- +name: peek +description: Quick search memories — compact one-liner results +--- + +# 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?" + +### Step 2: Search + +Run 2 parallel `search_memories` calls: + +1. Broad: `query=`, `filters={"AND": [{"user_id": ""}, {"app_id": ""}]}`, `top_k=10` +2. Targeted: `query=`, `filters={"AND": [{"user_id": ""}, {"app_id": ""}, {"metadata": {"type": "decision"}}]}`, `top_k=5` + +### Step 3: Display + +Deduplicate by ID, then show compact results: + +``` +## mem0 peek: "" ( 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: `. [] () [mem0:]` + +If no results: +``` +No memories matching "" for project . +``` diff --git a/mem0-plugin/skills/pin/SKILL.md b/mem0-plugin/skills/pin/SKILL.md new file mode 100644 index 000000000..cde56c1d4 --- /dev/null +++ b/mem0-plugin/skills/pin/SKILL.md @@ -0,0 +1,75 @@ +--- +name: pin +description: Pin a memory to protect it from consolidation +--- + +# 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": ""}, {"app_id": ""}]}`, `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 + +#### 3a: Pinning a new memory (full protection) + +Use `add_memory` with `immutable: true`: + +```python +add_memory( + messages=[{"role": "user", "content": ""}], + user_id=, + immutable=True, + metadata={"pinned": True}, +) +``` + +#### 3b: Pinning an existing memory + +Call `update_memory` with: +- `memory_id=` +- `text=` (preserve existing content) +- `metadata=` merge `original_metadata` with `{"pinned": true}` + +```python +updated_meta = {**original_metadata, "pinned": True} +update_memory(memory_id=, text=, metadata=updated_meta) +``` + +**Important:** `update_memory` requires the `text` parameter. Passing only metadata may error or wipe content. + +### Step 4: Confirm + +``` +Pinned: "..." +Memory ID: +``` + +### Unpin + +If the user says "unpin": +1. Call `get_memory` to read current content and metadata. +2. Set `metadata.pinned = false`: + ```python + updated_meta = {**original_metadata, "pinned": False} + update_memory(memory_id=, text=, metadata=updated_meta) + ``` +3. Print: `Unpinned: "..."` diff --git a/mem0-plugin/skills/mem0-mcp/SKILL.md b/mem0-plugin/skills/protocol/SKILL.md similarity index 90% rename from mem0-plugin/skills/mem0-mcp/SKILL.md rename to mem0-plugin/skills/protocol/SKILL.md index 49cbe2848..a33e759b4 100644 --- a/mem0-plugin/skills/mem0-mcp/SKILL.md +++ b/mem0-plugin/skills/protocol/SKILL.md @@ -1,17 +1,36 @@ --- -name: mem0-mcp -description: > - Mem0 memory protocol for agents using the mem0 MCP tools (Claude Code, Cursor, - Codex, and any other MCP-aware runtime). Decide deliberately when memory context - would help, run targeted searches with metadata filters when it would, and store - key learnings as work completes. Use the mem0 MCP tools (add_memory, - search_memories, get_memories, etc.) for all memory operations. +name: protocol +description: Memory protocol — when to search, how to filter, what to store --- # Mem0 MCP Memory Protocol You have access to persistent memory via the mem0 MCP tools. Follow this protocol to maintain context across sessions. +## Natural language → skill routing + +When a user says something in natural language, route to the right mem0 skill automatically: + +| User says | Invoke | +|---|---| +| "remember this", "save this", "store this fact" | `/mem0:remember` | +| "forget this", "delete memory about X", "remove that memory", "undo last write" | `/mem0:forget` | +| "what do we know about X", "quick search", "peek at memories", "context for X" | `/mem0:peek` | +| "show my memories", "browse memories", "memory tour", "list all memories" | `/mem0:tour` | +| "clean up memories", "consolidate", "merge duplicates", "prune" | `/mem0:dream` | +| "export memories", "backup memories", "download memories" | `/mem0:export` | +| "import memories", "load memories from file" | `/mem0:import` | +| "check mem0 health", "is mem0 working", "diagnose mem0" | `/mem0:health` | +| "memory stats", "how many memories", "weekly digest" | `/mem0:stats` | +| "set up mem0", "initialize mem0", "configure mem0" | `/mem0:onboard` | +| "switch project", "change mem0 project" | `/mem0:switch-project` | +| "list projects", "show all projects" | `/mem0:list-projects` | +| "pin this memory", "protect this memory" | `/mem0:pin` | +| "unpin this memory", "unprotect this memory" | `/mem0:pin` | +| "check memory quality", "any duplicates?", "review memories" | `/mem0:memory-reviewer` | + +If unsure, use the MCP tools directly (search/add/delete) without invoking a skill. + ## On every new task Decide whether persistent memory context would improve your response, then act accordingly. Don't search by default — search deliberately. @@ -56,7 +75,7 @@ After receiving search results, scan for contradictions before using them: 1. If 2+ results address the **same topic** (same `metadata.type`, overlapping file paths or entity names) but assert **opposing facts**, surface BOTH to the user instead of silently picking one. 2. Format: ``` - ⚠️ Conflicting memories found: + Conflicting memories: - [mem0:] "" (confidence: , ) - [mem0:] "" (confidence: , ) Which is current? @@ -76,9 +95,9 @@ When you do search, run **2–4 parallel** `search_memories` calls at different **Metadata filters** match the same `type` values written under "After completing significant work" below. -Two rules from the v2 filter spec: +Filter rules: -1. The root **must** be a logical operator (`AND` / `OR` / `NOT`) with an array. A bare `{"user_id": "..."}` won't work. +1. Prefer `AND` / `OR` / `NOT` operators at the root. A bare `{"user_id": "..."}` works as shorthand but cannot combine multiple fields. 2. Metadata uses a **nested** object, not a dotted key. `{"metadata": {"type": "decision"}}`, never `{"metadata.type": "decision"}`. Only top-level metadata keys are filterable. Combine `user_id` + `app_id` with one metadata clause per call: @@ -324,7 +343,7 @@ search_memories( {"metadata.files": {"contains": "src/middleware/auth.ts"}}, ] }, - limit=5, + top_k=5, ) ``` @@ -348,12 +367,12 @@ current_meta["last_accessed"] = datetime.datetime.now(datetime.timezone.utc).iso # 3. Update with preserved content and bumped metadata update_memory( memory_id=, - data=current_text, # preserve original text — required parameter + text=current_text, # preserve original text — required parameter metadata=current_meta, # pass updated access_count and last_accessed ) ``` -**Important:** `update_memory` requires the `data` (text) parameter. Always `get_memory` first to read the current content, then pass it back unchanged. A metadata-only update may error or wipe the content. +**Important:** `update_memory` requires the `text` parameter. Always `get_memory` first to read the current content, then pass it back unchanged. A metadata-only update may error or wipe the content. **When to increment:** Only when you actually used the memory to answer. Don't bump on every search hit — that inflates counts for memories that were returned but irrelevant. Aim for 1-3 bumps per response at most. diff --git a/mem0-plugin/skills/mem0-remember/SKILL.md b/mem0-plugin/skills/remember/SKILL.md similarity index 80% rename from mem0-plugin/skills/mem0-remember/SKILL.md rename to mem0-plugin/skills/remember/SKILL.md index e465d5fcb..785feec8e 100644 --- a/mem0-plugin/skills/mem0-remember/SKILL.md +++ b/mem0-plugin/skills/remember/SKILL.md @@ -1,9 +1,6 @@ --- -name: mem0-remember -description: > - Quick-add a memory from the user's input. No extraction pass — stores verbatim. - TRIGGER: user runs /mem0:remember , or says "remember this", "save this", - "store this fact", "don't forget that". +name: remember +description: Save a memory verbatim from your input --- # Mem0 Remember @@ -48,6 +45,8 @@ Call `add_memory` with: Print: ``` -Remembered as : "..." +Remembered as : "" Memory ID: ``` + +Append `...` only if content was truncated (longer than 80 chars). diff --git a/mem0-plugin/skills/stats/SKILL.md b/mem0-plugin/skills/stats/SKILL.md new file mode 100644 index 000000000..e82ad09e0 --- /dev/null +++ b/mem0-plugin/skills/stats/SKILL.md @@ -0,0 +1,121 @@ +--- +name: stats +description: Show memory usage stats for this session and project +--- + +# Mem0 Stats + +Show session and lifetime memory statistics. + +## Execution + +### Step 1: Gather session stats + +Run the session stats reporter: + +```bash +SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts" +python3 "$SCRIPT_DIR/session_stats.py" peek 2>/dev/null || echo "{}" +``` + +The `peek` command returns JSON without clearing the stats file (unlike `report`). + +If the script returns empty or errors, note "No session data available" and continue. + +### Step 2: Fetch lifetime stats from API + +Call `get_memories` with: +- `user_id=` +- `app_id=` +- `page_size=100` + +Count the returned memories. Group them by: +1. `categories[0]` (platform-assigned) — primary grouping +2. `metadata.type` (agent-assigned) — secondary if no categories +3. `created_at` date — for age analysis + +Also run a `search_memories` call with `query="project"`, `limit=1` to measure round-trip latency (time the call). + +### Step 3: Display + +Print a minimal dashboard. No ASCII bar charts — use a clean table layout: + +``` +## mem0 stats + +**Session** — 3 written, 5 searches, categories: decision, convention + +**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:** +- Category table: sort by count descending, omit categories with 0 memories +- Age: single line with dot-separated buckets, computed from `created_at` +- Session line: skip if no session data available +- 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": ""}, {"app_id": ""}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `limit=20` +2. `query="bugs errors fixes"`, same time filter, `limit=20` +3. `query="patterns conventions learnings"`, same time filter, `limit=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: + +``` +### 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.md` (overwrite). Append one-line to +`~/.mem0/digest-history.log`: +``` + | | + memories | top: +``` + +### W5: Empty state + +If no new memories in 7 days: +``` +No new memories in the past week. Total: memories in . +``` diff --git a/mem0-plugin/skills/mem0-switch-project/SKILL.md b/mem0-plugin/skills/switch-project/SKILL.md similarity index 80% rename from mem0-plugin/skills/mem0-switch-project/SKILL.md rename to mem0-plugin/skills/switch-project/SKILL.md index 923b5edec..477155da1 100644 --- a/mem0-plugin/skills/mem0-switch-project/SKILL.md +++ b/mem0-plugin/skills/switch-project/SKILL.md @@ -1,10 +1,6 @@ --- -name: mem0-switch-project -description: > - Manually override project_id for the current directory. - Useful for monorepos, nested git dirs, or non-git directories. - TRIGGER: user runs /mem0:switch-project , or asks "switch mem0 project", - "change project scope", "override project_id". +name: switch-project +description: Override the auto-detected project scope --- # Mem0 Switch Project diff --git a/mem0-plugin/skills/mem0-tour/SKILL.md b/mem0-plugin/skills/tour/SKILL.md similarity index 89% rename from mem0-plugin/skills/mem0-tour/SKILL.md rename to mem0-plugin/skills/tour/SKILL.md index 6c808118a..9f8d7c7f4 100644 --- a/mem0-plugin/skills/mem0-tour/SKILL.md +++ b/mem0-plugin/skills/tour/SKILL.md @@ -1,10 +1,6 @@ --- -name: mem0-tour -description: > - Show what mem0 knows about the current project. Dumps top memories - grouped by category. Power-user-friendly proof of value. - TRIGGER: user runs /mem0:tour, or asks "what do you know about this project", - "show me my memories", "what has mem0 stored". +name: tour +description: Browse stored memories grouped by category --- # Mem0 Project Tour @@ -22,18 +18,14 @@ When invoked with `--all-projects` (e.g., `/mem0:tour --all-projects` or 3. Group results by `app_id` first, then by category within each project. 4. Display: ``` - ## Cross-Project Tour for - - ### ( memories) - #### Architecture Decisions - - + ## ( memories) ← current + **Architecture Decisions** — ... - ### ( memories) + ## ( memories) ... - --- - Total: memories across projects + memories across projects ``` 5. Mark the current project with `← (current)` in the heading. @@ -126,9 +118,7 @@ For groups with zero results, skip them entirely — don't print empty groups. ### Step 5: Print totals ``` ---- -Total: unique memories across categories for project -Branch: + memories across categories — project: , branch: ``` ### Step 6: Empty state diff --git a/mem0-plugin/tests/test_on_file_read.py b/mem0-plugin/tests/test_on_file_read.py new file mode 100644 index 000000000..6eafa7ea6 --- /dev/null +++ b/mem0-plugin/tests/test_on_file_read.py @@ -0,0 +1,105 @@ +"""Tests for on_file_read.sh hook — skip rules and dedup logic.""" + +from __future__ import annotations + +import json +import os +import subprocess + +SCRIPT = os.path.join(os.path.dirname(__file__), "..", "scripts", "on_file_read.sh") + + +def _run_hook(file_path: str, env_overrides: dict | None = None) -> subprocess.CompletedProcess: + env = { + **os.environ, + "MEM0_API_KEY": "", + "CLAUDE_PLUGIN_OPTION_API_KEY": "", + "CLAUDE_PLUGIN_OPTION_MEM0_API_KEY": "", + "USER": "testuser", + } + if env_overrides: + env.update(env_overrides) + + payload = json.dumps({"tool_name": "Read", "tool_input": {"file_path": file_path}}) + return subprocess.run( + ["bash", SCRIPT], + input=payload, + capture_output=True, + text=True, + env=env, + timeout=8, + ) + + +def test_skip_png(): + result = _run_hook("/project/logo.png") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_lockfile(): + result = _run_hook("/project/package-lock.json") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_min_js(): + result = _run_hook("/project/dist/bundle.min.js") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_node_modules(): + result = _run_hook("/project/node_modules/lodash/index.js") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_git_dir(): + result = _run_hook("/project/.git/config") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_pycache(): + result = _run_hook("/project/__pycache__/module.cpython-311.pyc") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_no_api_key(tmp_path): + """Code file but no API key — should exit cleanly with no output.""" + result = _run_hook("/project/src/app.py", {"MEM0_API_KEY": "", "HOME": str(tmp_path)}) + assert result.returncode == 0 + assert result.stdout == "" + + +def test_skip_empty_file_path(): + """Empty file_path — should exit cleanly.""" + result = _run_hook("") + assert result.returncode == 0 + assert result.stdout == "" + + +def test_dedup_repeated_reads(tmp_path): + """Same file read twice — second should be skipped (via dedup file).""" + dedup_file = str(tmp_path / "mem0_recent_reads_testuser") + env = {"MEM0_API_KEY": "fake-key", "USER": "testuser"} + + # Write the file path into the dedup tracking file + with open(dedup_file, "w") as f: + f.write("/project/src/app.py\n") + + # Patch RECENT_FILE by providing the USER env and ensuring /tmp has the file + # Since the script uses /tmp/mem0_recent_reads_${USER}, we create it there + tmp_dedup = "/tmp/mem0_recent_reads_testuser" + try: + with open(tmp_dedup, "w") as f: + f.write("/project/src/app.py\n") + + result = _run_hook("/project/src/app.py", env) + assert result.returncode == 0 + assert result.stdout == "" + finally: + if os.path.exists(tmp_dedup): + os.remove(tmp_dedup) diff --git a/mem0-plugin/tests/test_rubric_dedup.py b/mem0-plugin/tests/test_rubric_dedup.py new file mode 100644 index 000000000..3e80d69c0 --- /dev/null +++ b/mem0-plugin/tests/test_rubric_dedup.py @@ -0,0 +1,58 @@ +"""Tests for rubric deduplication in on_user_prompt.sh.""" + +from __future__ import annotations + +import json +import os +import subprocess + +import pytest + +SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts") + + +@pytest.fixture(autouse=True) +def _clean_rubric_flag(tmp_path, monkeypatch): + """Use a temp dir for the rubric flag file.""" + monkeypatch.setenv("MEM0_RUBRIC_DIR", str(tmp_path)) + yield + + +def _run_hook(prompt: str, env_overrides: dict | None = None, session_id: str = "test-sess-001") -> str: + """Run on_user_prompt.sh with a simulated prompt and return stdout.""" + env = { + **os.environ, + "USER": "testuser", + "MEM0_API_KEY": "test-key-123", + "MEM0_RESOLVED_USER_ID": "testuser", + "MEM0_PROJECT_ID": "test-project", + "MEM0_BRANCH": "main", + } + if env_overrides: + env.update(env_overrides) + + input_json = json.dumps({"prompt": prompt, "session_id": session_id}) + result = subprocess.run( + ["bash", os.path.join(SCRIPTS_DIR, "on_user_prompt.sh")], + input=input_json, + capture_output=True, + text=True, + env=env, + timeout=10, + ) + return result.stdout + + +def test_first_prompt_gets_full_rubric(): + """First substantial prompt of session gets full memory check rubric.""" + output = _run_hook("How should we refactor the auth module?") + assert "Search mem0" in output + assert "Search tips" in output + assert "metadata.type" in output + + +def test_second_prompt_gets_no_rubric(): + """Second prompt of session emits nothing — rubric and tips only on first prompt.""" + _run_hook("How should we refactor the auth module?") + output = _run_hook("What about the database layer?") + assert output.strip() == "" diff --git a/mem0-plugin/tests/test_search.py b/mem0-plugin/tests/test_search.py new file mode 100644 index 000000000..fd9f91de7 --- /dev/null +++ b/mem0-plugin/tests/test_search.py @@ -0,0 +1,116 @@ +"""Tests for _search.py — shared mem0 search API helper.""" + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + + +def test_search_memories_returns_results(): + from _search import search_memories + + fake_results = [ + {"id": "abc123", "memory": "Use Postgres for auth", "metadata": {"type": "decision"}}, + {"id": "def456", "memory": "Never use floats for money", "metadata": {"type": "anti_pattern"}}, + ] + + def mock_urlopen(req, timeout=None): + resp = MagicMock() + resp.read.return_value = json.dumps({"results": fake_results}).encode() + resp.__enter__ = lambda s: s + resp.__exit__ = MagicMock(return_value=False) + return resp + + with patch("urllib.request.urlopen", side_effect=mock_urlopen): + results = search_memories("test-key", "user1", "proj1", "auth decisions") + + assert len(results) == 2 + assert results[0]["id"] == "abc123" + + +def test_search_memories_with_metadata_type(): + from _search import search_memories + + captured_body = {} + + def mock_urlopen(req, timeout=None): + captured_body.update(json.loads(req.data.decode())) + resp = MagicMock() + resp.read.return_value = json.dumps({"results": []}).encode() + resp.__enter__ = lambda s: s + resp.__exit__ = MagicMock(return_value=False) + return resp + + with patch("urllib.request.urlopen", side_effect=mock_urlopen): + search_memories("key", "user", "proj", "query", metadata_type="decision") + + filters = captured_body["filters"] + assert {"metadata": {"type": "decision"}} in filters["AND"] + + +def test_search_memories_handles_api_error(): + from _search import search_memories + + with patch("urllib.request.urlopen", side_effect=Exception("timeout")): + results = search_memories("key", "user", "proj", "query") + + assert results == [] + + +def test_search_memories_handles_list_response(): + from _search import search_memories + + fake_results = [{"id": "abc", "memory": "test"}] + + def mock_urlopen(req, timeout=None): + resp = MagicMock() + resp.read.return_value = json.dumps(fake_results).encode() + resp.__enter__ = lambda s: s + resp.__exit__ = MagicMock(return_value=False) + return resp + + with patch("urllib.request.urlopen", side_effect=mock_urlopen): + results = search_memories("key", "user", "proj", "query") + + assert len(results) == 1 + + +def test_search_memories_respects_top_k(): + from _search import search_memories + + captured_body = {} + + def mock_urlopen(req, timeout=None): + captured_body.update(json.loads(req.data.decode())) + resp = MagicMock() + resp.read.return_value = json.dumps({"results": []}).encode() + resp.__enter__ = lambda s: s + resp.__exit__ = MagicMock(return_value=False) + return resp + + with patch("urllib.request.urlopen", side_effect=mock_urlopen): + search_memories("key", "user", "proj", "query", top_k=5) + + assert captured_body["top_k"] == 5 + + +def test_search_memories_no_api_key_returns_empty(): + from _search import search_memories + + results = search_memories("", "user", "proj", "query") + assert results == [] + + +def test_format_results_for_context(): + from _search import format_results_for_context + + memories = [ + {"id": "abc12345-long-id", "memory": "Use Postgres for auth", "metadata": {"type": "decision"}}, + {"id": "def67890-long-id", "memory": "JWT tokens expire in 1h", "metadata": {"type": "convention"}}, + ] + + output = format_results_for_context(memories, heading="Relevant memories") + assert "Relevant memories" in output + assert "[decision]" in output + assert "Use Postgres for auth" in output + assert "abc12345" in output diff --git a/mem0-plugin/tests/test_write_path.py b/mem0-plugin/tests/test_write_path.py index 0014e3062..7dce282d4 100644 --- a/mem0-plugin/tests/test_write_path.py +++ b/mem0-plugin/tests/test_write_path.py @@ -190,5 +190,6 @@ def test_resolve_api_key_returns_empty_when_neither_set(monkeypatch): from _identity import resolve_api_key monkeypatch.delenv("MEM0_API_KEY", raising=False) + monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_API_KEY", raising=False) monkeypatch.delenv("CLAUDE_PLUGIN_OPTION_MEM0_API_KEY", raising=False) assert resolve_api_key() == ""