Skip to content

Latest commit

 

History

History
1875 lines (1611 loc) · 60.1 KB

File metadata and controls

1875 lines (1611 loc) · 60.1 KB

TMUX + SHELL + CLAUDE SETUP

You are setting up tmux and the development environment on a fresh VPS where security is already configured. Execute ALL phases in order. Write EVERY file exactly as shown using the Write tool (not echo/heredoc). Replace ${VARIABLES} with values from the wizard.


PHASE 1: WIZARD

Ask these questions interactively, one by one:

1. Client/brand name? (e.g., "DentistryGPT")
2. Short alias for commands? (e.g., "dent" → you'll type c-dent to open)
3. VPS IP address? (detect with: curl -s ifconfig.me)
4. SSH port? (default: 22)
5. Git email? (e.g., dev@example.com)
6. GitHub username? (e.g., my-github)
7. Projects - for each give: name, alias, port
   Example:
     - website, site, 3001
     - mobile-app, app, 3002
     - dashboard, dash, 3003
     - api, api, 4001
8. Telegram bot token + chat ID? (or "skip")

Store all answers in variables. These are used throughout every phase below.


PHASE 2: PACKAGES

sudo apt update -qq && sudo apt install -y -qq tmux fzf jq htop zsh

If zsh is not the default shell:

sudo chsh -s $(which zsh) $(whoami)

If Node.js/Bun/Claude Code are not installed:

# NVM + Node
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install --lts

# Bun
curl -fsSL https://bun.sh/install | bash

# Claude Code
npm install -g @anthropic-ai/claude-code

PHASE 3: DIRECTORIES

mkdir -p ~/VibeCoding/projects ~/scripts ~/tools ~/Downloads
mkdir -p ~/.claude/{rules,commands,lib,agents,templates,config}
mkdir -p ~/.tmux/{scripts,state}
mkdir -p ~/.config/tmux-claude ~/.local/bin

For each project from the wizard:

mkdir -p ~/VibeCoding/projects/${PROJECT_NAME}

PHASE 4: TMUX.CONF

Write ~/.tmux.conf with this exact content (replace ${CLIENT_NAME}):

# ══════════════════════════════════════════════════════════════
# TMUX CONFIG - ${CLIENT_NAME} VPS
# ══════════════════════════════════════════════════════════════

# ====== TERMINAL ======
set -g default-terminal "tmux-256color"
set -s terminal-overrides "linux*:AX@,xterm-256color:Tc,xterm:Tc,*:Ss=\\E[%p1%d q:Se=\\E[2 q"
set -s terminal-features "xterm*:clipboard:ccolour:cstyle:focus:title,screen*:title,rxvt*:ignorefkeys"
set -q -g status-utf8 on
setw -q -g utf8 on

# ====== BASIC ======
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g history-limit 100000
set -s escape-time 50

# ====== PASTE FIX (prevents PTY deadlock with Claude Code) ======
set -s buffer-limit 100
set -g assume-paste-time 0
set -s set-clipboard on
set -g allow-passthrough off
setw -g alternate-screen on

# Focus events
set -g focus-events on
set-hook -g client-focus-in "refresh-client -S"

# ====== MOUSE + SCROLL ======
set -g mouse on
bind -n WheelUpPane copy-mode -e \; send-keys -M
bind -n WheelDownPane send-keys -M
bind -T copy-mode WheelUpPane send-keys -X -N 5 scroll-up
bind -T copy-mode WheelDownPane send-keys -X -N 5 scroll-down
bind -T copy-mode-vi WheelUpPane send-keys -X -N 5 scroll-up
bind -T copy-mode-vi WheelDownPane send-keys -X -N 5 scroll-down
bind -T copy-mode Escape send-keys -X cancel
bind -T copy-mode q send-keys -X cancel
bind -n M-Up copy-mode \; send-keys -X scroll-up
bind -n M-Down send-keys -X scroll-down
bind -n S-Up copy-mode \; send-keys -X halfpage-up
bind -n S-Down send-keys -X halfpage-down

# ====== STYLES (theme-neutral) ======
set -g status-style bg=default,fg=default
setw -g window-status-current-style bg=default,fg=default,bold
setw -g window-status-style bg=default,fg=default,dim
set -g pane-border-style fg=default,dim
set -g pane-active-border-style fg=colour137
set -g pane-border-status top
set -g pane-border-format " #{?pane_active,#[fg=colour137 bold],#[fg=default dim]}#P: #{pane_title} #{?pane_active,#[fg=colour137]●,○} "
set -g message-style bg=default,fg=default,bold
setw -g mode-style reverse

# ====== STATUS BAR (all values cached - no subprocess spawning) ======
set -g status-position bottom
set -g status on
set -g status-interval 30
set -g status-left-length 150
set -g status-right-length 80
set -g status-left '  #[fg=colour137]#(~/.tmux/scripts/read-cache.sh pomodoro) #[fg=default]• #[fg=colour137,bold]#S #[fg=default,nobold]• TS #[fg=colour137]#(~/.tmux/scripts/read-cache.sh sessions-count) #[fg=default]• #[fg=colour137]#(~/.tmux/scripts/read-cache.sh last-push) #[fg=default]• #[fg=colour137]#(~/.tmux/scripts/read-cache.sh git-branch) '
set -g status-right '#[fg=default]Disk #[fg=colour137]#(~/.tmux/scripts/read-cache.sh disk-usage) #[fg=default]• CPU #[fg=colour137]#(~/.tmux/scripts/read-cache.sh cpu-usage) #[fg=default]• RAM #[fg=colour137]#(~/.tmux/scripts/read-cache.sh ram-usage)  '
setw -g window-status-format ''
setw -g window-status-current-format ''

# ====== SAFE PASTE ======
bind v run-shell "~/.tmux/scripts/safe-paste.sh"
bind ] run-shell "~/.tmux/scripts/safe-paste.sh"
bind P display-popup -E -w 80% -h 50% -T " Safe Paste " "~/.tmux/scripts/paste-popup.sh"

# ====== CACHE DAEMON ======
if-shell "! pgrep -f cached-status.sh" "run-shell -b 'nohup bash ~/.tmux/scripts/cached-status.sh >/dev/null 2>&1 &'"

# ====== KEYBINDINGS ======
bind r source-file ~/.tmux.conf \; display-message "Config reloaded"
bind -n M-g refresh-client -S
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
bind -n M-c copy-mode
bind -n M-s copy-mode -e

# ====== MOBILE SCROLL ======
bind u copy-mode \; send-keys -X halfpage-up
bind d send-keys -X halfpage-down
bind g copy-mode \; send-keys -X history-top
bind G send-keys -X history-bottom
bind C-b copy-mode -e

# ====== SESSION NAVIGATOR (Ctrl+b z / Alt+z / Ctrl+l) ======
bind z display-popup -E -w 100% -h 100% -T " Sessions " "~/.tmux/scripts/session-manager-v2.sh"
bind -n M-z display-popup -E -w 100% -h 100% -T " Sessions " "~/.tmux/scripts/session-manager-v2.sh"
bind -n C-l display-popup -E -w 100% -h 100% -T " Sessions " "~/.tmux/scripts/session-manager-v2.sh"

# ====== AGENT SWITCHER (Ctrl+b t) ======
bind-key t display-popup -E -w 80% -h 60% -T " Switch Agent " "~/.tmux/scripts/agent-picker.sh"

# Alt+1-8 quick pane switch
bind -n M-1 select-pane -t 1
bind -n M-2 select-pane -t 2
bind -n M-3 select-pane -t 3
bind -n M-4 select-pane -t 4
bind -n M-5 select-pane -t 5
bind -n M-6 select-pane -t 6
bind -n M-7 select-pane -t 7
bind -n M-8 select-pane -t 8

PHASE 5: TMUX SCRIPTS

Write ALL these files in ~/.tmux/scripts/. Every single one. Make ALL executable at the end with chmod +x ~/.tmux/scripts/*.sh.

~/.tmux/scripts/ram-usage.sh

#!/bin/bash
TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
echo "$(( (TOTAL - AVAILABLE) * 100 / TOTAL ))%"

~/.tmux/scripts/cpu-usage.sh

#!/bin/bash
read cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
total=$((user + nice + system + idle + iowait + irq + softirq + steal))
used=$((total - idle - iowait))
echo "$((used * 100 / total))%"

~/.tmux/scripts/disk-usage.sh

#!/bin/bash
echo "$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')%"

~/.tmux/scripts/sessions-count.sh

#!/bin/bash
tmux list-sessions 2>/dev/null | wc -l

~/.tmux/scripts/last-push.sh

#!/bin/bash
PANE_PATH=$(tmux display-message -p '#{pane_current_path}' 2>/dev/null)
if [ -d "$PANE_PATH/.git" ]; then
    cd "$PANE_PATH"
    BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
    LAST=$(git log -1 --format="%at" origin/$BRANCH 2>/dev/null)
    if [ -n "$LAST" ]; then
        NOW=$(date +%s); DIFF=$((NOW - LAST))
        if [ $DIFF -lt 3600 ]; then echo "up $((DIFF / 60))m"
        elif [ $DIFF -lt 86400 ]; then echo "up $((DIFF / 3600))h"
        else echo "up $((DIFF / 86400))d"; fi
        exit 0
    fi
fi
echo "-"

~/.tmux/scripts/git-branch.sh

#!/bin/bash
PANE_PATH=$(tmux display-message -p '#{pane_current_path}' 2>/dev/null)
if [ -d "$PANE_PATH/.git" ]; then
    cd "$PANE_PATH"
    BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
    if [ -n "$BRANCH" ]; then
        DIRTY=""
        [ -n "$(git status --porcelain 2>/dev/null)" ] && DIRTY="*"
        echo "${BRANCH}${DIRTY}"
    fi
fi

~/.tmux/scripts/pomodoro.sh

#!/bin/bash
STATE_FILE="$HOME/.tmux/state/pomodoro"
WORK_MINS=90; BREAK_MINS=15
if [ ! -f "$STATE_FILE" ]; then
    mkdir -p "$HOME/.tmux/state"
    echo "$(date +%s) work" > "$STATE_FILE"
fi
read -r start_time mode < "$STATE_FILE"
now=$(date +%s); elapsed=$((now - start_time))
if [ "$mode" = "work" ]; then
    rem=$((WORK_MINS * 60 - elapsed))
    if [ $rem -le 0 ]; then
        echo "$(date +%s) break" > "$STATE_FILE"
        printf "brk %2d:00" $BREAK_MINS
    else
        printf "wrk %2d:%02d" $((rem / 60)) $((rem % 60))
    fi
else
    rem=$((BREAK_MINS * 60 - elapsed))
    if [ $rem -le 0 ]; then
        echo "$(date +%s) work" > "$STATE_FILE"
        printf "wrk %2d:00" $WORK_MINS
    else
        printf "brk %2d:%02d" $((rem / 60)) $((rem % 60))
    fi
fi

~/.tmux/scripts/tunnel-status.sh

#!/bin/bash
COUNT=0
for port in 9222 9223 9224 9225 9226 9227; do
    ss -tln 2>/dev/null | grep -q ":$port " && ((COUNT++))
done
echo "$COUNT"

~/.tmux/scripts/cached-status.sh

#!/usr/bin/env bash
CACHE_DIR="/tmp/tmux-status-cache"
mkdir -p "$CACHE_DIR"
update_cache() {
    for script in pomodoro sessions-count last-push git-branch disk-usage cpu-usage ram-usage tunnel-status; do
        local sp="$HOME/.tmux/scripts/${script}.sh"
        if [ -x "$sp" ]; then
            local tmp="${CACHE_DIR}/${script}.tmp"
            local cache="${CACHE_DIR}/${script}"
            timeout 3 bash "$sp" > "$tmp" 2>/dev/null && mv "$tmp" "$cache" || echo "?" > "$cache"
        fi
    done
}
update_cache
while true; do sleep 30; update_cache; done

~/.tmux/scripts/read-cache.sh

#!/usr/bin/env bash
CACHE="/tmp/tmux-status-cache/${1}"
if [ -f "$CACHE" ]; then
    cat "$CACHE"
else
    SCRIPT="$HOME/.tmux/scripts/${1}.sh"
    [ -x "$SCRIPT" ] && timeout 2 bash "$SCRIPT" 2>/dev/null || echo "?"
fi

~/.tmux/scripts/safe-paste.sh

#!/usr/bin/env bash
set -euo pipefail
CHUNK_SIZE=48
CHUNK_DELAY=0.05
MAX_KEYSTROKE=512

CONTENT=$(tmux show-buffer 2>/dev/null) || { tmux display-message "No content in paste buffer"; exit 1; }
TOTAL=${#CONTENT}
[ "$TOTAL" -eq 0 ] && { tmux display-message "Paste buffer empty"; exit 1; }

if [ "$TOTAL" -lt 256 ]; then
    tmux paste-buffer -dp
    exit 0
fi

if [ "$TOTAL" -lt "$MAX_KEYSTROKE" ]; then
    tmux display-message "Safe paste: ${TOTAL} chars..."
    OFFSET=0
    while [ "$OFFSET" -lt "$TOTAL" ]; do
        tmux send-keys -l -- "${CONTENT:$OFFSET:$CHUNK_SIZE}"
        OFFSET=$((OFFSET + CHUNK_SIZE))
        sleep "$CHUNK_DELAY"
    done
    tmux display-message "Pasted ${TOTAL} chars OK"
    exit 0
fi

PERSIST_FILE="/tmp/claude-paste.txt"
printf '%s' "$CONTENT" > "$PERSIST_FILE"
MSG="Read /tmp/claude-paste.txt - that's my pasted input, use it as my message"
tmux display-message "Large paste (${TOTAL} chars) saved to file"
sleep 0.3
OFFSET=0
while [ "$OFFSET" -lt "${#MSG}" ]; do
    tmux send-keys -l -- "${MSG:$OFFSET:$CHUNK_SIZE}"
    OFFSET=$((OFFSET + CHUNK_SIZE))
    sleep "$CHUNK_DELAY"
done
sleep 0.2
tmux send-keys Enter

~/.tmux/scripts/paste-popup.sh

#!/usr/bin/env bash
set -euo pipefail
PASTE_FILE="/tmp/tmux-paste-$$.txt"
PERSIST_FILE="/tmp/claude-paste.txt"
CHUNK_SIZE=48
CHUNK_DELAY=0.05
TARGET_PANE=$(tmux display-message -p '#{pane_id}')
cleanup() { rm -f "$PASTE_FILE"; }
trap cleanup EXIT
> "$PASTE_FILE"

echo -e "\033[1;36m  SAFE PASTE MODE\033[0m"
echo -e "  Paste your text (Cmd+V), then press \033[1;32mCtrl+D\033[0m to send"
echo -e "  Or \033[1;31mCtrl+C\033[0m to cancel"
echo ""

cat > "$PASTE_FILE" 2>/dev/null || { echo "Cancelled."; exit 0; }
TOTAL=$(wc -c < "$PASTE_FILE")
[ "$TOTAL" -eq 0 ] && { echo "Nothing to paste."; exit 0; }
CONTENT=$(cat "$PASTE_FILE")
echo ""

if [ "$TOTAL" -lt 512 ]; then
    echo -e "\033[1;32mSending ${TOTAL} bytes...\033[0m"
    OFFSET=0
    while [ "$OFFSET" -lt "${#CONTENT}" ]; do
        tmux send-keys -l -t "$TARGET_PANE" -- "${CONTENT:$OFFSET:$CHUNK_SIZE}"
        OFFSET=$((OFFSET + CHUNK_SIZE))
        sleep "$CHUNK_DELAY"
    done
    echo -e "\033[1;32mDone\033[0m"
    sleep 0.5
    exit 0
fi

cp "$PASTE_FILE" "$PERSIST_FILE"
chmod 644 "$PERSIST_FILE"
LINES=$(wc -l < "$PERSIST_FILE")
echo -e "\033[1;32mSaved ${TOTAL} bytes (${LINES} lines) to ${PERSIST_FILE}\033[0m"
MSG="Read /tmp/claude-paste.txt - that's my pasted input, use it as my message"
sleep 0.3
OFFSET=0
while [ "$OFFSET" -lt "${#MSG}" ]; do
    tmux send-keys -l -t "$TARGET_PANE" -- "${MSG:$OFFSET:$CHUNK_SIZE}"
    OFFSET=$((OFFSET + CHUNK_SIZE))
    sleep "$CHUNK_DELAY"
done
sleep 0.2
tmux send-keys -t "$TARGET_PANE" Enter
echo -e "\033[1;32mDone! Claude reads from the file.\033[0m"
sleep 1

~/.tmux/scripts/agent-picker.sh

#!/bin/bash
SESSION=$(tmux display-message -p '#{session_name}')
WINDOW=$(tmux display-message -p '#{window_index}')
PANES=""
while IFS= read -r line; do
    idx=$(echo "$line" | cut -d'|' -f1)
    title=$(echo "$line" | cut -d'|' -f2)
    active=$(echo "$line" | cut -d'|' -f3)
    [ "$active" = "1" ] && marker="" || marker="  "
    PANES="${PANES}${marker}[${idx}] ${title}\n"
done < <(tmux list-panes -t "${SESSION}:${WINDOW}" -F '#{pane_index}|#{pane_title}|#{pane_active}')
PANE_COUNT=$(tmux list-panes -t "${SESSION}:${WINDOW}" | wc -l)
[ "$PANE_COUNT" -le 1 ] && { echo "Only 1 pane."; sleep 1; exit 0; }
if command -v fzf &>/dev/null; then
    SELECTED=$(echo -e "$PANES" | fzf --ansi --no-sort --reverse --prompt="Switch to > " --header="Session: ${SESSION}" --header-first --height=100%)
    [ -n "$SELECTED" ] && { PANE_IDX=$(echo "$SELECTED" | grep -oP '\[\K[0-9]+'); tmux select-pane -t "${SESSION}:${WINDOW}.${PANE_IDX}"; }
else
    echo "=== Agents ==="
    echo -e "$PANES"
    echo -n "Pane #: "
    read -r PANE_IDX
    [ -n "$PANE_IDX" ] && tmux select-pane -t "${SESSION}:${WINDOW}.${PANE_IDX}"
fi

~/.tmux/scripts/session-manager-v2.sh

#!/usr/bin/env bash
# Session Manager v2 - FZF session picker with Claude status detection

CURRENT_SESSION=$(tmux display-message -p '#S')
NOW=$(date +%s)
TMPDIR_SM="/tmp/.sm-$$"
mkdir -p "$TMPDIR_SM"
trap "rm -rf '$TMPDIR_SM'" EXIT

human_time() {
    local secs=$1
    if [ "$secs" -lt 60 ]; then echo "${secs}s"
    elif [ "$secs" -lt 3600 ]; then echo "$((secs/60))m"
    elif [ "$secs" -lt 86400 ]; then echo "$((secs/3600))h$((secs%3600/60))m"
    else echo "$((secs/86400))d$((secs%86400/3600))h"
    fi
}

detect_claude_status() {
    local session="$1" outfile="$2"
    local pane_data=$(tmux list-panes -t "$session" -F '#{pane_pid} #{pane_current_command}' 2>/dev/null)
    local result=""
    while read -r pane_pid cmd; do
        if [ "$cmd" = "claude" ]; then
            local claude_pid=$(pgrep -P "$pane_pid" -x "claude" 2>/dev/null | head -1)
            if [ -n "$claude_pid" ]; then
                local has_prompt=0
                local last_lines=$(tmux capture-pane -t "$session" -p -S -4 2>/dev/null)
                echo "$last_lines" | grep -q '' && has_prompt=1
                local real_work=0
                for bpid in $(ps --ppid "$claude_pid" --no-headers -o pid,comm 2>/dev/null \
                    | awk '$2=="zsh"||$2=="bash"||$2=="sh"{print $1}'); do
                    local real_kids=$(ps --ppid "$bpid" --no-headers -o comm 2>/dev/null \
                        | grep -v -E '^(ps|sleep|cat|grep|tail|head|wc|sed|awk|echo|test|zsh|bash|sh|tee|tr|sort|xargs|date|timeout)$' \
                        | wc -l)
                    [ "${real_kids:-0}" -gt 0 ] && real_work=1 && break
                done
                local cpu=$(ps -p "$claude_pid" -o %cpu= 2>/dev/null | xargs)
                local cpu_int=${cpu%.*}
                if [ "$real_work" -eq 1 ] || [ "${cpu_int:-0}" -gt 30 ]; then
                    result="[working]"
                elif [ "$has_prompt" -eq 1 ]; then
                    result="[idle]"
                else
                    result="[working]"
                fi
            else
                result="[idle]"
            fi
            break
        fi
    done <<< "$pane_data"
    echo "$result" > "$outfile"
}

# Main loop
while true; do
    CPU=$(top -bn1 2>/dev/null | awk '/^%Cpu/{printf "%.0f", $2+$4}')
    DISK=$(df -h / 2>/dev/null | awk 'NR==2{print $5}')
    RAM=$(free 2>/dev/null | awk '/Mem:/{printf "%.0f", $3/$2*100}')

    SESSION_DATA=$(tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_attached}|#{session_activity}|#{session_created}' 2>/dev/null)
    [ -z "$SESSION_DATA" ] && exit 0

    # Parallel claude detection
    while IFS='|' read -r sname _rest; do
        detect_claude_status "$sname" "$TMPDIR_SM/$sname.status" &
    done <<< "$SESSION_DATA"
    wait

    # Build display list
    DISPLAY_LIST=""
    CURRENT_LINE=1
    LINE_NUM=0

    while IFS='|' read -r name wins attached activity created; do
        [ -z "$name" ] && continue
        panes=$(tmux list-panes -t "$name" 2>/dev/null | wc -l)
        if [ "$name" = "$CURRENT_SESSION" ]; then marker=">"
        elif [ "$attached" = "1" ]; then marker="*"
        else marker=" "; fi
        age=""
        [ -n "$created" ] && [ "$created" -gt 0 ] 2>/dev/null && age=$(human_time $((NOW-created)))
        claude_tag=""
        [ -f "$TMPDIR_SM/$name.status" ] && claude_tag=$(cat "$TMPDIR_SM/$name.status")
        pane_path=$(tmux display-message -t "$name" -p '#{pane_current_path}' 2>/dev/null)
        branch=""
        [ -n "$pane_path" ] && [ -d "$pane_path/.git" ] && branch=$(git -C "$pane_path" branch --show-current 2>/dev/null)
        branch_tag=""
        [ -n "$branch" ] && branch_tag="$branch"
        proj=$(echo "$pane_path" | sed "s|$HOME/VibeCoding/projects/|proj/|;s|$HOME|~|")
        printf -v line "%s  %-18s  %-10s  %dw %dp  %-6s  %-16s  %s" \
            "$marker" "$name" "$claude_tag" "$wins" "$panes" "$age" "$branch_tag" "$proj"
        LINE_NUM=$((LINE_NUM + 1))
        [ "$name" = "$CURRENT_SESSION" ] && CURRENT_LINE=$LINE_NUM
        [ -n "$DISPLAY_LIST" ] && DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"${line}" || DISPLAY_LIST="${line}"
    done <<< "$SESSION_DATA"

    # Actions
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"────────────────────────────────────────────────────"
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"   new home session"
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"   open project..."
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"   clean RAM"
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"   close all"
    DISPLAY_LIST="${DISPLAY_LIST}"$'\n'"   close inactive"

    HEADER="cpu ${CPU}%  disk ${DISK}  ram ${RAM}%"

    SELECTED=$(echo "$DISPLAY_LIST" | fzf \
        --no-multi --reverse --no-info --header-first \
        --header="$HEADER" --prompt="> " --expect="x" --pointer=">" --border=none \
        --bind "load:pos($CURRENT_LINE)" \
        --preview="line={}; if echo \"\$line\" | grep -q 'close\|───\|new home\|open project\|clean RAM'; then echo ''; else session=\$(echo \"\$line\" | sed 's/^[^ ]* *//' | sed 's/  .*//' | xargs); echo \"\$session\"; echo; tmux capture-pane -t \"\$session\" -p -S -30 2>/dev/null | tail -25; fi" \
        --preview-window=down,40%,wrap \
        --color="fg:-1,bg:-1,hl:-1:underline,fg+:-1:bold,bg+:8,hl+:-1:bold:underline,info:-1,prompt:-1:dim,pointer:-1,marker:-1,spinner:-1,header:-1:dim,border:-1,preview-fg:-1,preview-bg:-1,gutter:-1")

    KEY=$(echo "$SELECTED" | head -1)
    CHOICE=$(echo "$SELECTED" | tail -1)
    [ -z "$CHOICE" ] && exit 0
    echo "$CHOICE" | grep -q '───' && continue

    # new home session
    if echo "$CHOICE" | grep -q 'new home session'; then
        last_num=0
        for s in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep '^Home'); do
            num=$(echo "$s" | sed 's/Home-\?//'); [ -z "$num" ] && num=0
            [ "$num" -gt "$last_num" ] 2>/dev/null && last_num="$num"
        done
        new_name="Home-$((last_num + 1))"
        tmux new-session -d -s "$new_name" -c "$HOME" 2>/dev/null
        tmux switch-client -t "$new_name"; exit 0
    fi

    # open project
    if echo "$CHOICE" | grep -q 'open project'; then
        PROJECT_MENU=""
        for dir in $(ls -d "$HOME/VibeCoding/projects"/*/ 2>/dev/null); do
            name=$(basename "$dir")
            PROJECT_MENU+="     ${name}"$'\n'
        done
        [ -z "$PROJECT_MENU" ] && { echo "No projects found"; sleep 1; continue; }
        PROJ_SELECTED=$(echo "$PROJECT_MENU" | sed '/^$/d' | fzf \
            --no-multi --reverse --no-info \
            --header="Select project  Enter=open  Esc=back" \
            --prompt="project > " --pointer=">" --border=none \
            --preview="name=\$(echo {} | xargs); p=\"$HOME/VibeCoding/projects/\$name\"; [ -f \"\$p/package.json\" ] && echo 'Stack:' && grep -E '\"(next|react|expo|convex|clerk|stripe)\"' \"\$p/package.json\" 2>/dev/null | head -8; echo; [ -d \"\$p/.git\" ] && echo 'Branch:' && git -C \"\$p\" branch --show-current 2>/dev/null" \
            --preview-window=down,30%,wrap \
            --color="fg:-1,bg:-1,hl:-1:underline,fg+:-1:bold,bg+:8,hl+:-1:bold:underline,info:-1,prompt:-1:dim,pointer:-1,marker:-1,spinner:-1,header:-1:dim,border:-1,preview-fg:-1,preview-bg:-1,gutter:-1")
        [ -z "$PROJ_SELECTED" ] && continue
        PROJ_NAME=$(echo "$PROJ_SELECTED" | xargs)
        PROJ_PATH="$HOME/VibeCoding/projects/$PROJ_NAME"
        if ! tmux has-session -t "$PROJ_NAME" 2>/dev/null; then
            NEW_SESS="$PROJ_NAME"
        else
            _i=2; while tmux has-session -t "${PROJ_NAME}-${_i}" 2>/dev/null; do ((_i++)); done
            NEW_SESS="${PROJ_NAME}-${_i}"
        fi
        tmux new-session -d -s "$NEW_SESS" -c "$PROJ_PATH"
        tmux send-keys -t "$NEW_SESS" "claude --dangerously-skip-permissions" Enter
        tmux switch-client -t "$NEW_SESS"; exit 0
    fi

    # clean RAM
    if echo "$CHOICE" | grep -q 'clean RAM'; then
        sync; echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1
        rm -rf /tmp/.sm-* 2>/dev/null; continue
    fi

    # close all
    if echo "$CHOICE" | grep -q 'close all$'; then
        for s in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -v "^${CURRENT_SESSION}$"); do
            tmux kill-session -t "$s" 2>/dev/null
        done; exit 0
    fi

    # close inactive
    if echo "$CHOICE" | grep -q 'close inactive'; then
        for s in $(tmux list-sessions -F '#{session_name}' 2>/dev/null); do
            [ "$s" = "$CURRENT_SESSION" ] && continue
            local_tag=""; [ -f "$TMPDIR_SM/$s.status" ] && local_tag=$(cat "$TMPDIR_SM/$s.status")
            [ "$local_tag" = "[working]" ] && continue
            tmux kill-session -t "$s" 2>/dev/null
        done; continue
    fi

    # Session switch or kill
    TARGET=$(echo "$CHOICE" | sed 's/^[^ ]* *//' | sed 's/  .*//' | xargs)
    if [ "$KEY" = "x" ]; then
        if [ "$TARGET" = "$CURRENT_SESSION" ]; then
            OTHER=$(tmux list-sessions -F '#{session_name}' | grep -v "^${TARGET}$" | head -1)
            [ -n "$OTHER" ] && { tmux switch-client -t "$OTHER"; CURRENT_SESSION="$OTHER"; tmux kill-session -t "$TARGET"; }
        else
            tmux kill-session -t "$TARGET"
        fi
        continue
    else
        [ "$TARGET" != "$CURRENT_SESSION" ] && tmux switch-client -t "$TARGET"
        exit 0
    fi
done

Make all executable

chmod +x ~/.tmux/scripts/*.sh

PHASE 6: TMUX-PROJECT (interactive per-project menu)

Write ~/.local/bin/tmux-project with this EXACT content:

#!/bin/bash
# tmux-project v6 - Menu complet avec Dev Server, Git, Init, Background
# Usage: tmux-project <session_name> <project_path> [--no-claude]

SESSION_BASE="$1"
PROJECT_PATH="$2"
NO_CLAUDE=false

# Check for --no-claude flag (Home mode)
if [[ "$3" == "--no-claude" ]] || [[ "$SESSION_BASE" == "Home" ]]; then
    NO_CLAUDE=true
fi

YELLOW='\033[1;33m'
GREEN='\033[1;32m'
RED='\033[1;31m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'

# Ports par projet - customize in ~/.config/tmux-claude/ports.conf
# Format: ProjectName=PORT (one per line)
declare -A PROJECT_PORTS=()

# Load user-defined ports
PORTS_FILE="$HOME/.config/tmux-claude/ports.conf"
if [[ -f "$PORTS_FILE" ]]; then
    while IFS='=' read -r name port; do
        [[ -z "$name" || "$name" == \#* ]] && continue
        PROJECT_PORTS["$name"]="$port"
    done < "$PORTS_FILE"
fi

# Default port detection based on package.json
get_default_port() {
    local pkg="$PROJECT_PATH/package.json"
    if [[ -f "$pkg" ]]; then
        # Check for Next.js
        if grep -q '"next"' "$pkg" 2>/dev/null; then
            echo "3000"
            return
        fi
        # Check for Vite
        if grep -q '"vite"' "$pkg" 2>/dev/null; then
            echo "5173"
            return
        fi
    fi
    echo "3000"
}

# Telegram notify state file
NOTIFY_FILE="$HOME/.config/tmux-notify-state"

get_sessions() {
    if [ "$NO_CLAUDE" = true ]; then
        # HOME MODE: show ALL sessions
        tmux list-sessions -F "#{session_name}|#{session_created}|#{session_windows}" 2>/dev/null | sort
    else
        # PROJECT MODE: show only project sessions (exact name or name-N)
        tmux list-sessions -F "#{session_name}|#{session_created}|#{session_windows}" 2>/dev/null | grep -E "^${SESSION_BASE}(\||$|-[0-9])" | sort
    fi
}

count_sessions() {
    local c=$(get_sessions | grep -c . 2>/dev/null)
    echo "${c:-0}"
}

get_git_info() {
    if [ -d "$PROJECT_PATH/.git" ]; then
        cd "$PROJECT_PATH"
        local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
        local dirty=$(git status --porcelain 2>/dev/null | wc -l)

        # Commits ahead/behind
        local ahead=$(git rev-list --count @{u}..HEAD 2>/dev/null || echo "0")
        local behind=$(git rev-list --count HEAD..@{u} 2>/dev/null || echo "0")

        # Last push time
        local last_push=""
        local last_ts=$(git log -1 --format="%at" origin/$branch 2>/dev/null)
        if [ -n "$last_ts" ]; then
            local now=$(date +%s)
            local diff=$((now - last_ts))
            if [ $diff -lt 3600 ]; then
                last_push="$((diff / 60))m"
            elif [ $diff -lt 86400 ]; then
                last_push="$((diff / 3600))h"
            else
                last_push="$((diff / 86400))d"
            fi
        fi

        echo "$branch|$dirty|$ahead|$behind|$last_push"
    fi
}

get_ram() {
    local total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    local available=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    echo "$((($total - $available) * 100 / $total))%"
}

get_cpu() {
    read cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
    local total=$((user + nice + system + idle + iowait + irq + softirq + steal))
    local used=$((total - idle - iowait))
    echo "$((used * 100 / total))%"
}

get_port() {
    local port="${PROJECT_PORTS[$SESSION_BASE]}"
    if [[ -z "$port" || "$port" == "0" ]]; then
        port=$(get_default_port)
    fi
    echo "$port"
}

get_dev_server_status() {
    local port=$(get_port)
    if [ "$port" -eq 0 ]; then
        echo "no-port"
    elif curl -s -o /dev/null -w "%{http_code}" "http://localhost:$port" 2>/dev/null | grep -q "200\|304"; then
        echo "running"
    elif ss -tln 2>/dev/null | grep -q ":$port "; then
        echo "listening"
    else
        echo "stopped"
    fi
}

get_notify_state() {
    if [ -f "$NOTIFY_FILE" ] && grep -q "^$SESSION_BASE=on$" "$NOTIFY_FILE" 2>/dev/null; then
        echo "on"
    else
        echo "off"
    fi
}

toggle_notify() {
    mkdir -p "$(dirname $NOTIFY_FILE)"
    if [ "$(get_notify_state)" = "on" ]; then
        sed -i "/^$SESSION_BASE=/d" "$NOTIFY_FILE" 2>/dev/null
        echo "off"
    else
        echo "$SESSION_BASE=on" >> "$NOTIFY_FILE"
        echo "on"
    fi
}

get_bg_tasks() {
    pgrep -c -f "claude.*background" 2>/dev/null || echo "0"
}


next_session_name() {
    # First session: no suffix
    if ! tmux has-session -t "$SESSION_BASE" 2>/dev/null; then
        echo "$SESSION_BASE"
        return
    fi
    # Find next available number (skip existing names)
    local i=2
    while tmux has-session -t "${SESSION_BASE}-${i}" 2>/dev/null; do
        ((i++))
    done
    echo "${SESSION_BASE}-${i}"
}

# Attach or switch depending on context
attach_session() {
    local name="$1"
    if [ -n "$TMUX" ]; then
        # Already inside tmux - switch client
        tmux switch-client -t "$name"
    else
        # Outside tmux - attach
        tmux attach -t "$name"
    fi
}

create_session_bare() {
    local name="$1"
    tmux new-session -d -s "$name" -c "$PROJECT_PATH"
    echo "$name"
}

# Sous-menu NEW
show_new_menu() {
    clear
    echo ""
    echo -e "  ${BOLD}New Session${RESET}"
    echo ""
    echo -e "  ${YELLOW}1${RESET}) Claude - Fresh"
    echo -e "  ${YELLOW}2${RESET}) Claude - Resume"
    echo -e "  ${YELLOW}3${RESET}) Shell only"
    echo ""
    echo -e "  ${YELLOW}q${RESET}) Back"
    echo ""
}

handle_new() {
    while true; do
        show_new_menu
        echo -n "  > "
        read -r choice
        case "$choice" in
            1)
                local name=$(create_session_bare "$(next_session_name)")
                tmux send-keys -t "$name" "claude --dangerously-skip-permissions" Enter
                attach_session "$name"
                exit 0
                ;;
            2)
                local name=$(create_session_bare "$(next_session_name)")
                tmux send-keys -t "$name" "claude --dangerously-skip-permissions --resume" Enter
                attach_session "$name"
                exit 0
                ;;
            3)
                local name=$(create_session_bare "$(next_session_name)")
                attach_session "$name"
                exit 0
                ;;
            q|Q|"") return ;;
        esac
    done
}

# Sous-menu DEV SERVER
show_dev_menu() {
    clear
    local port=$(get_port)
    local status=$(get_dev_server_status)

    echo ""
    echo -e "  ${BOLD}Dev Server${RESET}"
    echo -e "  Port: ${YELLOW}$port${RESET} | Status: "
    case "$status" in
        running) echo -e "${GREEN}running${RESET}" ;;
        listening) echo -e "${YELLOW}listening${RESET}" ;;
        stopped) echo -e "${RED}stopped${RESET}" ;;
        *) echo -e "${DIM}no port configured${RESET}" ;;
    esac
    echo ""
    echo -e "  ${YELLOW}1${RESET}) Start (npm run dev)"
    echo -e "  ${YELLOW}2${RESET}) Start background (nohup)"
    echo -e "  ${YELLOW}3${RESET}) Clean + Restart"
    echo -e "  ${YELLOW}4${RESET}) Stop (kill port)"
    echo -e "  ${YELLOW}5${RESET}) View logs"
    echo ""
    echo -e "  ${YELLOW}q${RESET}) Back"
    echo ""
}

handle_dev() {
    local port=$(get_port)
    while true; do
        show_dev_menu
        echo -n "  > "
        read -r choice
        case "$choice" in
            1)
                local name=$(create_session_bare "$(next_session_name)")
                tmux send-keys -t "$name" "PORT=$port npm run dev" Enter
                attach_session "$name"
                exit 0
                ;;
            2)
                cd "$PROJECT_PATH"
                PORT=$port nohup npm run dev > /tmp/${SESSION_BASE,,}-dev.log 2>&1 &
                echo "  Started in background. PID: $!"
                sleep 2
                ;;
            3)
                echo "  Cleaning cache..."
                cd "$PROJECT_PATH"
                rm -rf node_modules/.cache .next/cache .turbo 2>/dev/null
                lsof -ti:$port | xargs kill -9 2>/dev/null
                sleep 1
                PORT=$port nohup npm run dev > /tmp/${SESSION_BASE,,}-dev.log 2>&1 &
                echo "  Restarted. PID: $!"
                sleep 2
                ;;
            4)
                lsof -ti:$port | xargs kill -9 2>/dev/null
                echo "  Stopped"
                sleep 1
                ;;
            5)
                local name=$(create_session_bare "$(next_session_name)")
                tmux send-keys -t "$name" "tail -f /tmp/${SESSION_BASE,,}-dev.log" Enter
                attach_session "$name"
                exit 0
                ;;
            q|Q|"") return ;;
        esac
    done
}

# Sous-menu INIT
show_init_menu() {
    clear
    echo ""
    echo -e "  ${BOLD}Init Project${RESET}"
    echo ""

    # Check existing files
    local has_claude_md="no"
    local has_claude_dir="no"
    [ -f "$PROJECT_PATH/CLAUDE.md" ] && has_claude_md="yes"
    [ -d "$PROJECT_PATH/.claude" ] && has_claude_dir="yes"

    echo -e "  CLAUDE.md: $([ "$has_claude_md" = "yes" ] && echo -e "${GREEN}exists${RESET}" || echo -e "${RED}missing${RESET}")"
    echo -e "  .claude/: $([ "$has_claude_dir" = "yes" ] && echo -e "${GREEN}exists${RESET}" || echo -e "${RED}missing${RESET}")"
    echo ""
    echo -e "  ${YELLOW}1${RESET}) Init New (create CLAUDE.md from template)"
    echo -e "  ${YELLOW}2${RESET}) Init Refresh (update existing, re-index)"
    echo -e "  ${YELLOW}3${RESET}) View CLAUDE.md"
    echo ""
    echo -e "  ${YELLOW}q${RESET}) Back"
    echo ""
}

handle_init() {
    while true; do
        show_init_menu
        echo -n "  > "
        read -r choice
        case "$choice" in
            1)
                if [ ! -f "$PROJECT_PATH/CLAUDE.md" ]; then
                    cat > "$PROJECT_PATH/CLAUDE.md" << 'TEMPLATE'
# CLAUDE.md

## Project Overview

[Description du projet]

## Tech Stack

- Framework:
- Database:
- Auth:

## Development

```bash
npm run dev

TEMPLATE echo " CLAUDE.md created" else echo " CLAUDE.md already exists" fi

            mkdir -p "$PROJECT_PATH/.claude/rules"
            echo "  .claude/ directory created"
            sleep 2
            ;;
        2)
            echo "  Refreshing project context..."
            mkdir -p "$PROJECT_PATH/.claude/rules"
            echo "  Done. Run /init inside Claude to complete."
            sleep 2
            ;;
        3)
            if [ -f "$PROJECT_PATH/CLAUDE.md" ]; then
                less "$PROJECT_PATH/CLAUDE.md"
            else
                echo "  CLAUDE.md not found"
                sleep 1
            fi
            ;;
        q|Q|"") return ;;
    esac
done

}

Sous-menu BACKGROUND TASKS

show_bg_menu() { clear echo "" echo -e " ${BOLD}Background Tasks${RESET}" echo ""

local tasks=$(ps aux | grep -E "claude.*background|claude.*-b" | grep -v grep)
if [ -n "$tasks" ]; then
    echo "$tasks" | while read line; do
        local pid=$(echo "$line" | awk '{print $2}')
        local cmd=$(echo "$line" | awk '{for(i=11;i<=NF;i++) printf $i" "}')
        echo -e "  ${YELLOW}$pid${RESET} $cmd"
    done
else
    echo -e "  ${DIM}No background tasks${RESET}"
fi

echo ""
echo -e "  ${YELLOW}k${RESET}) Kill a task (enter PID)"
echo -e "  ${YELLOW}a${RESET}) Kill all background tasks"
echo ""
echo -e "  ${YELLOW}q${RESET}) Back"
echo ""

}

handle_bg() { while true; do show_bg_menu echo -n " > " read -r choice case "$choice" in k|K) echo -n " PID to kill: " read -r pid kill $pid 2>/dev/null && echo " Killed $pid" || echo " Failed" sleep 1 ;; a|A) pkill -f "claude.background" 2>/dev/null pkill -f "claude.-b" 2>/dev/null echo " All background tasks killed" sleep 1 ;; q|Q|"") return ;; esac done }

show_menu() { clear local git_info=$(get_git_info) local branch=$(echo "$git_info" | cut -d'|' -f1) local dirty=$(echo "$git_info" | cut -d'|' -f2 | tr -d '\n' | grep -o '[0-9]') local ahead=$(echo "$git_info" | cut -d'|' -f3 | tr -d '\n' | grep -o '[0-9]') local behind=$(echo "$git_info" | cut -d'|' -f4 | tr -d '\n' | grep -o '[0-9]*') local push_time=$(echo "$git_info" | cut -d'|' -f5 | tr -d '\n') dirty=${dirty:-0} ahead=${ahead:-0} behind=${behind:-0}

local ram=$(get_ram)
local cpu=$(get_cpu)
local port=$(get_port)
local dev_status=$(get_dev_server_status)
local notify=$(get_notify_state)
local bg_count=$(get_bg_tasks)

# Header
echo ""
echo -e "  ${BOLD}$SESSION_BASE${RESET}"

# Git line
if [ -n "$branch" ]; then
    printf "  %s" "$branch"
    [ "$dirty" -gt 0 ] && printf "*"
    [ "$ahead" -gt 0 ] && printf " ${GREEN}+$ahead${RESET}"
    [ "$behind" -gt 0 ] && printf " ${RED}-$behind${RESET}"
    [ -n "$push_time" ] && printf " ${DIM}push:$push_time${RESET}"
    echo ""
fi

# Stats line
local stats_line="  RAM $ram ${DIM}|${RESET} CPU $cpu"

if [ "$NO_CLAUDE" = false ]; then
    # Project mode: show port and dev status
    [ "$port" -gt 0 ] 2>/dev/null && stats_line+=" ${DIM}|${RESET} :$port "
    case "$dev_status" in
        running) stats_line+="${GREEN}up${RESET}" ;;
        listening) stats_line+="${YELLOW}listen${RESET}" ;;
        stopped) stats_line+="${RED}down${RESET}" ;;
    esac
    local bg_num=$(echo "$bg_count" | tr -d '\n' | grep -o '[0-9]*' || echo "0")
    [ "$bg_num" -gt 0 ] 2>/dev/null && stats_line+=" ${DIM}|${RESET} BG:$bg_num"
    stats_line+=" ${DIM}|${RESET} Notif:$notify"
fi

echo -e "$stats_line"
echo ""

# Sessions
local session_list=$(get_sessions)
if [ -n "$session_list" ]; then
    local i=1
    while IFS='|' read -r name created windows; do
        [ -z "$name" ] && continue
        local time=$(date -d "@$created" "+%H:%M" 2>/dev/null || echo "??:??")
        echo -e "  ${YELLOW}$i${RESET}) $name ${DIM}$time${RESET}"
        ((i++))
    done <<< "$session_list"
else
    echo -e "  ${DIM}~ nothing open ~${RESET}"
fi

echo ""
echo -e "  ${DIM}─────${RESET}"
echo ""

if [ "$NO_CLAUDE" = true ]; then
    # HOME MODE - simplified menu
    printf "  ${YELLOW}n${RESET} %-10s ${YELLOW}d${RESET} %-10s ${YELLOW}k${RESET} %-10s\n" "New" "Delete" "Kill all"
    printf "  ${YELLOW}u${RESET} %-10s ${YELLOW}q${RESET} %-10s\n" "Usage" "Quit"
    printf "  ${YELLOW}c${RESET} %-10s ${YELLOW}x${RESET} %-10s\n" "Clean" "Nuclear"
else
    # PROJECT MODE - full menu
    printf "  ${YELLOW}n${RESET} %-10s ${YELLOW}d${RESET} %-10s ${YELLOW}k${RESET} %-10s\n" "New" "Delete" "Kill all"

    if [ -n "$branch" ]; then
        printf "  ${YELLOW}p${RESET} %-10s ${YELLOW}g${RESET} %-10s ${YELLOW}s${RESET} %-10s\n" "Pull" "Push" "Sync"
    fi

    printf "  ${YELLOW}v${RESET} %-10s ${YELLOW}i${RESET} %-10s ${YELLOW}b${RESET} %-10s\n" "Dev" "Init" "Background"
    printf "  ${YELLOW}t${RESET} %-10s ${YELLOW}u${RESET} %-10s ${YELLOW}q${RESET} %-10s\n" "Notif $notify" "Usage" "Quit"
    printf "  ${YELLOW}c${RESET} %-10s ${YELLOW}x${RESET} %-10s\n" "Clean" "Nuclear"
fi
echo ""

}

main() { while true; do show_menu echo -n " > " read -r choice

    case "$choice" in
        ""|1)
            local first=$(get_sessions | head -1 | cut -d'|' -f1)
            if [ -n "$first" ]; then
                attach_session "$first"
                exit 0
            else
                handle_new
            fi
            ;;
        [0-9]*)
            if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -gt 1 ]; then
                local target=$(get_sessions | sed -n "${choice}p" | cut -d'|' -f1)
                if [ -n "$target" ]; then
                    attach_session "$target"
                    exit 0
                fi
            fi
            ;;
        n|N) handle_new ;;
        n1|N1)
            local name=$(create_session_bare "$(next_session_name)")
            tmux send-keys -t "$name" "claude --dangerously-skip-permissions" Enter
            attach_session "$name"
            exit 0
            ;;
        n2|N2)
            local name=$(create_session_bare "$(next_session_name)")
            tmux send-keys -t "$name" "claude --dangerously-skip-permissions --resume" Enter
            attach_session "$name"
            exit 0
            ;;
        n3|N3)
            local name=$(create_session_bare "$(next_session_name)")
            attach_session "$name"
            exit 0
            ;;
        d|D)
            echo -n "  delete #: "
            read -r num
            local target=$(get_sessions | sed -n "${num}p" | cut -d'|' -f1)
            if [ -n "$target" ]; then
                tmux kill-session -t "$target"
            fi
            ;;
        k|K)
            get_sessions | cut -d'|' -f1 | xargs -I{} tmux kill-session -t {}
            if [ "$NO_CLAUDE" = true ]; then
                echo "  Killed ALL sessions"
            else
                echo "  Killed $SESSION_BASE sessions"
            fi
            sleep 1
            ;;
        p|P)
            if [ "$NO_CLAUDE" = false ]; then
                cd "$PROJECT_PATH" && git pull 2>/dev/null
                sleep 1
            fi
            ;;
        g|G)
            cd "$PROJECT_PATH"
            echo -n "  commit msg: "
            read -r msg
            git add -A && git commit -m "$msg" && git push
            sleep 1
            ;;
        s|S)
            cd "$PROJECT_PATH" && git pull && git push
            sleep 1
            ;;
        v|V) handle_dev ;;
        i|I) handle_init ;;
        b|B) handle_bg ;;
        t|T)
            local new_state=$(toggle_notify)
            echo "  Notifications: $new_state"
            sleep 1
            ;;
        c|C)
            echo "  Cleaning..."
            sync
            echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1
            pkill -f "node.*orphan" 2>/dev/null
            npm cache clean --force 2>/dev/null
            echo "  Done"
            sleep 1
            ;;
        x|X)
            if [ "$NO_CLAUDE" = true ]; then
                echo "  [1/4] Killing Claude..."
                pkill -f "claude" 2>/dev/null
                echo "  [2/4] Killing ALL sessions..."
                tmux kill-server 2>/dev/null || true
                echo "  [3/4] Cleaning caches..."
                npm cache clean --force 2>/dev/null
                bun pm cache rm 2>/dev/null
                rm -rf /tmp/claude-* /tmp/bun-* 2>/dev/null
                echo "  [4/4] Freeing RAM..."
                sync
                echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1
                echo "  NUKED"
                sleep 1
            else
                get_sessions | cut -d'|' -f1 | xargs -I{} tmux kill-session -t {} 2>/dev/null
                cd "$PROJECT_PATH"
                rm -rf node_modules/.cache .next/cache .turbo .eslintcache tsconfig.tsbuildinfo 2>/dev/null
                sync
                echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1
                echo "  Nuked"
                sleep 1
            fi
            ;;
        u|U)
            clear
            echo ""
            echo -e "  ${BOLD}Usage${RESET}"
            echo ""
            local claude_procs=$(pgrep -c -f "claude" 2>/dev/null || echo "0")
            echo -e "  Claude processes: ${YELLOW}$claude_procs${RESET}"
            local account="-"
            if [ -f ~/.claude/.pool-status.json ]; then
                account=$(cat ~/.claude/.pool-status.json | jq -r '.current' 2>/dev/null || echo "-")
            fi
            echo -e "  Account: ${YELLOW}$account${RESET}"
            echo -e "  RAM: ${YELLOW}$(get_ram)${RESET}"
            echo -e "  CPU: ${YELLOW}$(get_cpu)${RESET}"
            local ts_count=$(tmux list-sessions 2>/dev/null | wc -l)
            echo -e "  Tmux sessions: ${YELLOW}$ts_count${RESET}"
            echo ""
            echo -e "  ${DIM}For Claude usage: run /usage inside Claude${RESET}"
            echo ""
            echo -n "  Press Enter..."
            read
            ;;
        q|Q)
            exit 0
            ;;
    esac
done

}

main


Make executable: `chmod +x ~/.local/bin/tmux-project`

---

## PHASE 7: TMUX-SELECT (global session picker)

Write `~/.local/bin/tmux-select` with this EXACT content:

```bash
#!/bin/bash
# tmux-select v3 - Menu CLI clean
# Usage: ts

YELLOW='\033[1;33m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'

categorize() {
    local path="$1"
    if [[ "$path" == *"/VibeCoding/projects/"* ]]; then
        echo "projects"
    elif [[ "$path" == "$HOME" ]] || [[ "$path" == "$HOME/" ]]; then
        echo "system"
    else
        echo "other"
    fi
}

get_stats() {
    local total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    local available=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    local ram="$((($total - $available) * 100 / $total))%"

    read cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
    local cpu_total=$((user + nice + system + idle + iowait + irq + softirq + steal))
    local cpu_used=$((cpu_total - idle - iowait))
    local cpu="$((cpu_used * 100 / cpu_total))%"

    echo "$ram|$cpu"
}

get_branch() {
    local path="$1"
    if [ -d "$path/.git" ]; then
        cd "$path"
        local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
        local dirty=""
        [ -n "$(git status --porcelain 2>/dev/null)" ] && dirty="*"
        echo "${branch}${dirty}"
    fi
}

# Attach or switch depending on context
attach_session() {
    local name="$1"
    if [ -n "$TMUX" ]; then
        tmux switch-client -t "$name"
    else
        tmux attach -t "$name"
    fi
}

show_menu() {
    clear
    local stats=$(get_stats)
    local ram=$(echo "$stats" | cut -d'|' -f1)
    local cpu=$(echo "$stats" | cut -d'|' -f2)

    echo ""
    echo -e "  ${BOLD}Sessions${RESET} ${DIM}|${RESET} RAM $ram ${DIM}|${RESET} CPU $cpu"
    echo ""

    local sessions=$(tmux list-sessions -F "#{session_name}|#{session_created}|#{pane_current_path}" 2>/dev/null)

    if [ -z "$sessions" ]; then
        echo -e "  ${DIM}no active sessions${RESET}"
        echo ""
    else
        local i=1
        local first_session=""
        local current_cat=""

        # Sort by category
        for cat in "projects" "system" "other"; do
            local has_cat=0

            while IFS='|' read -r name created path; do
                [ -z "$name" ] && continue
                local sess_cat=$(categorize "$path")
                [ "$sess_cat" != "$cat" ] && continue

                if [ $has_cat -eq 0 ]; then
                    echo -e "  ${DIM}$cat${RESET}"
                    has_cat=1
                fi

                local time=$(date -d "@$created" "+%H:%M" 2>/dev/null || echo "??:??")
                local branch=$(get_branch "$path")

                if [ -z "$first_session" ]; then
                    first_session="$name"
                fi

                printf "  ${YELLOW}%d${RESET}) %-14s %-10s ${DIM}%s${RESET}\n" "$i" "$name" "$branch" "$time"
                ((i++))
            done <<< "$sessions"

            [ $has_cat -eq 1 ] && echo ""
        done

        echo -e "  ${DIM}Enter: attach $first_session${RESET}"
        echo "$first_session" > /tmp/ts_first_session
    fi

    echo ""
    echo -e "  ${DIM}─────${RESET}"
    echo ""
    printf "  ${YELLOW}d${RESET} %-10s ${YELLOW}k${RESET} %-10s ${YELLOW}c${RESET} %-10s\n" "Delete" "Kill all" "Clean RAM"
    printf "  ${YELLOW}r${RESET} %-10s ${YELLOW}u${RESET} %-10s ${YELLOW}q${RESET} %-10s\n" "Refresh" "Usage" "Quit"
    echo ""
}

main() {
    while true; do
        show_menu
        echo -n "  > "
        read -r choice

        local first_session=$(cat /tmp/ts_first_session 2>/dev/null)
        local all_sessions=$(tmux list-sessions -F "#{session_name}" 2>/dev/null)

        case "$choice" in
            "")
                if [ -n "$first_session" ]; then
                    attach_session "$first_session"
                    exit 0
                fi
                ;;
            [0-9]*)
                if [[ "$choice" =~ ^[0-9]+$ ]]; then
                    local target=$(echo "$all_sessions" | sed -n "${choice}p")
                    if [ -n "$target" ]; then
                        attach_session "$target"
                        exit 0
                    fi
                fi
                ;;
            d|D)
                echo -n "  delete #: "
                read -r num
                local target=$(echo "$all_sessions" | sed -n "${num}p")
                if [ -n "$target" ]; then
                    tmux kill-session -t "$target"
                fi
                ;;
            k|K)
                tmux kill-server 2>/dev/null
                exit 0
                ;;
            c|C)
                sync
                echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1
                ;;
            r|R)
                ;;
            u|U)
                clear
                echo ""
                echo -e "  ${BOLD}Usage${RESET}"
                echo ""
                local claude_procs=$(pgrep -c -f "claude" 2>/dev/null || echo "0")
                echo -e "  Claude processes: ${YELLOW}$claude_procs${RESET}"
                local account="-"
                if [ -f ~/.claude/.pool-status.json ]; then
                    account=$(cat ~/.claude/.pool-status.json | jq -r '.current' 2>/dev/null || echo "-")
                fi
                echo -e "  Account: ${YELLOW}$account${RESET}"
                local stats=$(get_stats)
                echo -e "  RAM: ${YELLOW}$(echo $stats | cut -d'|' -f1)${RESET}"
                echo -e "  CPU: ${YELLOW}$(echo $stats | cut -d'|' -f2)${RESET}"
                local ts_count=$(tmux list-sessions 2>/dev/null | wc -l)
                echo -e "  Tmux sessions: ${YELLOW}$ts_count${RESET}"
                echo ""
                echo -e "  ${DIM}For Claude usage: run /usage inside Claude${RESET}"
                echo ""
                echo -n "  Press Enter..."
                read
                ;;
            q|Q)
                exit 0
                ;;
        esac
    done
}

main

Make executable: chmod +x ~/.local/bin/tmux-select


PHASE 8: PORTS CONFIG

Write ~/.config/tmux-claude/ports.conf with one line per project from the wizard:

# Project=Port
${PROJECT_DISPLAY_NAME}=${PORT}

Example for DentistryGPT with projects website (3001), app (3002), dashboard (3003):

Website=3001
App=3002
Dashboard=3003

The display name MUST match the session name used in the tmux-project aliases.


PHASE 9: ZSH CONFIG

Write ~/.zshrc. Here is a complete example for a client called "DentistryGPT" with alias "dent" and projects website/app/dashboard:

# === ZSH CONFIG - ${CLIENT_NAME} VPS ===

# History
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt appendhistory sharehistory histignoredups

# Completion
autoload -Uz compinit && compinit

# Prompt
PROMPT='%n@%m:%~$ '

# Colors
autoload -U colors && colors

# === NVM ===
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
export PATH="$HOME/.local/bin:$PATH"

# === KEYBOARD NAVIGATION (Mac SSH) ===
bindkey "^[[1;3D" backward-word      # Option + Left Arrow
bindkey "^[[1;3C" forward-word       # Option + Right Arrow
bindkey "\e[1;9D" backward-word      # iTerm2
bindkey "\e[1;9C" forward-word       # iTerm2
bindkey "\eb" backward-word          # ESC + b
bindkey "\ef" forward-word           # ESC + f
bindkey "^[[1;5D" backward-word      # Ctrl + Left
bindkey "^[[1;5C" forward-word       # Ctrl + Right
bindkey "^[[3;3~" kill-word          # Option + Delete
bindkey "\e^?" backward-kill-word    # Option + Backspace

# === NAVIGATION ===
alias vibe='cd ~/VibeCoding'
alias projects='cd ~/VibeCoding/projects'

# === GIT ===
alias gs='git status'
alias ga='git add -A'
alias gc='git commit -m'
alias gp='git push'
alias save='git add -A && git commit -m "WIP: $(date +%Y-%m-%d\ %H:%M)" && git push'

# === DEV ===
alias ports='netstat -tulpn | grep LISTEN'
alias ram='free -h'
alias procs='ps aux | grep node'

# === SAFETY ===
alias rm='rm -i'
alias cp='cp -i'
alias mv='mv -i'

# === LS ===
alias ls='ls --color=auto'
alias ll='ls -alF'
alias la='ls -A'
alias grep='grep --color=auto'

# === CLAUDE CODE ===
alias cc='claude'
alias claude='claude --dangerously-skip-permissions'
export DISPLAY=:99

# === Bun ===
[ -s "$HOME/.bun/_bun" ] && source "$HOME/.bun/_bun"
export BUN_INSTALL="$HOME/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"

# ═══════════════════════════════════════════════════════════════
# TMUX SESSIONS - Interactive project menus
# ═══════════════════════════════════════════════════════════════

# Global
alias ts='tmux-select'
alias tps='tmux ls 2>/dev/null || echo "No sessions"'

# Projects (GENERATED FROM WIZARD)
# Format: alias c-ALIAS='tmux-project DISPLAY_NAME ~/VibeCoding/projects/DIR_NAME'
# === REPLACE THESE WITH YOUR ACTUAL PROJECTS ===
# alias c-site='tmux-project Website ~/VibeCoding/projects/website'
# alias c-app='tmux-project App ~/VibeCoding/projects/app'
# alias c-dash='tmux-project Dashboard ~/VibeCoding/projects/dashboard'

# Home (no Claude)
alias c-home='tmux-project Home $HOME --no-claude'

# ═══════════════════════════════════════════════════════════════
# X-KILL - Nuclear: ALL Claude + ALL tmux + clean RAM
# ═══════════════════════════════════════════════════════════════
x-kill() {
    local RED='\033[1;31m'
    local GREEN='\033[1;32m'
    local YELLOW='\033[1;33m'
    local DIM='\033[2m'
    local BOLD='\033[1m'
    local RESET='\033[0m'

    echo ""
    echo -e "  ${RED}${BOLD}☠  X-KILL - TOTAL ANNIHILATION  ☠${RESET}"
    echo ""

    local claude_count=$(pgrep -c -f "claude" 2>/dev/null || echo "0")
    echo -e "  ${YELLOW}[1/6]${RESET} Killing Claude processes... ${DIM}($claude_count found)${RESET}"
    pkill -9 -f "claude" 2>/dev/null
    sleep 1

    local node_count=$(pgrep -c -f "node" 2>/dev/null || echo "0")
    echo -e "  ${YELLOW}[2/6]${RESET} Killing Node/Bun processes... ${DIM}($node_count found)${RESET}"
    pkill -9 -f "next-server" 2>/dev/null
    pkill -9 -f "next dev" 2>/dev/null
    pkill -9 -f "npm run dev" 2>/dev/null
    pkill -9 -f "vite" 2>/dev/null
    pkill -9 -f "expo start" 2>/dev/null
    pkill -9 -f "convex dev" 2>/dev/null

    local tmux_count=$(tmux list-sessions 2>/dev/null | wc -l)
    echo -e "  ${YELLOW}[3/6]${RESET} Killing ALL tmux sessions... ${DIM}($tmux_count sessions)${RESET}"
    tmux kill-server 2>/dev/null

    echo -e "  ${YELLOW}[4/6]${RESET} Cleaning project caches..."
    find ~/VibeCoding -maxdepth 4 -type d -name ".next" -exec rm -rf {}/cache \; 2>/dev/null
    find ~/VibeCoding -maxdepth 4 -type d -name ".turbo" -exec rm -rf {} + 2>/dev/null

    echo -e "  ${YELLOW}[5/6]${RESET} Cleaning system caches..."
    npm cache clean --force 2>/dev/null
    rm -rf /tmp/claude-* /tmp/bun-* /tmp/next-* /tmp/turbo-* 2>/dev/null

    echo -e "  ${YELLOW}[6/6]${RESET} Freeing RAM..."
    sync
    echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null 2>&1

    sleep 1
    local ram_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    local ram_avail=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    local ram_pct=$(( ($ram_total - $ram_avail) * 100 / $ram_total ))
    local ram_free_gb=$(( $ram_avail / 1024 / 1024 ))

    echo ""
    echo -e "  ${GREEN}${BOLD}☠  ANNIHILATED  ☠${RESET}"
    echo ""
    echo -e "  RAM: ${GREEN}${ram_pct}%${RESET} used (${ram_free_gb}GB free)"
    echo -e "  Claude: ${GREEN}$(pgrep -c -f 'claude' 2>/dev/null || echo '0')${RESET} processes"
    echo -e "  Node: ${GREEN}$(pgrep -c -f 'node' 2>/dev/null || echo '0')${RESET} processes"
    echo -e "  Tmux: ${GREEN}$(tmux list-sessions 2>/dev/null | wc -l || echo '0')${RESET} sessions"
    echo ""
}

IMPORTANT: Uncomment and fill in the project aliases based on wizard answers. For each project:

alias c-${ALIAS}='tmux-project ${DISPLAY_NAME} ~/VibeCoding/projects/${DIR_NAME}'

PHASE 10: CLAUDE CODE CONFIG

~/CLAUDE.md

Write a global CLAUDE.md with:

  • Project name, VPS IP, SSH port, username
  • Table of all projects with ports and dev URLs
  • Git config (email, account)
  • Table of available commands: ts, tps, c-<alias> for each project, x-kill
  • Key paths: ~/VibeCoding/projects/, ~/.claude/, ~/.tmux/

~/.claude/rules/00-server-rules.md

# Server Rules

- Shell: ZSH (not bash). Config: ~/.zshrc
- User: ALWAYS use current user, NEVER root
- SSH port: ${SSH_PORT}
- VPS IP: ${VPS_IP}
- Projects: ~/VibeCoding/projects/

~/.claude/rules/01-project-ports.md

Table of all projects with their ports and dev URLs.

Git config

git config --global user.email "${GIT_EMAIL}"
git config --global user.name "${GIT_ACCOUNT}"
git config --global init.defaultBranch main
git config --global pull.rebase false

PHASE 11: TELEGRAM (skip if user said "skip")

Write ~/.claude/config/telegram.json and ~/.claude/lib/telegram.sh with send/notify/file commands. Symlink: ln -sf ~/.claude/lib/telegram.sh ~/.local/bin/telegram


PHASE 12: VERIFY

tmux -V
test -f ~/.tmux.conf && echo "tmux.conf OK"
ls ~/.tmux/scripts/*.sh | wc -l  # Should be >= 13
test -x ~/.local/bin/tmux-project && echo "tmux-project OK"
test -x ~/.local/bin/tmux-select && echo "tmux-select OK"
test -f ~/CLAUDE.md && echo "CLAUDE.md OK"
ls ~/.claude/rules/*.md | wc -l
tmux new-session -d -s _test && tmux kill-session -t _test && echo "Tmux works"
source ~/.zshrc

Show a summary of all shortcuts and what they do.


EXAMPLE: DentistryGPT Setup

For a client called DentistryGPT with alias "dent", projects: website (3001), api (4001), dashboard (3003):

What gets created:

Aliases in ~/.zshrc:

alias c-site='tmux-project Website ~/VibeCoding/projects/website'
alias c-api='tmux-project Api ~/VibeCoding/projects/api'
alias c-dash='tmux-project Dashboard ~/VibeCoding/projects/dashboard'
alias c-home='tmux-project Home $HOME --no-claude'

Ports in ~/.config/tmux-claude/ports.conf:

Website=3001
Api=4001
Dashboard=3003

What c-site shows:

  Website
  main* +2 push:3h

  RAM 45% | CPU 12% | :3001 up | Notif:off

  1) Website      14:30
  2) Website-2    15:12

  ─────

  n New        d Delete     k Kill all
  p Pull       g Push       s Sync
  v Dev        i Init       b Background
  t Notif off  u Usage      q Quit
  c Clean      x Nuclear

  >

What ts shows:

  Sessions | RAM 45% | CPU 12%

  projects
  1) Website       main*      14:30
  2) Website-2     main*      15:12
  3) Dashboard     dev        13:00

  system
  4) Home          -          10:00

  ─────

  d Delete     k Kill all   c Clean RAM
  r Refresh    u Usage      q Quit

  >

CRITICAL RULES

  1. Write EVERY file with the Write tool. NOT with echo/heredoc in bash.
  2. ALL scripts in ~/.tmux/scripts/ and ~/.local/bin/ MUST be chmod +x.
  3. Replace ALL ${VARIABLE} placeholders with actual wizard values.
  4. The session-manager-v2.sh dynamically scans ~/VibeCoding/projects/.
  5. tmux-project and tmux-select are COMPLETE scripts (not summaries). Write them EXACTLY as shown above.
  6. Test tmux launches correctly at the end.
  7. EVERY file shown in this document must be created. Do not skip any.