diff --git a/bin/rbtop b/bin/rbtop index 2efe832..7186ada 100755 --- a/bin/rbtop +++ b/bin/rbtop @@ -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" diff --git a/bin/rclaude b/bin/rclaude index cfc1278..35e3549 100755 --- a/bin/rclaude +++ b/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: -# \tsession\t\t\t · +# \tsession\t\t\t · \t +# (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 (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 's ~/.claude/projects//.jsonl +# to 's ~/.claude/projects//.jsonl, rewriting every +# `"cwd":""` occurrence to point at . 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" <&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 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.