feat(mem0-plugin): improve auto-triggering — pre-fetch, dedup, skill enforcement v0.2.3 (#5237)

This commit is contained in:
Kartik
2026-05-23 17:20:54 +05:30
committed by GitHub
parent 16a7702d09
commit 99beb007ab
54 changed files with 1817 additions and 1162 deletions
+1 -1
View File
@@ -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"
}
]
}
+1 -1
View File
@@ -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"
}
]
}
+20 -4
View File
@@ -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):
<CodeGroup>
```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
```
</CodeGroup>
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`
<CardGroup cols={2}>
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
+11 -3
View File
@@ -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"
<CodeGroup>
```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
```
</CodeGroup>
## Installation
### Option A — Direct MCP (Recommended)
+11 -3
View File
@@ -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"
<CodeGroup>
```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
```
</CodeGroup>
<Warning>
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.
</Warning>
+12 -3
View File
@@ -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
}
}
}
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+2 -1
View File
@@ -5,7 +5,8 @@
"url": "https://mcp.mem0.ai/mcp/",
"headers": {
"Authorization": "Token ${MEM0_API_KEY}"
}
},
"authorizationUrl": "https://mcp.mem0.ai/authorize"
}
}
}
+12 -2
View File
@@ -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
}
]
}
+7 -2
View File
@@ -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
}
]
}
+12 -2
View File
@@ -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
}
]
}
@@ -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:
```
[<type>] <content, max 80 chars> [mem0:<short_id>]
```
## 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
+32 -3
View File
@@ -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:
+31 -7
View File
@@ -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"
+61
View File
@@ -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)
+6
View File
@@ -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)
@@ -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))
+52
View File
@@ -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()))
+52 -41
View File
@@ -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 <<EOF
# Extract query (first 80 chars of error line)
ERROR_QUERY=$(echo "$ERROR_LINE" | cut -c1-80)
## Error detected in command output
\`$COMMAND\` produced an error:
> $ERROR_LINE
EOF
if [ -n "$FILE_DISPLAY" ]; then
cat <<EOF
**Files in stack trace:**
$FILE_DISPLAY
EOF
fi
cat <<EOF
Search mem0 for prior occurrences — this error may have been seen before:
- \`search_memories(query="$(echo "$ERROR_LINE" | cut -c1-60)", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "anti_pattern"}}]})\`
- \`search_memories(query="$(echo "$ERROR_LINE" | cut -c1-60)", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "bug_fix"}}]})\`
EOF
if [ -n "$TRACE_FILES" ]; then
FIRST_FILE=$(echo "$TRACE_FILES" | head -1 | sed 's/:[0-9]*//')
cat <<EOF
- \`search_memories(query="$FIRST_FILE", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}]})\`
EOF
fi
cat <<EOF
If mem0 returns relevant context, use it to debug faster.
If you solve this, store the fix as an \`anti_pattern\` or \`bug_fix\` memory for next time.
EOF
# Telemetry
# Telemetry (fire regardless of API key)
python3 "$SCRIPT_DIR/telemetry.py" bash_error --error_detected 2>/dev/null &
# No API key — skip output entirely
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
# Pre-fetch memories: anti_pattern and bug_fix searches
RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_QUERY="$ERROR_QUERY" MEM0_SEARCH_USER="$USER_ID" \
MEM0_API_KEY="${MEM0_API_KEY}" MEM0_PROJECT_ID="${MEM0_PROJECT_ID:-unknown}" \
python3 -c "
import os, sys
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
from _search import search_memories, format_results_for_context
api_key = os.environ.get('MEM0_API_KEY', '')
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
project_id = os.environ.get('MEM0_PROJECT_ID', 'unknown')
query = os.environ.get('MEM0_SEARCH_QUERY', '')
r1 = search_memories(api_key, user_id, project_id, query, metadata_type='anti_pattern', top_k=3)
r2 = search_memories(api_key, user_id, project_id, query, metadata_type='bug_fix', top_k=3)
seen = set()
combined = []
for m in r1 + r2:
mid = m.get('id', '')
if mid not in seen:
seen.add(mid)
combined.append(m)
print(format_results_for_context(combined, heading='Prior error memories'), end='')
" 2>/dev/null || echo "")
# 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
+65
View File
@@ -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
+4 -4
View File
@@ -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
+81 -67
View File
@@ -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 <<BANNER
## Mem0 — First Run Setup
\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none\`
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):
\`\`\`
Mem0 Active | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
\`\`\`
BANNER
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. Set up API key (get from dashboard, export to .zshrc, verify)
2. MCP OAuth login (authenticate via browser at mcp.mem0.ai)
3. Verify connectivity and show identity
4. Detect and import project files (CLAUDE.md, AGENTS.md, .cursorrules, etc.)
5. Install coding categories optimized for development
6. 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.
EOF
exit 0
elif [ -f "$_ONBOARD_MARKER" ]; then
# State 2: OAuth-authed — onboarded previously without API key
MEM0_AUTH_MODE="oauth"
else
# State 3: Not first startup, not onboarded — inactive
cat <<BANNER
## Mem0 Inactive
\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none\`
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):
\`\`\`
Mem0 Inactive | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
\`\`\`
Set MEM0_API_KEY to enable persistent memory. Get a key at https://app.mem0.ai/dashboard/api-keys or run \`/mem0:onboard\` to set up.
BANNER
exit 0
fi
fi
# Check for failed dependency installation and warn the user
@@ -51,8 +96,9 @@ if [ -f "${_DATA_DIR}/.install-failed" ]; then
fi
# Fetch project-scoped memory count (best-effort, don't block on failure, 5s timeout)
# Skip REST call when OAuth-authed — no API key to authenticate with
MEM0_COUNT="?"
if command -v python3 >/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 <<BANNER
## Mem0 Active
\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=$MEM0_AUTH_MODE\`
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):
\`\`\`
Mem0 Active | user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT
\`\`\`
Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:
- user_id: \`$MEM0_RESOLVED_USER_ID\`
- app_id: \`$MEM0_PROJECT_ID\` (project scope — passed as top-level \`app_id\`, NOT in metadata)
BANNER
# Load mem0.md project config if present (best-effort, non-blocking)
MEM0_PROJECT_CONFIG=""
@@ -121,32 +169,12 @@ if [ "$SOURCE" = "startup" ]; then
_SAFE_PID=$(printf '%s' "$MEM0_PROJECT_ID" | tr '/:' '--')
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}"
if [ ! -f "$_ONBOARD_MARKER" ]; then
mkdir -p "$HOME/.mem0" 2>/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
+1 -13
View File
@@ -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 "")
+1 -10
View File
@@ -25,16 +25,7 @@ _PROJECT="${MEM0_PROJECT_ID:-unknown}"
cat <<EOF
Task completed: "$TASK_SUBJECT"
Extract key learnings from this completed task and store them using the mem0 \`add_memory\` tool:
1. What strategy worked well? -> 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)
+3 -2
View File
@@ -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
+76 -41
View File
@@ -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 <<EOF
## Memory check
if [ -n "$HAS_RESUME" ]; then
RESUME_RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" python3 -c "
import os, sys
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
from _search import search_memories, format_results_for_context
Before responding, decide whether persistent memory context from mem0 would
improve your answer. The agent -- not this hook -- owns this decision.
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')
**Search WHEN** the user:
- references past work, decisions, or things "we" built
- asks "how should we...", "best way to...", or any decision-style question
- hits an error, bug, or asks for debugging help
- requests work that touches their stack, tools, conventions, or preferences
- starts a non-trivial task in a known project
state = search_memories(api_key, user_id, project_id, 'session state current task', metadata_type='session_state', top_k=3)
decisions = search_memories(api_key, user_id, project_id, 'recent decisions and learnings', metadata_type='decision', top_k=3)
**Skip WHEN:**
- the prompt is an acknowledgement or continuation
- the user is *stating* new info -- that's a write trigger (\`add_memory\`), not a search
- it's a pure syntax / factual question answerable from general knowledge
- you already searched this scope earlier in the turn
all_r = state + decisions
seen = set()
unique = []
for m in all_r:
mid = m.get('id', '')
if mid not in seen:
seen.add(mid)
unique.append(m)
if unique:
print(format_results_for_context(unique, heading='Session context recovered from mem0'))
print()
print('Use these memories to resume work. Do NOT ask the user to repeat context that is already in these memories.')
else:
print('No session state found in mem0. Ask the user what they want to continue.')
" 2>/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 <<EOF
Search mem0 when the user references past work, asks decision questions, hits errors, or starts non-trivial tasks. Skip for acknowledgements, new info (store instead), or pure factual questions.
**Search tips:** Use noun-phrase queries, run 2-4 parallel calls with different \`metadata.type\` filters (decision, anti_pattern, user_preference, convention). Always include \`user_id\` + \`app_id\` in filters. Empty results are normal.
EOF
touch "$RUBRIC_FLAG" 2>/dev/null || true
fi
if [ -n "$HAS_ERROR" ]; then
cat <<EOF
**ERROR DETECTED in prompt.** You SHOULD search mem0 for prior occurrences:
- \`search_memories(query="<error class or message>", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "anti_pattern"}}]})\`
- \`search_memories(query="<module or file from stack trace>", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "task_learning"}}]})\`
This surfaces past debugging context and known failure modes.
**ERROR DETECTED in prompt.** Search mem0 for prior occurrences — use \`anti_pattern\` and \`task_learning\` type filters with the error class or filename from the stack trace.
EOF
fi
@@ -94,29 +151,7 @@ if [ -n "$FILE_PATHS" ]; then
cat <<EOF
**FILE PATHS detected:** \`$FILE_PATHS\`
Search mem0 for context about these files using the \`contains\` operator on \`metadata.files\`:
- \`search_memories(query="<filename>", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata.files": {"contains": "<filename>"}}]})\`
- Also run a broader text search without the files filter as fallback:
- \`search_memories(query="<filename without extension>", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}]})\`
EOF
fi
cat <<EOF
**If searching, do it well:**
- Run **2-4 parallel** \`search_memories\` calls with different angles, not one
query that echoes the user's prompt.
- Phrase queries as **nouns** ("auth module decisions"), not full sentences.
- Filter shape: the root must be a logical operator (\`AND\` / \`OR\` / \`NOT\`)
with an array, and metadata uses a **nested** object (not dotted keys).
Combine \`user_id\` + \`app_id\` with one \`metadata.type\` clause per call:
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "decision"}}]}\` -- design / architecture
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "anti_pattern"}}]}\` -- debugging, error handling
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "user_preference"}}]}\` -- tooling, stack, style
- \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}, {"metadata": {"type": "convention"}}]}\` -- established patterns
- Or scope with just \`{"AND": [{"user_id": "$USER_ID"}, {"app_id": "$MEM0_PROJECT_ID"}]}\` when no metadata filter fits.
- **Recency boost:** For state-related queries ("where were we", "current task", "latest"), add a \`created_at\` filter: \`{"created_at": {"gte": "<90 days ago YYYY-MM-DD>"}}\`. Skip recency for durable facts (conventions, decisions).
- Empty results are normal -- proceed without context.
EOF
exit 0
@@ -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 <N> memories for "<task summary>"
- [decision] <content> [mem0:<short_id>]
- [convention] <content> [mem0:<short_id>]
- [anti_pattern] <content> [mem0:<short_id>]
```
5. If **zero results**: output nothing. Don't announce empty context.
## Constraints
- **Read-only** — never modify or delete memories
- **Max 10 memories** returned (most relevant only)
- **Silent on empty** — only surfaces findings if relevant context exists
- Skip memories already visible in current session context
+224
View File
@@ -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 "<PLUGIN_ROOT>/scripts/parse_mem0_config.py" "<cwd>"
```
Parse the JSON output (a dict of `category → days | null`). If the script fails
or returns `{}`, fall back to these built-in defaults:
| `metadata.type` | Default retention |
|---|---|
| `session_state` | 90 days |
| `compact_summary` | 90 days |
| all others | no pruning |
Store the resolved policies for use in Step 3.
---
## Step 2: Fetch ALL Project Memories
Call `get_memories` to retrieve every memory for the active project:
```python
get_memories(
filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]},
page_size=200,
)
```
If the response indicates more pages exist, paginate until all memories are fetched.
Collect the full list before proceeding. If zero memories are found, print:
```
No memories found for project <project_id>. Nothing to consolidate.
```
…and stop.
---
## Step 3: Analyze — Find Issues
Work entirely in-memory; do not modify anything yet.
Group memories by `metadata.type` (use `"unknown"` when the field is absent).
For each group, identify the following:
### 3a. Near-duplicate pairs (merge candidates)
Two memories are near-duplicates when they express the same fact or decision but
phrased differently (e.g., "Use PostgreSQL for auth" and "Auth DB is PostgreSQL").
Heuristics — two memories are near-duplicates if **all** of these hold:
- Similarity threshold: estimated cosine similarity > 0.9 (use noun/keyword overlap as proxy — if >60% of significant nouns overlap, treat as >0.9 similarity).
- Same `metadata.type`.
- Neither memory is pinned (`metadata.pinned != true`).
For each qualifying pair, draft a merged version that is more complete and specific
than either original.
### 3b. Contradictions
Two memories contradict when they assert opposing facts about the same topic
(e.g., "Deploy to ECS" vs. "Deploy to Vercel").
Identify the likely winner: the more recent memory with higher confidence wins.
Store both IDs and their content for user review.
### 3c. Prune candidates
A memory is a prune candidate when **any** of the following is true:
1. Its `metadata.type` has a retention policy and the memory is older than the
configured number of days (compare `created_at` to today).
2. Its confidence score is below 0.3 AND it contains no information unique to
this project (no file paths, identifiers, or domain-specific nouns).
**Always skip memories where `metadata.pinned == true`**, regardless of age or
confidence.
---
## Step 4: Print Diff Report (item 15)
Print a structured diff to the terminal before making any changes. Use exactly
this format:
```
## dream — consolidation report
Merges (<N>):
[mem0:<id1>] + [mem0:<id2>] → "<merged content, 100 chars>"
Conflicts (<N>):
[mem0:<idA>] vs [mem0:<idB>] — "<topic>" [A/B/skip]
Prune (<N>):
[mem0:<id>] — <type>, <age>d old
Proposed: <N> merges, <N> prunes, <N> conflicts. Apply? [Y/n]
```
If there are zero items in any category, omit that section entirely.
If there are zero total proposals (no merges, no prunes, no conflicts), print:
```
Dream complete. No duplicate, contradictory, or stale memories found.
```
…and stop.
---
## Step 5: Wait for User Input and Apply
### 5a. Contradictions
For each `CONFLICT` pair in the report, wait for the user to type `A`, `B`, or
`skip` (case-insensitive). If they enter nothing (empty), treat as `skip`.
Record the winner for each pair before proceeding to the final apply confirmation.
### 5b. Final confirmation
After all conflict resolutions are collected, prompt:
```
Apply? [Y/n]
```
If the user types `n` or `no` (case-insensitive), print `Cancelled. No changes made.`
and stop.
If the user confirms (`Y`, `yes`, or empty / Enter), apply all changes in this order:
#### Merges
For each approved merge pair:
1. `delete_memory(<id1>)`
2. `delete_memory(<id2>)`
3. `add_memory` with:
- `messages=[{"role": "user", "content": "<merged content>"}]`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>` (top-level, not in metadata)
- `metadata={"type": "<original type>", "branch": "<active_branch>", "confidence": <higher of the two original scores>, "source": "mem0-dream"}`
- `infer=False`
#### Contradictions (resolved)
For each resolved conflict where the user chose A or B:
- Identify the loser (the non-chosen memory).
- First call `get_memory(<loser_id>)` to read its current text content.
- Then call `update_memory(<loser_id>, text=<original_text_content>)` 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(<memory_id>)`
---
## Step 6: Print Summary
After all changes are applied, print:
```
Dream complete — merged: <N>, pruned: <N>, conflicts resolved: <N>, skipped: <N>
```
---
## Auto mode
When invoked with `--auto` (e.g., `/mem0:dream --auto`), run non-interactively:
- **Merges**: applied automatically (no contradiction, both are compatible).
- **Prunes**: applied automatically (age/confidence-based, no ambiguity).
- **Contradictions**: skipped — they require human judgment.
In auto mode:
1. Load policies and fetch memories (Steps 1–3) as normal.
2. Apply merges and prunes silently without printing the diff or prompting.
3. Print a compact summary:
```
[mem0-dream --auto] project=<id> merged=<N> pruned=<N> conflicts_skipped=<N>
```
4. If contradictions were detected but skipped, store a reminder memory:
```python
add_memory(
messages=[{"role": "user", "content": "mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively."}],
user_id="<active_user_id>",
app_id="<active_project_id>",
metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": "<active_branch>"},
infer=False,
)
```
## See also
- `/mem0:forget` — targeted deletion of specific memories (search + confirm + delete)
- `/mem0:health --deep` — quick quality scan without applying changes
@@ -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
+71
View File
@@ -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 <memory_id>`
If no argument, ask: "What should I forget? Provide a search query or memory ID."
### Step 2: Find memories
**If memory ID provided** (looks like a UUID or hex string):
- Call `get_memory` with the ID to verify it exists.
- Show: `Found: "<memory content first 120 chars>" (created <date>)`
**If search query provided:**
- Call `search_memories` with:
- `query=<user's query>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<project_id>"}]}`
- `top_k=10`
- Show numbered list:
```
Found <N> memories matching "<query>":
1. <content, 120 chars> (type: <type>, created: <date>) [ID: <short_id>]
2. ...
```
### Step 3: Confirm
Ask: "Delete which memories? Enter numbers (e.g., 1,3,5), 'all', or 'cancel'."
For a single memory ID, ask: "Delete this memory? [y/N]"
**Never delete without confirmation.** This is destructive.
### Step 4: Delete
For each confirmed memory, call `delete_memory` with the memory ID.
### Step 5: Report
```
Deleted <N> memories.
```
If any deletions failed, report which ones and why.
## Undo recent writes
If the user says "undo last N memories" or "undo last write":
1. Read session stats to get recently written memory IDs:
```bash
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts"
python3 "$SCRIPT_DIR/session_stats.py" peek
```
2. Parse the `recent_ids` array from the JSON output. Each entry has `id`, `category`, `ts`.
3. Show the last N entries (default 1) and ask for confirmation.
4. Delete confirmed entries via `delete_memory`.
If `recent_ids` is empty, tell the user: "No recent memory IDs tracked this session. Try `/mem0:tour` to browse recent memories, or `/mem0:forget <search query>` to find specific ones."
@@ -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: <N>
```
## Memory Quality
| Metric | Count | Action |
|----------------|-------|---------------------------------|
| Duplicates | <N> | Run /mem0:dream to merge |
| Stale | <N> | Run /mem0:dream to prune |
| Contradictions | <N> | Run /mem0:dream to resolve |
| Orphans | <N> | Consider retagging via MCP |
Duplicates: <N> · Stale: <N> · Contradictions: <N> · Orphans: <N>
```
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: <N> stale, <N> low-confidence
Retagged: <N> orphans
Skipped: <N> duplicates (use /mem0:dream), <N> contradictions (use /mem0:dream)
```
To fix issues found by `--deep`, run `/mem0:dream` for automated consolidation (merges, prunes, conflict resolution).
@@ -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: <N> memories
Copilot: <N> memories
Total: <N> memories imported into project <project_id>
Imported <N> memories into <project_id> (cursor: <N>, copilot: <N>)
```
Notes: `infer=False`, tagged `metadata.source=<tool>-import`, sections <50 chars
+54
View File
@@ -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": "<active_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
<app_id_1> <count> memories (last: <date>) ← current
<app_id_2> <count> memories (last: <date>)
<N> projects, <M> total memories
```
Mark current project with `← current`. Sort by memory count descending.
### Step 4: Empty state
If zero memories found:
```
No projects found. Run /mem0:onboard to get started.
```
-396
View File
@@ -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 "<PLUGIN_ROOT>/scripts/parse_mem0_config.py" "<cwd>"
```
Parse the JSON output (a dict of `category → days | null`). If the script fails
or returns `{}`, fall back to these built-in defaults:
| `metadata.type` | Default retention |
|---|---|
| `session_state` | 90 days |
| `compact_summary` | 90 days |
| all others | no pruning |
Store the resolved policies for use in Step 3.
---
## Step 2: Fetch ALL Project Memories
Call `get_memories` to retrieve every memory for the active project:
```python
get_memories(
user_id="<active_user_id>",
app_id="<active_project_id>",
page_size=200,
)
```
If the response indicates more pages exist, paginate until all memories are fetched.
Collect the full list before proceeding. If zero memories are found, print:
```
No memories found for project <project_id>. Nothing to consolidate.
```
…and stop.
---
## Step 3: Analyze — Find Issues
Work entirely in-memory; do not modify anything yet.
Group memories by `metadata.type` (use `"unknown"` when the field is absent).
For each group, identify the following:
### 3a. Near-duplicate pairs (merge candidates)
Two memories are near-duplicates when they express the same fact or decision but
phrased differently (e.g., "Use PostgreSQL for auth" and "Auth DB is PostgreSQL").
Heuristics — two memories are near-duplicates if **all** of these hold:
- Similarity threshold: estimated cosine similarity > 0.9 (use noun/keyword overlap as proxy — if >60% of significant nouns overlap, treat as >0.9 similarity).
- Same `metadata.type`.
- Neither memory is pinned (`metadata.pinned != true`).
For each qualifying pair, draft a merged version that is more complete and specific
than either original.
### 3b. Contradictions
Two memories contradict when they assert opposing facts about the same topic
(e.g., "Deploy to ECS" vs. "Deploy to Vercel").
Identify the likely winner: the more recent memory with higher confidence wins.
Store both IDs and their content for user review.
### 3c. Prune candidates
A memory is a prune candidate when **any** of the following is true:
1. Its `metadata.type` has a retention policy and the memory is older than the
configured number of days (compare `created_at` to today).
2. Its confidence score is below 0.3 AND it contains no information unique to
this project (no file paths, identifiers, or domain-specific nouns).
**Always skip memories where `metadata.pinned == true`**, regardless of age or
confidence.
---
## Step 4: Print Diff Report (item 15)
Print a structured diff to the terminal before making any changes. Use exactly
this format:
```
## Dream — Memory Consolidation Report
### Merge proposals (<N> pairs)
MERGE [mem0:<id1>] + [mem0:<id2>] → NEW
- Original 1: "<content of memory 1, truncated to 120 chars>"
- Original 2: "<content of memory 2, truncated to 120 chars>"
- Merged: "<drafted merged content>"
### Contradictions (<N> pairs)
CONFLICT [mem0:<idA>] vs [mem0:<idB>]
- A: "<content>" (<created_at date>, confidence: <score>)
- B: "<content>" (<created_at date>, confidence: <score>)
Which is current? [A/B/skip]
### Prune candidates (<N> memories)
PRUNE [mem0:<id>] — <metadata.type>, <age>d old (policy: <policy_days>d)
---
Proposed: <N> merges, <N> prunes, <N> conflicts
Apply? [Y/n]
```
If there are zero items in any category, omit that section entirely.
If there are zero total proposals (no merges, no prunes, no conflicts), print:
```
Dream complete. No duplicate, contradictory, or stale memories found.
```
…and stop.
---
## Step 5: Wait for User Input and Apply
### 5a. Contradictions
For each `CONFLICT` pair in the report, wait for the user to type `A`, `B`, or
`skip` (case-insensitive). If they enter nothing (empty), treat as `skip`.
Record the winner for each pair before proceeding to the final apply confirmation.
### 5b. Final confirmation
After all conflict resolutions are collected, prompt:
```
Apply? [Y/n]
```
If the user types `n` or `no` (case-insensitive), print `Cancelled. No changes made.`
and stop.
If the user confirms (`Y`, `yes`, or empty / Enter), apply all changes in this order:
#### Merges
For each approved merge pair:
1. `delete_memory(<id1>)`
2. `delete_memory(<id2>)`
3. `add_memory` with:
- `messages=[{"role": "user", "content": "<merged content>"}]`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>` (top-level, not in metadata)
- `metadata={"type": "<original type>", "branch": "<active_branch>", "confidence": <higher of the two original scores>, "source": "mem0-dream"}`
- `infer=False`
#### Contradictions (resolved)
For each resolved conflict where the user chose A or B:
- Identify the loser (the non-chosen memory).
- First call `get_memory(<loser_id>)` to read its current text content.
- Then call `update_memory(<loser_id>, data=<original_text_content>)` 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(<memory_id>)`
---
## Step 6: Print Summary
After all changes are applied, print:
```
Dream complete.
Merged: <N> pairs → <N> new memories
Pruned: <N> memories deleted
Flagged: <N> contradictions resolved, <N> 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=<id> merged=<N> pruned=<N> conflicts_skipped=<N>
```
4. If contradictions were detected but skipped, store a reminder memory:
```python
add_memory(
messages=[{"role": "user", "content": "mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively."}],
user_id="<active_user_id>",
app_id="<active_project_id>",
metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": "<active_branch>"},
infer=False,
)
```
## Forget mode (targeted deletion)
When invoked with `--forget` (e.g., `/mem0:dream --forget auth module decisions`
or `/mem0:dream --forget <memory_id>`), 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 <memory_id>`
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: "<memory content first 120 chars>" (created <date>)`
**If search query provided:**
- Call `search_memories` with:
- `query=<user's query>`
- `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<project_id>"}]}`
- `limit=10`
- Show numbered list:
```
Found <N> memories matching "<query>":
1. <content, 120 chars> (type: <type>, created: <date>) [ID: <short_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 <N> 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 <frequency> /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: <frequency>
Routine name: mem0-dream-<project_id>
Next run: <next scheduled time>
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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.mem0.dream</string>
<key>ProgramArguments</key>
<array>
<string>claude</string>
<string>-p</string>
<string>/mem0:dream --auto</string>
<string>--allowedTools</string>
<string>mcp__mem0__*</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key><integer>0</integer>
<key>Hour</key><integer>3</integer>
<key>Minute</key><integer>0</integer>
</dict>
<key>StandardOutPath</key><string>/tmp/mem0-dream.log</string>
<key>StandardErrorPath</key><string>/tmp/mem0-dream.err</string>
<key>WorkingDirectory</key><string>PROJECT_DIR</string>
</dict>
</plist>
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
```
@@ -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=<active_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 <user_id>
| Project | Memories | Last Active | Top Categories |
|---------|----------|-------------|---------------|
| <app_id_1> | <count> | <date> | decision, convention, anti_pattern |
| <app_id_2> | <count> | <date> | task_learning, environmental |
| ... | | | |
Active project: <current_project_id> ← (current)
Total: <N> projects, <M> total memories
```
Mark the current project with `← (current)`.
### Step 4: Empty state
If zero memories found:
```
No projects found for user <user_id>.
Run /mem0:onboard in a project directory to get started.
```
### Step 5: Suggest next actions
```
Switch project: /mem0:switch-project <name>
Search across all: /mem0:tour --all-projects
```
-108
View File
@@ -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=<active_user_id>`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `limit=1` to verify connectivity.
- Print: `Connected. user=<user_id>, project=<project_id>, branch=<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 `<filename>` (<size> 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: <filename>\n\nProject: <project_id>\n\n<file_content>"}]`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "project_profile", "file": "<filename>", "source": "onboard", "branch": "<active_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' "<active_project_id>" | 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: <user_id>
project_id: <project_id> (app_id)
imported: <N> files
categories: <installed or skipped>
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.
```
-97
View File
@@ -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 <query or ID>, 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": "<text to pin>"}],
user_id=<user_id>,
immutable=True,
metadata={"pinned": True},
)
```
#### 3b: Pinning an existing memory
Call `update_memory` with:
- `memory_id=<selected_id>`
- `data=<original_text>` (preserve the existing content)
- `metadata=` merge `original_metadata` with `{"pinned": true}`
```python
updated_meta = {**original_metadata, "pinned": True}
update_memory(memory_id=<selected_id>, data=<original_text>, 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 content, first 80 chars>..."
Memory ID: <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 content, first 80 chars>..."
Memory ID: <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=<id>, data=<original_text>, metadata=updated_meta)
```
3. Print: `Unpinned: "<content>..."`
-149
View File
@@ -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=<active_user_id>`
- `app_id=<active_project_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: <N>
Searches run: <N>
Categories touched: <list>
### Project Lifetime (<project_id>)
Total memories: <N>
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: <date>
Newest memory: <date>
### Health
API latency: <N>ms
User: <user_id>
Project: <project_id>
Branch: <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": "<id>"}, {"app_id": "<pid>"}, {"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 (<start_date> to <today>)
New Memories This Week: <N>
<category>: <count>
- <memory summary, 80 chars> (<date>)
- ...
Activity Pattern
Most active day: <day> (<N> memories)
Categories touched: <list>
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`:
```
<YYYY-MM-DD> | <project_id> | +<new_count> memories | top: <top_category>
```
### W5: Empty state
If no new memories in 7 days:
```
No new memories in the past week for <project_id>.
Total project memories: <N>.
```
+20 -26
View File
@@ -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 <your-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) |
@@ -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.
---
@@ -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=<id> total=<N>
duplicates: <N> found
contradictions: <N> found
low_confidence: <N> found
untagged: <N> found
stale: <N> found
```
5. **If issues found**, list them with memory IDs:
```
Issues:
[duplicate] "<memory_a>" ≈ "<memory_b>" [mem0:<id_a>, mem0:<id_b>]
[contradiction] "<memory_x>" vs "<memory_y>" [mem0:<id_x>, mem0:<id_y>]
[low_conf] "<memory_z>" (confidence: 0.1) [mem0:<id_z>]
```
6. **Suggest action**: "Run `/mem0:dream` to consolidate duplicates and resolve contradictions."
## Constraints
- **Read-only** — never modify or delete memories (that's `/mem0:dream`'s job)
- **Max 200 memories** per scan
- Report findings, let user decide on action
+156
View File
@@ -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=<active_user_id>`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `limit=1` to verify connectivity.
Print:
```
- Connected
user: <user_id>
project: <project_id>
branch: <branch>
```
If the search fails, troubleshoot the API key and MCP connection.
## Step 4: 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 `<filename>` (<size> 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: <filename>\n\nProject: <project_id>\n\n<file_content>"}]`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "project_profile", "file": "<filename>", "source": "onboard", "branch": "<active_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' "<active_project_id>" | 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: <user_id>
project_id: <project_id> (app_id)
imported: <N> files
categories: <installed or skipped>
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.
```
+42
View File
@@ -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=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`
### Step 3: Display
Deduplicate by ID, then show compact results:
```
## mem0 peek: "<query>" (<N> results)
1. [decision] Auth module uses JWT with RS256 keys (2025-05-15) [mem0:a3f8b2c1]
2. [anti_pattern] Don't use symmetric HS256 — leaked in env (2025-05-10) [mem0:7e2d9f4a]
3. [convention] All middleware in src/middleware/ (2025-05-08) [mem0:c4d5e6f7]
```
Format: `<number>. [<type>] <content, 80 chars> (<date>) [mem0:<short_id>]`
If no results:
```
No memories matching "<query>" for project <project_id>.
```
+75
View File
@@ -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": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=5`.
- Show numbered list with content previews.
- Ask: "Which memory to pin? Enter a number."
### Step 2: Read current content
Call `get_memory` with the selected memory ID. Store:
- `original_text` — the memory's text content
- `original_metadata` — the existing `metadata` dict
### Step 3: Pin it
#### 3a: Pinning a new memory (full protection)
Use `add_memory` with `immutable: true`:
```python
add_memory(
messages=[{"role": "user", "content": "<text to pin>"}],
user_id=<user_id>,
immutable=True,
metadata={"pinned": True},
)
```
#### 3b: Pinning an existing memory
Call `update_memory` with:
- `memory_id=<selected_id>`
- `text=<original_text>` (preserve existing content)
- `metadata=` merge `original_metadata` with `{"pinned": true}`
```python
updated_meta = {**original_metadata, "pinned": True}
update_memory(memory_id=<selected_id>, text=<original_text>, metadata=updated_meta)
```
**Important:** `update_memory` requires the `text` parameter. Passing only metadata may error or wipe content.
### Step 4: Confirm
```
Pinned: "<memory content, first 80 chars>..."
Memory ID: <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=<id>, text=<original_text>, metadata=updated_meta)
```
3. Print: `Unpinned: "<content>..."`
@@ -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:<id1>] "<content1>" (confidence: <score1>, <date1>)
- [mem0:<id2>] "<content2>" (confidence: <score2>, <date2>)
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=<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.
@@ -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 <text>, 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 <type>: "<first 80 chars of content>..."
Remembered as <type>: "<content, first 80 chars>"
Memory ID: <id>
```
Append `...` only if content was truncated (longer than 80 chars).
+121
View File
@@ -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=<active_user_id>`
- `app_id=<active_project_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": "<id>"}, {"app_id": "<pid>"}, {"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`:
```
<YYYY-MM-DD> | <project_id> | +<new_count> memories | top: <top_category>
```
### W5: Empty state
If no new memories in 7 days:
```
No new memories in the past week. Total: <N> memories in <project_id>.
```
@@ -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 <name>, or asks "switch mem0 project",
"change project scope", "override project_id".
name: switch-project
description: Override the auto-detected project scope
---
# Mem0 Switch Project
@@ -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 <user_id>
### <app_id_1> (<N> memories)
#### Architecture Decisions
- <memory content>
## <app_id_1> (<N> memories) ← current
**Architecture Decisions** — <memory content>
...
### <app_id_2> (<N> memories)
## <app_id_2> (<N> memories)
...
---
Total: <N> memories across <M> projects
<N> memories across <M> 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: <N> unique memories across <M> categories for project <project_id>
Branch: <active_branch>
<N> memories across <M> categories — project: <project_id>, branch: <active_branch>
```
### Step 6: Empty state
+105
View File
@@ -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)
+58
View File
@@ -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() == ""
+116
View File
@@ -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
+1
View File
@@ -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() == ""