feat(mem0-plugin): v0.2.5 — fix identity scoping, skill param bugs, add checklists (#5247)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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 &
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &
|
||||
|
||||
@@ -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)
|
||||
|
||||
Executable
+158
@@ -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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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>"},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>..."`
|
||||
|
||||
@@ -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"}`
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
```
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user