feat(mem0-plugin): v0.2.5 — fix identity scoping, skill param bugs, add checklists (#5247)

This commit is contained in:
Kartik
2026-05-25 19:21:10 +05:30
committed by GitHub
parent 0da3359a1a
commit f9c52baf21
43 changed files with 970 additions and 439 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.4"
"version": "0.2.5"
}
]
}
+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.4"
"version": "0.2.5"
}
]
}
+1 -1
View File
@@ -180,7 +180,7 @@ The Codex plugin manifest (`.codex-plugin/plugin.json`) follows the Codex plugin
```json
{
"name": "mem0",
"version": "0.1.0",
"version": "0.2.5",
"description": "Mem0 memory layer for AI applications.",
"skills": "./skills/",
"mcpServers": "./.codex-mcp.json",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.4",
"version": "0.2.5",
"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.4",
"version": "0.2.5",
"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.4",
"version": "0.2.5",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
+44
View File
@@ -2,6 +2,50 @@
All notable changes to the Mem0 plugin will be documented in this file.
## 0.2.5
### Fixed
- **PostToolUse field name: `tool_output` → `tool_response`:** All three PostToolUse scripts (`on_bash_output.sh`, `on_post_commit.sh`, `on_post_tool_use.sh`) were reading `.tool_output` from stdin JSON — a field that never existed in the Claude Code hooks spec. The correct field is `.tool_response` (confirmed via official docs at code.claude.com/docs/en/hooks). This was silently `null` on every invocation, meaning bash error detection and post-commit checks never actually fired.
- **Stop hook invalid `hookSpecificOutput`:** `on_stop.sh` returned `hookSpecificOutput` with `hookEventName: "Stop"` — but `Stop` is not a valid `hookEventName` discriminant. Claude Code rejected the JSON with "Hook JSON output validation failed". Replaced with spec-compliant `{ decision: "block", reason: "..." }`.
- **SessionStart banner invisible:** Switched from JSON `hookSpecificOutput.additionalContext` (discrete/hidden system reminder) back to raw text `cat <<BANNER` (shown directly in transcript). Per official docs, raw stdout from SessionStart hooks is visible context; `additionalContext` is not.
- **`Write|Edit` matcher missing `MultiEdit`:** `block_memory_write.sh` could be bypassed via `MultiEdit` tool. Matcher now `Write|Edit|MultiEdit`.
- **PreToolUse MCP matcher coverage gap:** `enforce_metadata_defaults.sh` not triggered for `get_memory` or `update_memory`. Added 4 tool name variants.
- **`capture_compact_summary.py` never called:** `on_session_start.sh` compact branch never spawned it. Post-compaction summaries were not stored. Added background spawn.
- **Unguarded variables under `set -u` in `on_session_start.sh`:** Bare `${MEM0_RESOLVED_USER_ID}` etc. without `:-` fallbacks. Script died silently if `_identity.sh` failed to source. All references now use `${VAR:-default}`.
- **`enforce_metadata_defaults.sh` silent failure on jq error:** Final `jq -n --argjson` had no error guard under `set -euo pipefail`. Added `|| true`.
- **Single-quote injection in `on_git_commit_capture.sh`:** Shell variables interpolated via `'$VAR'` inside `python3 -c`. Filenames with `'` broke Python syntax. Replaced with `os.environ.get()`.
- **Identity injection on `add_memory` (`enforce_metadata_defaults.sh`):** Hook now sources `_identity.sh` and injects `user_id` and `app_id` as top-level params when the agent omits them. Prevents orphaned memories with null scoping that were invisible to filtered queries. Root cause of onboarding writes landing with `user_id=null, app_id=null`.
- **Identity injection on `search_memories` and `get_memories`:** Hook now intercepts these tools and injects `user_id`/`app_id` into `filters.AND[]` when missing. Handles three filter states: no filters (creates from scratch), flat filters (converts to AND format), existing AND array (appends missing clauses). Prevents MCP server from auto-injecting wrong identity.
- **Identity injection on `delete_all_memories`:** Hook injects top-level `user_id`/`app_id` to prevent accidental cross-scope deletion.
- **`/mem0:export` incorrect `get_memories` call:** Was passing `user_id`/`app_id` as top-level params; MCP tool only accepts them inside `filters`. Changed to `filters={"AND": [{"user_id": "..."}, {"app_id": "..."}]}`.
- **`/mem0:stats` same `get_memories` issue:** Fixed both lifetime and session stat queries to use `filters` instead of top-level identity params.
- **`/mem0:pin` passes `metadata` to `update_memory`:** MCP `update_memory` tool only accepts `memory_id`, `text`, `source` — no `metadata` param. Pin/unpin now uses `[PINNED]` text prefix marker instead. Also added explicit `user_id`/`app_id` to `add_memory` for new pinned memories.
- **`/mem0:health --deep` ambiguous `get_memories` call:** Clarified identity goes in `filters`, not as top-level params.
- **`/mem0:memory-reviewer` same ambiguity:** Explicit `filters={"AND": [...]}` for `get_memories`.
- **`/mem0:dream` stale artifact:** Removed `(item 15)` from Step 4 heading.
- **`/mem0:dream` contradiction resolution no-op:** `update_memory` on loser with its own text did nothing. Changed to `delete_memory(memory_id=<loser_id>)` to actually remove the losing memory.
- **`/mem0:health` Check 3 `search_memories` top-level `user_id`:** Removed top-level `user_id` param; identity only in `filters.AND[]`. Changed `limit` to `top_k`.
- **`/mem0:health` Check 4 `add_memory` missing `infer=False`:** Health probe wasted LLM tokens on extraction. Added `infer=False`. Also fixed: was expecting `memory_id` in response but v3 returns `event_id`. Now uses `get_event_status` to get memory ID for cleanup.
- **`/mem0:tour` `get_memories` top-level identity:** Both standard and cross-project modes passed `user_id`/`app_id` as top-level params. Moved to `filters.AND[]`.
- **`/mem0:onboard` `search_memories` top-level `user_id`:** Removed extra top-level `user_id` param from connectivity check.
- **`/mem0:context-loader` incomplete filter table:** Filter examples showed only `metadata.type` without `user_id`/`app_id`. Now shows full `AND` filter structure.
- **7 skills used `limit` instead of `top_k` for `search_memories`:** MCP tool param is `top_k`, not `limit`. Fixed in: health, onboard, tour (3 places), switch-project, stats (weekly mode + latency probe).
- **`/mem0:stats` latency probe missing `filters`:** `search_memories` call had no identity filters. Added `user_id`/`app_id` in `filters.AND[]`.
### Added
- **`stop_hook_check.py`:** Pure-stdlib transcript analyzer for the Stop hook. Reads last 500 lines of transcript JSONL, parses tool calls, file modifications, and git commands. Returns `{"should_block": bool, "context": "..."}`. Trivial sessions (< 3 tool calls, no file edits) skip capture entirely.
- **Checklist for `/mem0:dream`:** 6-step progress tracker per Claude skill best practices for complex multi-step workflows.
- **Checklist for `/mem0:onboard`:** 7-step progress tracker for onboarding wizard.
- **Expanded hook matcher (all 3 configs):** `enforce_metadata_defaults.sh` now triggers on `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, and `delete_all_memories` (12 tool name variants covering both MCP naming conventions).
### Changed
- **Stop hook uses MCP-driven capture:** When meaningful work detected and no memories stored, returns `decision: "block"` asking Claude to call `add_memory` via MCP. One-shot flag prevents infinite loops. REST API capture runs in background as fallback.
- **SessionStart banner uses raw text stdout:** Replaced JSON `additionalContext` with `cat <<BANNER` for reliable display per official hooks spec.
- **`enforce_metadata_defaults.sh` rewritten:** Handler-based dispatch for 4 tool types. `add_memory` gets top-level identity + metadata defaults. `search_memories`/`get_memories` get filter identity injection. `delete_all_memories` gets top-level identity. Never overrides explicitly-passed identity.
## 0.2.4
### Fixed
+9 -9
View File
@@ -6,17 +6,17 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_file_read.sh",
"command": "${PLUGIN_ROOT}/scripts/on_file_read.sh",
"timeout": 8
}
]
},
{
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"command": "${PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"timeout": 3
}
]
@@ -28,7 +28,7 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_session_start.sh",
"command": "${PLUGIN_ROOT}/scripts/on_session_start.sh",
"statusMessage": "Loading mem0 context..."
}
]
@@ -39,7 +39,7 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
"command": "${PLUGIN_ROOT}/scripts/on_user_prompt.sh",
"statusMessage": "Checking memory relevance...",
"timeout": 12
}
@@ -52,7 +52,7 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
"command": "${PLUGIN_ROOT}/scripts/on_post_tool_use.sh",
"timeout": 3
}
]
@@ -62,12 +62,12 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_post_commit.sh",
"command": "${PLUGIN_ROOT}/scripts/on_post_commit.sh",
"timeout": 5
},
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_bash_output.sh",
"command": "${PLUGIN_ROOT}/scripts/on_bash_output.sh",
"timeout": 12
}
]
@@ -78,7 +78,7 @@
"hooks": [
{
"type": "command",
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_stop_codex.sh",
"command": "${PLUGIN_ROOT}/scripts/on_stop_codex.sh",
"timeout": 10
}
]
+1 -1
View File
@@ -18,7 +18,7 @@
},
{
"command": "${CURSOR_PLUGIN_ROOT}/scripts/enforce_metadata_defaults.sh",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"timeout": 3
}
],
+4 -4
View File
@@ -37,7 +37,7 @@
],
"PreToolUse": [
{
"matcher": "Write|Edit",
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
@@ -66,7 +66,7 @@
]
},
{
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory",
"matcher": "mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory|mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories|mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories|mcp__mem0__get_memory|mcp__plugin_mem0_mem0__get_memory|mcp__mem0__update_memory|mcp__plugin_mem0_mem0__update_memory|mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories",
"hooks": [
{
"type": "command",
@@ -78,7 +78,7 @@
],
"PostToolUse": [
{
"matcher": "mcp__mem0__|mcp__plugin_mem0_mem0__",
"matcher": "mcp__mem0__.*|mcp__plugin_mem0_mem0__.*",
"hooks": [
{
"type": "command",
@@ -120,7 +120,7 @@
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_stop.sh",
"timeout": 10
"timeout": 30
}
]
}
+100 -6
View File
@@ -45,6 +45,34 @@ API_URL = "https://api.mem0.ai"
MAX_FILE_SIZE = 100_000 # skip files over 100 KB
TARGET_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", ".windsurfrules", "mem0.md"]
HASH_STORE = os.path.expanduser("~/.mem0/file_hashes.json")
LOCK_FILE = os.path.expanduser("~/.mem0/auto_import.lock")
def _acquire_lock() -> bool:
"""Try to acquire a file lock. Returns False if another instance is running."""
try:
os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.write(fd, str(os.getpid()).encode())
os.close(fd)
return True
except FileExistsError:
try:
mtime = os.path.getmtime(LOCK_FILE)
import time
if time.time() - mtime > 120:
os.unlink(LOCK_FILE)
return _acquire_lock()
except OSError:
pass
return False
def _release_lock() -> None:
try:
os.unlink(LOCK_FILE)
except OSError:
pass
def _git_root(cwd: str) -> str:
@@ -103,7 +131,7 @@ def already_imported(api_key: str, user_id: str, project_id: str, filename: str)
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 3,
"top_k": 10,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
@@ -118,13 +146,70 @@ def already_imported(api_key: str, user_id: str, project_id: str, filename: str)
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", ""):
file_field = meta.get("file", "")
if file_field == filename or file_field.startswith(f"{filename}["):
return True
return False
except Exception:
return False
def _delete_stale_chunks(api_key: str, user_id: str, project_id: str, filename: str) -> int:
"""Find and delete existing chunks for a file before re-import. Returns count deleted."""
body = json.dumps({
"query": filename,
"filters": {
"AND": [
{"user_id": user_id},
{"app_id": project_id},
{"metadata": {"source": "auto-import"}},
]
},
"top_k": 20,
"threshold": 0.0,
}).encode()
req = urllib.request.Request(
f"{API_URL}/v3/memories/search/",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
method="POST",
)
ids_to_delete = []
try:
with urllib.request.urlopen(req, timeout=10) as r:
data = json.loads(r.read())
results = data if isinstance(data, list) else data.get("results", [])
for result in results:
if not isinstance(result, dict):
continue
meta = result.get("metadata", {})
file_field = meta.get("file", "")
if file_field == filename or file_field.startswith(f"{filename}["):
mid = result.get("id")
if mid:
ids_to_delete.append(mid)
except Exception as e:
log.warning("Failed to search for stale chunks of %s: %s", filename, e)
return 0
deleted = 0
for mid in ids_to_delete:
try:
del_req = urllib.request.Request(
f"{API_URL}/v3/memories/{mid}/",
headers={"Authorization": f"Token {api_key}"},
method="DELETE",
)
with urllib.request.urlopen(del_req, timeout=10):
deleted += 1
except Exception as e:
log.warning("Failed to delete stale chunk %s: %s", mid, e)
if deleted:
log.info("Deleted %d stale chunk(s) for %s before re-import", deleted, filename)
return deleted
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str, branch: str = "") -> bool:
"""POST a project profile memory to the Mem0 REST API."""
metadata = {
@@ -229,12 +314,14 @@ def main() -> None:
continue
seen_content_hashes.add(current_hash)
hash_key = f"{project_id}:{filename}"
hash_key = f"{project_id}:{branch}:{filename}" if branch else f"{project_id}:{filename}"
if hashes.get(hash_key) == current_hash:
log.debug("Unchanged, skipping: %s", filename)
continue
if already_imported(api_key, user_id, project_id, filename):
log.debug("Unchanged and still in mem0, skipping: %s", filename)
continue
log.info("Hash matches but memories missing server-side, re-importing: %s", filename)
if already_imported(api_key, user_id, project_id, filename):
elif already_imported(api_key, user_id, project_id, filename):
log.debug("Already in mem0, updating hash store: %s", filename)
hashes[hash_key] = current_hash
updated = True
@@ -247,6 +334,8 @@ def main() -> None:
log.debug("Cannot read %s: %s", filename, e)
continue
_delete_stale_chunks(api_key, user_id, project_id, filename)
is_markdown = filename.endswith(".md")
if is_markdown:
chunks = filter_and_truncate(split_by_headers(content))
@@ -273,8 +362,13 @@ def main() -> None:
if __name__ == "__main__":
if not _acquire_lock():
log.debug("Another auto_import instance is running — skipping")
sys.exit(0)
try:
main()
except Exception as e:
log.error("Unexpected error: %s", e)
finally:
_release_lock()
sys.exit(0)
+18 -1
View File
@@ -168,8 +168,25 @@ def main():
log.debug("No isCompactSummary entry found")
return
if len(summary.strip()) < 100:
log.debug("Compact summary too short (%d chars) — skipping", len(summary.strip()))
return
marker_dir = os.path.expanduser("~/.mem0")
marker_file = os.path.join(marker_dir, f"compact_captured_{session_id}")
if session_id and os.path.isfile(marker_file):
log.info("Compact summary already captured for session %s — skipping", session_id)
return
log.info("Capturing compact summary (%d chars)", len(summary))
store_summary(api_key, summary, user_id, session_id, project_id, branch)
if store_summary(api_key, summary, user_id, session_id, project_id, branch):
if session_id:
try:
os.makedirs(marker_dir, exist_ok=True)
with open(marker_file, "w") as f:
f.write("")
except OSError:
pass
if __name__ == "__main__":
+148 -35
View File
@@ -1,8 +1,14 @@
#!/usr/bin/env bash
# PreToolUse hook for mcp__mem0__add_memory.
# Injects default metadata fields (confidence, files, source, type) when the
# agent omits them. Uses the hookSpecificOutput.updatedInput contract to
# actually modify the tool call parameters.
# PreToolUse hook for mem0 MCP tools.
# Injects identity (user_id, app_id) and metadata defaults when the agent
# omits them. Uses the hookSpecificOutput.updatedInput contract to modify
# tool call parameters before execution.
#
# Handles:
# add_memory — top-level user_id, app_id, metadata defaults
# search_memories — user_id/app_id into filters.AND[]
# get_memories — user_id/app_id into filters.AND[]
# delete_all_memories — top-level user_id, app_id
#
# Hook contract:
# exit 0 = allow. If stdout contains {"hookSpecificOutput": {"updatedInput": ...}},
@@ -11,18 +17,35 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""' 2>/dev/null)
# Determine which handler to use based on tool name
HANDLER=""
case "$TOOL_NAME" in
mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory) ;;
mcp__mem0__add_memory|mcp__plugin_mem0_mem0__add_memory)
HANDLER="add_memory" ;;
mcp__mem0__search_memories|mcp__plugin_mem0_mem0__search_memories)
HANDLER="search_memories" ;;
mcp__mem0__get_memories|mcp__plugin_mem0_mem0__get_memories)
HANDLER="get_memories" ;;
mcp__mem0__delete_all_memories|mcp__plugin_mem0_mem0__delete_all_memories)
HANDLER="delete_all" ;;
*) exit 0 ;;
esac
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // "{}"' 2>/dev/null)
_PATCH_OUT="/tmp/mem0_enforce_$$"
_MEM0_TOOL_INPUT="$TOOL_INPUT" python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
_MEM0_TOOL_INPUT="$TOOL_INPUT" \
_MEM0_USER_ID="${MEM0_RESOLVED_USER_ID:-}" \
_MEM0_APP_ID="${MEM0_PROJECT_ID:-}" \
_MEM0_HANDLER="$HANDLER" \
python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
import json, os, sys
raw = os.environ.get("_MEM0_TOOL_INPUT", "{}")
@@ -31,40 +54,130 @@ try:
except Exception:
sys.exit(0)
meta = inp.get("metadata") or {}
handler = os.environ.get("_MEM0_HANDLER", "")
resolved_uid = os.environ.get("_MEM0_USER_ID", "")
resolved_aid = os.environ.get("_MEM0_APP_ID", "")
changed = False
if "confidence" not in meta:
meta["confidence"] = 0.7
changed = True
if "files" not in meta:
meta["files"] = ["*"]
changed = True
if "source" not in meta:
meta["source"] = "auto_capture"
changed = True
if "type" not in meta:
meta["type"] = "task_learning"
changed = True
if meta.get("confidence", 0) >= 1.0 and "infer" not in inp:
inp["infer"] = False
changed = True
def inject_top_level_identity(inp, uid, aid):
"""Inject user_id/app_id as top-level params (for add_memory, delete_all)."""
changed = False
if uid and not inp.get("user_id"):
inp["user_id"] = uid
changed = True
if aid and not inp.get("app_id"):
inp["app_id"] = aid
changed = True
return changed
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
def inject_filter_identity(inp, uid, aid):
"""Inject user_id/app_id into filters.AND[] (for search/get_memories)."""
changed = False
if not uid and not aid:
return False
filters = inp.get("filters")
if filters is None:
# No filters at all — create from scratch
and_clauses = []
if uid:
and_clauses.append({"user_id": uid})
if aid:
and_clauses.append({"app_id": aid})
inp["filters"] = {"AND": and_clauses}
return True
if not isinstance(filters, dict):
return False
# Check if filters already contain user_id/app_id
and_clauses = filters.get("AND")
if and_clauses is None:
# Filters exist but no AND — could be flat like {"user_id": "x"}
has_uid = "user_id" in filters
has_aid = "app_id" in filters
if has_uid and has_aid:
return False
# Convert flat filters to AND format and add missing identity
existing = []
for k, v in list(filters.items()):
existing.append({k: v})
if uid and not has_uid:
existing.append({"user_id": uid})
changed = True
if aid and not has_aid:
existing.append({"app_id": aid})
changed = True
if changed:
inp["filters"] = {"AND": existing}
return changed
if not isinstance(and_clauses, list):
return False
# AND array exists — check for existing user_id/app_id
has_uid = any("user_id" in c for c in and_clauses if isinstance(c, dict))
has_aid = any("app_id" in c for c in and_clauses if isinstance(c, dict))
if uid and not has_uid:
and_clauses.append({"user_id": uid})
changed = True
if aid and not has_aid:
and_clauses.append({"app_id": aid})
changed = True
return changed
if handler == "add_memory":
changed = inject_top_level_identity(inp, resolved_uid, resolved_aid)
meta = inp.get("metadata") or {}
if "confidence" not in meta:
meta["confidence"] = 0.7
changed = True
if "files" not in meta:
meta["files"] = ["*"]
changed = True
if "source" not in meta:
meta["source"] = "auto_capture"
changed = True
if "type" not in meta:
meta["type"] = "task_learning"
changed = True
if meta.get("confidence", 0) >= 1.0 and "infer" not in inp:
inp["infer"] = False
changed = True
if "run_id" not in inp:
sid = os.environ.get("MEM0_SESSION_ID", "")
if not sid:
session_file = "/tmp/mem0_session_id_" + os.environ.get("USER", "default")
if os.path.isfile(session_file):
try:
with open(session_file) as f:
sid = f.read().strip()
except OSError:
pass
if sid:
inp["run_id"] = sid
changed = True
if changed:
inp["metadata"] = meta
elif handler in ("search_memories", "get_memories"):
changed = inject_filter_identity(inp, resolved_uid, resolved_aid)
elif handler == "delete_all":
changed = inject_top_level_identity(inp, resolved_uid, resolved_aid)
if changed:
inp["metadata"] = meta
print(json.dumps(inp))
PYEOF
PATCHED=$(cat "$_PATCH_OUT" 2>/dev/null)
@@ -77,7 +190,7 @@ if [ -n "$PATCHED" ] && echo "$PATCHED" | jq empty 2>/dev/null; then
"permissionDecision": "allow",
"updatedInput": $updated
}
}'
}' 2>/dev/null || true
fi
exit 0
+50 -6
View File
@@ -17,6 +17,7 @@ Exit: 0 always
from __future__ import annotations
import hashlib
import json
import os
import sys
@@ -33,6 +34,31 @@ from _identity import resolve_api_key, resolve_user_id
from _project import resolve_branch, resolve_project_id
API_URL = "https://api.mem0.ai"
HASH_STORE = os.path.expanduser("~/.mem0/import_hashes.json")
def _load_hashes() -> dict[str, str]:
if not os.path.isfile(HASH_STORE):
return {}
try:
with open(HASH_STORE) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
return {}
def _save_hashes(hashes: dict[str, str]) -> None:
os.makedirs(os.path.dirname(HASH_STORE), exist_ok=True)
try:
with open(HASH_STORE, "w") as f:
json.dump(hashes, f, indent=2)
except OSError:
pass
def _content_hash(content: str) -> str:
return hashlib.sha256(content.encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
# API helpers
@@ -73,12 +99,30 @@ def post_memory(api_key: str, content: str, user_id: str, project_id: str, branc
return False
def import_chunks(chunks: list[str], api_key: str, user_id: str, project_id: str, branch: str, source: str) -> int:
"""Import a list of content chunks; return number of successful imports."""
def import_chunks(chunks: list[str], api_key: str, user_id: str, project_id: str, branch: str, source: str, hash_key: str = "") -> int:
"""Import a list of content chunks; return number of successful imports.
Skips import if content hash matches a previous run for the same hash_key."""
if hash_key:
combined = "\n".join(chunks)
current_hash = _content_hash(combined)
hashes = _load_hashes()
if hashes.get(hash_key) == current_hash:
print(f"Already imported (unchanged) -- skipping: {hash_key}")
return 0
else:
current_hash = ""
hashes = {}
success = 0
for chunk in chunks:
if post_memory(api_key, chunk, user_id, project_id, branch, source):
success += 1
if success > 0 and hash_key and current_hash:
hashes[hash_key] = current_hash
_save_hashes(hashes)
return success
@@ -123,7 +167,7 @@ def cmd_cursorrules(args: list[str]) -> None:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
@@ -152,7 +196,7 @@ def cmd_copilot(args: list[str]) -> None:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
@@ -188,7 +232,7 @@ def cmd_cline(args: list[str]) -> None:
if not content:
continue
chunks = filter_and_truncate([content])
n = import_chunks(chunks, api_key, user_id, project_id, branch, source)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{filepath}")
total += n
print(f"Imported {total} memories from {source} ({dir_path})")
@@ -219,7 +263,7 @@ def cmd_continue(args: list[str]) -> None:
raw_chunks = [content.strip()] if content.strip() else []
chunks = filter_and_truncate(raw_chunks)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source)
n = import_chunks(chunks, api_key, user_id, project_id, branch, source, hash_key=f"{project_id}:{source}:{path}")
print(f"Imported {n} memories from {source} ({path})")
+2 -2
View File
@@ -4,7 +4,7 @@
Codex discovers hooks only at ~/.codex/hooks.json or <repo>/.codex/hooks.json,
and has no plugin-host mechanism for auto-wiring hooks from an installed
plugin. This installer reads the template at hooks/codex-hooks.json, rewrites
the ${CODEX_PLUGIN_ROOT} placeholder to the absolute install path of this
the ${PLUGIN_ROOT} placeholder to the absolute install path of this
plugin, then merges the entries into ~/.codex/hooks.json.
Re-running is idempotent: existing Mem0 entries (identified by the plugin
@@ -44,7 +44,7 @@ OWNER_MARKER = "mem0-plugin"
def load_template() -> dict:
raw = TEMPLATE_FILE.read_text()
raw = raw.replace("${CODEX_PLUGIN_ROOT}", str(PLUGIN_ROOT))
raw = raw.replace("${PLUGIN_ROOT}", str(PLUGIN_ROOT))
return json.loads(raw)
+15 -10
View File
@@ -9,14 +9,14 @@
# typed message). This hook catches errors in COMMAND OUTPUT — e.g.,
# when `npm test` or `python script.py` fails with a traceback.
#
# Input: JSON on stdin with tool_name, tool_input, tool_output
# Input: JSON on stdin with tool_name, tool_input, tool_response
# Output: Context injected into Claude's next response (exit 0)
set -uo pipefail
INPUT=$(cat)
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_output // ""' 2>/dev/null || echo "")
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_response // ""' 2>/dev/null || echo "")
# Skip short output (< 50 chars unlikely to contain a real stack trace)
if [ ${#TOOL_RESULT} -lt 50 ]; then
@@ -98,20 +98,25 @@ for m in r1 + r2:
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"
# Build context string for JSON output
CTX="Error detected in command output\n\n"
CTX="${CTX}\`${COMMAND}\` produced an error:\n> ${ERROR_LINE}\n"
if [ -n "$FILE_DISPLAY" ]; then
printf '**Files in stack trace:**\n%s\n\n' "$FILE_DISPLAY"
CTX="${CTX}\nFiles in stack trace:\n${FILE_DISPLAY}\n"
fi
if [ -n "$RESULTS" ]; then
printf '%s\n' "$RESULTS"
else
printf 'No prior memories found for this error.\n\n'
CTX="${CTX}\n${RESULTS}\n"
fi
printf 'If you solve this, store the fix as an `anti_pattern` or `bug_fix` memory for next time.\n'
CTX="${CTX}\nResolved errors are stored as anti_pattern or bug_fix memories for future reference."
jq -cn --arg ctx "$CTX" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}' 2>/dev/null || true
exit 0
+4 -4
View File
@@ -41,12 +41,12 @@ fi
USER_ID="${MEM0_RESOLVED_USER_ID:-$USER}"
PROJECT_ID="${MEM0_PROJECT_ID:-unknown}"
CONTEXT=$(python3 -c "
CONTEXT=$(_MEM0_UID="$USER_ID" _MEM0_AID="$PROJECT_ID" _MEM0_FILES="$CHANGED_FILES" python3 -c "
import json, urllib.request, os
api_key = os.environ.get('MEM0_API_KEY', os.environ.get('CLAUDE_PLUGIN_OPTION_MEM0_API_KEY', ''))
user_id = '$USER_ID'
app_id = '$PROJECT_ID'
files = '$CHANGED_FILES'
user_id = os.environ.get('_MEM0_UID', '')
app_id = os.environ.get('_MEM0_AID', '')
files = os.environ.get('_MEM0_FILES', '')
first_file = files.split(',')[0].strip()
body = json.dumps({
'query': f'changes to {files}',
+11 -32
View File
@@ -6,7 +6,7 @@
# and prompts Claude to ask the user if this change should be stored as
# a learning.
#
# Input: JSON on stdin with tool_name, tool_input, tool_output
# Input: JSON on stdin with tool_name, tool_input, tool_response
# Output: Context injected into Claude's next response (exit 0)
#
# This implements Spec #28 — interactive pre-commit memory check.
@@ -31,7 +31,7 @@ case "$COMMAND" in
esac
# Check if the command actually succeeded (look for commit hash in output)
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_output // ""' 2>/dev/null || echo "")
TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_response // ""' 2>/dev/null || echo "")
if echo "$TOOL_RESULT" | grep -qiE '(error|fatal|abort|nothing to commit|no changes)'; then
exit 0
fi
@@ -52,47 +52,26 @@ if [ "$COMMIT_PROMPTS" != "true" ]; then
fi
# Extract changed files from commit
CHANGED_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | head -10 || echo "")
CHANGED_FILES=$(git -C "$MEM0_CWD" diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null | head -10 || echo "")
if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
# Get the commit message
COMMIT_MSG=$(git log -1 --format='%s' HEAD 2>/dev/null || echo "")
COMMIT_MSG=$(git -C "$MEM0_CWD" log -1 --format='%s' HEAD 2>/dev/null || echo "")
# Build file list for display
FILE_LIST=$(echo "$CHANGED_FILES" | sed 's/^/ - /' | head -5)
FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ')
cat <<EOF
CTX="Commit captured: ${COMMIT_MSG}\nFiles changed (${FILE_COUNT}):\n${FILE_LIST}\n\nCommit learnings (decision, task_learning, anti_pattern, convention) are stored via add_memory with user_id=${MEM0_RESOLVED_USER_ID}, app_id=${MEM0_PROJECT_ID}, branch=${MEM0_BRANCH}."
## Commit captured — save as memory?
**Commit:** $COMMIT_MSG
**Files changed ($FILE_COUNT):**
$FILE_LIST
This commit may contain learnings worth preserving. Ask the user:
> **Save this change as a memory?** I can store it as one of:
> - \`decision\` — if an architectural or design choice was made
> - \`task_learning\` — if a strategy or approach worked well
> - \`anti_pattern\` — if this fixes a bug or avoids a known pitfall
> - \`convention\` — if this establishes a new pattern
>
> Type the category, or **skip** to move on.
If the user provides a category (or says yes/sure/go ahead):
1. Summarize the commit: what changed and why (from the diff + commit message)
2. Call \`add_memory\` with:
- \`messages=[{"role": "user", "content": "<your summary>"}]\`
- \`user_id="$MEM0_RESOLVED_USER_ID"\`
- \`app_id="$MEM0_PROJECT_ID"\`
- \`metadata={"type": "<chosen_category>", "branch": "$MEM0_BRANCH", "confidence": 0.8, "files": [<changed files>], "source": "post-commit"}\`
- \`infer=False\`
If the user says skip/no/nothing: proceed normally. Do NOT ask again for the same commit.
EOF
jq -cn --arg ctx "$CTX" '{
hookSpecificOutput: {
hookEventName: "PostToolUse",
additionalContext: $ctx
}
}' 2>/dev/null || true
# Telemetry
python3 "$SCRIPT_DIR/telemetry.py" post_commit --files_count="$FILE_COUNT" 2>/dev/null &
+2 -25
View File
@@ -1,11 +1,8 @@
#!/usr/bin/env bash
# Hook: PostCompact (matcher: manual|auto)
#
# Fires after context compaction completes. Injects a recovery prompt
# telling the agent to reload context from mem0.
#
# Input: JSON on stdin with trigger, messages_retained, messages_removed
# Output: Context injected into Claude's post-compaction context (exit 0)
# Fires after context compaction completes. Runs silently — recovery
# is handled by SessionStart hook with source=compact.
set -uo pipefail
@@ -24,24 +21,4 @@ REMOVED=$(echo "$INPUT" | jq -r '.messages_removed // "?"' 2>/dev/null || echo "
# Telemetry (background)
python3 "$SCRIPT_DIR/telemetry.py" post_compact --trigger="$TRIGGER" --retained="$RETAINED" --removed="$REMOVED" 2>/dev/null &
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
USER_ID="${MEM0_RESOLVED_USER_ID:-$USER}"
PROJECT_ID="${MEM0_PROJECT_ID:-unknown}"
cat <<EOF
## Mem0 Post-Compaction Recovery
Compaction complete ($TRIGGER). $REMOVED messages removed, $RETAINED retained.
You lost most conversation history. Recover context NOW:
1. \`search_memories(query="session state current task", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$PROJECT_ID"}, {"metadata": {"type": "session_state"}}]})\`
2. \`search_memories(query="recent decisions and learnings", filters={"AND": [{"user_id": "$USER_ID"}, {"app_id": "$PROJECT_ID"}, {"metadata": {"type": "decision"}}]})\`
Run both in parallel. Use results to resume work without asking user to repeat context.
EOF
exit 0
+1 -1
View File
@@ -5,7 +5,7 @@
# mcp__mem0__add_memory → record an add
# mcp__mem0__search_memories → record a search
#
# Input: JSON on stdin with tool_name, tool_input, tool_output
# Input: JSON on stdin with tool_name, tool_input, tool_response
# Output: none (exit 0, non-blocking)
set -uo pipefail
+64 -18
View File
@@ -147,12 +147,14 @@ def build_content(state: dict, source: str) -> str:
"""
parts = []
if state["user_messages"]:
parts.append(f"Working on: {state['user_messages'][-1][:300]}")
if state["files_modified"]:
parts.append(f"Files touched: {', '.join(state['files_modified'][:15])}")
if state["bash_commands"]:
git_cmds = [c for c in state["bash_commands"] if "git " in c]
if git_cmds:
parts.append(f"Git operations: {len(git_cmds)}")
return "\n".join(parts)
@@ -200,15 +202,51 @@ def store_memory(api_key: str, content: str, user_id: str, source: str, session_
return False
def format_status(state: dict, source: str, stored: bool, skipped_reason: str = "") -> str:
"""Build a clean, readable status line for terminal display."""
files_count = len(state.get("files_modified", []))
git_cmds = [c for c in state.get("bash_commands", []) if "git " in c]
user_msgs = len(state.get("user_messages", []))
parts = []
if files_count:
parts.append(f"{files_count} file{'s' if files_count != 1 else ''} touched")
if git_cmds:
parts.append(f"{len(git_cmds)} git op{'s' if len(git_cmds) != 1 else ''}")
if user_msgs:
parts.append(f"{user_msgs} exchange{'s' if user_msgs != 1 else ''}")
activity = ", ".join(parts) if parts else "minimal activity"
if source == "pre-compaction":
icon = "✨" # ✨
label = "Pre-compaction snapshot"
else:
icon = "\U0001f4be" # 💾
label = "Session-end snapshot"
if skipped_reason:
return f"{icon} Mem0 {label} — {activity} — {skipped_reason}"
elif stored:
return f"{icon} Mem0 {label} — {activity} — saved to mem0"
else:
return f"{icon} Mem0 {label} — {activity} — nothing to capture"
def main():
source = "pre-compaction"
show_status = False
for arg in sys.argv[1:]:
if arg.startswith("--source="):
source = arg.split("=", 1)[1]
elif arg == "--status":
show_status = True
api_key = resolve_api_key()
if not api_key:
log.debug("MEM0_API_KEY not set, skipping capture")
if show_status:
print("✨ Mem0 — no API key, skipping capture")
return
try:
@@ -228,36 +266,44 @@ def main():
project_id = resolve_project_id(cwd)
branch = resolve_branch(cwd)
# Skip if agent already stored memories this session — avoid duplicate writes.
# This script is a fallback, not the primary capture path.
stats_file = f"/tmp/mem0_session_stats_{os.environ.get('USER', 'default')}.json"
try:
with open(stats_file) as f:
stats = json.load(f)
if stats.get("adds", 0) >= 2:
log.info("Agent stored %d memories this session — skipping fallback capture", stats["adds"])
return
except (OSError, json.JSONDecodeError):
pass # no stats = agent didn't store anything, proceed with fallback
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
log.debug("Transcript empty or unreadable: %s", transcript_path)
return
state = parse_transcript(lines)
if not state["user_messages"] and not state["files_modified"]:
log.debug("No meaningful session state to capture")
# Skip if agent already stored memories this session — avoid duplicate writes.
stats_file = f"/tmp/mem0_session_stats_{os.environ.get('USER', 'default')}.json"
try:
with open(stats_file) as f:
stats = json.load(f)
if stats.get("adds", 0) >= 1:
log.info("Agent stored %d memories this session — skipping fallback", stats["adds"])
if show_status:
print(format_status(state, source, False, f"agent already stored {stats['adds']} memor{'ies' if stats['adds'] != 1 else 'y'}"))
return
except (OSError, json.JSONDecodeError):
pass
if not state["files_modified"]:
log.debug("No files modified — skipping fallback capture")
if show_status:
print(format_status(state, source, False))
return
content = build_content(state, source)
if not content.strip():
log.debug("No content to store")
if show_status:
print(format_status(state, source, False))
return
log.info("Fallback capture: %d files modified", len(state["files_modified"]))
stored = store_memory(api_key, content, user_id, source, session_id, project_id, branch)
store_memory(api_key, content, user_id, source, session_id, project_id, branch)
if show_status:
print(format_status(state, source, stored))
if __name__ == "__main__":
+7 -48
View File
@@ -1,15 +1,10 @@
#!/usr/bin/env bash
# Hook: PreCompact
#
# Fires BEFORE context compaction. This is the last chance to capture
# the full context before it gets compressed.
#
# Output: Text instructions injected into Claude's context.
# Claude still has the full conversation and can write an accurate summary,
# which it stores via add_memory(infer=False) so the platform preserves
# the structure verbatim instead of running a second extraction pass.
# Fires BEFORE context compaction. Captures session state via REST API
# in the background. Runs silently — no output to user.
set -euo pipefail
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
@@ -20,45 +15,9 @@ INPUT=$(cat)
python3 "$SCRIPT_DIR/telemetry.py" pre_compact 2>/dev/null &
cat <<'EOF'
## Pre-Compaction: Extract and store durable facts
Context compaction is about to happen. Review the conversation and store only facts that would help a future agent with ZERO context.
### What to store
For each fact, ask: "Would a new agent — with no prior context — benefit from knowing this?" If no, skip it. Most sessions produce 0-3 facts worth storing.
Store each fact as a SEPARATE `add_memory` call. One fact per call. 15-50 words each. Third person. Include file paths when relevant.
Categories and when to use them:
- `decision` — architectural choices, trade-offs made ("Chose PostgreSQL over MongoDB for auth because of ACID requirements")
- `task_learning` — patterns that worked ("Running migrations before seed in this repo avoids FK violations")
- `anti_pattern` — approaches that failed ("Don't use batch insert for users table — triggers deadlock with audit log")
- `convention` — coding standards discovered ("This repo uses snake_case for all Python files, camelCase for TS")
- `user_preference` — how the user likes to work ("User prefers short PRs, one feature per branch")
### What NOT to store
- Session summaries or "what we did today" blobs
- Raw file lists or command histories
- Anything already stored in a prior `add_memory` this session
- One-time information that won't recur
- Transient state ("currently debugging X")
### How to store
```
add_memory(
messages=[{"role":"user","content":"<one fact, 15-50 words>"}],
user_id="<active user_id>",
app_id="<active project_id>",
metadata={"type":"<category>","branch":"<active branch>","confidence":0.8},
infer=False,
)
```
If nothing durable happened this session, store nothing. That is correct.
EOF
# Capture in background, no stdout
_TMP="/tmp/mem0_precompact_input_$$.json"
printf '%s' "$INPUT" > "$_TMP" 2>/dev/null
(python3 "$SCRIPT_DIR/on_pre_compact.py" --source=pre-compaction < "$_TMP" 2>/dev/null; rm -f "$_TMP") &
exit 0
+39 -44
View File
@@ -6,7 +6,7 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
@@ -25,29 +25,38 @@ fi
printf '%s' "$MEM0_SESSION_ID" > "/tmp/mem0_session_id_${USER}"
export MEM0_SESSION_ID
# Persist session ID and identity to Claude's env so Bash tool calls and other hooks see them
if [ -n "${CLAUDE_ENV_FILE:-}" ]; then
echo "export MEM0_SESSION_ID=\"$MEM0_SESSION_ID\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_RESOLVED_USER_ID=\"${MEM0_RESOLVED_USER_ID:-$USER}\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_PROJECT_ID=\"${MEM0_PROJECT_ID:-unknown}\"" >> "$CLAUDE_ENV_FILE"
echo "export MEM0_BRANCH=\"${MEM0_BRANCH:-unknown}\"" >> "$CLAUDE_ENV_FILE"
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
_UID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
_PID="${MEM0_PROJECT_ID:-unknown}"
_BR="${MEM0_BRANCH:-unknown}"
cat <<BANNER
## Mem0 — Setup Required
\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none\`
\`user=${_UID} | project=${_PID} | branch=${_BR} | 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):
IMPORTANT: In your FIRST response, display this exact status line as your opening line:
\`\`\`
Mem0 — Setup Required | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
Mem0 — Setup Required | user=${_UID} | project=${_PID} | branch=${_BR} | auth=none
\`\`\`
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.
MEM0_API_KEY is not set. Invoke the \`mem0:onboard\` skill immediately using the Skill tool to set up the API key and configure mem0 for this project.
BANNER
exit 0
fi
_DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}"
_INSTALL_WARN=""
if [ -f "${_DATA_DIR}/.install-failed" ]; then
echo ""
echo "mem0 SDK installation failed. Some features may not work."
echo "Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
echo ""
_INSTALL_WARN="mem0 SDK installation failed. Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
fi
MEM0_COUNT="?"
@@ -78,77 +87,63 @@ except Exception:
" 2>/dev/null || echo "?")
fi
_UID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
_ANN="${_MEM0_IDENTITY_ANNOTATION:-}"
_PID="${MEM0_PROJECT_ID:-unknown}"
_BR="${MEM0_BRANCH:-unknown}"
cat <<BANNER
## Mem0 Active
\`user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key\`
\`user=${_UID}${_ANN} | project=${_PID} | branch=${_BR} | memories=${MEM0_COUNT}\`
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):
IMPORTANT: In your FIRST response, display this exact status line as your opening line:
\`\`\`
Mem0 Active | user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key
Mem0 Active | user=${_UID}${_ANN} | project=${_PID} | branch=${_BR} | 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)
- user_id: \`${_UID}\`
- app_id: \`${_PID}\` (project scope — passed as top-level \`app_id\`, NOT in metadata)
BANNER
if [ -n "$_INSTALL_WARN" ]; then
echo "$_INSTALL_WARN"
fi
MEM0_CWD_RESOLVED=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
if command -v python3 >/dev/null 2>&1; then
MEM0_PROJECT_CONFIG=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --full "$MEM0_CWD_RESOLVED" 2>/dev/null || echo "{}")
if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
_CONFIG_KEYS=$(echo "$MEM0_PROJECT_CONFIG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "?")
echo "### Project Config (mem0.md)"
echo "\`mem0.md loaded (${_CONFIG_KEYS} sections configured)\`"
if [ -n "${MEM0_DEBUG:-}" ]; then
echo "\`\`\`json"
echo "$MEM0_PROJECT_CONFIG"
echo "\`\`\`"
fi
echo ""
echo "mem0.md loaded (${_CONFIG_KEYS} sections configured)."
fi
fi
if [ "$SOURCE" = "startup" ]; then
if [ "$MEM0_COUNT" = "0" ]; then
cat <<'EOF'
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
echo "New project with 0 memories. Invoke the mem0:onboard skill to import project files and install coding categories."
else
cat <<'EOF'
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
echo "Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type."
fi
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
_MEMORY_MD="$HOME/.claude/projects/${_PROJ_KEY}/memory/MEMORY.md"
if [ -f "$_MEMORY_MD" ] && [ -s "$_MEMORY_MD" ]; then
cat <<MEMEOF
### Native auto-memory detected
Found \`$_MEMORY_MD\`. The mem0 plugin handles all memory storage.
To avoid two parallel memory systems:
- Add \`"autoMemoryEnabled": false\` to \`~/.claude/settings.json\`
- Or run \`/mem0:import\` to migrate existing MEMORY.md content into mem0
MEMEOF
echo "Native MEMORY.md detected at ${_MEMORY_MD}. Add autoMemoryEnabled:false to settings.json or run /mem0:import."
fi
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
elif [ "$SOURCE" = "resume" ]; then
cat <<'EOF'
Session resumed. Search mem0 for `session_state` and `decision` memories to pick up where you left off. Run 2 parallel searches.
EOF
echo "Session resumed. Search mem0 for session_state and decision memories to pick up where you left off. Run 2 parallel searches."
elif [ "$SOURCE" = "compact" ]; then
cat <<'EOF'
Context compacted. Search mem0 for `session_state` and `decision` memories to recover context. Run 2 parallel searches.
EOF
echo "Context compacted. Search mem0 for session_state and decision memories to recover context. Run 2 parallel searches."
printf '%s' "$INPUT" | python3 "$SCRIPT_DIR/capture_compact_summary.py" 2>/dev/null &
fi
python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null &
+60 -8
View File
@@ -1,4 +1,13 @@
#!/usr/bin/env bash
# Hook: Stop (Claude Code)
#
# Fires when Claude finishes responding. If meaningful work happened and
# no memories were stored, blocks stop so Claude can call MCP add_memory.
# REST API capture runs in background as fallback.
#
# Input: JSON on stdin with session_id, transcript_path, cwd, response_text
# Output: JSON { decision: "block", reason: "..." } or exit 0
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
@@ -6,19 +15,62 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# Telemetry
_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 &
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
# Check if meaningful work happened
CHECK_RESULT=$(echo "$INPUT" | python3 "$SCRIPT_DIR/stop_hook_check.py" 2>/dev/null || echo '{"should_block":false}')
_SHOULD_CAPTURE=$(echo "$CHECK_RESULT" | jq -r '.should_block // false' 2>/dev/null || echo "false")
if [ "$_SHOULD_CAPTURE" != "true" ]; then
exit 0
fi
# Log session report
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
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
# Background REST API capture (fallback)
_TMP="/tmp/mem0_stop_input_$$.json"
printf '%s' "$INPUT" > "$_TMP" 2>/dev/null
(python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end < "$_TMP" 2>/dev/null; rm -f "$_TMP") &
# Check if agent already stored memories this session
_STATS_FILE="/tmp/mem0_session_stats_${USER:-default}.json"
_MEM_COUNT=0
if [ -f "$_STATS_FILE" ]; then
_MEM_COUNT=$(python3 -c "import json; d=json.load(open('$_STATS_FILE')); print(d.get('adds',0))" 2>/dev/null || echo "0")
fi
if [ "${_MEM_COUNT:-0}" -gt 0 ] 2>/dev/null; then
exit 0
fi
# One-shot guard: only block once per session
_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
_BLOCK_FLAG="/tmp/mem0_stop_blocked_${_SESSION_ID:-$$}"
if [ -f "$_BLOCK_FLAG" ]; then
exit 0
fi
touch "$_BLOCK_FLAG" 2>/dev/null || true
# No API key — can't use MCP tools
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
fi
# Block stop — Claude will call MCP add_memory then stop naturally
jq -cn '{
decision: "block",
reason: "Store 1-3 learnings via add_memory before stopping."
}' 2>/dev/null || true
exit 0
+9 -20
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Hook: SubagentStop
#
# Fires when a subagent finishes. Injects a reminder to capture any
# learnings the subagent produced that the parent agent should store.
# Fires when a subagent finishes. Stdout is fed to parent agent
# as context, prompting it to capture reusable learnings.
#
# Input: JSON on stdin with agent_type, result_summary
# Output: Context injected into parent agent's context (exit 0)
# Output: Text context for parent agent (exit 0)
set -uo pipefail
@@ -19,7 +19,7 @@ INPUT=$(cat)
AGENT_TYPE=$(echo "$INPUT" | jq -r '.agent_type // ""' 2>/dev/null || echo "")
RESULT_SUMMARY=$(echo "$INPUT" | jq -r '.result_summary // ""' 2>/dev/null || echo "")
# Skip short/empty results — nothing worth capturing
# Skip short/empty results
if [ ${#RESULT_SUMMARY} -lt 50 ]; then
exit 0
fi
@@ -32,12 +32,10 @@ else
_SKIP_PATTERN="Explore|Plan"
fi
# Skip explorer/plan agents — read-only, rarely produce storable learnings
case "$AGENT_TYPE" in
$_SKIP_PATTERN)
exit 0
;;
esac
# Skip read-only agents
if echo "$AGENT_TYPE" | grep -qE "^(${_SKIP_PATTERN})$" 2>/dev/null; then
exit 0
fi
if [ -z "${MEM0_API_KEY:-}" ]; then
exit 0
@@ -45,15 +43,6 @@ fi
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
cat <<EOF
## Subagent completed: $AGENT_TYPE
Review the subagent result for learnings worth persisting to mem0.
If the subagent discovered something reusable (a fix, pattern, decision, or anti-pattern),
store it via \`add_memory\` with appropriate metadata type.
Only store if genuinely valuable — skip trivial subagent results.
EOF
echo "Subagent completed: ${AGENT_TYPE}. Reusable learnings from subagents are stored via add_memory."
exit 0
+10 -10
View File
@@ -1,13 +1,12 @@
#!/usr/bin/env bash
# Hook: TaskCompleted
#
# Fires when a task is marked as completed. Reminds Claude to extract
# and store learnings via the mem0 MCP tools.
# Fires when a task is marked as completed. Stdout is fed back to Claude
# as context, prompting it to capture learnings.
#
# Input: JSON on stdin with task_id, task_subject, task_description
# Output: Text that becomes feedback to the model (exit 0)
# Output: Text feedback to Claude (exit 0)
# Intentionally omit -e so the reminder always emits even if identity resolution fails.
set -uo pipefail
if [ -n "${MEM0_DEBUG:-}" ]; then
@@ -18,15 +17,16 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
INPUT=$(cat)
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // ""' 2>/dev/null || echo "")
# Skip trivial or unknown tasks
if [ -z "$TASK_SUBJECT" ] || [ "$TASK_SUBJECT" = "unknown task" ] || [ ${#TASK_SUBJECT} -lt 10 ]; then
exit 0
fi
_PROJECT="${MEM0_PROJECT_ID:-unknown}"
cat <<EOF
Task completed: "$TASK_SUBJECT"
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
echo "Task completed: ${TASK_SUBJECT}. Learnings (decision, task_learning, anti_pattern, convention) are captured via add_memory with app_id=${_PROJECT}."
# Telemetry (background, fire-and-forget)
python3 "$SCRIPT_DIR/telemetry.py" task_completed 2>/dev/null &
+29 -23
View File
@@ -27,7 +27,7 @@ fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=_identity.sh
. "$SCRIPT_DIR/_identity.sh"
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
# Rubric dedup: only inject full rubric once per session.
# Key on session ID (from stdin JSON) to avoid cross-session interference.
@@ -78,16 +78,27 @@ python3 "$SCRIPT_DIR/telemetry.py" user_prompt $_TELEM_ARGS 2>/dev/null &
# No API key — emit detections only, skip search rubric
if [ -z "${MEM0_API_KEY:-}" ]; then
_PROMPT_CTX=""
if [ -n "$HAS_ERROR" ]; then
echo "**ERROR DETECTED in prompt.** Set MEM0_API_KEY to search past debugging context."
_PROMPT_CTX="Error detected in prompt. Set MEM0_API_KEY to search past debugging context."
fi
if [ -n "$FILE_PATHS" ]; then
echo "**FILE PATHS detected:** \`$FILE_PATHS\`"
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
fi
if [ -n "$_PROMPT_CTX" ]; then
jq -cn --arg ctx "$_PROMPT_CTX" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
fi
exit 0
fi
USER_ID="$MEM0_RESOLVED_USER_ID"
_PROMPT_CTX=""
if [ -n "$HAS_RESUME" ]; then
RESUME_RESULTS=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" python3 -c "
import os, sys
@@ -113,45 +124,40 @@ for m in all_r:
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.')
print('These memories provide context for resuming work.')
else:
print('No session state found in mem0. Ask the user what they want to continue.')
print('No session state found in mem0.')
" 2>/dev/null || echo "")
if [ -n "$RESUME_RESULTS" ]; then
echo ""
echo "$RESUME_RESULTS"
_PROMPT_CTX="${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
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Remember intent detected. The /mem0:remember skill auto-classifies, sets confidence=1.0, and stores verbatim."
fi
if [ -z "$RUBRIC_ALREADY_SHOWN" ]; then
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
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id."
touch "$RUBRIC_FLAG" 2>/dev/null || true
fi
if [ -n "$HAS_ERROR" ]; then
cat <<EOF
**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
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}Error detected in prompt. Prior occurrences are available in mem0 via anti_pattern and task_learning type filters."
fi
if [ -n "$FILE_PATHS" ]; then
cat <<EOF
_PROMPT_CTX="${_PROMPT_CTX:+${_PROMPT_CTX}\n}File paths detected: ${FILE_PATHS}"
fi
**FILE PATHS detected:** \`$FILE_PATHS\`
EOF
if [ -n "$_PROMPT_CTX" ]; then
jq -cn --arg ctx "$_PROMPT_CTX" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: $ctx
}
}'
fi
exit 0
@@ -150,6 +150,22 @@ def _print_categories(label: str, cats):
print()
def _categories_match(current: list | None, proposed: list) -> bool:
"""Compare categories by key sets, tolerating order differences and extra API fields."""
if not current:
return False
current_keys = {k for d in current if isinstance(d, dict) for k in d}
proposed_keys = {k for d in proposed if isinstance(d, dict) for k in d}
if current_keys != proposed_keys:
return False
current_map = {k: v for d in current if isinstance(d, dict) for k, v in d.items()}
proposed_map = {k: v for d in proposed if isinstance(d, dict) for k, v in d.items()}
return all(
current_map.get(k, "").strip() == v.strip()
for k, v in proposed_map.items()
)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument(
@@ -201,6 +217,10 @@ def main() -> int:
print("Dry-run only -- no changes made. Re-run with --apply to write.")
return 0
if _categories_match(current_cats, CODING_CATEGORIES):
print("Categories already match -- skipping update.")
return 0
print("Applying coding categories...")
try:
response = client.project.update(custom_categories=CODING_CATEGORIES)
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Decide whether the Stop hook should block, and build context for Claude.
Reads the transcript to determine if meaningful work happened.
Outputs JSON: {"should_block": bool, "context": "..."}.
Called by on_stop.sh with the hook input JSON on stdin.
"""
from __future__ import annotations
import json
import sys
MAX_TAIL_LINES = 500
MAX_USER_MESSAGES = 30
MAX_BASH_COMMANDS = 20
def tail_lines(filepath: str, n: int) -> list[str]:
try:
with open(filepath, "rb") as f:
f.seek(0, 2)
file_size = f.tell()
if file_size == 0:
return []
chunk_size = min(file_size, n * 4096)
f.seek(max(0, file_size - chunk_size))
data = f.read().decode("utf-8", errors="replace")
return data.splitlines()[-n:]
except OSError:
return []
def parse_transcript(lines: list[str]) -> dict:
user_messages: list[str] = []
files_modified: set[str] = set()
bash_commands: list[str] = []
tool_calls: int = 0
for line in lines:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
entry_type = entry.get("type")
if entry_type not in ("user", "assistant"):
continue
if entry.get("isSidechain"):
continue
message = entry.get("message", {})
content_blocks = message.get("content", [])
if entry_type == "user":
parts = []
if isinstance(content_blocks, str):
parts.append(content_blocks)
elif isinstance(content_blocks, list):
for block in content_blocks:
if isinstance(block, str):
parts.append(block)
elif isinstance(block, dict) and block.get("type") == "text":
parts.append(block.get("text", ""))
text = "\n".join(parts).strip()
if text and len(text) > 10 and not text.startswith("<"):
user_messages.append(text[:300])
elif entry_type == "assistant":
for block in content_blocks:
if not isinstance(block, dict):
continue
if block.get("type") == "tool_use":
tool_calls += 1
tool_name = block.get("name", "")
tool_input = block.get("input", {})
if tool_name in ("Write", "Edit"):
fp = tool_input.get("file_path", "")
if fp:
files_modified.add(fp)
elif tool_name == "Bash":
cmd = tool_input.get("command", "")
if cmd:
bash_commands.append(cmd[:200])
return {
"user_messages": user_messages[-MAX_USER_MESSAGES:],
"files_modified": sorted(files_modified),
"bash_commands": bash_commands[-MAX_BASH_COMMANDS:],
"tool_calls": tool_calls,
}
def should_block(state: dict) -> bool:
if state["files_modified"]:
return True
if state["tool_calls"] >= 3:
return True
git_commands = [c for c in state["bash_commands"] if "git " in c]
if git_commands:
return True
return False
def build_context(state: dict) -> str:
parts = []
if state["files_modified"]:
files = state["files_modified"][:10]
parts.append(f"Files modified this session: {', '.join(files)}")
git_commits = [c for c in state["bash_commands"] if "git commit" in c]
if git_commits:
parts.append(f"Git commits made: {len(git_commits)}")
if state["user_messages"]:
tasks = []
for msg in state["user_messages"][-5:]:
first_line = msg.split("\n")[0][:150]
tasks.append(f" - {first_line}")
parts.append("User requests:\n" + "\n".join(tasks))
return "\n".join(parts)
def main():
try:
hook_input = json.loads(sys.stdin.read())
except (json.JSONDecodeError, OSError):
json.dump({"should_block": False, "context": ""}, sys.stdout)
return
transcript_path = hook_input.get("transcript_path", "")
if not transcript_path:
json.dump({"should_block": False, "context": ""}, sys.stdout)
return
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
if not lines:
json.dump({"should_block": False, "context": ""}, sys.stdout)
return
state = parse_transcript(lines)
block = should_block(state)
context = build_context(state) if block else ""
json.dump({"should_block": block, "context": context}, sys.stdout)
if __name__ == "__main__":
try:
main()
except Exception:
json.dump({"should_block": False, "context": ""}, sys.stdout)
+7 -14
View File
@@ -22,7 +22,6 @@ import hashlib
import json
import os
import platform
import random
import sys
import urllib.error
import urllib.request
@@ -42,8 +41,7 @@ POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"
POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"
REQUEST_TIMEOUT = 2
# All events sampled at 10% to keep PostHog costs predictable.
SAMPLE_RATE = 0.1
SAMPLE_RATE = 1.0
def _sha256(value: str) -> str:
@@ -60,23 +58,21 @@ def _distinct_id() -> str:
def detect_platform() -> str:
if os.environ.get("PLUGIN_ROOT"):
return "codex"
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_PLUGIN_ROOT"):
return "claude-code"
if os.environ.get("CURSOR_PLUGIN_ROOT"):
return "cursor"
if os.environ.get("CODEX_PLUGIN_ROOT"):
return "codex"
return "unknown"
if os.environ.get("WINDSURF_PLUGIN_ROOT"):
return "windsurf"
return "plugin"
def is_enabled() -> bool:
return os.environ.get("MEM0_TELEMETRY", "true").lower() not in ("false", "0", "no", "off")
def _should_sample() -> bool:
return random.random() < SAMPLE_RATE
def build_posthog_payload(event_name: str, properties: dict | None = None) -> dict:
project_id = os.environ.get("MEM0_PROJECT_ID") or "unknown"
return {
@@ -115,10 +111,7 @@ def send(payload: dict) -> None:
def emit(event_type: str, properties: dict | None = None) -> None:
if not is_enabled():
return
event_name = f"plugin.{event_type}"
if not _should_sample():
return
send(build_posthog_payload(event_name, properties))
send(build_posthog_payload(f"plugin.{event_type}", properties))
def main() -> int:
+4 -6
View File
@@ -22,12 +22,10 @@ Pre-fetches relevant memories to prime context before working on a task.
| Query angle | Filter | Purpose |
|---|---|---|
| Feature/module name | `{"metadata": {"type": "decision"}}` | Architecture decisions |
| File paths mentioned | `{"metadata": {"type": "convention"}}` | Coding patterns |
| Error keywords (if any) | `{"metadata": {"type": "anti_pattern"}}` | Known pitfalls |
| Broad project context | no metadata filter | Catch-all |
All calls must include `user_id` and `app_id` filters.
| Feature/module name | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}` | Architecture decisions |
| File paths mentioned | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "convention"}}]}` | Coding patterns |
| Error keywords (if any) | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "anti_pattern"}}]}` | Known pitfalls |
| Broad project context | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}` | Catch-all |
3. **Deduplicate** results by memory ID across all search responses.
+17 -9
View File
@@ -10,7 +10,7 @@ identifies near-duplicates, flags contradictions, and prunes stale entries based
configured retention policies. All proposed changes are shown as a diff for user
approval before anything is modified.
---
**IMPORTANT: Execute steps strictly in order (1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead.**
## Step 1: Load Retention Policies
@@ -99,7 +99,7 @@ confidence.
---
## Step 4: Print Diff Report (item 15)
## Step 4: Print Diff Report
Print a structured diff to the terminal before making any changes. Use exactly
this format:
@@ -159,7 +159,7 @@ For each approved merge pair:
1. `delete_memory(<id1>)`
2. `delete_memory(<id2>)`
3. `add_memory` with:
- `messages=[{"role": "user", "content": "<merged content>"}]`
- `text="<merged content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>` (top-level, not in metadata)
- `metadata={"type": "<original type>", "branch": "<active_branch>", "confidence": <higher of the two original scores>, "source": "mem0-dream"}`
@@ -168,10 +168,7 @@ For each approved merge pair:
#### Contradictions (resolved)
For each resolved conflict where the user chose A or B:
- Identify the loser (the non-chosen memory).
- First call `get_memory(<loser_id>)` to read its current text content.
- Then call `update_memory(<loser_id>, text=<original_text_content>)` to preserve the text while updating it.
- **Important:** `update_memory` requires the `text` parameter. A metadata-only call may error or wipe the content. Always read first, then update with the original text.
- Delete the loser (the non-chosen memory): `delete_memory(memory_id=<loser_id>)`
Contradictions where the user chose `skip` are left untouched.
@@ -200,6 +197,14 @@ When invoked with `--auto` (e.g., `/mem0:dream --auto`), run non-interactively:
- **Prunes**: applied automatically (age/confidence-based, no ambiguity).
- **Contradictions**: skipped — they require human judgment.
### Concurrency guard
Before doing any work, check for a lock file at `/tmp/mem0_dream_auto.lock`:
- If the lock file exists and is less than 10 minutes old, print `[mem0-dream --auto] Another run in progress — skipping.` and stop.
- Otherwise, create the lock file (write the current timestamp). Delete it when done (in all exit paths).
### Execution
In auto mode:
1. Load policies and fetch memories (Steps 1–3) as normal.
2. Apply merges and prunes silently without printing the diff or prompting.
@@ -207,10 +212,13 @@ In auto mode:
```
[mem0-dream --auto] project=<id> merged=<N> pruned=<N> conflicts_skipped=<N>
```
4. If contradictions were detected but skipped, store a reminder memory:
4. If contradictions were detected but skipped, check if a `mem0-dream-auto` reminder already exists before storing one:
- Search for existing reminders: `search_memories(query="mem0-dream contradictions manual review", filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}, {"metadata": {"source": "mem0-dream-auto"}}]}, top_k=1)`
- If a result exists with similarity > 0.9, skip storing the reminder (one already exists).
- If no match, store the reminder:
```python
add_memory(
messages=[{"role": "user", "content": "mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively."}],
text="mem0-dream detected <N> contradiction(s) requiring manual review. Run /mem0:dream to resolve them interactively.",
user_id="<active_user_id>",
app_id="<active_project_id>",
metadata={"type": "task_learning", "source": "mem0-dream-auto", "branch": "<active_branch>"},
+1 -2
View File
@@ -18,8 +18,7 @@ Determine the active identity:
### Step 2: Fetch all memories
Call `get_memories` with:
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=200`
If the response is paginated (i.e. the result contains a `next` cursor or the count equals `page_size`), continue fetching pages until all memories are retrieved.
+30 -13
View File
@@ -14,26 +14,38 @@ Run ALL checks, then display a single summary. Do not stop on the first failure.
### Check 1: API key
```bash
echo "${MEM0_API_KEY:-${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-NOT_SET}}"
_KEY="${MEM0_API_KEY:-${CLAUDE_PLUGIN_OPTION_MEM0_API_KEY:-}}"
[ -n "$_KEY" ] && echo "${_KEY:0:6}..." || echo "NOT_SET"
```
- If `NOT_SET`: FAIL — "No API key configured"
- If set: PASS — show first 6 chars + `...` (never print the full key)
- If set: PASS — the command already prints only the first 6 chars
### Check 2: Identity resolution
Read the active identity from the SessionStart banner or resolve manually:
- `user_id`: from `MEM0_RESOLVED_USER_ID` or `$USER`
- `project_id`: from `MEM0_PROJECT_ID` or current directory name
- `branch`: from `MEM0_BRANCH` or `git rev-parse --abbrev-ref HEAD`
Resolve identity using the plugin's own resolver scripts to match what hooks use:
```bash
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts"
source "$SCRIPT_DIR/_identity.sh" 2>/dev/null
echo "user_id=${MEM0_RESOLVED_USER_ID:-}"
echo "project_id=${MEM0_PROJECT_ID:-}"
echo "branch=${MEM0_BRANCH:-}"
```
If `CLAUDE_PLUGIN_ROOT` is not available, fall back to:
- `user_id`: from `MEM0_USER_ID` or `$USER`
- `project_id`: from `MEM0_PROJECT_ID` or check `~/.mem0/project_map.json` for `$PWD`
- `branch`: from `git branch --show-current`
PASS if all three are non-empty. WARN if any falls back to defaults.
### Check 3: MCP server connectivity
Call `search_memories` with:
- `query="health check"`, `user_id=<id>`, `limit=1`
- `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<project_id>"}]}`
- `query="health check"`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `top_k=1`
- If returns successfully (even empty): PASS
- If errors: FAIL — show the error message
@@ -41,12 +53,17 @@ Call `search_memories` with:
### Check 4: Memory write capability
Call `add_memory` with:
- `messages=[{"role": "user", "content": "Health check probe — safe to delete."}]`
- `user_id=<id>`, `app_id=<project_id>`
- `text="Health check probe — safe to delete."`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "health_check", "probe": true}`
- `infer=False`
If it returns a memory ID: PASS — then immediately call `delete_memory` with that ID to clean up.
If it errors: FAIL — show the error.
The response returns `event_id` (v3 writes are async). Call `get_event_status(event_id=<event_id>)` to check processing.
- If status is `SUCCEEDED`: PASS — extract the memory ID from the event result, then call `delete_memory` with that ID to clean up.
- If status is `PENDING` after 5 seconds: PASS (write accepted, processing delayed)
- If errors: FAIL — show the error.
### Check 5: Session stats tracker
@@ -85,7 +102,7 @@ When invoked with `--deep` (e.g., `/mem0:health --deep`), run the standard 5 che
### Quality Check 1: Duplicates
Call `get_memories` with `user_id`, `app_id`, `page_size=200`. Compare all pairs within the same `metadata.type` group for high textual overlap (shared nouns/keywords > 60%). Report:
Call `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Compare all pairs within the same `metadata.type` group for high textual overlap (shared nouns/keywords > 60%). Report:
```
Potential duplicates: <N> pairs
+2 -2
View File
@@ -63,7 +63,7 @@ Determine the active identity:
For each record in the parsed JSON array, call `add_memory` with:
- `messages=[{"role": "user", "content": "<record.content>"}]`
- `text="<record.content>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={`
@@ -156,7 +156,7 @@ detects native auto-memory and the user chooses to import:
2. Split by non-empty lines. Each line becomes one memory.
3. Skip lines shorter than 20 characters or lines that are just headers (`#`).
4. For each line, call `add_memory` with:
- `messages=[{"role": "user", "content": "<line>"}]`
- `text="<line>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "task_learning", "source": "memory-md-import", "confidence": 0.8}`
+1 -1
View File
@@ -16,7 +16,7 @@ Audits memory quality for the active project. Finds duplicates, contradictions,
## Steps
1. **Fetch all memories** for active project via `get_memories` with `user_id` and `app_id`. Paginate if needed — cap at 200 memories.
1. **Fetch all memories** for active project via `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `page_size=200`. Paginate if needed — cap at 200 memories.
2. **Group by `metadata.type`**. Common types: `decision`, `convention`, `anti_pattern`, `task_learning`, `project_profile`, `user_preference`, `session_state`.
+65 -28
View File
@@ -7,6 +7,8 @@ description: Sets up mem0 for a new project including API key configuration, MCP
Run this wizard to set up the mem0 plugin for the current project. Complete in ~60 seconds.
**IMPORTANT: Execute steps strictly in order (0 → 1 → 2 → 3 → 4 → 5 → 6). Each step depends on the previous one. Do NOT run steps in parallel or skip ahead. Complete one step fully before starting the next.**
## Step 0: Ensure mem0ai SDK is installed
The plugin installs the `mem0ai` Python SDK automatically on session start via a venv in `${CLAUDE_PLUGIN_DATA}/venv`. If Step 5 (categories) fails with an import error, run:
@@ -22,10 +24,12 @@ This is silent and idempotent — safe to run anytime.
Check if `MEM0_API_KEY` is already set by running:
```bash
echo $MEM0_API_KEY
[ -n "$MEM0_API_KEY" ] && echo "SET" || echo "NOT_SET"
```
### If API key IS set (non-empty output)
IMPORTANT: Never run `echo $MEM0_API_KEY` — that prints the secret in plaintext to the conversation log.
### If API key IS set (output is "SET")
Print: `- API key found.` and proceed to Step 2.
@@ -46,15 +50,18 @@ Step 1: Setting up API key.
source ~/.zshrc
3. Verify:
echo $MEM0_API_KEY
# Should print your key
[ -n "$MEM0_API_KEY" ] && echo "SET" || echo "NOT_SET"
```
After the user confirms they've set the key, verify it by running `echo $MEM0_API_KEY`. If still empty, repeat the instructions. If set, proceed to Step 2.
After the user confirms they've set the key, verify it by running `[ -n "$MEM0_API_KEY" ] && echo "SET" || echo "NOT_SET"`. If NOT_SET, repeat the instructions. If SET, proceed to Step 2.
## Step 2: MCP OAuth login
## Step 2: MCP server connection
Now authenticate the MCP server connection. Tell the user:
First, check if MCP tools are already available using ToolSearch with query `"mem0 search_memories"`. The exact tool name varies by install method (may be `mcp__mem0__search_memories` or `mcp__plugin_mem0_mem0__search_memories`).
**If MCP tools ARE found:** Print `- MCP already connected.` and proceed to Step 3.
**If MCP tools are NOT found:** Guide the user through OAuth:
```
Step 2: MCP OAuth login.
@@ -65,7 +72,7 @@ Step 2: MCP OAuth login.
4. Return here after authenticating in your browser
```
After the user completes OAuth, verify MCP tools are available using ToolSearch with query `"mem0 search_memories"`. The exact tool name varies by install method (may be `mcp__mem0__search_memories` or `mcp__plugin_mem0_mem0__search_memories`).
After the user completes OAuth, verify MCP tools again using ToolSearch.
- If MCP tools found: Print `- MCP connected.` and proceed to Step 3.
- If NOT found: Troubleshoot before giving up:
@@ -77,7 +84,7 @@ After the user completes OAuth, verify MCP tools are available using ToolSearch
## Step 3: Verify connectivity and show identity
Call `search_memories` with `query="project setup"`, `user_id=<active_user_id>`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `limit=1` to verify connectivity.
Call `search_memories` with `query="project setup"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1` to verify connectivity.
Print:
```
@@ -89,29 +96,54 @@ Print:
If the search fails, troubleshoot the API key and MCP connection.
## Step 4: Detect and import project files
## Step 4: Import project files
Check for these files in the project root:
1. `CLAUDE.md`
2. `AGENTS.md`
3. `.cursorrules`
4. `.windsurfrules`
5. `mem0.md`
Project files (CLAUDE.md, AGENTS.md, etc.) are automatically imported into mem0 when a session starts. This step verifies import status and triggers a re-import if needed.
For each file found, ask the user: "Found `<filename>` (<size> bytes). Import into mem0? [Y/n]"
### 4a: Detect project files
If user says yes (or default):
- Read the file content
- Call `add_memory` with:
- `messages=[{"role": "user", "content": "## Project Profile: <filename>\n\nProject: <project_id>\n\n<file_content>"}]`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "project_profile", "file": "<filename>", "source": "onboard", "branch": "<active_branch>"}`
- `infer=False`
- The response contains `event_id` (writes are async). Do not block on each — continue importing. The summary reflects files submitted, not confirmed processed.
```bash
for f in CLAUDE.md AGENTS.md .cursorrules .windsurfrules mem0.md; do
[ -f "$f" ] && echo "FOUND: $f ($(wc -c < "$f") bytes)"
done || true
```
If no files found, print `- No project files found. Skipping import.` and proceed to Step 5.
### 4b: Check and import
Run auto_import in foreground to check status and import if needed:
```bash
MEM0_DEBUG=1 MEM0_CWD="$PWD" python3 "${CLAUDE_PLUGIN_ROOT}/scripts/auto_import.py"
```
### 4c: Report to user
Parse the auto_import output and print a user-friendly summary:
- If output contains `Imported` lines:
```
- Importing project files into mem0... done.
<N> file(s) imported (<M> chunks). These are stored verbatim for future context.
```
- If output contains only `skipping` lines:
```
- Project files already in mem0 (imported during session start). Verified server-side.
```
- If output contains `re-importing`:
```
- Project files were missing from mem0. Re-imported successfully.
```
- If output contains errors or no files were processed:
```
- Project file import failed. Check API key and retry with: /mem0:onboard
```
## Step 5: Install coding categories
The setup script is idempotent — it compares existing categories against the proposed set and skips the API call if they already match (tolerates order differences and extra API fields). Safe to re-run.
Ask: "Install coding categories optimized for development workflows? [Y/n]"
If yes, run the setup script using the plugin's venv python:
@@ -125,6 +157,11 @@ else
fi
```
Parse the output:
- `"Categories already match -- skipping update."` → Print: `- Coding categories already installed. Skipped.`
- `"Done."` → Print: `- Coding categories installed (<N> categories).`
- Error → Print the error and suggest re-running `/mem0:onboard`.
If the script fails with "mem0ai SDK not found", run the dependency installer first:
```bash
"${CLAUDE_PLUGIN_ROOT}/scripts/ensure_deps.sh"
@@ -138,8 +175,8 @@ Print a summary:
- Onboarding complete.
user_id: <user_id>
project_id: <project_id> (app_id)
imported: <N> files
categories: <installed or skipped>
files: <N> found, <M> imported
categories: <N installed | already installed | skipped by user>
Memory is now active for this project. Start working — mem0 will
automatically search relevant context and capture learnings.
+14 -13
View File
@@ -29,20 +29,21 @@ Call `get_memory` with the selected memory ID. Store:
### Step 3: Pin it
Call `update_memory` with:
- `memory_id=<selected_id>`
- `text=<original_text>` (preserve existing content)
- `metadata=` merge `original_metadata` with `{"pinned": true}`
The MCP `update_memory` tool only accepts `memory_id`, `text`, and `source` — it
does not accept a `metadata` parameter. To pin, append a pin marker to the text:
```python
updated_meta = {**original_metadata, "pinned": True}
update_memory(memory_id=<selected_id>, text=<original_text>, metadata=updated_meta)
pinned_text = "[PINNED] " + original_text if not original_text.startswith("[PINNED]") else original_text
update_memory(memory_id=<selected_id>, text=pinned_text)
```
**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}`
1. Call `add_memory` with:
- `text="[PINNED] <the user's text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"pinned": true, "type": "decision", "confidence": 1.0}`
- `infer=False`
2. The response contains `event_id`. Call `get_event_status(event_id=<event_id>)` once to retrieve the memory ID, then confirm.
### Step 4: Confirm
@@ -57,10 +58,10 @@ Append `...` only if content exceeds 80 characters.
### Unpin
If the user says "unpin":
1. Call `get_memory` to read current content and metadata.
2. Set `metadata.pinned = false`:
1. Call `get_memory` to read current content.
2. Remove the pin marker from the text:
```python
updated_meta = {**original_metadata, "pinned": False}
update_memory(memory_id=<id>, text=<original_text>, metadata=updated_meta)
unpinned_text = original_text.removeprefix("[PINNED] ")
update_memory(memory_id=<id>, text=unpinned_text)
```
3. Print: `Unpinned: "<content>..."`
+1 -1
View File
@@ -32,7 +32,7 @@ Based on the content, pick the best `metadata.type`:
### Step 3: Store
Call `add_memory` with:
- `messages=[{"role": "user", "content": "<the user's text>"}]`
- `text="<the user's text>"`
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `metadata={"type": "<classified_type>", "branch": "<active_branch>", "confidence": 1.0, "source": "remember_command"}`
+6 -7
View File
@@ -26,8 +26,7 @@ If the script returns empty or errors, note "No session data available" and cont
**Lifetime stats:**
Call `get_memories` with:
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=100`
Count the returned memories. Group them by:
@@ -38,14 +37,14 @@ Count the returned memories. Group them by:
**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>"}]}`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}, {"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).
Also run a `search_memories` MCP tool call with `query="project"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`, `top_k=1` to measure round-trip latency. Note the time before and after the MCP call — do NOT attempt raw HTTP calls to the API.
### Step 3: Display
@@ -88,9 +87,9 @@ activity digest after the standard stats dashboard:
### W1: Fetch recent memories
Call `search_memories` in parallel with time-scoped queries:
1. `query="decisions made this week"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `limit=20`
2. `query="bugs errors fixes"`, same time filter, `limit=20`
3. `query="patterns conventions learnings"`, same time filter, `limit=20`
1. `query="decisions made this week"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"created_at": {"gte": "<7 days ago YYYY-MM-DD>"}}]}`, `top_k=20`
2. `query="bugs errors fixes"`, same time filter, `top_k=20`
3. `query="patterns conventions learnings"`, same time filter, `top_k=20`
### W2: Analyze
+1 -1
View File
@@ -36,7 +36,7 @@ The user provides a project name as an argument: `/mem0:switch-project <project-
(Replace `<PROJECT_NAME>` with the user's chosen project name.)
3. Verify by searching for existing memories:
- Call `search_memories` with `query="project"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<PROJECT_NAME>"}]}`, `limit=1`
- Call `search_memories` with `query="project"`, `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<PROJECT_NAME>"}]}`, `top_k=1`
4. Print:
```
+9 -14
View File
@@ -12,9 +12,9 @@ Show the user what mem0 has stored for the current project.
When invoked with `--all-projects` (e.g., `/mem0:tour --all-projects` or
`/mem0:tour --all-projects auth middleware`), search across ALL projects:
1. Call `get_memories` with `user_id=<active_user_id>`, `page_size=200` — **no `app_id` filter**.
1. Call `get_memories` with `filters={"AND": [{"user_id": "<active_user_id>"}]}`, `page_size=200` — **no `app_id` filter**.
2. If a search query was also provided, run `search_memories` with `query=<query>`,
`filters={"AND": [{"user_id": "<id>"}]}`, `limit=20` — again no `app_id`.
`filters={"AND": [{"user_id": "<active_user_id>"}]}`, `top_k=20` — again no `app_id`.
3. Group results by `app_id` first, then by category within each project.
4. Display:
```
@@ -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`, `rerank=true`
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `limit=5`, `rerank=true`
- Broad: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- Targeted: `query=<query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
2. Deduplicate by ID, display compact results:
```
## mem0 search: "<query>" (<N> results)
@@ -57,23 +57,18 @@ If no query argument and no `--all-projects` flag, use the full tour flow below.
### Step 1: Fetch ALL memories for this project
Call `get_memories` with:
- `user_id=<active_user_id>`
- `app_id=<active_project_id>`
- `filters={"AND": [{"user_id": "<active_user_id>"}, {"app_id": "<active_project_id>"}]}`
- `page_size=100`
This returns every memory scoped to the project — no semantic filtering, no missed results.
If `get_memories` doesn't support `app_id` as a direct parameter, use:
- `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`
Pass `page_size=100` (or the maximum allowed) to get a full picture.
### Step 2: Run supplementary semantic searches
In parallel, run these `search_memories` calls to get relevance-ranked results for key topics:
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `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`
- `query="architecture decisions design choices"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- `query="bugs errors failures anti-patterns"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
- `query="project setup tooling conventions preferences"`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
**Do NOT filter by `metadata.type` in these calls.** The platform auto-assigns `categories` — filtering on `metadata.type` misses memories that were auto-categorized but don't have an explicit `metadata.type`.
+1 -15
View File
@@ -147,24 +147,10 @@ def test_platform_codex(monkeypatch):
monkeypatch.delenv("CLAUDECODE", raising=False)
monkeypatch.delenv("CLAUDE_PLUGIN_ROOT", raising=False)
monkeypatch.delenv("CURSOR_PLUGIN_ROOT", raising=False)
monkeypatch.setenv("CODEX_PLUGIN_ROOT", "/path")
monkeypatch.setenv("PLUGIN_ROOT", "/path")
assert telemetry.detect_platform() == "codex"
def test_sampling_drops_at_high_random(monkeypatch):
import telemetry
monkeypatch.setattr(telemetry.random, "random", lambda: 0.5)
assert telemetry._should_sample() is False
def test_sampling_sends_at_low_random(monkeypatch):
import telemetry
monkeypatch.setattr(telemetry.random, "random", lambda: 0.05)
assert telemetry._should_sample() is True
def test_send_fails_silently(monkeypatch):
import telemetry