feat(mem0-plugin): v0.2.4 — fix stats, session scoping, reduce noise, improve skill discovery (#5244)

This commit is contained in:
Kartik
2026-05-24 22:13:26 +05:30
committed by GitHub
parent 6b9707fee9
commit 0da3359a1a
38 changed files with 355 additions and 652 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.3"
"version": "0.2.4"
}
]
}
+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.3"
"version": "0.2.4"
}
]
}
+33
View File
@@ -99,6 +99,39 @@ Add to your Claude Code MCP config (`.mcp.json`):
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
</Info>
## Post-Installation: Run `/mem0:onboard`
After installing the plugin, start a new Claude Code session and run:
```
/mem0:onboard
```
This runs the setup wizard which:
1. Verifies your API key and MCP connection
2. Detects and imports project files (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`)
3. Installs coding-optimized memory categories
4. Shows your identity (user ID, project scope, branch)
The onboarding is idempotent — safe to re-run anytime. It auto-triggers on first session in a new project, but you can always invoke it manually.
## Available Skills
The plugin includes 17 skills accessible via `/mem0:` commands:
| Command | Description |
|---------|-------------|
| `/mem0:remember` | Store a memory verbatim — decisions, preferences, conventions |
| `/mem0:tour` | Browse all memories grouped by category |
| `/mem0:peek` | Quick search with compact one-liner results |
| `/mem0:stats` | Session and project memory statistics |
| `/mem0:dream` | Consolidate memories — merge duplicates, resolve contradictions |
| `/mem0:pin` | Protect critical memories from pruning |
| `/mem0:forget` | Delete memories by search or ID |
| `/mem0:health` | Diagnose connectivity, API key, and read/write |
| `/mem0:export` | Export memories to portable Markdown |
| `/mem0:import` | Import memories from export file or MEMORY.md |
## What's Included
| Component | Plugin Install | MCP Only |
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.3",
"version": "0.2.4",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.3",
"version": "0.2.4",
"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.3",
"version": "0.2.4",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
+29 -1
View File
@@ -2,6 +2,34 @@
All notable changes to the Mem0 plugin will be documented in this file.
## 0.2.4
### Fixed
- **PostToolUse matcher mismatch (`hooks.json:81`):** Changed from `mcp__mem0__` to `mcp__mem0__|mcp__plugin_mem0_mem0__`. Root cause of session stats recording nothing — 146 consecutive "no memory operations" entries. Unblocks `/mem0:stats` session line, stop-hook report, and `session_stats.py` tracking.
- **File-read hook noise (`on_file_read.sh`):** Replaced bare-filename semantic search with `metadata.files` filter + score threshold (≥ 0.4). Falls back to basename search if metadata filter returns nothing. Eliminates irrelevant context injection on every Read.
- **`/mem0:pin` uses v3-removed `immutable=True`:** Removed Step 3a entirely. Standardized on `metadata.pinned: true` (which `dream/SKILL.md` already respects during pruning). Fixed unconditional `...` appended to short pin confirmations.
- **`/mem0:list-projects` undercounts (implicit null scoping):** Dual-query approach — runs both null-scoped and app-scoped `get_memories` calls, merges by ID. Handles legacy `metadata.project_id` and `metadata.project` fields.
- **`/mem0:peek` free-text search on memory IDs:** Detects bare hex IDs (`^[a-f0-9]{8}$`) and `[mem0:<hex>]` citation refs, routes to `get_memory` direct lookup instead of semantic search.
- **Dead `PostToolUseFailure` hook block:** Removed entirely — this hook event does not exist in Claude Code.
### Added
- **Session ID capture (`on_session_start.sh`):** Extracts `session_id` from Claude Code stdin JSON, persists to `/tmp/mem0_session_id_$USER`. Falls back to timestamp-based ID. Enables `run_id`-based session scoping.
- **`run_id` injection (`enforce_metadata_defaults.sh`):** Reads session ID from temp file, injects as `run_id` into every `add_memory` call. Tags all memories with session identity for entity-scoped filtering.
- **`min_score`, `metadata_filters`, `rerank`, `threshold` params (`_search.py`):** `search_memories()` now accepts `min_score: float` to filter low-relevance results, `metadata_filters: dict` for field-level filtering, `rerank: bool` for managed reranker, and `threshold: float` (default 0.3, up from platform default 0.1) for server-side relevance gating. Existing callers unaffected (new params have defaults).
- **Rerank on tour/peek:** `/mem0:tour` and `/mem0:peek` search calls now pass `rerank=true` for better result ordering (+150–200ms latency, significantly improved precision).
- **`/mem0:stats` session query via `run_id`:** Queries memories by `run_id` filter for API-backed session counts. Cross-checks against local stats file. Shows truncated session ID in output.
### Removed
- **`/mem0:protocol` skill:** Routing table fully superseded by individual skill descriptions (auto-trigger). Operational guidelines (search patterns, metadata rules) covered by `enforce_metadata_defaults.sh` hook and individual skill bodies.
### Changed
- **All 17 skill descriptions:** Rewritten per Claude skill best practices — each now includes what the skill does AND when to trigger it, with specific keywords for auto-discovery. Average length 200–270 chars (under 1024 max). Third person, action verbs.
- **Onboarding auto-trigger (`on_session_start.sh`):** Replaced 3-state marker-file logic with memory-count detection. New project (0 memories) → prompts Claude to invoke `/mem0:onboard`. No marker files, no OAuth state. Simplified no-API-key path to single inactive banner.
## 0.2.3
### Added
@@ -32,7 +60,7 @@ All notable changes to the Mem0 plugin will be documented in this file.
- **Compact prompts:** `on_task_completed.sh` and `on_stop.sh` replaced multi-step checklists with single-line directives (0–2 durable facts max).
- **`/mem0:dream`:** Removed `--forget` (now standalone `forget` skill) and `--schedule` flags.
- **`/mem0:health`:** Removed `--fix` auto-fix mode; output condensed to `PASS/FAIL CheckName Detail` one-liners.
- **`/mem0:protocol` (was `mem0-mcp`):** Added 14-entry natural-language-to-skill routing table.
- **`/mem0:protocol` (was `mem0-mcp`):** Added 14-entry natural-language-to-skill routing table. _(Removed in 0.2.4 — superseded by skill descriptions.)_
- **CLI config fallback removed:** `_identity.sh`/`_identity.py` no longer read `~/.mem0/config.json`; API key resolution is env-var-only.
- **`auto_import.py`:** Added content-hash deduplication to skip files with identical content within a single import run.
- **All skill descriptions:** Shortened to concise one-liners.
+41 -6
View File
@@ -147,13 +147,50 @@ Add the following to your `.cursor/mcp.json`:
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks and the Mem0 SDK skill.
## Post-Installation: Run `/mem0:onboard`
After installing, start a new session and run:
```
/mem0:onboard
```
This runs the setup wizard which:
1. Verifies your API key and MCP connection
2. Detects and imports project files (`CLAUDE.md`, `AGENTS.md`, `.cursorrules`)
3. Installs coding-optimized memory categories
4. Shows your identity (user ID, project scope, branch)
The onboarding is idempotent — safe to re-run anytime. On first session in a new project (0 memories), Claude is prompted to run it automatically.
## Verify it works
After installing, confirm the MCP server is connected:
After onboarding, confirm everything is connected:
1. Start a new session (or restart your current one)
2. Ask: *"List my mem0 entities"* or *"Search my memories for hello"*
3. If the `mem0` tools appear and respond, you're all set
1. Run `/mem0:health` to check connectivity
2. Run `/mem0:stats` to see memory counts
3. Try `/mem0:remember "we use TypeScript"` then `/mem0:tour` to see it stored
## Available Skills
The plugin includes 17 skills accessible via `/mem0:` commands:
| Command | Description |
|---------|-------------|
| `/mem0:remember` | Store a memory verbatim — decisions, preferences, conventions |
| `/mem0:tour` | Browse all memories grouped by category |
| `/mem0:peek` | Quick search with compact one-liner results |
| `/mem0:stats` | Session and project memory statistics |
| `/mem0:dream` | Consolidate memories — merge duplicates, resolve contradictions |
| `/mem0:pin` | Protect critical memories from pruning |
| `/mem0:forget` | Delete memories by search or ID |
| `/mem0:health` | Diagnose connectivity, API key, and read/write |
| `/mem0:export` | Export memories to portable Markdown |
| `/mem0:import` | Import memories from export file or MEMORY.md |
| `/mem0:list-projects` | List all projects with stored memories |
| `/mem0:switch-project` | Override auto-detected project scope |
| `/mem0:memory-reviewer` | Audit memory quality — duplicates, contradictions, stale |
| `/mem0:context-loader` | Pre-load relevant memories for current task |
## What's included
@@ -162,12 +199,10 @@ After installing, confirm the MCP server is connected:
| MCP Server | Yes | Yes | Yes | Yes | Yes |
| Lifecycle Hooks | Yes | Yes | No | Opt-in | No |
| Mem0 SDK Skill | Yes | Yes | No | Yes | No |
| Memory Protocol Skill | No | No | No | Yes | No |
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
- **Lifecycle Hooks** — Automatic memory capture at key points. Claude Code and Cursor wire hooks up natively when the plugin is installed (session start, context compaction, task completion, session end). Codex hooks are opt-in via a one-time installer (`scripts/install_codex_hooks.py`) that writes entries into `~/.codex/hooks.json` for `SessionStart`, `UserPromptSubmit`, and `Stop`.
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Complements the lifecycle hooks on Codex.
## Updating the plugin
+1 -13
View File
@@ -78,7 +78,7 @@
],
"PostToolUse": [
{
"matcher": "mcp__mem0__",
"matcher": "mcp__mem0__|mcp__plugin_mem0_mem0__",
"hooks": [
{
"type": "command",
@@ -148,18 +148,6 @@
]
}
],
"PostToolUseFailure": [
{
"matcher": "mcp__mem0__",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_tool_failure.sh",
"timeout": 5
}
]
}
],
"PostCompact": [
{
"matcher": "manual|auto",
+6
View File
@@ -30,6 +30,12 @@ _mem0_resolve_identity() {
MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)"
export MEM0_RESOLVED_USER_ID
_MEM0_IDENTITY_ANNOTATION=""
if [ -n "${MEM0_USER_ID:-}" ] && [ "$MEM0_USER_ID" != "${USER:-default}" ]; then
_MEM0_IDENTITY_ANNOTATION=" (override; default: ${USER:-default})"
fi
export _MEM0_IDENTITY_ANNOTATION
# Load settings from ~/.mem0/settings.json
if command -v python3 >/dev/null 2>&1; then
_SETTINGS_JSON=$(PYTHONPATH="$_SCRIPT_DIR" python3 -c "from load_settings import load_settings; import json; print(json.dumps(load_settings()))" 2>/dev/null || echo "{}")
+6
View File
@@ -53,6 +53,12 @@ _mem0_resolve_project_id() {
_mem0_slug="${_mem0_slug//:/-}"
if [ -n "$_mem0_slug" ]; then
printf '%s' "$_mem0_slug"
_MEM0_PERSIST_CWD="$PWD" _MEM0_PERSIST_SLUG="$_mem0_slug" python3 -c "
import os, sys
sys.path.insert(0, '$(dirname "${BASH_SOURCE[0]:-$0}")')
from _project import save_project_mapping
save_project_mapping(os.environ['_MEM0_PERSIST_CWD'], os.environ['_MEM0_PERSIST_SLUG'])
" 2>/dev/null || true
return
fi
fi
+15 -4
View File
@@ -19,7 +19,11 @@ def search_memories(
project_id: str,
query: str,
metadata_type: str | None = None,
metadata_filters: dict | None = None,
top_k: int = 3,
min_score: float = 0.0,
rerank: bool = False,
threshold: float = 0.3,
) -> list[dict]:
if not api_key:
return []
@@ -27,8 +31,14 @@ def search_memories(
filters: dict = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
if metadata_type:
filters["AND"].append({"metadata": {"type": metadata_type}})
if metadata_filters:
for key, value in metadata_filters.items():
filters["AND"].append({"metadata": {key: value}})
body = json.dumps({"query": query, "filters": filters, "top_k": top_k}).encode()
payload: dict = {"query": query, "filters": filters, "top_k": top_k, "threshold": threshold}
if rerank:
payload["rerank"] = True
body = json.dumps(payload).encode()
req = urllib.request.Request(
SEARCH_URL,
data=body,
@@ -38,9 +48,10 @@ def search_memories(
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", [])
results = data if isinstance(data, list) else data.get("results", [])
if min_score > 0:
results = [m for m in results if m.get("score", 0) >= min_score]
return results
except Exception:
return []
+43 -1
View File
@@ -23,7 +23,7 @@ import urllib.request
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _chunking import filter_and_truncate, split_by_headers
from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
from _project import resolve_branch, resolve_project_id, save_project_mapping
log = logging.getLogger("mem0-auto-import")
log.setLevel(logging.DEBUG)
@@ -93,6 +93,38 @@ def save_hashes(hashes: dict[str, str]) -> None:
log.warning("Could not save hash store: %s", e)
def already_imported(api_key: str, user_id: str, project_id: str, filename: str) -> bool:
body = json.dumps({
"query": filename,
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 3,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as r:
data = json.loads(r.read())
results = data if isinstance(data, list) else data.get("results", [])
for result in results:
meta = result.get("metadata", {}) if isinstance(result, dict) else {}
if filename in meta.get("file", ""):
return True
return False
except Exception:
return False
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str, branch: str = "") -> bool:
"""POST a project profile memory to the Mem0 REST API."""
metadata = {
@@ -149,6 +181,8 @@ def main() -> None:
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
save_project_mapping(cwd, project_id)
git_root = _git_root(cwd)
search_dirs = [cwd]
if git_root and os.path.realpath(git_root) != os.path.realpath(cwd):
@@ -172,6 +206,8 @@ def main() -> None:
log.debug("Not found, skipping: %s", filename)
continue
filepath = os.path.realpath(filepath)
try:
file_size = os.path.getsize(filepath)
except OSError:
@@ -198,6 +234,12 @@ def main() -> None:
log.debug("Unchanged, skipping: %s", filename)
continue
if already_imported(api_key, user_id, project_id, filename):
log.debug("Already in mem0, updating hash store: %s", filename)
hashes[hash_key] = current_hash
updated = True
continue
try:
with open(filepath, encoding="utf-8", errors="replace") as f:
content = f.read()
+1 -1
View File
@@ -26,7 +26,7 @@ if [ -z "$FILE_PATH" ]; then
fi
case "$FILE_PATH" in
*/MEMORY.md|*/.claude/memory/*)
*/.claude/*/MEMORY.md|*/.claude/memory/*)
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
exit 2
;;
@@ -21,43 +21,56 @@ esac
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // "{}"' 2>/dev/null)
PATCHED=$(python3 -c "
import json, sys
_PATCH_OUT="/tmp/mem0_enforce_$$"
_MEM0_TOOL_INPUT="$TOOL_INPUT" python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
import json, os, sys
raw = sys.stdin.read()
raw = os.environ.get("_MEM0_TOOL_INPUT", "{}")
try:
inp = json.loads(raw)
except Exception:
sys.exit(0)
meta = inp.get('metadata') or {}
meta = inp.get("metadata") or {}
changed = False
if 'confidence' not in meta:
meta['confidence'] = 0.7
if "confidence" not in meta:
meta["confidence"] = 0.7
changed = True
if 'files' not in meta:
meta['files'] = ['*']
if "files" not in meta:
meta["files"] = ["*"]
changed = True
if 'source' not in meta:
meta['source'] = 'auto_capture'
if "source" not in meta:
meta["source"] = "auto_capture"
changed = True
if 'type' not in meta:
meta['type'] = 'task_learning'
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
if meta.get("confidence", 0) >= 1.0 and "infer" not in inp:
inp["infer"] = False
changed = True
if "run_id" not in inp:
session_file = "/tmp/mem0_session_id_" + os.environ.get("USER", "default")
if os.path.isfile(session_file):
try:
with open(session_file) as f:
sid = f.read().strip()
if sid:
inp["run_id"] = sid
changed = True
except OSError:
pass
if changed:
inp['metadata'] = meta
inp["metadata"] = meta
print(json.dumps(inp))
" <<< "$TOOL_INPUT" 2>/dev/null || true)
PYEOF
PATCHED=$(cat "$_PATCH_OUT" 2>/dev/null)
rm -f "$_PATCH_OUT"
if [ -n "$PATCHED" ]; then
# Use hookSpecificOutput.updatedInput to actually modify the tool call
if [ -n "$PATCHED" ] && echo "$PATCHED" | jq empty 2>/dev/null; then
jq -n --argjson updated "$PATCHED" '{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
+19 -4
View File
@@ -31,7 +31,7 @@ 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}"
RECENT_FILE="/tmp/mem0_recent_reads_${USER}_${MEM0_PROJECT_ID:-unknown}"
if [ -f "$RECENT_FILE" ] && grep -qxF "$FILE_PATH" "$RECENT_FILE" 2>/dev/null; then
exit 0
fi
@@ -42,8 +42,13 @@ 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 "
CWD="${MEM0_CWD:-$(pwd)}"
REL_PATH="${FILE_PATH#$CWD/}"
if [ "$REL_PATH" = "$FILE_PATH" ]; then
REL_PATH="$BASENAME"
fi
CONTEXT=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" MEM0_SEARCH_PROJECT="$PROJECT_ID" MEM0_SEARCH_QUERY="$BASENAME" MEM0_SEARCH_RELPATH="$REL_PATH" python3 -c "
import os, sys
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
from _search import search_memories, format_results_for_context
@@ -52,8 +57,18 @@ 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', '')
relpath = os.environ.get('MEM0_SEARCH_RELPATH', '')
results = search_memories(api_key, user_id, project_id, filename, top_k=3)
results = search_memories(
api_key, user_id, project_id, filename,
metadata_filters={'files': {'contains': relpath}} if relpath else None,
top_k=3, min_score=0.4,
)
if not results and relpath:
results = search_memories(
api_key, user_id, project_id, filename,
top_k=3, min_score=0.4,
)
if results:
print(format_results_for_context(results, heading=f'mem0 context for {filename}'))
" 2>/dev/null || true)
+6 -20
View File
@@ -1,13 +1,4 @@
#!/usr/bin/env bash
# Hook: SessionEnd
#
# Fires when session actually terminates (after Stop).
# Last-chance capture: if on_stop.sh background REST call didn't complete,
# this fires a synchronous capture attempt.
#
# Input: JSON on stdin with session_id, transcript_path, cwd, reason
# Output: ignored (session is ending)
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
@@ -18,25 +9,20 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INPUT=$(cat)
REASON=$(echo "$INPUT" | jq -r '.reason // "other"' 2>/dev/null || echo "other")
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
# Print session-end report (last chance — Stop hook output may not render on /exit)
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
if [ -n "$REPORT" ] && [ "$REPORT" != "Session: no memory operations." ]; then
echo ""
echo "---"
echo "mem0 $REPORT"
echo "---"
# Append to persistent session log
mkdir -p "$HOME/.mem0" 2>/dev/null || true
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
_LOG_FILE="$HOME/.mem0/session-log.md"
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$_LOG_FILE" 2>/dev/null || true
_LINE_COUNT=$(wc -l < "$_LOG_FILE" 2>/dev/null || echo 0)
if [ "$_LINE_COUNT" -gt 500 ]; then
tail -500 "$_LOG_FILE" > "${_LOG_FILE}.tmp" 2>/dev/null && mv "${_LOG_FILE}.tmp" "$_LOG_FILE" 2>/dev/null || true
fi
fi
# Telemetry (fire-and-forget — session dying, best-effort)
python3 "$SCRIPT_DIR/telemetry.py" session_end --reason="$REASON" 2>/dev/null &
# Clean up old capture markers (> 7 days)
find "$HOME/.mem0" -name ".captured_*" -mtime +7 -delete 2>/dev/null || true
exit 0
+24 -83
View File
@@ -1,14 +1,4 @@
#!/usr/bin/env bash
# Hook: SessionStart (matcher: startup|resume|compact)
#
# Bootstraps mem0 context at the start of every session.
# Output becomes part of Claude's context so it calls mem0 MCP tools.
#
# Input: JSON on stdin with session_id, source, transcript_path, model, cwd
# Output: Text injected into Claude's context (exit 0)
# Intentionally omit -e so the script always outputs a bootstrap prompt
# even if jq is missing or stdin is malformed.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
@@ -16,89 +6,52 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
# 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")
MEM0_AUTH_MODE="api_key"
if [ "$SOURCE" = "startup" ]; then
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
rm -f /tmp/mem0_recent_reads_${USER}_* 2>/dev/null || true
fi
PYTHONPATH="$SCRIPT_DIR" python3 "$SCRIPT_DIR/load_settings.py" init 2>/dev/null || true
rm -f "/tmp/mem0_rubric_injected_${USER}" 2>/dev/null || true
rm -f /tmp/mem0_rubric_* 2>/dev/null || true
MEM0_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
if [ -z "$MEM0_SESSION_ID" ]; then
MEM0_SESSION_ID="ses_$(date +%s)_$$"
fi
printf '%s' "$MEM0_SESSION_ID" > "/tmp/mem0_session_id_${USER}"
export MEM0_SESSION_ID
if [ -z "${MEM0_API_KEY:-}" ]; then
_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
## Mem0 — Setup Required
\`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
Mem0 — Setup Required | 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.
MEM0_API_KEY is not set. You MUST run the \`mem0:onboard\` skill now using the Skill tool to set up the API key and configure mem0 for this project. Do not ask — just invoke it.
BANNER
exit 0
fi
fi
# Check for failed dependency installation and warn the user
_DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}"
if [ -f "${_DATA_DIR}/.install-failed" ]; then
echo ""
echo "⚠️ mem0 SDK installation failed. Some features may not work."
echo "mem0 SDK installation failed. Some features may not work."
echo "Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
echo ""
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 [ "$MEM0_AUTH_MODE" = "api_key" ] && command -v python3 >/dev/null 2>&1; then
if command -v python3 >/dev/null 2>&1; then
MEM0_COUNT=$(python3 -c "
import json, os, urllib.request, urllib.error
api_key = os.environ.get('MEM0_API_KEY', '')
@@ -125,19 +78,15 @@ except Exception:
" 2>/dev/null || echo "?")
fi
# Identity line is emitted before every bootstrap variant so the agent
# 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.
cat <<BANNER
## Mem0 Active
\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=$MEM0_AUTH_MODE\`
\`user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key\`
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
Mem0 Active | user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key
\`\`\`
Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:
@@ -146,12 +95,9 @@ Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and
BANNER
# Load mem0.md project config if present (best-effort, non-blocking)
MEM0_PROJECT_CONFIG=""
MEM0_CWD_RESOLVED=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
if command -v python3 >/dev/null 2>&1; then
MEM0_PROJECT_CONFIG=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --full "$MEM0_CWD_RESOLVED" 2>/dev/null || echo "{}")
fi
if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
_CONFIG_KEYS=$(echo "$MEM0_PROJECT_CONFIG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "?")
echo "### Project Config (mem0.md)"
@@ -163,14 +109,12 @@ if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
fi
echo ""
fi
fi
if [ "$SOURCE" = "startup" ]; then
# First-run detection: auto-trigger onboarding for new projects
_SAFE_PID=$(printf '%s' "$MEM0_PROJECT_ID" | tr '/:' '--')
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}"
if [ ! -f "$_ONBOARD_MARKER" ]; then
if [ "$MEM0_COUNT" = "0" ]; then
cat <<'EOF'
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).
This is a new project with 0 memories. You MUST invoke the `mem0:onboard` skill now using the Skill tool to import project files and install coding categories. Do not ask the user — just invoke it immediately before responding.
EOF
else
cat <<'EOF'
@@ -178,7 +122,6 @@ Search mem0 for recent decisions and task learnings before responding to the use
EOF
fi
# Detect native Claude Code auto-memory for THIS project
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
_MEMORY_MD="$HOME/.claude/projects/${_PROJ_KEY}/memory/MEMORY.md"
if [ -f "$_MEMORY_MD" ] && [ -s "$_MEMORY_MD" ]; then
@@ -194,7 +137,6 @@ To avoid two parallel memory systems:
MEMEOF
fi
# Auto-import declarative project files in background
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
@@ -209,7 +151,6 @@ Context compacted. Search mem0 for `session_state` and `decision` memories to re
EOF
fi
# Telemetry (background, fire-and-forget)
python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null &
exit 0
-31
View File
@@ -1,16 +1,4 @@
#!/usr/bin/env bash
# Hook: Stop
#
# Fires when Claude finishes responding.
# Reminds Claude to store any unsaved learnings, then spawns a background
# process to capture transcript state via the Mem0 REST API directly.
#
# Input: JSON on stdin with stop_hook_active, transcript_path, cwd
# Output: Text that becomes Claude's context (exit 0), or nothing
#
# IMPORTANT: Check stop_hook_active to avoid infinite loops.
# Intentionally omit -e so the reminder always emits even if session_stats fails.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
@@ -26,30 +14,11 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# Telemetry: fire before report() deletes stats file
_TELEM_CAT=$(python3 "$SCRIPT_DIR/session_stats.py" peek 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('categories',[])))" 2>/dev/null || echo "0")
python3 "$SCRIPT_DIR/telemetry.py" stop --categories_count="$_TELEM_CAT" 2>/dev/null &
# Print session-end report
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
if [ -n "$REPORT" ]; then
echo ""
echo "---"
echo "**mem0 $REPORT**"
echo "---"
echo ""
fi
# Append to persistent session log
if [ -n "$REPORT" ]; then
mkdir -p "$HOME/.mem0" 2>/dev/null || true
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
fi
cat <<'EOF'
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 "")
exit 0
-6
View File
@@ -90,12 +90,6 @@ def report() -> str:
searches = stats.get("searches", 0)
categories = stats.get("categories", [])
# Clean up temp file after reading
try:
os.unlink(STATS_FILE)
except OSError:
pass
if adds == 0 and searches == 0:
return ""
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: context-loader
description: Pre-load relevant memories for current task
description: Searches and injects relevant memories into context before starting work on a task. Use when beginning a new task, switching context, or when project history, past decisions, or coding conventions need to be loaded.
---
# Context Loader
@@ -9,7 +9,7 @@ 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)
- Session start (invoke manually or auto-triggered by skill description matching)
- User starts work on a specific feature or file set
- Complex multi-step task begins
- User says "what do we know about X" or "context for X"
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: dream
description: Consolidate memories — merge duplicates, resolve contradictions, prune stale
description: Consolidates stored memories by merging duplicates, resolving contradictions, and pruning stale entries. Use when memory count is high, search results feel noisy or repetitive, or periodic cleanup is needed to maintain memory quality.
---
# Mem0 Dream — Memory Consolidation
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: export
description: Export project memories to a portable Markdown file
description: Exports all project memories to a portable Markdown file for backup or migration. Use when backing up memories, migrating to another project, sharing memory state with teammates, or archiving before cleanup.
---
# Mem0 Export
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: forget
description: Delete memories by search or ID — confirms before deleting
description: Deletes memories by search query or memory ID with confirmation before removal. Use when removing outdated decisions, incorrect memories, sensitive data, or cleaning up after experiments. Also handles undo of recent additions.
---
# Mem0 Forget
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: health
description: Diagnose mem0 connection, API key, and memory read/write
description: Diagnoses mem0 connectivity, API key validity, and memory read/write functionality. Use when memory operations fail, searches return empty, add_memory errors occur, MCP connection drops, or to verify the plugin is working correctly.
---
# Mem0 Health Check
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: import
description: Import memories from an export file into this project
description: Imports memories from an exported Markdown file or MEMORY.md into the current project. Use when migrating from another project, restoring from backup, importing Claude Code native MEMORY.md content, or setting up a new project with existing knowledge.
---
# Mem0 Import
+17 -11
View File
@@ -1,6 +1,6 @@
---
name: list-projects
description: List all projects that have stored memories
description: Lists all projects with stored memories for the current user, showing memory counts and last activity dates. Use when checking which projects have memories, comparing memory distribution across repos, or finding a specific project scope.
---
# Mem0 List Projects
@@ -12,23 +12,29 @@ Show all known project scopes for the current user.
### 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.
the user's memories across all scopes.
Call `get_memories` with:
- `filters={"user_id": "<active_user_id>"}`
- `page_size=200`
**Important:** A filter with only `user_id` triggers implicit null scoping — it
excludes memories that have a non-null `app_id`. Run two queries and merge:
Do NOT pass `app_id` — we want memories across ALL projects.
1. **Null-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}]}`, `page_size=200`
— catches memories without `app_id`
2. **App-scoped:** `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": {"exists": true}}]}`, `page_size=200`
— catches memories with any `app_id`
If the response indicates more pages, paginate until all are fetched (up to 1000
memories max to avoid excessive API calls).
Run both calls in parallel. Merge results, deduplicate by memory `id`.
If either response indicates more pages, paginate (up to 1000 total).
### Step 2: Extract distinct projects
For each memory, read the `app_id` field (may also appear as `metadata.app_id`
on older memories). Collect distinct values.
For each memory, determine project by:
1. Top-level `app_id` field (preferred)
2. `metadata.project_id` (legacy memories)
3. `metadata.project` (oldest format)
4. `"(unscoped)"` if none found
For each project, count:
Group by resolved project name. For each project, count:
- Total memories
- Most recent `created_at` date
- Top 3 `metadata.type` values by frequency
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: mem0
description: Mem0 SDK reference — Python, TypeScript, and integrations
description: Mem0 SDK reference covering Python and TypeScript APIs, memory client methods, configuration, and framework integrations. Use when writing code that calls mem0 APIs, configuring memory providers, or integrating mem0 into an application.
license: Apache-2.0
metadata:
author: mem0ai
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: memory-reviewer
description: Review memory quality — duplicates, contradictions, stale entries
description: Reviews stored memory quality by detecting duplicates, contradictions, and stale entries with actionable recommendations. Use when search results seem conflicting, before running dream consolidation, or for periodic memory hygiene audits.
---
# Memory Reviewer
+3 -11
View File
@@ -1,6 +1,6 @@
---
name: onboard
description: Set up mem0 for this project — API key setup, config, categories, identity
description: Sets up mem0 for a new project including API key configuration, MCP authentication, project file import, and coding categories. Use on first run in a new project, when API key needs updating, or to re-run initial setup after configuration changes.
---
# Mem0 Onboarding Wizard
@@ -108,6 +108,7 @@ If user says yes (or default):
- `app_id=<active_project_id>`
- `metadata={"type": "project_profile", "file": "<filename>", "source": "onboard", "branch": "<active_branch>"}`
- `infer=False`
- The response contains `event_id` (writes are async). Do not block on each — continue importing. The summary reflects files submitted, not confirmed processed.
## Step 5: Install coding categories
@@ -130,16 +131,7 @@ If the script fails with "mem0ai SDK not found", run the dependency installer fi
```
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
## Step 6: Summary
Print a summary:
```
+13 -3
View File
@@ -1,6 +1,6 @@
---
name: peek
description: Quick search memories — compact one-liner results
description: Searches memories and displays compact one-liner results, or looks up a specific memory by ID. Use for quick memory lookups, checking if a decision was recorded, resolving [mem0:id] citations, or browsing memories without full category detail.
---
# Mem0 Peek
@@ -15,12 +15,22 @@ The user provides a search query: `/mem0:peek auth middleware`
If no query provided, ask: "What should I search for?"
**Memory ID detection:** If the query matches any of these patterns, treat it as a
direct memory ID lookup instead of a search:
- Bare hex: `^[a-f0-9]{8}$` (short ID) or `^[a-f0-9]{8}-[a-f0-9-]+$` (full UUID)
- Citation ref: `[mem0:<hex>]` — extract the hex portion
When an ID is detected:
1. Call `get_memory(<id>)` directly (if short ID, try as prefix of full UUID)
2. If found, skip to Step 3 and display the single result
3. If not found, fall through to search using the ID as query text
### Step 2: Search
Run 2 parallel `search_memories` calls:
1. Broad: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`
1. Broad: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
### Step 3: Display
+8 -17
View File
@@ -1,6 +1,6 @@
---
name: pin
description: Pin a memory to protect it from consolidation
description: Pins or unpins a memory to protect it from pruning during dream consolidation. Use when a memory is critical and must never be removed, such as architecture decisions, security constraints, or immutable team conventions.
---
# Mem0 Pin
@@ -29,21 +29,6 @@ Call `get_memory` with the selected memory ID. Store:
### 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)
@@ -56,13 +41,19 @@ update_memory(memory_id=<selected_id>, text=<original_text>, metadata=updated_me
**Important:** `update_memory` requires the `text` parameter. Passing only metadata may error or wipe content.
**For new memories** (user wants to pin text that isn't stored yet):
1. Call `add_memory` with the text + `metadata={"pinned": true, "type": "decision", "confidence": 1.0}`
2. The response contains `event_id`. Call `get_event_status(event_id=<event_id>)` once to retrieve the memory ID, then confirm.
### Step 4: Confirm
```
Pinned: "<memory content, first 80 chars>..."
Pinned: "<memory content, first 80 chars>"
Memory ID: <id>
```
Append `...` only if content exceeds 80 characters.
### Unpin
If the user says "unpin":
-379
View File
@@ -1,379 +0,0 @@
---
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.
## Project scoping
Every memory operation MUST be scoped to the current project using `app_id` (entity-scoped memory):
- **On `add_memory`:** Always pass `app_id=<active_project_id>` as a **top-level parameter** (not in metadata).
- **On `search_memories`:** Always include `{"app_id": "<your_project_id>"}` in the AND filter.
Full filter template:
```python
filters={"AND": [
{"user_id": "<your_user_id>"},
{"app_id": "<your_project_id>"},
{"metadata": {"type": "decision"}}
]}
```
### Decide: search or skip?
**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
**Skip WHEN:**
- the prompt is an acknowledgement or continuation ("ok", "thanks", "continue")
- 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
Empty results are normal. Proceed without context — they don't mean the system is broken.
### Contradiction detection at search time
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:
- [mem0:<id1>] "<content1>" (confidence: <score1>, <date1>)
- [mem0:<id2>] "<content2>" (confidence: <score2>, <date2>)
Which is current?
```
3. After the user resolves: update the loser via `update_memory` to mark it superseded, or delete it. Store the winner's fact as authoritative if not already.
4. If the user doesn't resolve, default to the more recent memory with higher confidence, but note the ambiguity in your response.
### How to search well
When you do search, run **2–4 parallel** `search_memories` calls at different angles instead of one query echoing the user's prompt.
**Query phrasing:**
- Use **nouns**, not sentences. `"auth module decisions"` beats `"what did we decide about auth"`.
- Strip conversational filler. *"remember when we picked Postgres?"* → search `"Postgres choice"`.
- Use entity names, not pronouns. Resolve "that thing" from recent context first.
- Don't search on meta-questions ("what was that?") — use recent context or `get_memories` ordered by `created_at`.
**Metadata filters** match the same `type` values written under "After completing significant work" below.
Filter rules:
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:
| `metadata.type` clause | Use for |
|--------|---------|
| `{"metadata": {"type": "decision"}}` | design / architecture / "how should we" questions |
| `{"metadata": {"type": "anti_pattern"}}` | debugging, error handling, things that failed before |
| `{"metadata": {"type": "user_preference"}}` | tooling, stack, style — always include for code work |
| `{"metadata": {"type": "convention"}}` | established patterns in this project |
### Which categories to search by query intent
When a query clearly maps to one of the platform's custom categories, fan-out to 2–3 parallel `search_memories` calls scoped to those categories so recall is precise without being noisy. Use the `metadata.type` filter as your primary discriminator; treat the category column below as the semantic lens to pick the right query nouns.
| User intent / signal | Primary categories to search | Example query nouns |
|---|---|---|
| Design or architecture question | `architecture_decisions`, `api_contracts`, `data_model` | `"architecture decision"`, `"API schema"`, `"data model"` |
| Something failed / debugging | `anti_patterns`, `bug_fixes`, `security_constraints` | `"bug root cause"`, `"failure pattern"`, `"security constraint"` |
| How do we do X here? | `coding_conventions`, `team_norms`, `testing_patterns` | `"code convention"`, `"team norm"`, `"test strategy"` |
| Which library / version to use | `dependency_decisions`, `tooling_setup`, `architecture_decisions` | `"dependency choice"`, `"library version"`, `"tooling setup"` |
| Performance or scale concern | `performance_findings`, `architecture_decisions`, `data_model` | `"performance bottleneck"`, `"profiling result"`, `"optimisation"` |
| Security / auth / compliance | `security_constraints`, `api_contracts`, `coding_conventions` | `"auth rule"`, `"security requirement"`, `"compliance"` |
| Test strategy or coverage | `testing_patterns`, `coding_conventions`, `anti_patterns` | `"test framework"`, `"coverage target"`, `"fixture pattern"` |
| Schema / DB / domain object | `data_model`, `api_contracts`, `domain_glossary` | `"schema"`, `"column"`, `"domain object"` |
| API shape or versioning | `api_contracts`, `data_model`, `architecture_decisions` | `"endpoint"`, `"request schema"`, `"versioning"` |
| How to deploy / release / rollback | `deployment_runbook`, `tooling_setup`, `team_norms` | `"deploy step"`, `"rollback"`, `"CI pipeline"` |
| Team process / branching / PRs | `team_norms`, `coding_conventions`, `deployment_runbook` | `"branching strategy"`, `"PR review"`, `"working agreement"` |
| What does this term mean? | `domain_glossary`, `data_model`, `api_contracts` | `"glossary"`, `"abbreviation"`, `"domain term"` |
| Experiment / spike / A-B test | `experiment_results`, `performance_findings`, `anti_patterns` | `"experiment result"`, `"A/B test"`, `"spike outcome"` |
| User's tool / language preferences | `user_preferences`, `tooling_setup`, `coding_conventions` | `"user preference"`, `"preferred tool"`, `"language choice"` |
| Past task strategies that worked | `task_learnings`, `anti_patterns`, `coding_conventions` | `"task strategy"`, `"approach that worked"` |
| Environment / setup question | `tooling_setup`, `deployment_runbook`, `dependency_decisions` | `"environment setup"`, `"build tool"`, `"install step"` |
| Anything related to current state | `task_learnings`, `architecture_decisions`, `anti_patterns` | (combine with recency filter — see below) |
Full filter (replace `<your_user_id>` and `<your_project_id>` with the active values from SessionStart):
```python
filters={"AND": [{"user_id": "<your_user_id>"}, {"app_id": "<your_project_id>"}, {"metadata": {"type": "decision"}}]}
```
### Worked example
User asks: *"Refactor the auth module to use JWT."*
Don't:
```python
search_memories(query="Refactor the auth module to use JWT")
# Hits whatever shares words. Misses prior decisions and preferences.
```
Do (parallel — substitute the active `user_id` and `app_id` for the placeholders):
```python
search_memories(query="auth module decisions",
filters={"AND": [{"user_id": "<your_user_id>"}, {"app_id": "<your_project_id>"}, {"metadata": {"type": "decision"}}]})
search_memories(query="JWT",
filters={"AND": [{"user_id": "<your_user_id>"}, {"app_id": "<your_project_id>"}]})
search_memories(query="auth refactor failures",
filters={"AND": [{"user_id": "<your_user_id>"}, {"app_id": "<your_project_id>"}, {"metadata": {"type": "anti_pattern"}}]})
search_memories(query="auth",
filters={"AND": [{"user_id": "<your_user_id>"}, {"app_id": "<your_project_id>"}, {"metadata": {"type": "user_preference"}}]})
```
## After completing significant work
Extract key learnings and store them using the `add_memory` tool:
### REQUIRED metadata fields
Every `add_memory` call MUST include these metadata fields. Do NOT omit them:
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `type` | string | YES | Memory category: `decision`, `task_learning`, `anti_pattern`, `convention`, `user_preference`, `environmental`, `session_state`, `compact_summary` |
| `confidence` | float | YES | 0.0–1.0 confidence score. Default: `0.7` for inferred learnings, `0.9` for explicit user statements |
| `files` | list[str] | YES | File paths relevant to this memory. Use `["*"]` for project-wide learnings |
| `source` | string | YES | How the memory was captured: `user_request`, `auto_capture`, `post_commit`, `error_recovery` |
**Example:**
```json
{
"metadata": {
"type": "decision",
"confidence": 0.9,
"files": ["src/auth/login.py", "src/auth/middleware.py"],
"source": "user_request"
}
}
```
**If you omit `confidence` or `files`, the memory will be harder to rank and retrieve later.**
- **Decisions made** -> Include metadata `{"type": "decision"}`
- **Strategies that worked** -> Include metadata `{"type": "task_learning"}`
- **Failed approaches** -> Include metadata `{"type": "anti_pattern"}`
- **User preferences observed** -> Include metadata `{"type": "user_preference"}`
- **Environment/setup discoveries** -> Include metadata `{"type": "environmental"}`
- **Conventions established** -> Include metadata `{"type": "convention"}`
Always include `"branch": "<active_branch>"` in the metadata object alongside `type`. The active branch is shown in the SessionStart banner. This enables branch-scoped filtering later (e.g., "what did we do on feature/auth-rewrite?").
> `metadata.type` (which you set explicitly) and `categories` (which the platform auto-tags after the project's custom-category list — see `scripts/setup_coding_categories.py`) are complementary. Always set `metadata.type` for explicit filtering; the platform fills in `categories` on its own. Don't try to set `categories` on `add_memory` calls — per-request overrides aren't supported on the managed API.
### Expiration: high-churn vs durable
Some memory types are state snapshots that go stale fast; others are durable facts that should outlive the session that created them. Mark the difference with `expiration_date` on writes.
| Type | Expiration | Why |
|---|---|---|
| `session_state`, `compact_summary` | `expiration_date` ≈ today + 90 days | Describe a single moment of project state. Useless after a quarter; clutter the recall surface. |
| `decision`, `anti_pattern`, `convention`, `user_preference`, `task_learning`, `environmental` | omit `expiration_date` | Durable facts. A decision made last year is still a decision; same for a convention or a user preference. |
`add_memory` accepts `expiration_date` as a string (`"YYYY-MM-DD"`). The two server-side hooks (`on_pre_compact.py`, `capture_compact_summary.py`) already set this for the types they write. When you write directly via the MCP tool, follow the same rule.
### Recency filter on recall
When the user is asking about *current* state ("where were we", "what's the active task", "the latest decision on X"), filter recall to recent memories so stale snapshots don't surface:
```python
# Last 90 days only
{"AND": [{"user_id": "<id>"}, {"app_id": "<your_project_id>"}, {"metadata": {"type": "session_state"}}, {"created_at": {"gte": "<90 days ago, YYYY-MM-DD>"}}]}
```
Skip the recency filter when the user is asking about durable facts ("what conventions does this project use", "have we hit this bug before") — those are timeless and recency would hide them.
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.
### Use `infer=False` for already-structured content
When you've done the extraction work yourself — pre-compaction summaries, decisions, anti-patterns, conventions you've explicitly identified — pass `infer=False` so the platform stores your text verbatim instead of running a second extraction pass over it.
```python
add_memory(
messages=[{"role": "user", "content": "<your structured fact>"}],
user_id="<active user_id>",
app_id="<active project_id>",
metadata={"type": "decision", "branch": "<active branch>"},
infer=False,
)
```
Stick to one mode per distinct piece of content — don't mix `infer=True` (default) and `infer=False` for the same fact, you'll get duplicates. Default (`infer=True`) is right for raw conversational signal you want extracted; `infer=False` is right for pre-extracted structure.
## Before losing context
If context is about to be compacted or the session is ending, store a comprehensive session summary:
```
## Session Summary
### User's Goal
[What the user originally asked for]
### What Was Accomplished
[Numbered list of tasks completed]
### Key Decisions Made
[Architectural choices, trade-offs discussed]
### Files Created or Modified
[Important file paths with what changed]
### Current State
[What is in progress, pending items, next steps]
```
Include metadata: `{"type": "session_state"}`
## Inline citations
When your response is informed by specific memories, cite them so the user can trace provenance. Use the memory ID returned by `search_memories`.
Format: `[mem0:<short_id>]` where `<short_id>` is the first 8 characters of the memory ID.
Example:
> We chose Postgres over SQLite for production [mem0:a3f8b2c1] and the auth module uses JWT tokens [mem0:7e2d9f4a].
Rules:
- Only cite when the memory **directly informed** your answer. Don't cite for general knowledge.
- Place citations inline, at the end of the relevant sentence.
- If multiple memories support the same point, cite all: `[mem0:abc12345][mem0:def67890]`.
- Don't cite `session_state` or `compact_summary` memories — those are internal bookkeeping.
- Keep it subtle. One or two citations per response is typical. Don't over-cite.
## Memory hygiene
- Do NOT write to MEMORY.md or any file-based memory. Use mem0 MCP tools exclusively.
- Only store genuinely useful learnings. Skip trivial interactions.
- Use specific, searchable language in memory content.
### Confidence scoring on every add_memory
Every `add_memory` call MUST include a `confidence` field in its `metadata` object. This captures how certain the stored fact is, so downstream callers can filter out speculation.
| `metadata.confidence` value | Meaning | When to use |
|---|---|---|
| `1.0` | User explicitly stated it | User said "we use Postgres", "always lint before commit", "never use floats for currency" |
| `0.8` | Observed directly in code / config | You read it from a file, migration, or config — not inferred |
| `0.5` | Inferred from context | You derived it from surrounding evidence but the user didn't confirm it |
| `0.3` | Guessed / low-signal | Extrapolated from a single weak signal; treat as a tentative hypothesis |
Example:
```python
add_memory(
messages=[{"role": "user", "content": "We always use Postgres — never SQLite in production."}],
user_id="<active user_id>",
app_id="<active project_id>",
metadata={"type": "architecture_decisions", "branch": "<active branch>", "confidence": 1.0},
infer=False,
)
```
**Search guidance:** When recalling actionable facts (decisions, conventions, security constraints), optionally apply a confidence threshold of 0.6 or above to avoid surfacing low-confidence guesses. Only top-level metadata keys are filterable, so `confidence` filtering requires SDK-side post-filtering or a dedicated high-confidence write path — for now, include the confidence value in every write and document it in the memory content so it is searchable via text.
### File path tagging on every add_memory
Every `add_memory` call that is associated with specific files MUST include a `files` key in its `metadata` object. The value is an array of affected file paths relative to the project root.
```python
add_memory(
messages=[{"role": "user", "content": "The auth middleware lives in src/middleware/auth.ts and validates JWTs using the shared key in config/secrets.ts."}],
user_id="<active user_id>",
app_id="<active project_id>",
metadata={
"type": "architecture_decisions",
"branch": "<active branch>",
"confidence": 0.8,
"files": ["src/middleware/auth.ts", "config/secrets.ts"],
},
infer=False,
)
```
**Filtering by files:** Use the `contains` operator to filter by `metadata.files` at search time:
```python
search_memories(
query="auth middleware",
filters={
"AND": [
{"user_id": "<id>"},
{"app_id": "<project_id>"},
{"metadata.files": {"contains": "src/middleware/auth.ts"}},
]
},
top_k=5,
)
```
Also embed bare filenames in the memory content text as a fallback — the vector search will surface them even if the structured filter misses.
### Access counter: track memory usage
When you retrieve a memory via `search_memories` and **actually use it** in your response (i.e., it informed your answer or you cited it), increment its access counter and update the last-accessed timestamp by calling:
```python
# 1. Read current state
mem = get_memory(memory_id=<id>)
current_text = mem["content"] # or mem["memory"], depending on response shape
current_meta = mem.get("metadata", {})
# 2. Bump access_count and set last_accessed
import datetime
current_meta["access_count"] = current_meta.get("access_count", 0) + 1
current_meta["last_accessed"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
# 3. Update with preserved content and bumped metadata
update_memory(
memory_id=<id>,
text=current_text, # preserve original text — required parameter
metadata=current_meta, # pass updated access_count and last_accessed
)
```
**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.
**Why:** `access_count` and `last_accessed` feed into `/mem0:dream` pruning decisions. Memories that are never accessed after creation are candidates for cleanup. Frequently accessed memories are protected from pruning regardless of age.
+8 -3
View File
@@ -1,6 +1,6 @@
---
name: remember
description: Save a memory verbatim from your input
description: Stores a memory verbatim from user input with appropriate type classification and metadata. Use when the user says remember this, save this, store this, note that, or explicitly asks to record a decision, preference, convention, or learning.
---
# Mem0 Remember
@@ -43,10 +43,15 @@ Call `add_memory` with:
### Step 4: Confirm
Print:
The `add_memory` response returns `event_id` (not `memory_id`) because writes are async.
Call `get_event_status(event_id=<event_id>)` once.
- If status is `SUCCEEDED`: print the memory ID from the result.
- If status is `PENDING` or `processing`: print with the event ID as fallback.
```
Remembered as <type>: "<content, first 80 chars>"
Memory ID: <id>
Memory ID: <id from event status>
```
Append `...` only if content was truncated (longer than 80 chars).
+15 -4
View File
@@ -1,6 +1,6 @@
---
name: stats
description: Show memory usage stats for this session and project
description: Displays memory usage statistics for the current session and project including counts by category, age distribution, and API latency. Use when checking how many memories exist, reviewing session activity, or auditing memory distribution across categories.
---
# Mem0 Stats
@@ -22,8 +22,9 @@ 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
### Step 2: Fetch lifetime and session stats from API
**Lifetime stats:**
Call `get_memories` with:
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
@@ -34,7 +35,17 @@ Count the returned memories. Group them by:
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).
**Session stats (API-backed):**
Read the session ID file at `/tmp/mem0_session_id_$USER`. If it exists and contains
a non-empty value, also call `get_memories` with:
- `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"run_id": "<session_id>"}]}`
- `page_size=100`
This returns only memories written in the current session. Use this count to
cross-check the local stats file. If the API count is higher, use the API count
(the local tracker may have missed operations).
Also run a `search_memories` call with `query="project"`, `top_k=1` to measure round-trip latency (time the call).
### Step 3: Display
@@ -43,7 +54,7 @@ Print a minimal dashboard. No ASCII bar charts — use a clean table layout:
```
## mem0 stats
**Session** — 3 written, 5 searches, categories: decision, convention
**Session** (<session_id, first 12 chars>) — 3 written, 5 searches, categories: decision, convention
**Project: my-project** — 55 memories, API: 84ms
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: switch-project
description: Override the auto-detected project scope
description: Overrides the auto-detected project scope to read and write memories under a different project ID. Use when working across multiple projects, accessing memories from another repo, or when auto-detection resolves to the wrong project.
---
# Mem0 Switch Project
+7 -7
View File
@@ -1,6 +1,6 @@
---
name: tour
description: Browse stored memories grouped by category
description: Browses all stored memories grouped by category with full content display. Use when reviewing all project memories, exploring stored knowledge, onboarding to a project, or getting an overview of captured decisions, conventions, and learnings.
---
# Mem0 Project Tour
@@ -37,8 +37,8 @@ When `/mem0:tour` receives a search query argument (e.g., `/mem0:tour auth middl
WITHOUT `--all-projects`, run in **peek mode** — compact one-liner results:
1. Run 2 parallel `search_memories` calls:
- Broad: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `limit=5`
- Broad: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`, `rerank=true`
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `limit=5`, `rerank=true`
2. Deduplicate by ID, display compact results:
```
## mem0 search: "<query>" (<N> results)
@@ -71,9 +71,9 @@ Pass `page_size=100` (or the maximum allowed) to get a full picture.
In parallel, run these `search_memories` calls to get relevance-ranked results for key topics:
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`
- `query="bugs errors failures anti-patterns"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`
- `query="project setup tooling conventions preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`, `rerank=true`
- `query="bugs errors failures anti-patterns"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`, `rerank=true`
- `query="project setup tooling conventions preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `limit=10`, `rerank=true`
**Do NOT filter by `metadata.type` in these calls.** The platform auto-assigns `categories` — filtering on `metadata.type` misses memories that were auto-categorized but don't have an explicit `metadata.type`.
@@ -103,7 +103,7 @@ Map category names to display names:
### Step 4: Display results
For each group that has results, print:
Sort groups by descending memory count. For each group that has results, print:
```
## <display_name> (<count> memories)
+2 -2
View File
@@ -83,13 +83,13 @@ def test_report_empty_session(_isolate_stats_file):
assert result == ""
def test_report_cleans_up_file(_isolate_stats_file):
def test_report_preserves_file(_isolate_stats_file):
import session_stats
session_stats.init()
session_stats.record_add()
session_stats.report()
assert not os.path.isfile(_isolate_stats_file)
assert os.path.isfile(_isolate_stats_file)
def test_record_add_no_category(_isolate_stats_file):