103 lines
3.3 KiB
Bash
103 lines
3.3 KiB
Bash
#!/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 "{}"
|