rclaude: list/resume now cover on-disk Claude sessions, not just tmux
- new bin/_claude-projects helper: prints tab-sep mtime/cwd/count for every ~/.claude/projects/ dir (parses .jsonl entries for cwd field) - list_tmux_on / list_disk_on / list_all_on: unified enumeration with KIND col - rclaude list [all|tmux|disk]: filterable view, sorted by recency - rclaude resume <pattern>: matches against tmux + disk; tmux match attaches, disk match re-execs self to spawn fresh tmux + claude --continue at the cwd - helper streamed to remote via 'ssh host python3 -' so no install required on the remote side beyond python3
This commit is contained in:
parent
29dcfce7b6
commit
47434868e0
2 changed files with 118 additions and 19 deletions
50
bin/_claude-projects
Executable file
50
bin/_claude-projects
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env python3
|
||||
# Internal helper for rclaude — prints one tab-separated line per Claude
|
||||
# project directory under ~/.claude/projects/, sorted by most recent first.
|
||||
#
|
||||
# Output columns: <mtime_epoch>\t<cwd>\t<session_count>
|
||||
#
|
||||
# Used by `rclaude list` and `rclaude resume` to discover sessions that exist
|
||||
# on disk but have no live tmux session attached. Run locally or invoked
|
||||
# remotely via ssh.
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path.home() / ".claude" / "projects"
|
||||
if not root.is_dir():
|
||||
sys.exit(0)
|
||||
|
||||
rows = []
|
||||
for project_dir in root.iterdir():
|
||||
if not project_dir.is_dir():
|
||||
continue
|
||||
jsonls = sorted(project_dir.glob("*.jsonl"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if not jsonls:
|
||||
continue
|
||||
|
||||
latest = jsonls[0]
|
||||
cwd = None
|
||||
try:
|
||||
with latest.open() as f:
|
||||
for line in f:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if entry.get("cwd"):
|
||||
cwd = entry["cwd"]
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
if not cwd:
|
||||
continue
|
||||
|
||||
mtime = int(latest.stat().st_mtime)
|
||||
rows.append((mtime, cwd, len(jsonls)))
|
||||
|
||||
rows.sort(reverse=True)
|
||||
for mtime, cwd, count in rows:
|
||||
print(f"{mtime}\t{cwd}\t{count}")
|
||||
87
bin/rclaude
87
bin/rclaude
|
|
@ -39,12 +39,12 @@ is_local() {
|
|||
return 1
|
||||
}
|
||||
|
||||
# List claude-* tmux sessions on a host. Output: one line per session,
|
||||
# format: "<host>\t<session_name>\t<rest_from_tmux_ls>"
|
||||
list_sessions_on() {
|
||||
# List claude-* tmux sessions on a host. Output one row per session:
|
||||
# <host>\ttmux\t<session_name>\t<detail_from_tmux_ls>
|
||||
list_tmux_on() {
|
||||
_host=$1
|
||||
if is_local "$_host"; then
|
||||
command -v tmux >/dev/null 2>&1 || return 0 # no local tmux: nothing to list
|
||||
command -v tmux >/dev/null 2>&1 || return 0
|
||||
_raw=$(tmux ls 2>/dev/null || true)
|
||||
else
|
||||
_raw=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_host" 'tmux ls 2>/dev/null' || true)
|
||||
|
|
@ -55,11 +55,46 @@ list_sessions_on() {
|
|||
name=$1; sub(/:$/, "", name);
|
||||
$1="";
|
||||
sub(/^[[:space:]]+/, "");
|
||||
printf "%s\t%s\t%s\n", host, name, $0
|
||||
printf "%s\ttmux\t%s\t%s\n", host, name, $0
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# List on-disk Claude project sessions on a host (via _claude-projects helper).
|
||||
# Output one row per project:
|
||||
# <host>\tdisk\t<cwd>\t<sessions=N, last used <relative-time>>
|
||||
list_disk_on() {
|
||||
_host=$1
|
||||
_helper_dir=$(dirname "$0")
|
||||
if is_local "$_host"; then
|
||||
_raw=$("$_helper_dir/_claude-projects" 2>/dev/null || true)
|
||||
else
|
||||
# Send the helper over stdin so we don't depend on a pre-installed copy
|
||||
# on the remote (and to dodge quoting issues).
|
||||
_raw=$(ssh -o BatchMode=yes -o ConnectTimeout=3 "$_host" 'python3 -' < "$_helper_dir/_claude-projects" 2>/dev/null || true)
|
||||
fi
|
||||
_now=$(date +%s)
|
||||
printf %s "$_raw" | awk -F'\t' -v host="$_host" -v now="$_now" '
|
||||
function rel(secs, abs, s) {
|
||||
abs = (secs < 0) ? -secs : secs
|
||||
if (abs < 60) s = abs " seconds"
|
||||
else if (abs < 3600) s = int(abs/60) " min"
|
||||
else if (abs < 86400) s = int(abs/3600) " hours"
|
||||
else s = int(abs/86400) " days"
|
||||
return s " ago"
|
||||
}
|
||||
NF >= 3 {
|
||||
printf "%s\tdisk\t%s\tsessions=%s, last used %s\n", host, $2, $3, rel(now - $1)
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# Combined enumeration: tmux first (live), then on-disk.
|
||||
list_all_on() {
|
||||
list_tmux_on "$1"
|
||||
list_disk_on "$1"
|
||||
}
|
||||
|
||||
# All hosts to scan for list/resume.
|
||||
scan_hosts() {
|
||||
printf "local\n"
|
||||
|
|
@ -73,19 +108,24 @@ scan_hosts() {
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
cmd_list() {
|
||||
_any=0
|
||||
printf "%-10s %-50s %s\n" "HOST" "SESSION" "DETAIL"
|
||||
_mode=${1:-all} # all | tmux | disk
|
||||
printf "%-10s %-6s %-60s %s\n" "HOST" "KIND" "SESSION/CWD" "DETAIL"
|
||||
scan_hosts | while IFS= read -r h; do
|
||||
list_sessions_on "$h" | while IFS="$(printf '\t')" read -r host name detail; do
|
||||
printf "%-10s %-50s %s\n" "$host" "$name" "$detail"
|
||||
_any=1
|
||||
done
|
||||
case $_mode in
|
||||
tmux) list_tmux_on "$h" ;;
|
||||
disk) list_disk_on "$h" ;;
|
||||
*) list_all_on "$h" ;;
|
||||
esac | awk -F'\t' '{printf "%-10s %-6s %-60s %s\n", $1, $2, $3, $4}'
|
||||
done
|
||||
}
|
||||
|
||||
# Resume strategy:
|
||||
# - matches a tmux row → ssh+tmux attach (preserves the live conversation)
|
||||
# - matches a disk row → re-exec self with `<host> <cwd>` so the normal
|
||||
# launch path spins up a fresh tmux + claude --continue in that dir
|
||||
cmd_resume() {
|
||||
_pattern=${1:-}
|
||||
_matches=$(scan_hosts | while IFS= read -r h; do list_sessions_on "$h"; done)
|
||||
_matches=$(scan_hosts | while IFS= read -r h; do list_all_on "$h"; done)
|
||||
if [ -n "$_pattern" ]; then
|
||||
_matches=$(printf '%s\n' "$_matches" | grep -F -- "$_pattern" || true)
|
||||
fi
|
||||
|
|
@ -97,16 +137,25 @@ cmd_resume() {
|
|||
fi
|
||||
if [ "$_count" -gt 1 ]; then
|
||||
echo "multiple matches; refine pattern:" >&2
|
||||
printf '%s\n' "$_matches" | awk -F'\t' '{printf " %-10s %s\n", $1, $2}' >&2
|
||||
printf '%s\n' "$_matches" | awk -F'\t' '{printf " %-10s %-6s %s\n", $1, $2, $3}' >&2
|
||||
exit 1
|
||||
fi
|
||||
_host=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $1}')
|
||||
_name=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $2}')
|
||||
if is_local "$_host"; then
|
||||
exec tmux attach -t "$_name"
|
||||
else
|
||||
exec ssh -t "$_host" tmux attach -t "$_name"
|
||||
fi
|
||||
_kind=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $2}')
|
||||
_target=$(printf '%s\n' "$_matches" | awk -F'\t' 'NR==1{print $3}')
|
||||
case $_kind in
|
||||
tmux)
|
||||
if is_local "$_host"; then
|
||||
exec tmux attach -t "$_target"
|
||||
else
|
||||
exec ssh -t "$_host" tmux attach -t "$_target"
|
||||
fi
|
||||
;;
|
||||
disk)
|
||||
# Spawn a fresh tmux + claude --continue at the recorded cwd.
|
||||
exec "$0" "$_host" "$_target"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue