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.
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.
sudo apt update -qq && sudo apt install -y -qq tmux fzf jq htop zshIf 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-codemkdir -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/binFor each project from the wizard:
mkdir -p ~/VibeCoding/projects/${PROJECT_NAME}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 8Write ALL these files in ~/.tmux/scripts/. Every single one. Make ALL executable at the end with chmod +x ~/.tmux/scripts/*.sh.
#!/bin/bash
TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
echo "$(( (TOTAL - AVAILABLE) * 100 / TOTAL ))%"#!/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))%"#!/bin/bash
echo "$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')%"#!/bin/bash
tmux list-sessions 2>/dev/null | wc -l#!/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 "-"#!/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#!/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#!/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"#!/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#!/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#!/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#!/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#!/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#!/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
donechmod +x ~/.tmux/scripts/*.shWrite ~/.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 devTEMPLATE 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
}
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
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.
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}'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/
# 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/Table of all projects with their ports and dev URLs.
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 falseWrite ~/.claude/config/telegram.json and ~/.claude/lib/telegram.sh with send/notify/file commands. Symlink: ln -sf ~/.claude/lib/telegram.sh ~/.local/bin/telegram
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 ~/.zshrcShow a summary of all shortcuts and what they do.
For a client called DentistryGPT with alias "dent", projects: website (3001), api (4001), dashboard (3003):
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
>
- Write EVERY file with the Write tool. NOT with echo/heredoc in bash.
- ALL scripts in ~/.tmux/scripts/ and ~/.local/bin/ MUST be
chmod +x. - Replace ALL ${VARIABLE} placeholders with actual wizard values.
- The session-manager-v2.sh dynamically scans ~/VibeCoding/projects/.
- tmux-project and tmux-select are COMPLETE scripts (not summaries). Write them EXACTLY as shown above.
- Test tmux launches correctly at the end.
- EVERY file shown in this document must be created. Do not skip any.