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
+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