feat(mem0-plugin): v0.2.4 — fix stats, session scoping, reduce noise, improve skill discovery (#5244)
This commit is contained in:
@@ -30,6 +30,12 @@ _mem0_resolve_identity() {
|
||||
MEM0_RESOLVED_USER_ID="$(_mem0_resolve_identity)"
|
||||
export MEM0_RESOLVED_USER_ID
|
||||
|
||||
_MEM0_IDENTITY_ANNOTATION=""
|
||||
if [ -n "${MEM0_USER_ID:-}" ] && [ "$MEM0_USER_ID" != "${USER:-default}" ]; then
|
||||
_MEM0_IDENTITY_ANNOTATION=" (override; default: ${USER:-default})"
|
||||
fi
|
||||
export _MEM0_IDENTITY_ANNOTATION
|
||||
|
||||
# Load settings from ~/.mem0/settings.json
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
_SETTINGS_JSON=$(PYTHONPATH="$_SCRIPT_DIR" python3 -c "from load_settings import load_settings; import json; print(json.dumps(load_settings()))" 2>/dev/null || echo "{}")
|
||||
|
||||
@@ -53,6 +53,12 @@ _mem0_resolve_project_id() {
|
||||
_mem0_slug="${_mem0_slug//:/-}"
|
||||
if [ -n "$_mem0_slug" ]; then
|
||||
printf '%s' "$_mem0_slug"
|
||||
_MEM0_PERSIST_CWD="$PWD" _MEM0_PERSIST_SLUG="$_mem0_slug" python3 -c "
|
||||
import os, sys
|
||||
sys.path.insert(0, '$(dirname "${BASH_SOURCE[0]:-$0}")')
|
||||
from _project import save_project_mapping
|
||||
save_project_mapping(os.environ['_MEM0_PERSIST_CWD'], os.environ['_MEM0_PERSIST_SLUG'])
|
||||
" 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -19,7 +19,11 @@ def search_memories(
|
||||
project_id: str,
|
||||
query: str,
|
||||
metadata_type: str | None = None,
|
||||
metadata_filters: dict | None = None,
|
||||
top_k: int = 3,
|
||||
min_score: float = 0.0,
|
||||
rerank: bool = False,
|
||||
threshold: float = 0.3,
|
||||
) -> list[dict]:
|
||||
if not api_key:
|
||||
return []
|
||||
@@ -27,8 +31,14 @@ def search_memories(
|
||||
filters: dict = {"AND": [{"user_id": user_id}, {"app_id": project_id}]}
|
||||
if metadata_type:
|
||||
filters["AND"].append({"metadata": {"type": metadata_type}})
|
||||
if metadata_filters:
|
||||
for key, value in metadata_filters.items():
|
||||
filters["AND"].append({"metadata": {key: value}})
|
||||
|
||||
body = json.dumps({"query": query, "filters": filters, "top_k": top_k}).encode()
|
||||
payload: dict = {"query": query, "filters": filters, "top_k": top_k, "threshold": threshold}
|
||||
if rerank:
|
||||
payload["rerank"] = True
|
||||
body = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
SEARCH_URL,
|
||||
data=body,
|
||||
@@ -38,9 +48,10 @@ def search_memories(
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=SEARCH_TIMEOUT) as r:
|
||||
data = json.loads(r.read())
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return data.get("results", [])
|
||||
results = data if isinstance(data, list) else data.get("results", [])
|
||||
if min_score > 0:
|
||||
results = [m for m in results if m.get("score", 0) >= min_score]
|
||||
return results
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import urllib.request
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from _chunking import filter_and_truncate, split_by_headers
|
||||
from _identity import resolve_api_key, resolve_user_id
|
||||
from _project import resolve_branch, resolve_project_id
|
||||
from _project import resolve_branch, resolve_project_id, save_project_mapping
|
||||
|
||||
log = logging.getLogger("mem0-auto-import")
|
||||
log.setLevel(logging.DEBUG)
|
||||
@@ -93,6 +93,38 @@ def save_hashes(hashes: dict[str, str]) -> None:
|
||||
log.warning("Could not save hash store: %s", e)
|
||||
|
||||
|
||||
def already_imported(api_key: str, user_id: str, project_id: str, filename: str) -> bool:
|
||||
body = json.dumps({
|
||||
"query": filename,
|
||||
"filters": {
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"app_id": project_id},
|
||||
{"metadata": {"source": "auto-import"}},
|
||||
]
|
||||
},
|
||||
"top_k": 3,
|
||||
"threshold": 0.0,
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v3/memories/search/",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json", "Authorization": f"Token {api_key}"},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as r:
|
||||
data = json.loads(r.read())
|
||||
results = data if isinstance(data, list) else data.get("results", [])
|
||||
for result in results:
|
||||
meta = result.get("metadata", {}) if isinstance(result, dict) else {}
|
||||
if filename in meta.get("file", ""):
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def post_memory(api_key: str, content: str, user_id: str, filename: str, project_id: str, branch: str = "") -> bool:
|
||||
"""POST a project profile memory to the Mem0 REST API."""
|
||||
metadata = {
|
||||
@@ -149,6 +181,8 @@ def main() -> None:
|
||||
project_id = resolve_project_id(cwd)
|
||||
branch = resolve_branch(cwd)
|
||||
|
||||
save_project_mapping(cwd, project_id)
|
||||
|
||||
git_root = _git_root(cwd)
|
||||
search_dirs = [cwd]
|
||||
if git_root and os.path.realpath(git_root) != os.path.realpath(cwd):
|
||||
@@ -172,6 +206,8 @@ def main() -> None:
|
||||
log.debug("Not found, skipping: %s", filename)
|
||||
continue
|
||||
|
||||
filepath = os.path.realpath(filepath)
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(filepath)
|
||||
except OSError:
|
||||
@@ -198,6 +234,12 @@ def main() -> None:
|
||||
log.debug("Unchanged, skipping: %s", filename)
|
||||
continue
|
||||
|
||||
if already_imported(api_key, user_id, project_id, filename):
|
||||
log.debug("Already in mem0, updating hash store: %s", filename)
|
||||
hashes[hash_key] = current_hash
|
||||
updated = True
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(filepath, encoding="utf-8", errors="replace") as f:
|
||||
content = f.read()
|
||||
|
||||
@@ -26,7 +26,7 @@ if [ -z "$FILE_PATH" ]; then
|
||||
fi
|
||||
|
||||
case "$FILE_PATH" in
|
||||
*/MEMORY.md|*/.claude/memory/*)
|
||||
*/.claude/*/MEMORY.md|*/.claude/memory/*)
|
||||
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
|
||||
exit 2
|
||||
;;
|
||||
|
||||
@@ -21,43 +21,56 @@ esac
|
||||
|
||||
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // "{}"' 2>/dev/null)
|
||||
|
||||
PATCHED=$(python3 -c "
|
||||
import json, sys
|
||||
_PATCH_OUT="/tmp/mem0_enforce_$$"
|
||||
_MEM0_TOOL_INPUT="$TOOL_INPUT" python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
|
||||
import json, os, sys
|
||||
|
||||
raw = sys.stdin.read()
|
||||
raw = os.environ.get("_MEM0_TOOL_INPUT", "{}")
|
||||
try:
|
||||
inp = json.loads(raw)
|
||||
except Exception:
|
||||
sys.exit(0)
|
||||
|
||||
meta = inp.get('metadata') or {}
|
||||
meta = inp.get("metadata") or {}
|
||||
changed = False
|
||||
|
||||
if 'confidence' not in meta:
|
||||
meta['confidence'] = 0.7
|
||||
if "confidence" not in meta:
|
||||
meta["confidence"] = 0.7
|
||||
changed = True
|
||||
if 'files' not in meta:
|
||||
meta['files'] = ['*']
|
||||
if "files" not in meta:
|
||||
meta["files"] = ["*"]
|
||||
changed = True
|
||||
if 'source' not in meta:
|
||||
meta['source'] = 'auto_capture'
|
||||
if "source" not in meta:
|
||||
meta["source"] = "auto_capture"
|
||||
changed = True
|
||||
if 'type' not in meta:
|
||||
meta['type'] = 'task_learning'
|
||||
if "type" not in meta:
|
||||
meta["type"] = "task_learning"
|
||||
changed = True
|
||||
|
||||
# If confidence is 1.0 (user explicitly stated), ensure infer=False
|
||||
if meta.get('confidence', 0) >= 1.0 and 'infer' not in inp:
|
||||
inp['infer'] = False
|
||||
if meta.get("confidence", 0) >= 1.0 and "infer" not in inp:
|
||||
inp["infer"] = False
|
||||
changed = True
|
||||
|
||||
if "run_id" not in inp:
|
||||
session_file = "/tmp/mem0_session_id_" + os.environ.get("USER", "default")
|
||||
if os.path.isfile(session_file):
|
||||
try:
|
||||
with open(session_file) as f:
|
||||
sid = f.read().strip()
|
||||
if sid:
|
||||
inp["run_id"] = sid
|
||||
changed = True
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if changed:
|
||||
inp['metadata'] = meta
|
||||
inp["metadata"] = meta
|
||||
print(json.dumps(inp))
|
||||
" <<< "$TOOL_INPUT" 2>/dev/null || true)
|
||||
PYEOF
|
||||
PATCHED=$(cat "$_PATCH_OUT" 2>/dev/null)
|
||||
rm -f "$_PATCH_OUT"
|
||||
|
||||
if [ -n "$PATCHED" ]; then
|
||||
# Use hookSpecificOutput.updatedInput to actually modify the tool call
|
||||
if [ -n "$PATCHED" ] && echo "$PATCHED" | jq empty 2>/dev/null; then
|
||||
jq -n --argjson updated "$PATCHED" '{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
|
||||
@@ -31,7 +31,7 @@ fi
|
||||
|
||||
# Skip repeated reads: track last 10 files in a temp file.
|
||||
# Use full FILE_PATH (not basename) to avoid false dedup.
|
||||
RECENT_FILE="/tmp/mem0_recent_reads_${USER}"
|
||||
RECENT_FILE="/tmp/mem0_recent_reads_${USER}_${MEM0_PROJECT_ID:-unknown}"
|
||||
if [ -f "$RECENT_FILE" ] && grep -qxF "$FILE_PATH" "$RECENT_FILE" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -42,8 +42,13 @@ USER_ID="${MEM0_RESOLVED_USER_ID:-${USER:-default}}"
|
||||
PROJECT_ID="${MEM0_PROJECT_ID:-unknown}"
|
||||
BASENAME=$(basename "$FILE_PATH")
|
||||
|
||||
# SECURITY: pass data via env vars, never interpolate into python3 -c
|
||||
CONTEXT=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" MEM0_SEARCH_PROJECT="$PROJECT_ID" MEM0_SEARCH_QUERY="$BASENAME" python3 -c "
|
||||
CWD="${MEM0_CWD:-$(pwd)}"
|
||||
REL_PATH="${FILE_PATH#$CWD/}"
|
||||
if [ "$REL_PATH" = "$FILE_PATH" ]; then
|
||||
REL_PATH="$BASENAME"
|
||||
fi
|
||||
|
||||
CONTEXT=$(PYTHONPATH="$SCRIPT_DIR" MEM0_SEARCH_USER="$USER_ID" MEM0_SEARCH_PROJECT="$PROJECT_ID" MEM0_SEARCH_QUERY="$BASENAME" MEM0_SEARCH_RELPATH="$REL_PATH" python3 -c "
|
||||
import os, sys
|
||||
sys.path.insert(0, os.environ.get('PYTHONPATH', '.'))
|
||||
from _search import search_memories, format_results_for_context
|
||||
@@ -52,8 +57,18 @@ api_key = os.environ.get('MEM0_API_KEY', '')
|
||||
user_id = os.environ.get('MEM0_SEARCH_USER', 'default')
|
||||
project_id = os.environ.get('MEM0_SEARCH_PROJECT', 'unknown')
|
||||
filename = os.environ.get('MEM0_SEARCH_QUERY', '')
|
||||
relpath = os.environ.get('MEM0_SEARCH_RELPATH', '')
|
||||
|
||||
results = search_memories(api_key, user_id, project_id, filename, top_k=3)
|
||||
results = search_memories(
|
||||
api_key, user_id, project_id, filename,
|
||||
metadata_filters={'files': {'contains': relpath}} if relpath else None,
|
||||
top_k=3, min_score=0.4,
|
||||
)
|
||||
if not results and relpath:
|
||||
results = search_memories(
|
||||
api_key, user_id, project_id, filename,
|
||||
top_k=3, min_score=0.4,
|
||||
)
|
||||
if results:
|
||||
print(format_results_for_context(results, heading=f'mem0 context for {filename}'))
|
||||
" 2>/dev/null || true)
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: SessionEnd
|
||||
#
|
||||
# Fires when session actually terminates (after Stop).
|
||||
# Last-chance capture: if on_stop.sh background REST call didn't complete,
|
||||
# this fires a synchronous capture attempt.
|
||||
#
|
||||
# Input: JSON on stdin with session_id, transcript_path, cwd, reason
|
||||
# Output: ignored (session is ending)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
@@ -18,25 +9,20 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
REASON=$(echo "$INPUT" | jq -r '.reason // "other"' 2>/dev/null || echo "other")
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Print session-end report (last chance — Stop hook output may not render on /exit)
|
||||
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
|
||||
if [ -n "$REPORT" ] && [ "$REPORT" != "Session: no memory operations." ]; then
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "mem0 $REPORT"
|
||||
echo "---"
|
||||
|
||||
# Append to persistent session log
|
||||
mkdir -p "$HOME/.mem0" 2>/dev/null || true
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
|
||||
_LOG_FILE="$HOME/.mem0/session-log.md"
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$_LOG_FILE" 2>/dev/null || true
|
||||
_LINE_COUNT=$(wc -l < "$_LOG_FILE" 2>/dev/null || echo 0)
|
||||
if [ "$_LINE_COUNT" -gt 500 ]; then
|
||||
tail -500 "$_LOG_FILE" > "${_LOG_FILE}.tmp" 2>/dev/null && mv "${_LOG_FILE}.tmp" "$_LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Telemetry (fire-and-forget — session dying, best-effort)
|
||||
python3 "$SCRIPT_DIR/telemetry.py" session_end --reason="$REASON" 2>/dev/null &
|
||||
|
||||
# Clean up old capture markers (> 7 days)
|
||||
find "$HOME/.mem0" -name ".captured_*" -mtime +7 -delete 2>/dev/null || true
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: SessionStart (matcher: startup|resume|compact)
|
||||
#
|
||||
# Bootstraps mem0 context at the start of every session.
|
||||
# Output becomes part of Claude's context so it calls mem0 MCP tools.
|
||||
#
|
||||
# Input: JSON on stdin with session_id, source, transcript_path, model, cwd
|
||||
# Output: Text injected into Claude's context (exit 0)
|
||||
|
||||
# Intentionally omit -e so the script always outputs a bootstrap prompt
|
||||
# even if jq is missing or stdin is malformed.
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
@@ -16,89 +6,52 @@ if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
# shellcheck source=_identity.sh
|
||||
. "$SCRIPT_DIR/_identity.sh"
|
||||
|
||||
# Initialize session stats tracker and settings file
|
||||
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
|
||||
PYTHONPATH="$SCRIPT_DIR" python3 "$SCRIPT_DIR/load_settings.py" init 2>/dev/null || true
|
||||
rm -f "/tmp/mem0_rubric_injected_${USER}" 2>/dev/null || true
|
||||
rm -f /tmp/mem0_rubric_* 2>/dev/null || true
|
||||
rm -f "/tmp/mem0_recent_reads_${USER}" 2>/dev/null || true
|
||||
|
||||
INPUT=$(cat)
|
||||
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
|
||||
|
||||
MEM0_AUTH_MODE="api_key"
|
||||
if [ "$SOURCE" = "startup" ]; then
|
||||
python3 "$SCRIPT_DIR/session_stats.py" init 2>/dev/null || true
|
||||
rm -f /tmp/mem0_recent_reads_${USER}_* 2>/dev/null || true
|
||||
fi
|
||||
PYTHONPATH="$SCRIPT_DIR" python3 "$SCRIPT_DIR/load_settings.py" init 2>/dev/null || true
|
||||
rm -f "/tmp/mem0_rubric_injected_${USER}" 2>/dev/null || true
|
||||
rm -f /tmp/mem0_rubric_* 2>/dev/null || true
|
||||
MEM0_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
if [ -z "$MEM0_SESSION_ID" ]; then
|
||||
MEM0_SESSION_ID="ses_$(date +%s)_$$"
|
||||
fi
|
||||
printf '%s' "$MEM0_SESSION_ID" > "/tmp/mem0_session_id_${USER}"
|
||||
export MEM0_SESSION_ID
|
||||
|
||||
if [ -z "${MEM0_API_KEY:-}" ]; then
|
||||
_SAFE_PID=$(printf '%s' "${MEM0_PROJECT_ID:-unknown}" | tr '/:' '--')
|
||||
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}"
|
||||
|
||||
if [ ! -f "$_ONBOARD_MARKER" ] && [ "${SOURCE:-startup}" = "startup" ]; then
|
||||
# State 1: First run — trigger onboarding
|
||||
cat <<BANNER
|
||||
## Mem0 — First Run Setup
|
||||
cat <<BANNER
|
||||
## Mem0 — Setup Required
|
||||
|
||||
\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none\`
|
||||
|
||||
IMPORTANT: In your FIRST response to the user, you MUST display the identity banner exactly as shown below (copy-paste it as your opening line before any other output):
|
||||
|
||||
\`\`\`
|
||||
Mem0 Active | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
|
||||
Mem0 — Setup Required | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
|
||||
\`\`\`
|
||||
|
||||
MEM0_API_KEY is not set. You MUST run the \`mem0:onboard\` skill now using the Skill tool to set up the API key and configure mem0 for this project. Do not ask — just invoke it.
|
||||
BANNER
|
||||
cat <<'EOF'
|
||||
## Mem0 First Run — Automatic Onboarding
|
||||
|
||||
This is the first time mem0 is running for this project. You MUST run the onboarding wizard NOW before doing anything else.
|
||||
|
||||
Execute the `/mem0:onboard` skill immediately. This will:
|
||||
1. Set up API key (get from dashboard, export to .zshrc, verify)
|
||||
2. MCP OAuth login (authenticate via browser at mcp.mem0.ai)
|
||||
3. Verify connectivity and show identity
|
||||
4. Detect and import project files (CLAUDE.md, AGENTS.md, .cursorrules, etc.)
|
||||
5. Install coding categories optimized for development
|
||||
6. Show a summary of what was set up
|
||||
|
||||
Do NOT skip this. Do NOT ask the user if they want to onboard. Just run `/mem0:onboard` now.
|
||||
EOF
|
||||
exit 0
|
||||
elif [ -f "$_ONBOARD_MARKER" ]; then
|
||||
# State 2: OAuth-authed — onboarded previously without API key
|
||||
MEM0_AUTH_MODE="oauth"
|
||||
else
|
||||
# State 3: Not first startup, not onboarded — inactive
|
||||
cat <<BANNER
|
||||
## Mem0 Inactive
|
||||
|
||||
\`user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none\`
|
||||
|
||||
IMPORTANT: In your FIRST response to the user, you MUST display the identity banner exactly as shown below (copy-paste it as your opening line before any other output):
|
||||
|
||||
\`\`\`
|
||||
Mem0 Inactive | user=${MEM0_RESOLVED_USER_ID:-$USER} | project=${MEM0_PROJECT_ID:-unknown} | branch=${MEM0_BRANCH:-unknown} | auth=none
|
||||
\`\`\`
|
||||
|
||||
Set MEM0_API_KEY to enable persistent memory. Get a key at https://app.mem0.ai/dashboard/api-keys or run \`/mem0:onboard\` to set up.
|
||||
BANNER
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check for failed dependency installation and warn the user
|
||||
_DATA_DIR="${CLAUDE_PLUGIN_DATA:-$HOME/.mem0/plugin-data}"
|
||||
if [ -f "${_DATA_DIR}/.install-failed" ]; then
|
||||
echo ""
|
||||
echo "⚠️ mem0 SDK installation failed. Some features may not work."
|
||||
echo "mem0 SDK installation failed. Some features may not work."
|
||||
echo "Run: ${CLAUDE_PLUGIN_ROOT:-$SCRIPT_DIR/..}/scripts/ensure_deps.sh"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Fetch project-scoped memory count (best-effort, don't block on failure, 5s timeout)
|
||||
# Skip REST call when OAuth-authed — no API key to authenticate with
|
||||
MEM0_COUNT="?"
|
||||
if [ "$MEM0_AUTH_MODE" = "api_key" ] && command -v python3 >/dev/null 2>&1; then
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
MEM0_COUNT=$(python3 -c "
|
||||
import json, os, urllib.request, urllib.error
|
||||
api_key = os.environ.get('MEM0_API_KEY', '')
|
||||
@@ -125,19 +78,15 @@ except Exception:
|
||||
" 2>/dev/null || echo "?")
|
||||
fi
|
||||
|
||||
# Identity line is emitted before every bootstrap variant so the agent
|
||||
# uses the same user_id the hooks resolved. Without this, the agent's
|
||||
# search_memories/add_memory MCP calls may bind to a different bucket
|
||||
# than what the hooks write to.
|
||||
cat <<BANNER
|
||||
## Mem0 Active
|
||||
|
||||
\`user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=$MEM0_AUTH_MODE\`
|
||||
\`user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key\`
|
||||
|
||||
IMPORTANT: In your FIRST response to the user, you MUST display the identity banner exactly as shown below (copy-paste it as your opening line before any other output):
|
||||
|
||||
\`\`\`
|
||||
Mem0 Active | user=$MEM0_RESOLVED_USER_ID | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT
|
||||
Mem0 Active | user=$MEM0_RESOLVED_USER_ID${_MEM0_IDENTITY_ANNOTATION} | project=$MEM0_PROJECT_ID | branch=$MEM0_BRANCH | memories=$MEM0_COUNT | auth=api_key
|
||||
\`\`\`
|
||||
|
||||
Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and \`add_memory\` call:
|
||||
@@ -146,31 +95,26 @@ Always include \`user_id\` + \`app_id\` in every \`search_memories\` filter and
|
||||
|
||||
BANNER
|
||||
|
||||
# Load mem0.md project config if present (best-effort, non-blocking)
|
||||
MEM0_PROJECT_CONFIG=""
|
||||
MEM0_CWD_RESOLVED=$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
MEM0_PROJECT_CONFIG=$(python3 "$SCRIPT_DIR/parse_mem0_config.py" --full "$MEM0_CWD_RESOLVED" 2>/dev/null || echo "{}")
|
||||
fi
|
||||
if [ -n "$MEM0_PROJECT_CONFIG" ] && [ "$MEM0_PROJECT_CONFIG" != "{}" ]; then
|
||||
_CONFIG_KEYS=$(echo "$MEM0_PROJECT_CONFIG" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d))" 2>/dev/null || echo "?")
|
||||
echo "### Project Config (mem0.md)"
|
||||
echo "\`mem0.md loaded (${_CONFIG_KEYS} sections configured)\`"
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
echo "\`\`\`json"
|
||||
echo "$MEM0_PROJECT_CONFIG"
|
||||
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 ""
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [ "$SOURCE" = "startup" ]; then
|
||||
# First-run detection: auto-trigger onboarding for new projects
|
||||
_SAFE_PID=$(printf '%s' "$MEM0_PROJECT_ID" | tr '/:' '--')
|
||||
_ONBOARD_MARKER="$HOME/.mem0/.onboarded_${_SAFE_PID}"
|
||||
if [ ! -f "$_ONBOARD_MARKER" ]; then
|
||||
if [ "$MEM0_COUNT" = "0" ]; then
|
||||
cat <<'EOF'
|
||||
IMPORTANT: This is the first run for this project. You MUST invoke the `mem0:onboard` skill now using the Skill tool BEFORE responding to the user. Do not just suggest it — actually call it. This runs the setup wizard (API check, file import, coding categories).
|
||||
This is a new project with 0 memories. You MUST invoke the `mem0:onboard` skill now using the Skill tool to import project files and install coding categories. Do not ask the user — just invoke it immediately before responding.
|
||||
EOF
|
||||
else
|
||||
cat <<'EOF'
|
||||
@@ -178,7 +122,6 @@ Search mem0 for recent decisions and task learnings before responding to the use
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Detect native Claude Code auto-memory for THIS project
|
||||
_PROJ_KEY=$(printf '%s' "$MEM0_CWD_RESOLVED" | tr '/' '-')
|
||||
_MEMORY_MD="$HOME/.claude/projects/${_PROJ_KEY}/memory/MEMORY.md"
|
||||
if [ -f "$_MEMORY_MD" ] && [ -s "$_MEMORY_MD" ]; then
|
||||
@@ -194,7 +137,6 @@ To avoid two parallel memory systems:
|
||||
MEMEOF
|
||||
fi
|
||||
|
||||
# Auto-import declarative project files in background
|
||||
MEM0_CWD="$(echo "$INPUT" | jq -r '.cwd // "."' 2>/dev/null || echo ".")" \
|
||||
python3 "$SCRIPT_DIR/auto_import.py" 2>/dev/null &
|
||||
|
||||
@@ -209,7 +151,6 @@ Context compacted. Search mem0 for `session_state` and `decision` memories to re
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Telemetry (background, fire-and-forget)
|
||||
python3 "$SCRIPT_DIR/telemetry.py" session_start --source="$SOURCE" --memory_count="${MEM0_COUNT:-0}" 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,16 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop
|
||||
#
|
||||
# Fires when Claude finishes responding.
|
||||
# Reminds Claude to store any unsaved learnings, then spawns a background
|
||||
# process to capture transcript state via the Mem0 REST API directly.
|
||||
#
|
||||
# Input: JSON on stdin with stop_hook_active, transcript_path, cwd
|
||||
# Output: Text that becomes Claude's context (exit 0), or nothing
|
||||
#
|
||||
# IMPORTANT: Check stop_hook_active to avoid infinite loops.
|
||||
|
||||
# Intentionally omit -e so the reminder always emits even if session_stats fails.
|
||||
set -uo pipefail
|
||||
|
||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||
@@ -26,30 +14,11 @@ if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Telemetry: fire before report() deletes stats file
|
||||
_TELEM_CAT=$(python3 "$SCRIPT_DIR/session_stats.py" peek 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('categories',[])))" 2>/dev/null || echo "0")
|
||||
python3 "$SCRIPT_DIR/telemetry.py" stop --categories_count="$_TELEM_CAT" 2>/dev/null &
|
||||
|
||||
# Print session-end report
|
||||
REPORT=$(python3 "$SCRIPT_DIR/session_stats.py" report 2>/dev/null || echo "")
|
||||
if [ -n "$REPORT" ]; then
|
||||
echo ""
|
||||
echo "---"
|
||||
echo "**mem0 $REPORT**"
|
||||
echo "---"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Append to persistent session log
|
||||
if [ -n "$REPORT" ]; then
|
||||
mkdir -p "$HOME/.mem0" 2>/dev/null || true
|
||||
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | $REPORT" >> "$HOME/.mem0/session-log.md" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
Store 0-2 durable facts from this turn via `add_memory` — only decisions, anti-patterns, or conventions that would help a future agent. Skip if nothing new was learned.
|
||||
EOF
|
||||
|
||||
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo "")
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -90,12 +90,6 @@ def report() -> str:
|
||||
searches = stats.get("searches", 0)
|
||||
categories = stats.get("categories", [])
|
||||
|
||||
# Clean up temp file after reading
|
||||
try:
|
||||
os.unlink(STATS_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if adds == 0 and searches == 0:
|
||||
return ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user