feat(@scripts): add cross-host session migration

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-17 04:26:18 -07:00
parent 57a428cee6
commit a28b2209af
2 changed files with 84 additions and 3 deletions

View file

@ -1,3 +1,3 @@
#!/bin/bash
# rbtop - Connect to apricot and run btop (transient install)
ssh -t apricot.lan "dnf install -y btop && btop"
ssh -t apricot.lan "sudo dnf install -y --transient btop && btop"

View file

@ -118,7 +118,8 @@ list_disk_on() {
# List on-disk Claude sessions per UUID on a host (via _claude-projects --sessions).
# Output one row per session jsonl:
# <host>\tsession\t<uuid>\t<snippet>\t<cwd> · <relative-time>
# <host>\tsession\t<uuid>\t<snippet>\t<cwd> · <relative-time>\t<mtime_epoch>
# (col 6 is hidden — used for cross-host dedup/sort, stripped before display)
list_sessions_on() {
_host=$1
_helper_dir=$(dirname "$(resolve_self)")
@ -139,7 +140,7 @@ list_sessions_on() {
}
NF >= 3 {
snippet = ($4 == "" ? "(no user text)" : $4)
printf "%s\tsession\t%s\t%s\t%s · %s\n", host, $2, snippet, $3, rel(now - $1)
printf "%s\tsession\t%s\t%s\t%s · %s\t%s\n", host, $2, snippet, $3, rel(now - $1), $1
}
'
}
@ -156,6 +157,86 @@ list_search_on() {
list_sessions_on "$1"
}
# Get $HOME on <host> (cached per host in /tmp for the life of the shell).
get_home() {
_h=$1
_cache="/tmp/rclaude-home.$(whoami).$(printf %s "$_h" | tr -c 'A-Za-z0-9' '_')"
if [ -s "$_cache" ]; then
cat "$_cache"; return
fi
if is_local "$_h"; then
_v=$HOME
else
_v=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_h" 'printf %s "$HOME"' 2>/dev/null || true)
fi
[ -n "$_v" ] && printf '%s' "$_v" > "$_cache" && printf %s "$_v"
}
# Compute Claude's project-slug from a cwd path. Claude replaces every
# non-alphanumeric character with `-` (so `/` and `@` both become `-`).
# /Users/natalie/Code/@projects/@lilith → -Users-natalie-Code--projects--lilith
claude_slug() {
printf %s "$1" | sed 's|[^A-Za-z0-9]|-|g'
}
# Mirror a session JSONL from <src_host>'s ~/.claude/projects/<src_slug>/<uuid>.jsonl
# to <dst_host>'s ~/.claude/projects/<dst_slug>/<uuid>.jsonl, rewriting every
# `"cwd":"<src_cwd...>"` occurrence to point at <dst_cwd...>. Skips if dst is
# already newer than src.
migrate_session() {
_src=$1; _dst=$2; _uuid=$3; _src_cwd=$4; _dst_cwd=$5
_src_slug=$(claude_slug "$_src_cwd")
_dst_slug=$(claude_slug "$_dst_cwd")
_src_path="\$HOME/.claude/projects/${_src_slug}/${_uuid}.jsonl"
_dst_path="\$HOME/.claude/projects/${_dst_slug}/${_uuid}.jsonl"
# Stream src jsonl → cwd rewrite → dst jsonl. Use python for safe JSON.
_rewrite_py=$(cat <<'PY'
import json, os, sys
old, new = sys.argv[1], sys.argv[2]
for line in sys.stdin:
try:
e = json.loads(line)
except Exception:
sys.stdout.write(line); continue
cwd = e.get("cwd")
if isinstance(cwd, str):
if cwd == old:
e["cwd"] = new
elif cwd.startswith(old + "/"):
e["cwd"] = new + cwd[len(old):]
sys.stdout.write(json.dumps(e) + "\n")
PY
)
# Pull src → local pipe → rewrite → push to dst. Two ssh hops.
if is_local "$_src"; then
_read="cat $(printf '%s/.claude/projects/%s/%s.jsonl' "$HOME" "$_src_slug" "$_uuid")"
_src_data=$(sh -c "$_read" 2>/dev/null || true)
else
_src_data=$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$_src" "cat $_src_path" 2>/dev/null || true)
fi
if [ -z "$_src_data" ]; then
echo "rclaude: source session not found on $_src ($_src_path)" >&2
return 1
fi
_rewritten=$(printf '%s' "$_src_data" | python3 -c "$_rewrite_py" "$_src_cwd" "$_dst_cwd") || {
echo "rclaude: cwd rewrite failed" >&2; return 1; }
_mkdir="mkdir -p \$HOME/.claude/projects/${_dst_slug}"
_write="cat > $_dst_path"
if is_local "$_dst"; then
sh -c "$_mkdir && $_write" <<EOF
$_rewritten
EOF
else
ssh -o BatchMode=yes -o ConnectTimeout=5 "$_dst" "$_mkdir && $_write" <<EOF
$_rewritten
EOF
fi || { echo "rclaude: failed to write to $_dst" >&2; return 1; }
printf 'rclaude: mirrored session %s → %s (%s)\n' "$(printf %s "$_uuid" | cut -c1-8)" "$_dst" "$_dst_cwd" >&2
}
# Push the canonical session-tools tmux fragment to <host> and ensure
# ~/.tmux.conf sources it. Idempotent; runs on every launch so config changes
# in the repo propagate without re-running install.sh on each host.