#!/bin/bash # MEMPALACE PRE-COMPACT HOOK (REMOTE) — emergency save before compaction. # # Drop-in replacement for mempal_precompact_hook.sh when MemPalace runs # on a server. Always synchronous: we wait for the upload to complete # before returning so the transcript is on the server before the # conversation gets compressed. # # Required env vars (same as the save hook): # MEMPAL_REMOTE_URL e.g. https://unraid.local:8443 # MEMPAL_REMOTE_TOKEN bearer token # Optional: # MEMPAL_REMOTE_WING explicit wing override # MEMPAL_REMOTE_INSECURE "1" for self-signed cert # # === INSTALL === # Add to .claude/settings.local.json: # # "hooks": { # "PreCompact": [{ # "hooks": [{ # "type": "command", # "command": "/abs/path/to/mempal_precompact_hook_remote.sh", # "timeout": 60 # }] # }] # } set -u STATE_DIR="$HOME/.mempalace/hook_state" mkdir -p "$STATE_DIR" 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 if [ -z "${MEMPAL_REMOTE_URL:-}" ] || [ -z "${MEMPAL_REMOTE_TOKEN:-}" ]; then echo "[$(date '+%H:%M:%S')] PRE-COMPACT: MEMPAL_REMOTE_URL/TOKEN not set — skipping" \ >> "$STATE_DIR/hook.log" echo "{}" exit 0 fi INPUT=$(cat) 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') tp = data.get('transcript_path', '') safe = lambda s: re.sub(r'[^a-zA-Z0-9_/.\-~]', '', str(s)) print(safe(sid)) print(safe(tp)) " 2>/dev/null) SESSION_ID="${_mempal_parsed[0]:-unknown}" TRANSCRIPT_PATH="${_mempal_parsed[1]:-}" 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 } echo "[$(date '+%H:%M:%S')] PRE-COMPACT triggered for session $SESSION_ID" \ >> "$STATE_DIR/hook.log" # Synchronous upload — pre-compact is the safety net, blocking is correct # here. The Claude Code hook timeout (set in settings.local.json) bounds # how long we'll wait. if is_valid_transcript_path "$TRANSCRIPT_PATH" && [ -f "$TRANSCRIPT_PATH" ]; then CURL_OPTS=("-sS" "--max-time" "55" "-X" "POST") [ "${MEMPAL_REMOTE_INSECURE:-0}" = "1" ] && CURL_OPTS+=("-k") WING_HEADER=() [ -n "${MEMPAL_REMOTE_WING:-}" ] && WING_HEADER=(-H "X-Wing: $MEMPAL_REMOTE_WING") 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')] PRE-COMPACT ingest ok" >> "$STATE_DIR/hook.log" \ || echo "[$(date '+%H:%M:%S')] PRE-COMPACT ingest FAILED — context will compact unsaved" \ >> "$STATE_DIR/hook.log" elif [ -n "$TRANSCRIPT_PATH" ]; then echo "[$(date '+%H:%M:%S')] PRE-COMPACT: invalid transcript path: $TRANSCRIPT_PATH" \ >> "$STATE_DIR/hook.log" fi echo "{}"