65 lines
2.4 KiB
Bash
Executable File
65 lines
2.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# backup-memory.sh — PreToolUse hook for memory write tools.
|
|
# Snapshots the memory graph FILE before a write. Debounced so one apply-run
|
|
# produces a single backup. Never blocks the tool call (always exits 0).
|
|
#
|
|
# Config (env, all optional):
|
|
# KG_MEMORY_FILE explicit path to the MCP memory server's JSON store
|
|
# (falls back to MEMORY_FILE_PATH, the official server's var)
|
|
# KG_BACKUP_DIR where backups land (default: ~/.claude/kg-backups)
|
|
# KG_BACKUP_DEBOUNCE seconds; skip if a newer backup exists (default: 120)
|
|
# KG_BACKUP_KEEP how many backups to retain (default: 20)
|
|
#
|
|
# Tip: set KG_MEMORY_FILE in your shell / Claude Code env so the hook knows
|
|
# which file to copy. Without it the hook logs a notice and exits cleanly.
|
|
|
|
set -uo pipefail
|
|
|
|
# Drain stdin (hook receives JSON; we don't need it) so the pipe never blocks.
|
|
cat >/dev/null 2>&1 || true
|
|
|
|
MEM="${KG_MEMORY_FILE:-${MEMORY_FILE_PATH:-}}"
|
|
BACKUP_DIR="${KG_BACKUP_DIR:-$HOME/.claude/kg-backups}"
|
|
DEBOUNCE="${KG_BACKUP_DEBOUNCE:-120}"
|
|
KEEP="${KG_BACKUP_KEEP:-20}"
|
|
LOG="$BACKUP_DIR/backup.log"
|
|
|
|
note() { mkdir -p "$BACKUP_DIR" 2>/dev/null || true; echo "[$(date -Iseconds)] $*" >>"$LOG" 2>/dev/null || true; }
|
|
|
|
# No file configured or file missing -> notice + clean exit (never block writes).
|
|
if [ -z "$MEM" ]; then
|
|
note "no KG_MEMORY_FILE/MEMORY_FILE_PATH set; skipping file backup"
|
|
exit 0
|
|
fi
|
|
if [ ! -f "$MEM" ]; then
|
|
note "memory file not found at '$MEM'; skipping"
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$BACKUP_DIR" 2>/dev/null || { note "cannot create $BACKUP_DIR"; exit 0; }
|
|
|
|
# Debounce: if the newest existing backup is younger than DEBOUNCE seconds, skip.
|
|
newest="$(ls -t "$BACKUP_DIR"/kg-backup-*.json 2>/dev/null | head -n1 || true)"
|
|
if [ -n "$newest" ]; then
|
|
age=$(( $(date +%s) - $(stat -c %Y "$newest" 2>/dev/null || echo 0) ))
|
|
if [ "$age" -lt "$DEBOUNCE" ]; then
|
|
note "debounced (last backup ${age}s ago < ${DEBOUNCE}s)"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
ts="$(date +%Y%m%dT%H%M%S)"
|
|
dest="$BACKUP_DIR/kg-backup-$ts.json"
|
|
if cp "$MEM" "$dest" 2>/dev/null; then
|
|
note "backup ok -> $dest"
|
|
else
|
|
note "backup FAILED copying $MEM"
|
|
exit 0
|
|
fi
|
|
|
|
# Prune: keep only the most recent $KEEP backups.
|
|
mapfile -t old < <(ls -t "$BACKUP_DIR"/kg-backup-*.json 2>/dev/null | tail -n +"$((KEEP+1))")
|
|
for f in "${old[@]:-}"; do [ -n "$f" ] && rm -f "$f" 2>/dev/null || true; done
|
|
|
|
exit 0
|