555e282cc4
ci / changelog_check (push) Waiting to run
ci / check_changes (push) Waiting to run
ci / build_mem0 (3.10) (push) Blocked by required conditions
ci / build_mem0 (3.11) (push) Blocked by required conditions
ci / build_mem0 (3.12) (push) Blocked by required conditions
CLI Node CI / lint (push) Waiting to run
CLI Node CI / test (20) (push) Waiting to run
CLI Node CI / test (22) (push) Waiting to run
CLI Node CI / build (push) Waiting to run
CLI Python CI / lint (push) Waiting to run
CLI Python CI / test (3.10) (push) Waiting to run
CLI Python CI / test (3.11) (push) Waiting to run
CLI Python CI / test (3.12) (push) Waiting to run
CLI Python CI / build (push) Waiting to run
openclaw checks / lint (push) Waiting to run
openclaw checks / test (20) (push) Waiting to run
openclaw checks / test (22) (push) Waiting to run
openclaw checks / build (push) Waiting to run
opencode-plugin checks / build (push) Waiting to run
pi-agent-plugin checks / lint (push) Waiting to run
pi-agent-plugin checks / test (20) (push) Waiting to run
pi-agent-plugin checks / test (22) (push) Waiting to run
pi-agent-plugin checks / build (push) Waiting to run
TypeScript SDK CI / check_changes (push) Waiting to run
TypeScript SDK CI / changelog_check (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / build_ts_sdk (22) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (20) (push) Blocked by required conditions
TypeScript SDK CI / integration_ts_sdk (22) (push) Blocked by required conditions
219 lines
6.8 KiB
Bash
Executable File
219 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 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": ...}},
|
|
# the updatedInput replaces the tool's input parameters.
|
|
# exit 2 = block (stderr shown as rejection reason).
|
|
|
|
set -uo 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)
|
|
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_$$"
|
|
trap 'rm -f "$_PATCH_OUT"' EXIT
|
|
_MEM0_TOOL_INPUT="$TOOL_INPUT" \
|
|
_MEM0_USER_ID="${MEM0_RESOLVED_USER_ID:-}" \
|
|
_MEM0_APP_ID="${MEM0_PROJECT_ID:-}" \
|
|
_MEM0_GLOBAL_SEARCH="${MEM0_GLOBAL_SEARCH:-false}" \
|
|
_MEM0_HANDLER="$HANDLER" \
|
|
python3 <<'PYEOF' > "$_PATCH_OUT" 2>/dev/null || true
|
|
import json, os, sys
|
|
|
|
raw = os.environ.get("_MEM0_TOOL_INPUT", "{}")
|
|
try:
|
|
inp = json.loads(raw)
|
|
except Exception:
|
|
sys.exit(0)
|
|
|
|
handler = os.environ.get("_MEM0_HANDLER", "")
|
|
resolved_uid = os.environ.get("_MEM0_USER_ID", "")
|
|
resolved_aid = os.environ.get("_MEM0_APP_ID", "")
|
|
global_search = os.environ.get("_MEM0_GLOBAL_SEARCH", "false") == "true"
|
|
changed = False
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
# Track session in metadata instead of run_id.
|
|
# run_id creates a separate entity partition in the v3 API,
|
|
# making memories invisible to search/get_memories calls
|
|
# that don't include a run_id filter.
|
|
if "session_id" not in meta:
|
|
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:
|
|
meta["session_id"] = sid
|
|
changed = True
|
|
|
|
if changed:
|
|
inp["metadata"] = meta
|
|
|
|
elif handler in ("search_memories", "get_memories"):
|
|
if global_search:
|
|
inp["filters"] = {"OR": [{"user_id": "*"}]}
|
|
changed = True
|
|
else:
|
|
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:
|
|
print(json.dumps(inp))
|
|
PYEOF
|
|
PATCHED=$(cat "$_PATCH_OUT" 2>/dev/null)
|
|
rm -f "$_PATCH_OUT"
|
|
|
|
if [ -n "$PATCHED" ] && echo "$PATCHED" | jq empty 2>/dev/null; then
|
|
jq -n --argjson updated "$PATCHED" '{
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PreToolUse",
|
|
"permissionDecision": "allow",
|
|
"updatedInput": $updated
|
|
}
|
|
}' 2>/dev/null || true
|
|
fi
|
|
|
|
# Track session stats here because PostToolUse hooks don't fire for plugin MCP tools.
|
|
case "$HANDLER" in
|
|
add_memory)
|
|
_CAT=$(echo "$TOOL_INPUT" | jq -r '.metadata.type // .metadata.category // ""' 2>/dev/null || echo "")
|
|
python3 "$SCRIPT_DIR/session_stats.py" add "$_CAT" 2>/dev/null &
|
|
;;
|
|
search_memories|get_memories)
|
|
python3 "$SCRIPT_DIR/session_stats.py" search 2>/dev/null &
|
|
;;
|
|
esac
|
|
|
|
exit 0
|