diff --git a/linux_configuration/zsh/claude-tty-fix.zsh b/linux_configuration/zsh/claude-tty-fix.zsh index af23e08c..e686edad 100644 --- a/linux_configuration/zsh/claude-tty-fix.zsh +++ b/linux_configuration/zsh/claude-tty-fix.zsh @@ -29,6 +29,20 @@ # Opt-out: `export CLAUDE_TTY_FIX_OFF=1` (falls back to the bare binary). # ============================================================================ +# ---------------------------------------------------------------------------- +# Safety net for the SIGTTIN-suspend case: when claude is *stopped* (not exited), +# the reset at the end of claude() below can NEVER run — the function is blocked +# inside `command claude`. So a suspended/killed claude leaves mouse tracking on +# and the terminal spews SGR mouse reports (e.g. `35;48;12M`). This precmd hook +# disables mouse tracking before every prompt, so the garble clears on your next +# Enter regardless of how claude died. Emits ONLY invisible mouse/cursor escapes +# (NOT bracketed-paste 2004, which zsh needs) so it can't disturb normal paste. +_claude_mouse_heal() { + [[ -t 2 ]] || return + printf '\e[?1000l\e[?1002l\e[?1003l\e[?1006l\e[?25h' > /dev/tty 2>/dev/null +} +autoload -Uz add-zsh-hook 2>/dev/null && add-zsh-hook precmd _claude_mouse_heal + claude() { # Escape sequences that undo the terminal state Claude may leave enabled if it # is stopped/killed before its own teardown runs: @@ -44,7 +58,23 @@ claude() { # `localoptions` confines the option change to this function; `nomonitor` # disables job control so claude shares the shell's (foreground) process group. setopt localoptions nomonitor - command claude "$@" + + # Always-on debug capture (turn off: `export CLAUDE_DEBUG_LOG_OFF=1`). The + # trace goes to a FILE, not the screen, so the next real startup-race failure + # is recorded faithfully — same wrapper, same env, same nomonitor, no + # `script` surrogate to mask the job-control layer. Keeps the last ~20 logs. + local _dbg="" + if [[ -z "${CLAUDE_DEBUG_LOG_OFF:-}" ]]; then + mkdir -p ~/.claude/logs 2>/dev/null + _dbg=~/.claude/logs/claude-$(date +%Y%m%d-%H%M%S)-$$.log + for f in ~/.claude/logs/claude-*.log(NOm[21,-1]); do rm -f -- "$f"; done + fi + + if [[ -n "$_dbg" ]]; then + command claude --debug-file "$_dbg" "$@" + else + command claude "$@" + fi local -r rc=$? # Only heal a real terminal, and write the reset to /dev/tty (the controlling @@ -56,3 +86,44 @@ claude() { fi return $rc } + +# ============================================================================ +# On-demand MCP servers — the niche fleet is NOT loaded every session. +# ---------------------------------------------------------------------------- +# Only ~9 broadly-useful servers live in ~/.claude.json and start every time. +# The niche ones live in ~/.claude/mcp-optional/*.json and load only when you +# ask for them via `claude --mcp-config ` (the flag MERGES them into the +# global set for that one session). These wrappers call the `claude` FUNCTION +# above, so an on-demand session still gets the TTY fix + self-heal. +# claude-rag knowledge-rag + memstack-skills (data dirs pinned, no cwd litter) +# claude-travel wander-agent + airbnb +# claude-sports sports-hub + sportscore + rundida +# claude-media video-analyzer + nakkas + open-museum + go-docs +# claude-extras ALL of the above at once +# ============================================================================ +claude-rag() { claude --mcp-config ~/.claude/mcp-optional/rag.json "$@"; } +claude-travel() { claude --mcp-config ~/.claude/mcp-optional/travel.json "$@"; } +claude-sports() { claude --mcp-config ~/.claude/mcp-optional/sports.json "$@"; } +claude-media() { claude --mcp-config ~/.claude/mcp-optional/media.json "$@"; } +claude-extras() { claude --mcp-config ~/.claude/mcp-optional/*.json "$@"; } + +# ============================================================================ +# claude-debug — RAW-binary A/B probe for the startup-interaction failure. +# ---------------------------------------------------------------------------- +# Runs the real binary via `command` (bypassing the claude() wrapper, so NO +# nomonitor) in your real interactive shell, with --debug-file capturing Claude's +# timestamped startup trace to a file (off-screen). This tests whether the +# failure needs the wrapper's nomonitor or happens on the bare binary too. +# NOTE: do NOT wrap this in script(1) — script runs Claude without zsh job +# control, which MASKS the SIGTTIN suspend we are hunting. +# Usage: run `claude-debug`, TYPE during startup to trigger it, then IMMEDIATELY +# run `jobs -l` and tell me the exact text the shell printed, plus paste the log. +# ============================================================================ +claude-debug() { + mkdir -p ~/.claude/logs + local dbg=~/.claude/logs/claude-RAW-$(date +%Y%m%d-%H%M%S)-$$.log + print -r -- "→ RAW binary (no nomonitor), trace: $dbg" + print -r -- "→ TYPE during startup to trigger it, then run: jobs -l" + command claude --debug-file "$dbg" "$@" + print -r -- "captured: $dbg (exit=$?)" +}