cleanup and remote only

This commit is contained in:
2026-05-09 10:52:25 -05:00
parent 2fc47a52fc
commit 40e5e5e3cc
136 changed files with 1502 additions and 349529 deletions
+170
View File
@@ -0,0 +1,170 @@
#!/bin/bash
# MEMPALACE SAVE HOOK (REMOTE) — Auto-save every N exchanges to a remote palace.
#
# Drop-in replacement for mempal_save_hook.sh when MemPalace runs on a
# server (e.g. Unraid) instead of the dev machine. Same trigger logic
# (count human messages, fire every SAVE_INTERVAL), but instead of running
# `mempalace mine` locally it POSTs the active transcript to the server's
# /ingest/transcript endpoint.
#
# Required env vars:
# MEMPAL_REMOTE_URL Base URL of the MemPalace server, e.g.
# https://unraid.local:8443
# MEMPAL_REMOTE_TOKEN Bearer token (same one configured in the server's
# .env / MEMPAL_TOKEN).
#
# Optional env vars:
# MEMPAL_REMOTE_WING Wing name to file under (defaults to the
# session-id-derived inbox name on the server).
# MEMPAL_REMOTE_INSECURE "1" to skip TLS verification — needed when
# the server uses Caddy's self-signed `tls
# internal` cert and the client hasn't trusted
# the Caddy root CA.
# SAVE_INTERVAL Override the default of 15 messages.
#
# === INSTALL ===
# Add to .claude/settings.local.json (Claude Code):
#
# "hooks": {
# "Stop": [{
# "matcher": "*",
# "hooks": [{
# "type": "command",
# "command": "/abs/path/to/mempal_save_hook_remote.sh",
# "timeout": 30
# }]
# }]
# }
#
# For Codex CLI, add the same shape to .codex/hooks.json.
set -u
SAVE_INTERVAL="${SAVE_INTERVAL:-15}"
STATE_DIR="$HOME/.mempalace/hook_state"
mkdir -p "$STATE_DIR"
# Resolve Python — used only for parsing the hook's stdin JSON.
MEMPAL_PYTHON_BIN="${MEMPAL_PYTHON:-}"
if [ -z "$MEMPAL_PYTHON_BIN" ] || [ ! -x "$MEMPAL_PYTHON_BIN" ]; then
MEMPAL_PYTHON_BIN="$(command -v python3 2>/dev/null || echo python3)"
fi
# Pre-flight: bail with a clean no-op if config is missing. Returning {}
# lets Claude Code stop normally; we log the reason for the user to find.
if [ -z "${MEMPAL_REMOTE_URL:-}" ] || [ -z "${MEMPAL_REMOTE_TOKEN:-}" ]; then
echo "[$(date '+%H:%M:%S')] MEMPAL_REMOTE_URL/TOKEN not set — skipping" \
>> "$STATE_DIR/hook.log"
echo "{}"
exit 0
fi
INPUT=$(cat)
# Parse session_id, stop_hook_active, transcript_path in one Python call —
# same sanitization shape as the local hook.
mapfile -t _mempal_parsed < <(echo "$INPUT" | "$MEMPAL_PYTHON_BIN" -c "
import sys, json, re
data = json.load(sys.stdin)
sid = data.get('session_id', 'unknown')
sha_raw = data.get('stop_hook_active', False)
tp = data.get('transcript_path', '')
safe = lambda s: re.sub(r'[^a-zA-Z0-9_/.\-~]', '', str(s))
sha = 'True' if sha_raw is True or str(sha_raw).lower() in ('true', '1', 'yes') else 'False'
print(safe(sid))
print(sha)
print(safe(tp))
" 2>/dev/null)
SESSION_ID="${_mempal_parsed[0]:-unknown}"
STOP_HOOK_ACTIVE="${_mempal_parsed[1]:-False}"
TRANSCRIPT_PATH="${_mempal_parsed[2]:-}"
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
is_valid_transcript_path() {
local path="$1"
[ -n "$path" ] || return 1
case "$path" in
*.json|*.jsonl) ;;
*) return 1 ;;
esac
case "/$path/" in
*/../*) return 1 ;;
esac
return 0
}
if [ "$STOP_HOOK_ACTIVE" = "True" ] || [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo "{}"
exit 0
fi
# Count human messages (same logic as local hook).
if [ -f "$TRANSCRIPT_PATH" ]; then
EXCHANGE_COUNT=$("$MEMPAL_PYTHON_BIN" - "$TRANSCRIPT_PATH" <<'PYEOF'
import json, sys
count = 0
with open(sys.argv[1]) as f:
for line in f:
try:
entry = json.loads(line)
msg = entry.get('message', {})
if isinstance(msg, dict) and msg.get('role') == 'user':
content = msg.get('content', '')
if isinstance(content, str) and '<command-message>' in content:
continue
count += 1
except Exception:
pass
print(count)
PYEOF
2>/dev/null)
else
EXCHANGE_COUNT=0
fi
LAST_SAVE_FILE="$STATE_DIR/${SESSION_ID}_last_save"
LAST_SAVE=0
if [ -f "$LAST_SAVE_FILE" ]; then
LAST_SAVE_RAW=$(cat "$LAST_SAVE_FILE")
if [[ "$LAST_SAVE_RAW" =~ ^[0-9]+$ ]]; then
LAST_SAVE="$LAST_SAVE_RAW"
fi
fi
SINCE_LAST=$((EXCHANGE_COUNT - LAST_SAVE))
echo "[$(date '+%H:%M:%S')] Session $SESSION_ID: $EXCHANGE_COUNT exchanges, $SINCE_LAST since last save" \
>> "$STATE_DIR/hook.log"
if [ "$SINCE_LAST" -ge "$SAVE_INTERVAL" ] && [ "$EXCHANGE_COUNT" -gt 0 ]; then
if is_valid_transcript_path "$TRANSCRIPT_PATH" && [ -f "$TRANSCRIPT_PATH" ]; then
echo "$EXCHANGE_COUNT" > "$LAST_SAVE_FILE"
CURL_OPTS=("-sS" "--max-time" "30" "-X" "POST")
[ "${MEMPAL_REMOTE_INSECURE:-0}" = "1" ] && CURL_OPTS+=("-k")
WING_HEADER=()
[ -n "${MEMPAL_REMOTE_WING:-}" ] && WING_HEADER=(-H "X-Wing: $MEMPAL_REMOTE_WING")
# Background the upload so we don't block the AI's stop. The hook
# exits immediately with {} — the next save retry will catch any
# transient failure (the miner is idempotent server-side).
(
curl "${CURL_OPTS[@]}" \
-H "Authorization: Bearer $MEMPAL_REMOTE_TOKEN" \
-H "X-Session-Id: $SESSION_ID" \
-H "Content-Type: application/octet-stream" \
"${WING_HEADER[@]}" \
--data-binary "@$TRANSCRIPT_PATH" \
"$MEMPAL_REMOTE_URL/ingest/transcript" \
>> "$STATE_DIR/hook.log" 2>&1 \
&& echo "[$(date '+%H:%M:%S')] ingest ok" >> "$STATE_DIR/hook.log" \
|| echo "[$(date '+%H:%M:%S')] ingest failed (will retry next save)" \
>> "$STATE_DIR/hook.log"
) &
disown
elif [ -n "$TRANSCRIPT_PATH" ]; then
echo "[$(date '+%H:%M:%S')] Skipping invalid transcript path: $TRANSCRIPT_PATH" \
>> "$STATE_DIR/hook.log"
fi
fi
echo "{}"