feat(@scripts): ✨ add cross-host session migration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
57a428cee6
commit
a28b2209af
2 changed files with 84 additions and 3 deletions
|
|
@ -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"
|
||||
|
|
|
|||
85
bin/rclaude
85
bin/rclaude
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue