482 lines
17 KiB
Bash
Executable File
482 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
# clawtap — CLI for managing AI coding sessions (Claude, Codex, Gemini)
|
|
#
|
|
# Usage:
|
|
# clawtap # Start server, show connection URLs
|
|
# clawtap new # New Claude session in tmux
|
|
# clawtap new --adapter codex # New Codex session
|
|
# clawtap -a # List active sessions (current project)
|
|
# clawtap -a --adapter gemini # List only Gemini sessions (current project)
|
|
# clawtap -A # List ALL active sessions (all projects)
|
|
# clawtap --continue # Resume most recent session
|
|
# clawtap --continue --adapter codex # Resume most recent Codex session
|
|
# clawtap --resume <session-id> # Resume a specific session
|
|
# clawtap hooks install --adapter claude # Install hooks for Claude only
|
|
# clawtap stop # Stop the server
|
|
#
|
|
# Sessions run inside tmux "clawtap".
|
|
# Mobile app auto-connects for real-time sync.
|
|
|
|
TMUX_SESSION="clawtap"
|
|
YOLO="--dangerously-skip-permissions"
|
|
SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
|
|
SERVER_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
PORT="${PORT:-3456}"
|
|
PID_FILE="$HOME/.clawtap/server.pid"
|
|
|
|
# --- Parse --adapter flag (before any command handling) ---
|
|
# Helper: set adapter variables from adapter name
|
|
set_adapter() {
|
|
case "$1" in
|
|
claude) ADAPTER="claude"; ADAPTER_CMD="claude"; YOLO="--dangerously-skip-permissions" ;;
|
|
codex) ADAPTER="codex"; ADAPTER_CMD="codex"; YOLO="--dangerously-bypass-approvals-and-sandbox" ;;
|
|
gemini) ADAPTER="gemini"; ADAPTER_CMD="gemini"; YOLO="--approval-mode yolo" ;;
|
|
esac
|
|
}
|
|
|
|
ADAPTER="claude"
|
|
ADAPTER_CMD="claude"
|
|
ADAPTER_EXPLICIT=false
|
|
prev_arg=""
|
|
for arg in "$@"; do
|
|
if [ "$prev_arg" = "--adapter" ]; then
|
|
ADAPTER_EXPLICIT=true
|
|
case "$arg" in
|
|
claude) set_adapter claude ;;
|
|
codex) set_adapter codex ;;
|
|
gemini) set_adapter gemini ;;
|
|
*) echo "Unknown adapter: $arg"; exit 1 ;;
|
|
esac
|
|
fi
|
|
prev_arg="$arg"
|
|
done
|
|
# Strip --adapter and its value from positional args
|
|
CLEANED_ARGS=()
|
|
skip_next=false
|
|
for arg in "$@"; do
|
|
if $skip_next; then skip_next=false; continue; fi
|
|
if [ "$arg" = "--adapter" ]; then skip_next=true; continue; fi
|
|
CLEANED_ARGS+=("$arg")
|
|
done
|
|
set -- "${CLEANED_ARGS[@]}"
|
|
|
|
# --- CLI flags (no server needed) ---
|
|
case "$1" in
|
|
--version|-v)
|
|
printf 'clawtap v%s\n' "$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$SERVER_DIR/package.json" | head -1)"
|
|
exit 0 ;;
|
|
--help|-h)
|
|
cat << 'HELP'
|
|
Usage: clawtap [options] [cli args...]
|
|
|
|
Commands:
|
|
new Start a new AI coding session in tmux
|
|
stop Stop the server (graceful cleanup)
|
|
hooks install Install hooks (all adapters, or use --adapter)
|
|
hooks uninstall Remove hooks (all adapters, or use --adapter)
|
|
cert Generate self-signed HTTPS certificate
|
|
|
|
Options:
|
|
-v, --version Show version
|
|
-h, --help Show this help
|
|
-a List active sessions (current project)
|
|
-A List ALL active sessions (all projects)
|
|
--adapter <name> Adapter: claude (default), codex, gemini
|
|
--resume <session-id> Resume a specific session by ID
|
|
--continue Resume the most recent session
|
|
<any args> Pass through to the adapter CLI
|
|
|
|
Examples:
|
|
clawtap new # New Claude session
|
|
clawtap new --adapter codex # New Codex session
|
|
clawtap -a --adapter gemini # List Gemini sessions (this project)
|
|
clawtap --continue --adapter codex # Resume latest Codex session
|
|
clawtap hooks install --adapter claude # Install hooks for Claude only
|
|
HELP
|
|
exit 0 ;;
|
|
stop)
|
|
if [ ! -f "$PID_FILE" ]; then
|
|
echo "Server is not running."
|
|
# Safety net: clean up hooks in case of previous ungraceful shutdown
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" uninstall 2>/dev/null
|
|
exit 0
|
|
fi
|
|
PID=$(cat "$PID_FILE")
|
|
if ! kill -0 "$PID" 2>/dev/null; then
|
|
echo "Server process (PID $PID) not running. Cleaning up stale PID file."
|
|
rm -f "$PID_FILE"
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" uninstall 2>/dev/null
|
|
exit 0
|
|
fi
|
|
echo "Stopping ClawTap server (PID $PID)..."
|
|
kill "$PID"
|
|
# Wait for graceful shutdown (up to 5s)
|
|
for i in $(seq 1 10); do
|
|
if ! kill -0 "$PID" 2>/dev/null; then
|
|
echo "Server stopped."
|
|
# Safety net: ensure hooks are cleaned up even if server didn't do it
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" uninstall 2>/dev/null
|
|
exit 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
echo "Server didn't stop gracefully, forcing..."
|
|
kill -9 "$PID" 2>/dev/null
|
|
rm -f "$PID_FILE"
|
|
# Graceful shutdown failed — force cleanup hooks
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" uninstall 2>/dev/null
|
|
echo "Server killed."
|
|
exit 0 ;;
|
|
hooks)
|
|
if [ "$ADAPTER_EXPLICIT" = true ]; then
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" "$2" "$ADAPTER"
|
|
else
|
|
npx tsx "$SCRIPT_DIR/hooks-cli.mjs" "$2"
|
|
fi
|
|
exit 0 ;;
|
|
cert)
|
|
CERT_DIR="$HOME/.clawtap"
|
|
CERT_FILE="$CERT_DIR/cert.pem"
|
|
KEY_FILE="$CERT_DIR/key.pem"
|
|
mkdir -p "$CERT_DIR"
|
|
if [ -f "$CERT_FILE" ] && [ -f "$KEY_FILE" ]; then
|
|
echo "Certificate already exists at $CERT_DIR/"
|
|
echo " cert.pem $(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null | sed 's/notAfter=/Expires: /')"
|
|
echo ""
|
|
read -p "Regenerate? (y/N) " REGEN
|
|
[ "$REGEN" != "y" ] && [ "$REGEN" != "Y" ] && exit 0
|
|
fi
|
|
echo "Generating self-signed certificate..."
|
|
openssl req -x509 -newkey rsa:2048 -nodes \
|
|
-keyout "$KEY_FILE" \
|
|
-out "$CERT_FILE" \
|
|
-days 365 \
|
|
-subj "/CN=ClawTap" \
|
|
-addext "subjectAltName=IP:$(ipconfig getifaddr en0 2>/dev/null || echo '0.0.0.0')" \
|
|
2>/dev/null
|
|
if [ $? -ne 0 ]; then
|
|
echo "Failed to generate certificate. Is openssl installed?"
|
|
exit 1
|
|
fi
|
|
chmod 600 "$KEY_FILE"
|
|
echo ""
|
|
echo "Certificate generated:"
|
|
echo " $CERT_FILE"
|
|
echo " $KEY_FILE"
|
|
echo ""
|
|
echo "Restart the server to use HTTPS:"
|
|
echo " clawtap stop && clawtap"
|
|
echo ""
|
|
echo "To trust on your phone (iOS):"
|
|
echo " 1. Send ~/.clawtap/cert.pem to your phone (AirDrop, email, etc.)"
|
|
echo " 2. Open it → Install Profile"
|
|
echo " 3. Settings → General → About → Certificate Trust Settings"
|
|
echo " → Enable full trust for 'ClawTap'"
|
|
echo ""
|
|
echo "To trust on your phone (Android):"
|
|
echo " 1. Send ~/.clawtap/cert.pem to your phone"
|
|
echo " 2. Settings → Security → Install certificate → CA certificate"
|
|
exit 0 ;;
|
|
esac
|
|
|
|
# --- Server management ---
|
|
|
|
# Detect HTTPS mode
|
|
if [ -f "$HOME/.clawtap/cert.pem" ] && [ -f "$HOME/.clawtap/key.pem" ]; then
|
|
PROTOCOL="https"
|
|
CURL_OPTS="-k" # allow self-signed certs
|
|
else
|
|
PROTOCOL="http"
|
|
CURL_OPTS=""
|
|
fi
|
|
|
|
ensure_server() {
|
|
# Check if server is already running
|
|
if curl -sf $CURL_OPTS --connect-timeout 2 "$PROTOCOL://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
# Fallback: check PID file (written by server on startup)
|
|
if [ -f "$PID_FILE" ]; then
|
|
SAVED_PID=$(cat "$PID_FILE")
|
|
if kill -0 "$SAVED_PID" 2>/dev/null; then
|
|
# Server process alive — retry health check with patience
|
|
for i in $(seq 1 5); do
|
|
if curl -sf $CURL_OPTS --connect-timeout 2 "$PROTOCOL://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
echo "Server process running (PID $SAVED_PID) but not responding on port $PORT"
|
|
echo "Check: $HOME/.clawtap/server.log"
|
|
exit 1
|
|
else
|
|
# Stale PID file — process dead, clean up
|
|
rm -f "$PID_FILE"
|
|
fi
|
|
fi
|
|
|
|
# Password is required
|
|
if [ -z "$CLAWTAP_PASSWORD" ]; then
|
|
echo "ClawTap server not running."
|
|
echo ""
|
|
echo "Set a password and try again:"
|
|
echo " export CLAWTAP_PASSWORD=your-password"
|
|
echo " clawtap"
|
|
echo ""
|
|
echo "Or start the server separately:"
|
|
echo " CLAWTAP_PASSWORD=your-password npm start"
|
|
exit 1
|
|
fi
|
|
|
|
echo "Starting ClawTap server on port $PORT..."
|
|
CLAWTAP_PASSWORD="$CLAWTAP_PASSWORD" PORT="$PORT" \
|
|
nohup npx tsx "$SERVER_DIR/server/index.ts" >"$HOME/.clawtap/server.log" 2>&1 &
|
|
SERVER_PID=$!
|
|
|
|
# Wait for server to be ready (up to 10s)
|
|
for i in $(seq 1 20); do
|
|
if curl -sf $CURL_OPTS --connect-timeout 2 "$PROTOCOL://127.0.0.1:$PORT/health" >/dev/null 2>&1; then
|
|
echo "ClawTap running on $PROTOCOL://0.0.0.0:$PORT (pid $SERVER_PID)"
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
echo "Server failed to start. Check $HOME/.clawtap/server.log"
|
|
exit 1
|
|
}
|
|
|
|
ensure_server
|
|
|
|
# Localhost requests are trusted by the server — no token needed
|
|
require_auth() {
|
|
AUTH_TOKEN=""
|
|
}
|
|
|
|
# No args → just start server, print URLs, exit
|
|
if [ $# -eq 0 ]; then
|
|
LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || echo "")
|
|
TS_HOST=$(tailscale status --self --json 2>/dev/null | \
|
|
python3 -c "import sys,json; print(json.load(sys.stdin)['Self']['DNSName'].rstrip('.'))" 2>/dev/null || echo "")
|
|
|
|
echo ""
|
|
echo "ClawTap server is running on port $PORT"
|
|
echo ""
|
|
echo " Open on your phone:"
|
|
if [ -n "$TS_HOST" ]; then echo " https://${TS_HOST} (Tailscale)"; fi
|
|
if [ -n "$LAN_IP" ]; then echo " ${PROTOCOL}://${LAN_IP}:${PORT} (LAN)"; fi
|
|
echo " http://localhost:${PORT} (this machine)"
|
|
echo ""
|
|
echo " New session: clawtap new [--adapter claude|codex|gemini]"
|
|
echo " List sessions: clawtap -a"
|
|
echo " Stop server: clawtap stop"
|
|
echo ""
|
|
exit 0
|
|
fi
|
|
|
|
# "new" subcommand → create tmux session (like old no-args behavior)
|
|
if [ "$1" = "new" ]; then
|
|
shift
|
|
fi
|
|
|
|
# Ensure tmux session exists
|
|
tmux has-session -t "$TMUX_SESSION" 2>/dev/null || \
|
|
tmux new-session -d -s "$TMUX_SESSION" -n main
|
|
|
|
# --- Attach mode (query server API for accurate adapter info) ---
|
|
if [ "$1" = "--attach" ] || [ "$1" = "-a" ] || [ "$1" = "-A" ]; then
|
|
ALL_MODE=false
|
|
[ "$1" = "-A" ] && ALL_MODE=true
|
|
|
|
require_auth
|
|
|
|
SESSIONS_JSON=$(curl -s $CURL_OPTS "$PROTOCOL://localhost:$PORT/api/active-sessions" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN")
|
|
|
|
if ! echo "$SESSIONS_JSON" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null; then
|
|
echo "Error: Failed to fetch sessions from server"
|
|
exit 1
|
|
fi
|
|
|
|
# Filter with Python: by adapter (if explicit), by cwd (if -a mode)
|
|
FILTERED=$(CWD="$(pwd)" ADAPTER="$ADAPTER" ADAPTER_EXPLICIT="$ADAPTER_EXPLICIT" ALL_MODE="$ALL_MODE" python3 -c "
|
|
import sys, json, os
|
|
sessions = json.load(sys.stdin)
|
|
adapter_filter = os.environ.get('ADAPTER') if os.environ.get('ADAPTER_EXPLICIT') == 'true' else None
|
|
cwd_filter = os.environ.get('CWD') if os.environ.get('ALL_MODE') == 'false' else None
|
|
results = []
|
|
for s in sessions:
|
|
if adapter_filter and s.get('adapter','') != adapter_filter:
|
|
continue
|
|
if cwd_filter and s.get('cwd','') != cwd_filter:
|
|
continue
|
|
results.append(s)
|
|
# Sort by lastActivity descending
|
|
results.sort(key=lambda x: x.get('lastActivity',''), reverse=True)
|
|
json.dump(results, sys.stdout)
|
|
" <<< "$SESSIONS_JSON" 2>/dev/null)
|
|
|
|
COUNT=$(echo "$FILTERED" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null)
|
|
|
|
if [ "$COUNT" = "0" ] || [ -z "$COUNT" ]; then
|
|
if [ "$ALL_MODE" = true ]; then
|
|
echo "No active sessions."
|
|
else
|
|
PROJECT_NAME=$(basename "$(pwd)")
|
|
HINT=""
|
|
[ "$ADAPTER_EXPLICIT" = true ] && HINT=" ($ADAPTER)"
|
|
echo "No active sessions${HINT} for project '$PROJECT_NAME'."
|
|
echo "Run 'clawtap -A' to see all projects, or 'clawtap new' to start a new session."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$ALL_MODE" = true ]; then
|
|
echo "Active sessions (all projects):"
|
|
else
|
|
echo "Active sessions for $(basename "$(pwd)"):"
|
|
fi
|
|
[ "$ADAPTER_EXPLICIT" = true ] && echo " (filtered: $ADAPTER)"
|
|
echo ""
|
|
|
|
# Display sessions
|
|
DISPLAY_OUTPUT=$(ALL_MODE="$ALL_MODE" python3 -c "
|
|
import sys, json, os
|
|
sessions = json.load(sys.stdin)
|
|
home = os.path.expanduser('~')
|
|
colors = {'claude': '\033[33m[Claude]\033[0m', 'codex': '\033[32m[Codex]\033[0m', 'gemini': '\033[34m[Gemini]\033[0m'}
|
|
all_mode = os.environ.get('ALL_MODE') == 'true'
|
|
for i, s in enumerate(sessions, 1):
|
|
label = colors.get(s.get('adapter',''), '\033[90m[?]\033[0m')
|
|
sid = s.get('sessionId','?')
|
|
prompt = (s.get('firstPrompt','') or '')[:60]
|
|
cwd = s.get('cwd','')
|
|
if cwd.startswith(home):
|
|
cwd = '~' + cwd[len(home):]
|
|
print(f' {i}) {label} {sid}')
|
|
if all_mode and cwd:
|
|
print(f' Dir: {cwd}')
|
|
if prompt:
|
|
print(f' {prompt}')
|
|
print()
|
|
" <<< "$FILTERED" 2>/dev/null)
|
|
echo "$DISPLAY_OUTPUT"
|
|
|
|
# Build session ID array for selection
|
|
declare -a SESS_IDS
|
|
while IFS= read -r sid; do
|
|
[ -z "$sid" ] && continue
|
|
SESS_IDS+=("$sid")
|
|
done < <(echo "$FILTERED" | python3 -c "import sys,json; [print(s['sessionId']) for s in json.load(sys.stdin)]" 2>/dev/null)
|
|
|
|
read -p "Select (1-${#SESS_IDS[@]}): " CHOICE
|
|
if [ -n "$CHOICE" ] && [ "$CHOICE" -ge 1 ] 2>/dev/null && [ "$CHOICE" -le "${#SESS_IDS[@]}" ] 2>/dev/null; then
|
|
SELECTED="${SESS_IDS[$((CHOICE-1))]}"
|
|
tmux select-window -t "$TMUX_SESSION:$SELECTED"
|
|
tmux attach -t "$TMUX_SESSION"
|
|
else
|
|
echo "Cancelled."
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# --- Resume mode ---
|
|
if [ "$1" = "--resume" ] && [ -n "$2" ]; then
|
|
RESUME_ID="$2"
|
|
shift 2
|
|
|
|
require_auth
|
|
|
|
BODY=$(printf '%s\n%s\n%s' "$RESUME_ID" "$ADAPTER" "$(pwd)" | python3 -c 'import sys,json; s,a,c=sys.stdin.read().strip().split("\n"); print(json.dumps({"sessionId":s,"adapter":a,"cwd":c}))' 2>/dev/null)
|
|
RESULT=$(curl -s $CURL_OPTS -X POST "$PROTOCOL://localhost:$PORT/api/sessions/resume" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$BODY")
|
|
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)
|
|
|
|
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
|
|
echo "Error: Failed to resume session"
|
|
echo "$RESULT"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if window already exists (session might already be active)
|
|
if tmux list-windows -t "$TMUX_SESSION" -F '#{window_name}' 2>/dev/null | grep -q "^${SESSION_ID}$"; then
|
|
tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
|
|
else
|
|
echo "Session resumed but window not found. Try refreshing."
|
|
fi
|
|
|
|
tmux attach -t "$TMUX_SESSION"
|
|
exit 0
|
|
|
|
# --- Continue mode ---
|
|
elif [ "$1" = "--continue" ]; then
|
|
shift
|
|
|
|
require_auth
|
|
|
|
# When --adapter is explicit, query API and pick most recent for that adapter
|
|
if [ "$ADAPTER_EXPLICIT" = true ]; then
|
|
SESSIONS_JSON=$(curl -s $CURL_OPTS "$PROTOCOL://localhost:$PORT/api/active-sessions?adapter=$ADAPTER" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN")
|
|
LATEST=$(echo "$SESSIONS_JSON" | python3 -c "
|
|
import sys, json
|
|
sessions = json.load(sys.stdin)
|
|
if not sessions:
|
|
sys.exit(1)
|
|
sessions.sort(key=lambda x: x.get('lastActivity',''), reverse=True)
|
|
print(sessions[0]['sessionId'])
|
|
" 2>/dev/null)
|
|
if [ -z "$LATEST" ]; then
|
|
echo "No active $ADAPTER sessions to continue."
|
|
echo "Run 'clawtap -a --adapter $ADAPTER' to check, or 'clawtap new --adapter $ADAPTER' to start one."
|
|
exit 1
|
|
fi
|
|
else
|
|
LATEST=$(tmux list-windows -t "$TMUX_SESSION" -F '#{window_activity} #{window_name}' 2>/dev/null | grep -v " main$" | sort -rn | head -1 | awk '{print $2}')
|
|
fi
|
|
|
|
if [ -n "$LATEST" ]; then
|
|
# Check if the process in the pane is still running
|
|
PANE_CMD=$(tmux display -t "$TMUX_SESSION:$LATEST" -p '#{pane_current_command}' 2>/dev/null)
|
|
if [ "$PANE_CMD" = "zsh" ] || [ "$PANE_CMD" = "bash" ]; then
|
|
# CLI process exited, shell is showing — resume via API
|
|
BODY=$(printf '%s\n%s\n%s' "$LATEST" "$ADAPTER" "$(pwd)" | python3 -c 'import sys,json; s,a,c=sys.stdin.read().strip().split("\n"); print(json.dumps({"sessionId":s,"adapter":a,"cwd":c}))' 2>/dev/null)
|
|
curl -s $CURL_OPTS -X POST "${PROTOCOL}://localhost:${PORT}/api/sessions/resume" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$BODY" >/dev/null 2>&1
|
|
fi
|
|
tmux select-window -t "$TMUX_SESSION:$LATEST"
|
|
else
|
|
echo "No active sessions to continue."
|
|
echo "Run 'clawtap new' to start a new session."
|
|
exit 1
|
|
fi
|
|
|
|
tmux attach -t "$TMUX_SESSION"
|
|
exit 0
|
|
|
|
# --- New session ---
|
|
else
|
|
require_auth
|
|
|
|
BODY=$(printf '%s\n%s' "$ADAPTER" "$(pwd)" | python3 -c 'import sys,json; a,c=sys.stdin.read().strip().split("\n"); print(json.dumps({"adapter":a,"cwd":c}))' 2>/dev/null)
|
|
RESULT=$(curl -s $CURL_OPTS -X POST "$PROTOCOL://localhost:$PORT/api/sessions/start" \
|
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$BODY")
|
|
SESSION_ID=$(echo "$RESULT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("sessionId",""))' 2>/dev/null)
|
|
|
|
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "null" ]; then
|
|
echo "Error: Failed to create session"
|
|
echo "$RESULT"
|
|
exit 1
|
|
fi
|
|
|
|
tmux select-window -t "$TMUX_SESSION:$SESSION_ID"
|
|
tmux attach -t "$TMUX_SESSION"
|
|
fi
|