2026-04-04 18:16:04 -07:00
|
|
|
#!/bin/bash
|
|
|
|
|
# MEMPALACE PRE-COMPACT HOOK — Emergency save before compaction
|
|
|
|
|
#
|
|
|
|
|
# Claude Code "PreCompact" hook. Fires RIGHT BEFORE the conversation
|
|
|
|
|
# gets compressed to free up context window space.
|
|
|
|
|
#
|
|
|
|
|
# This is the safety net. When compaction happens, the AI loses detailed
|
|
|
|
|
# context about what was discussed. This hook forces one final save of
|
|
|
|
|
# EVERYTHING before that happens.
|
|
|
|
|
#
|
|
|
|
|
# Unlike the save hook (which triggers every N exchanges), this ALWAYS
|
|
|
|
|
# blocks — because compaction is always worth saving before.
|
|
|
|
|
#
|
|
|
|
|
# === INSTALL ===
|
|
|
|
|
# Add to .claude/settings.local.json:
|
|
|
|
|
#
|
|
|
|
|
# "hooks": {
|
|
|
|
|
# "PreCompact": [{
|
|
|
|
|
# "hooks": [{
|
|
|
|
|
# "type": "command",
|
|
|
|
|
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
|
|
|
|
# "timeout": 30
|
|
|
|
|
# }]
|
|
|
|
|
# }]
|
|
|
|
|
# }
|
|
|
|
|
#
|
|
|
|
|
# For Codex CLI, add to .codex/hooks.json:
|
|
|
|
|
#
|
|
|
|
|
# "PreCompact": [{
|
|
|
|
|
# "type": "command",
|
|
|
|
|
# "command": "/absolute/path/to/mempal_precompact_hook.sh",
|
|
|
|
|
# "timeout": 30
|
|
|
|
|
# }]
|
|
|
|
|
#
|
|
|
|
|
# === HOW IT WORKS ===
|
|
|
|
|
#
|
|
|
|
|
# Claude Code sends JSON on stdin with:
|
|
|
|
|
# session_id — unique session identifier
|
|
|
|
|
#
|
|
|
|
|
# We always return decision: "block" with a reason telling the AI
|
|
|
|
|
# to save everything. After the AI saves, compaction proceeds normally.
|
|
|
|
|
#
|
|
|
|
|
# === MEMPALACE CLI ===
|
2026-04-27 00:32:35 -03:00
|
|
|
# The hook ALWAYS mines the active conversation transcript synchronously
|
|
|
|
|
# before compaction (via `mempalace mine <transcript-dir> --mode convos`).
|
|
|
|
|
# MEMPAL_DIR is an *additional*, optional target for project files — it
|
|
|
|
|
# does not replace the conversation mine.
|
2026-04-04 18:16:04 -07:00
|
|
|
|
|
|
|
|
STATE_DIR="$HOME/.mempalace/hook_state"
|
|
|
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
|
|
2026-04-27 00:32:35 -03:00
|
|
|
# Optional: project directory (code / notes / docs) to also mine before
|
|
|
|
|
# compaction. Mined with `--mode projects`. The conversation transcript
|
|
|
|
|
# is always mined regardless — this is purely additive.
|
|
|
|
|
# Example: MEMPAL_DIR="$HOME/projects/my_app"
|
2026-04-04 18:16:04 -07:00
|
|
|
MEMPAL_DIR=""
|
|
|
|
|
|
2026-04-13 19:47:03 -03:00
|
|
|
# Resolve the Python interpreter. Same contract as mempal_save_hook.sh:
|
|
|
|
|
# MEMPAL_PYTHON (explicit override) → $(command -v python3) → bare python3.
|
|
|
|
|
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
|
|
|
|
|
|
2026-04-04 18:16:04 -07:00
|
|
|
# Read JSON input from stdin
|
|
|
|
|
INPUT=$(cat)
|
|
|
|
|
|
2026-04-27 00:32:35 -03:00
|
|
|
# Parse session_id and transcript_path in one call. Sanitize both before
|
|
|
|
|
# interpolating into shell — same contract as mempal_save_hook.sh.
|
|
|
|
|
eval $(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(f'SESSION_ID=\"{safe(sid)}\"')
|
|
|
|
|
print(f'TRANSCRIPT_PATH=\"{safe(tp)}\"')
|
|
|
|
|
" 2>/dev/null)
|
|
|
|
|
|
|
|
|
|
# Expand ~ in path
|
|
|
|
|
TRANSCRIPT_PATH="${TRANSCRIPT_PATH/#\~/$HOME}"
|
2026-04-04 18:16:04 -07:00
|
|
|
|
|
|
|
|
echo "[$(date '+%H:%M:%S')] PRE-COMPACT triggered for session $SESSION_ID" >> "$STATE_DIR/hook.log"
|
|
|
|
|
|
2026-04-27 00:32:35 -03:00
|
|
|
# Run ingest synchronously so memories land before compaction. Two
|
|
|
|
|
# independent targets — both run if both are set:
|
|
|
|
|
# 1. TRANSCRIPT_PATH (from Claude Code) → parent dir, --mode convos
|
|
|
|
|
# 2. MEMPAL_DIR → --mode projects
|
|
|
|
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
|
|
|
mempalace mine "$(dirname "$TRANSCRIPT_PATH")" --mode convos \
|
|
|
|
|
>> "$STATE_DIR/hook.log" 2>&1
|
|
|
|
|
fi
|
2026-04-04 18:16:04 -07:00
|
|
|
if [ -n "$MEMPAL_DIR" ] && [ -d "$MEMPAL_DIR" ]; then
|
2026-04-27 00:32:35 -03:00
|
|
|
mempalace mine "$MEMPAL_DIR" --mode projects \
|
|
|
|
|
>> "$STATE_DIR/hook.log" 2>&1
|
2026-04-04 18:16:04 -07:00
|
|
|
fi
|
|
|
|
|
|
2026-04-14 12:39:35 -07:00
|
|
|
# Silent: return empty JSON to not block. "decision": "allow" is invalid —
|
|
|
|
|
# only "block" or {} are recognized.
|
|
|
|
|
echo '{}'
|