cleanup and remote only
This commit is contained in:
@@ -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 "{}"
|
||||
Reference in New Issue
Block a user