From 5604d871bf99ff0c2831c3d5fc8040bcfec62d47 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 13 Jul 2026 09:47:07 -0300 Subject: [PATCH 1/2] feat: restore cmux workflow integration --- README.md | 9 +- claude/statusline.sh | 15 + docs/cmux-workflow.md | 49 ++++ docs/herdr-workflow.md | 4 +- install.sh | 42 ++- local/env.zsh.example | 2 +- zsh/scripts/claude-helpers.zsh | 418 +++++----------------------- zsh/scripts/cmux-sidebar-refresh.sh | 54 ++++ zsh/scripts/ssh-helpers.zsh | 79 ++---- zsh/scripts/worktree-helpers.zsh | 7 +- zsh/zshrc | 3 +- 11 files changed, 264 insertions(+), 418 deletions(-) create mode 100644 docs/cmux-workflow.md create mode 100755 zsh/scripts/cmux-sidebar-refresh.sh diff --git a/README.md b/README.md index c53f98e..ccd4252 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ GitHub Actions ejecuta un smoke check mínimo en pull requests y pushes a `main` - **Keyboard remaps macOS**: [Karabiner-Elements](https://karabiner-elements.pqrs.org/) con profile `cortex` - **Editor terminal**: Neovim basado en LazyVim/Gentleman.Dots con overlay RefactorIA - **Ls**: [eza](https://github.com/eza-community/eza) -- **AI CLI UX**: Claude Code statusline, OpenCode helpers y Herdr remoto -- **Workflow docs**: [Herdr workflow](docs/herdr-workflow.md) y [keymaps prácticos](docs/keymaps.md) +- **AI CLI UX**: Claude Code statusline, OpenCode helpers y cmux local/remoto +- **Workflow docs**: [cmux workflow](docs/cmux-workflow.md), [Herdr workflow](docs/herdr-workflow.md) y [keymaps prácticos](docs/keymaps.md) - **Supply-chain guardrails**: defaults globales para `uv`, `npm`, `pnpm` y `bun` - **Fuente**: FiraCode Nerd Font + variante custom RefactorIA @@ -46,7 +46,7 @@ bash install.sh --dry-run ``` El instalador macOS: -1. Instala dependencias via Homebrew (starship, tmux, lazygit, neovim, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) +1. Instala dependencias via Homebrew (cmux, starship, tmux, lazygit, neovim, eza, sketchybar, yabai, skhd, Karabiner-Elements, FiraCode Nerd Font) 2. Hace backup de configs existentes con timestamp 3. Crea symlinks de los dotfiles y guardrails globales (`.npmrc`, `pnpm/rc`, `.bunfig.toml`, `uv.toml`) 4. Intenta seleccionar el profile `cortex` de Karabiner si `karabiner_cli` está disponible @@ -69,6 +69,8 @@ dotfiles/ │ ├── zshrc # Profile principal (~/.zshrc) │ └── scripts/ │ ├── claude-helpers.zsh # Integración Claude Code +│ ├── cmux-sidebar-refresh.sh # Metadata Cortex para cmux +│ ├── ssh-helpers.zsh # cmux SSH y SSH convencional │ ├── git-helpers.zsh # Identidades Git y clone helpers │ ├── tmux-helpers.zsh # Helpers tmux │ ├── worktree-helpers.zsh # Helpers git worktree @@ -127,6 +129,7 @@ Seguimiento: [cortex-dotfiles #31](https://github.com/barbatdev/cortex-dotfiles/ | `ccclip ` | Copiar código al clipboard | | `tcc`, `tdev`, `ta`, `tn`, `tl`, `tk` | Helpers tmux (`tcc` abre Claude Code en tmux) | | `wtadd`, `wtlist`, `wtremove` | Helpers de git worktrees | +| `sshx`, `sshc`, `sshx-doctor` | cmux SSH persistente, SSH convencional y diagnóstico | | `cortex.agent_state.v1`, `agent-state` | Reportar/listar estado local de agentes Cortex y, dentro de Herdr, actualizar el pane actual | | `ss [n]` | Listar últimos screenshots | | `last [-c\|-o]` | Último screenshot | diff --git a/claude/statusline.sh b/claude/statusline.sh index 93f480a..060dd10 100755 --- a/claude/statusline.sh +++ b/claude/statusline.sh @@ -225,6 +225,21 @@ SDD_MODE=$(echo "$SDD_DATA" | cut -d'|' -f1) SDD_SPEC=$(echo "$SDD_DATA" | cut -d'|' -f2) SDD_TASKS=$(echo "$SDD_DATA" | cut -d'|' -f3) +# cmux handles lifecycle/feed with its official hooks; publish only Cortex metadata here. +CMUX_STATUS_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/cortex/cmux-status" +CMUX_STATUS_KEY=$(printf '%s' "${CMUX_WORKSPACE_ID:-default}" | tr -c 'A-Za-z0-9_.-' '_') +CMUX_STATUS_CACHE="$CMUX_STATUS_DIR/$CMUX_STATUS_KEY" +CMUX_STATUS_NOW=$(date +%s) +CMUX_STATUS_LAST=$(cat "$CMUX_STATUS_CACHE" 2>/dev/null || echo 0) +if command -v cmux >/dev/null 2>&1 && [ $((CMUX_STATUS_NOW - CMUX_STATUS_LAST)) -ge 2 ] && cmux ping >/dev/null 2>&1; then + cmux set-status claude_model "$MODEL" >/dev/null 2>&1 || true + cmux set-progress "$(awk -v percent="$CTX_PERCENT" 'BEGIN { printf "%.2f", percent / 100 }')" --label "Context ${CTX_PERCENT}%" >/dev/null 2>&1 || true + umask 077 + mkdir -p "$CMUX_STATUS_DIR" + chmod 700 "$CMUX_STATUS_DIR" + printf '%s\n' "$CMUX_STATUS_NOW" > "$CMUX_STATUS_CACHE.$$" && mv "$CMUX_STATUS_CACHE.$$" "$CMUX_STATUS_CACHE" +fi + # Nombre del directorio DIR_NAME=$(basename "$DIR") diff --git a/docs/cmux-workflow.md b/docs/cmux-workflow.md new file mode 100644 index 0000000..71d689b --- /dev/null +++ b/docs/cmux-workflow.md @@ -0,0 +1,49 @@ +# cmux Workflow + +cmux is the default terminal surface for local and remote agent sessions. It reads the shared Ghostty configuration from `~/.config/ghostty/config`; there is no separate cmux theme file. + +## Agent Integrations + +Install the official lifecycle, Feed, and resume integrations after Claude Code and OpenCode are available: + +```bash +cmux hooks setup +``` + +The dotfiles layer does not replace those hooks. It adds Cortex metadata to the cmux sidebar: + +- project and Git branch +- configured brains +- active SDD spec +- Claude model and context progress + +## Remote Sessions + +Use `sshx` from the Mac to create a persistent cmux SSH workspace: + +```bash +sshx agent-dev +``` + +`cmux ssh` installs its relay daemon on the remote host. Commands such as `cmux notify`, `cmux set-status`, and `cmux set-progress` then update the local Mac workspace through that relay. + +Install agent hooks from inside the remote cmux session too, because the agent binaries and their user-level configuration live on the remote host: + +```bash +cmux hooks setup +``` + +Use `sshc agent-dev` only when a conventional SSH shell without cmux persistence or UI integration is intentional. + +## Commands + +| Command | Purpose | +|---------|---------| +| `cc [path]` | Run Claude Code and publish Cortex metadata | +| `oc [path]` | Run OpenCode and publish Cortex metadata | +| `sshx ` | Open a persistent cmux SSH workspace | +| `sshc ` | Open a conventional SSH connection | +| `sshx-doctor ` | Validate cmux, SSH, and host alias resolution | +| `wtadd [branch]` | Create a worktree and open its agent flow | + +cmux owns agent notifications and resume tokens. Cortex scripts should fail silently when the cmux relay is unavailable so the same shell configuration remains usable outside cmux. diff --git a/docs/herdr-workflow.md b/docs/herdr-workflow.md index 45b28af..29d71d9 100644 --- a/docs/herdr-workflow.md +++ b/docs/herdr-workflow.md @@ -16,9 +16,9 @@ This workflow keeps terminal state persistent across local and remote work while |------|-----|---------| | Local Herdr | Normal work on the current machine | `hhere [path]`, `hfocus [path]` | | Remote Herdr bridge | Persistent remote session rendered through the local client | `hremote [session]` | -| Plain SSH/Mosh first | You need a normal remote shell before deciding what to run there | `sshx `, `moshx [path]` | +| Plain SSH first | You need a normal remote shell before deciding what to run there | `sshc ` | -`hremote` is not the same thing as `ssh` followed by `herdr`. `hremote` uses `herdr --remote` and attaches to a named Herdr session on the host through Herdr's remote bridge. `sshx` or `moshx` gives you a regular remote shell; from there, `hhere` starts or attaches Herdr as a local process on that remote host. +`hremote` is not the same thing as `ssh` followed by `herdr`. `hremote` uses `herdr --remote` and attaches to a named Herdr session on the host through Herdr's remote bridge. `sshc` gives you a regular remote shell; from there, `hhere` starts or attaches Herdr as a local process on that remote host. In `hremote`, `Ctrl+V` is reserved by Herdr for remote image paste when supported by the terminal. Use normal shell paste bindings outside remote attach. diff --git a/install.sh b/install.sh index 4b57896..a0a5216 100755 --- a/install.sh +++ b/install.sh @@ -158,10 +158,8 @@ PY check_command starship warn check_command nvim warn check_command eza warn - check_command herdr warn - check_command mosh warn + check_command cmux warn check_command tmux warn - check_command herdr warn check_command lazygit warn check_command sketchybar warn check_command yabai warn @@ -179,8 +177,6 @@ PY check_command python3 warn check_command starship warn check_command nvim warn - check_command herdr warn - check_command mosh warn check_command tmux warn check_command lazygit warn ;; @@ -245,6 +241,9 @@ PY for path in "$DOTFILES"/zsh/scripts/*.zsh; do check_shell "$path" zsh done + for path in "$DOTFILES"/zsh/scripts/*.sh; do + check_shell "$path" bash + done check_json "$DOTFILES/karabiner/karabiner.json" for path in "$DOTFILES"/opencode/*.json "$DOTFILES"/opencode/**/*.json "$DOTFILES"/claude/themes/*.json; do check_json "$path" @@ -312,6 +311,21 @@ install_formula_if_missing() { fi } +install_cask_if_missing() { + local app_path="$1" + local cask="$2" + local tap="$3" + + if [[ -d "$app_path" ]]; then + echo " ✓ $cask ya instalado" + elif [[ "$DRY_RUN" == true ]]; then + echo " → Would install $cask via Homebrew cask" + else + brew tap "$tap" + brew install --cask "$cask" + fi +} + # --- Verificar dependencias base --- echo "📦 Verificando herramientas ($PLATFORM)..." @@ -327,7 +341,7 @@ if [[ "$PLATFORM" == "Darwin" ]]; then install_formula_if_missing starship starship "prompt" elif [[ "$PLATFORM" == "Linux" ]]; then - for tool in zsh starship nvim herdr mosh tmux lazygit; do + for tool in zsh starship nvim tmux lazygit; do if command -v "$tool" &>/dev/null; then echo " ✓ $tool ya instalado" else @@ -358,8 +372,7 @@ karabiner_cli_path() { if [[ "$PLATFORM" == "Darwin" ]]; then install_formula_if_missing nvim neovim "editor terminal" install_formula_if_missing eza eza "ls mejorado" - install_formula_if_missing herdr herdr "multiplexor remoto persistente" - install_formula_if_missing mosh mosh "SSH resiliente para workstations remotas" + install_cask_if_missing "/Applications/cmux.app" cmux manaflow-ai/cmux install_formula_if_missing tmux tmux "multiplexor de terminal" install_formula_if_missing lazygit lazygit "git TUI" install_formula_if_missing sketchybar sketchybar "barra macOS" @@ -496,6 +509,7 @@ create_symlink "$DOTFILES/opencode/tui.json" "$HOME/.config/opencode/tui.j create_symlink "$DOTFILES/opencode/themes" "$HOME/.config/opencode/themes" create_symlink "$DOTFILES/tmux/tmux.conf" "$HOME/.tmux.conf" run_or_plan "chmod +x $DOTFILES/claude/statusline.sh" chmod +x "$DOTFILES/claude/statusline.sh" +run_or_plan "chmod +x cmux integration scripts" chmod +x "$DOTFILES/zsh/scripts/cmux-sidebar-refresh.sh" create_symlink "$DOTFILES/claude/statusline.sh" "$HOME/.claude/statusline.sh" create_symlink "$DOTFILES/claude/themes" "$HOME/.claude/themes" create_symlink "$DOTFILES/lazygit/config.yml" "$HOME/.config/lazygit/config.yml" @@ -554,6 +568,16 @@ else echo " - sketchybar no disponible; se omite" fi +if [[ "$PLATFORM" == "Darwin" ]]; then + if [[ "$DRY_RUN" == true ]]; then + echo " → Would install cmux hooks for detected agents" + elif command -v cmux >/dev/null 2>&1; then + cmux hooks setup || echo " ! cmux hooks setup no pudo completar todas las integraciones" + else + echo " ! Abrí cmux y asegurá su CLI en PATH; después corré: cmux hooks setup" + fi +fi + if [[ "$PLATFORM" != "Darwin" ]]; then : elif [[ "$DRY_RUN" == true ]]; then @@ -623,7 +647,7 @@ if [[ "$PLATFORM" == "Darwin" ]]; then echo " 4. Si macOS bloqueó servicios, habilitá Accessibility y corré los fallbacks impresos arriba" echo " 5. Abrí Karabiner-Elements y habilitá Input Monitoring/Accessibility si macOS lo pide" else - echo " 3. Instalá zsh/starship/neovim/herdr/mosh/tmux/lazygit con el package manager del sistema si faltan" + echo " 3. Instalá zsh/starship/neovim/tmux/lazygit con el package manager del sistema si faltan" fi echo "" echo " Para medir el load time:" diff --git a/local/env.zsh.example b/local/env.zsh.example index 021bbd7..8ff6406 100644 --- a/local/env.zsh.example +++ b/local/env.zsh.example @@ -25,7 +25,7 @@ # export OPENCODE_DEFAULT_FLAGS="" # Multiplexor preferido para cc/oc y sesiones remotas. -# export CORTEX_MULTIPLEXER="herdr" +# export CORTEX_MULTIPLEXER="cmux" # Navegación rápida para proyectos de una organización/equipo # export INNIT_DIR="$BARBATDEV_DIR/innit" diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 7ba799b..7c9dd36 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -1,382 +1,115 @@ #region Claude Code Helpers -# Funciones de integración con Claude Code CLI _cmux_sidebar_refresh() { local target="${1:-$PWD}" - local dotfiles_dir="${_DOTFILES_DIR:-$HOME/dev/personal/cortex-dotfiles}" - local script="$dotfiles_dir/zsh/scripts/cmux-sidebar-refresh.sh" - if [[ -x "$script" ]]; then - "$script" "$target" >/dev/null 2>&1 || true - fi -} - -_workspace_name_for_path() { - local target="${1:-$PWD}" - local git_root - git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) - - if [[ -n "$git_root" ]]; then - local repo_name - local parent_name - repo_name=$(basename "$git_root" | tr '.' '-') - parent_name=$(basename "$(dirname "$git_root")" | tr '.' '-') - printf '%s-%s' "$parent_name" "$repo_name" - else - basename "$target" | tr '.' '-' - fi -} - -_cmux_rename_workspace() { - local workspace_id="$1" - local workspace_name="$2" - - if [[ -z "$workspace_id" || -z "$workspace_name" ]]; then - return 0 - fi - - if command -v cmux >/dev/null 2>&1; then - # env -u descarta el socket heredado del proceso padre (cc/oc/ccb/ocb operan sobre la - # intención explícita del usuario — el workspace destino que indicó al lanzar el comando —, - # no sobre el focused actual; por eso solo aplicamos env -u sin --no-caller) - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux rename-workspace --workspace "$workspace_id" "$workspace_name" >/dev/null 2>&1 || true - fi -} + local workspace="${2:-}" + local script="${_DOTFILES_DIR:-$HOME/.cortex/cortex-dotfiles}/zsh/scripts/cmux-sidebar-refresh.sh" -_cortex_use_herdr_runtime() { - [[ "${CORTEX_MULTIPLEXER:-}" == "herdr" || -n "${HERDR_ENV:-}" ]] + [[ -x "$script" ]] && "$script" "$target" "$workspace" >/dev/null 2>&1 || true } -# Abrir Claude Code en tmux -# Si no está en tmux, crea una sesión nueva -cc() { +_cortex_resolve_target() { local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" + resolved=$(cd -q "$target" 2>/dev/null && pwd) || { + echo "Directorio no encontrado: $target" return 1 - fi + } + printf '%s\n' "$resolved" +} - if _cortex_use_herdr_runtime; then - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions +_cortex_run_agent() { + local target="$1" + shift + local -a agent_command=("$@") + local command_string="" + local arg created workspace + + if [[ "${CORTEX_MULTIPLEXER:-cmux}" == "cmux" && -n "${CMUX_WORKSPACE_ID:-}" && "$target" != "$PWD" ]]; then + for arg in "${agent_command[@]}"; do + command_string+="${(q)arg} " + done + created=$(cmux workspace create --cwd "$target" --command "$command_string" --json) || return 1 + workspace=$(jq -er '.workspace_ref // .workspace_id' <<<"$created") || return 1 + _cmux_sidebar_refresh "$target" "$workspace" return fi - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Si el directorio objetivo es el actual, lanzar claude aquí mismo - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - claude --enable-auto-mode --dangerously-skip-permissions - return - fi - - # Directorio diferente: verificar si ya existe un workspace para no duplicar - # env -u descarta el socket heredado: cc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) + _cmux_sidebar_refresh "$target" + (cd -q "$target" && command "${agent_command[@]}") +} - if [[ -n "$existing_id" ]]; then - # El workspace ya existe: enfocarlo sin crear uno nuevo - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - # No existe: crear workspace nuevo con claude corriendo - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "claude --enable-auto-mode --dangerously-skip-permissions"; then - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && claude --enable-auto-mode --dangerously-skip-permissions - else - # No estamos en tmux: crear sesión - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && claude --enable-auto-mode --dangerously-skip-permissions" Enter - tmux attach -t "$session" - fi - fi +# Abre Claude Code en el path indicado. +# Uso: cc [path] +cc() { + local resolved ctxfile + resolved=$(_cortex_resolve_target "${1:-.}") || return + _cortex_run_agent "$resolved" claude --enable-auto-mode --dangerously-skip-permissions } -# Abrir OpenCode en tmux/cmux -# Mantiene el mismo patrón de uso que cc() pero usando opencode +# Abre OpenCode en el path indicado. +# Uso: oc [path] oc() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if _cortex_use_herdr_runtime; then - cd "$resolved" && eval "$oc_cmd" - return - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: oc opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi - fi + local -a flags + resolved=$(_cortex_resolve_target "${1:-.}") || return + flags=(${=${OPENCODE_DEFAULT_FLAGS:-}}) + _cortex_run_agent "$resolved" opencode "${flags[@]}" } -# Abrir Claude Code con bypass de permisos explícito +# Abre Claude Code con bypass explícito de permisos. +# Uso: ccb [path] ccb() { - local target="${1:-.}" local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local cc_cmd="claude --dangerously-skip-permissions" - - if _cortex_use_herdr_runtime; then - cd "$resolved" && eval "$cc_cmd" - return - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$cc_cmd" - return - fi - - # env -u descarta el socket heredado: ccb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$cc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando Claude Code en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$cc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $cc_cmd" Enter - tmux attach -t "$session" - fi - fi + resolved=$(_cortex_resolve_target "${1:-.}") || return + _cortex_run_agent "$resolved" claude --dangerously-skip-permissions } -# Abrir OpenCode con bypass de permisos explícito +# Alias simétrico de OpenCode; sus flags se configuran con OPENCODE_DEFAULT_FLAGS. +# Uso: ocb [path] ocb() { - local target="${1:-.}" - local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" - return 1 - fi - - local oc_cmd="opencode ${OPENCODE_DEFAULT_FLAGS:-}" - - if _cortex_use_herdr_runtime; then - cd "$resolved" && eval "$oc_cmd" - return - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - if [[ "$resolved" == "$PWD" ]]; then - _cmux_rename_workspace "$CMUX_WORKSPACE_ID" "$workspace_name" - _cmux_sidebar_refresh "$resolved" - eval "$oc_cmd" - return - fi - - # env -u descarta el socket heredado: ocb opera sobre el dir/workspace destino que el - # usuario indicó explícitamente (no sobre el focused), por eso solo env -u, sin --no-caller - local existing_id - existing_id=$(env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux list-workspaces 2>/dev/null | jq -r --arg name "$workspace_name" '.[] | select(.title == $name) | .id' 2>/dev/null | head -1) - - if [[ -n "$existing_id" ]]; then - _cmux_rename_workspace "$existing_id" "$workspace_name" - env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux select-workspace --workspace "$existing_id" - _cmux_sidebar_refresh "$resolved" - else - if ! env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "$oc_cmd"; then - echo "⚠️ cmux new-workspace falló, ejecutando OpenCode en el directorio actual" - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - fi - fi - elif [[ -n "$TMUX" ]]; then - _cmux_sidebar_refresh "$resolved" - cd "$resolved" && eval "$oc_cmd" - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && $oc_cmd" Enter - tmux attach -t "$session" - fi - fi + oc "$@" } -# Abrir Claude Code con contexto inicial en tmux +# Abre Claude Code con contexto inicial. +# Uso: ccx [path] ccx() { local context="$1" - local target="${2:-.}" + local resolved if [[ -z "$context" ]]; then echo "Uso: ccx [directorio]" return 1 fi - - local resolved - resolved=$(cd "$target" 2>/dev/null && pwd) - - if [[ -z "$resolved" ]]; then - echo "❌ Directorio no encontrado: $target" + resolved=$(_cortex_resolve_target "${2:-.}") || return + ctxfile=$(mktemp "${TMPDIR:-/tmp}/cortex-ccx.XXXXXX") || return 1 + chmod 600 "$ctxfile" + printf '%s\n' "$context" > "$ctxfile" + _cortex_run_agent "$resolved" sh -c 'claude < "$1"; status=$?; rm -f "$1"; exit $status' sh "$ctxfile" || { + rm -f "$ctxfile" return 1 - fi - - if _cortex_use_herdr_runtime; then - cd "$resolved" && echo "$context" | claude - return - fi - - if [[ -n "$CMUX_WORKSPACE_ID" ]]; then - # Estamos dentro de cmux: escribir contexto a tempfile y abrir workspace propio - local workspace_name - workspace_name="$(_workspace_name_for_path "$resolved")" - - # Archivo de contexto en ~/.claude/ para garantizar accesibilidad desde el workspace nuevo - local ctxfile="$HOME/.claude/ccx-ctx-$$.txt" - echo "$context" > "$ctxfile" - chmod 600 "$ctxfile" - - # Crear workspace nuevo — el cleanup va dentro del comando para que - # el archivo siga existiendo cuando cmux lo lea en el workspace nuevo - # env -u descarta el socket heredado: ccx opera sobre el workspace destino explícito - if env -u CMUX_SOCKET_PATH -u CMUX_SOCKET cmux new-workspace --name "$workspace_name" --cwd "$resolved" --command "sh -c 'claude < $ctxfile; rm -f $ctxfile'"; then - : # limpieza la hace el comando en el workspace nuevo - else - # cmux falló: limpiar archivo y ejecutar claude directo con el contexto - rm -f "$ctxfile" - echo "⚠️ cmux new-workspace falló, ejecutando claude en el directorio actual" - cd "$resolved" && echo "$context" | claude - fi - elif [[ -n "$TMUX" ]]; then - cd "$resolved" && echo "$context" | claude - else - local session - session="$(_workspace_name_for_path "$resolved")" - if tmux has-session -t "$session" 2>/dev/null; then - tmux attach -t "$session" - else - tmux new-session -d -s "$session" - tmux send-keys -t "$session" "cd '$resolved' && echo ${(q)context} | claude" Enter - tmux attach -t "$session" - fi - fi + } } -# Navegar al Claude workspace +# Navega al workspace principal o a uno de sus subdirectorios. +# Uso: ccd [subpath] ccd() { local subpath="${1:-}" local workspace="${WORKSPACE_DIR:-$HOME/dev}" - - local target - if [[ -n "$subpath" ]]; then - target="$workspace/$subpath" - else - target="$workspace" - fi + local target="${workspace}${subpath:+/$subpath}" if [[ -d "$target" ]]; then cd "$target" - echo "📂 Navegando a: $target" + echo "Navegando a: $target" else - echo "❌ Directorio no encontrado: $target" + echo "Directorio no encontrado: $target" return 1 fi } -# Copiar contexto de código al clipboard para Claude +# Copia archivos como bloques Markdown al clipboard. +# Uso: ccclip [-n|--line-numbers] ccclip() { if [[ $# -eq 0 ]]; then echo "Uso: ccclip [archivo2 ...] [-n|--line-numbers]" @@ -384,8 +117,8 @@ ccclip() { fi local with_numbers=false - local files=() - + local -a files=() + local arg file ext context="" for arg in "$@"; do case "$arg" in -n|--line-numbers) with_numbers=true ;; @@ -393,33 +126,24 @@ ccclip() { esac done - local context="" - for file in "${files[@]}"; do if [[ ! -f "$file" ]]; then - echo "⚠️ Archivo no encontrado: $file" + echo "Archivo no encontrado: $file" continue fi - local ext="${file##*.}" - context+="\`\`\`$ext\n" - context+="// File: $file\n" - + ext="${file##*.}" + context+="\`\`\`$ext\n// File: $file\n" if $with_numbers; then - local n=1 - while IFS= read -r line; do - context+=$(printf "%4d: %s\n" "$n" "$line") - (( n++ )) - done < "$file" + context+="$(nl -ba "$file")\n" else - context+="$(cat "$file")\n" + context+="$(<"$file")\n" fi - context+="\`\`\`\n\n" done - echo -e "$context" | pbcopy - echo "✓ Contexto copiado al clipboard (${#files[@]} archivo$([ ${#files[@]} -ne 1 ] && echo 's'))" + printf '%b' "$context" | pbcopy + echo "Contexto copiado al clipboard (${#files[@]} archivos)" } #endregion diff --git a/zsh/scripts/cmux-sidebar-refresh.sh b/zsh/scripts/cmux-sidebar-refresh.sh new file mode 100755 index 0000000..5700895 --- /dev/null +++ b/zsh/scripts/cmux-sidebar-refresh.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +set -u + +target="${1:-$PWD}" +workspace="${2:-}" + +command -v cmux >/dev/null 2>&1 || exit 0 +cmux ping >/dev/null 2>&1 || exit 0 + +set_status() { + local key="$1" + local value="$2" + + if [[ -n "$value" && -n "$workspace" ]]; then + cmux set-status "$key" "$value" --workspace "$workspace" >/dev/null 2>&1 || true + elif [[ -n "$value" ]]; then + cmux set-status "$key" "$value" >/dev/null 2>&1 || true + elif [[ -n "$workspace" ]]; then + cmux clear-status "$key" --workspace "$workspace" >/dev/null 2>&1 || true + else + cmux clear-status "$key" >/dev/null 2>&1 || true + fi +} + +git_root=$(git -C "$target" rev-parse --show-toplevel 2>/dev/null || true) +if [[ -n "$git_root" ]]; then + project=$(basename "$git_root") + branch=$(git -C "$git_root" branch --show-current 2>/dev/null || true) + [[ -n "$(git -C "$git_root" status --porcelain 2>/dev/null)" ]] && branch="${branch}*" +else + project=$(basename "$target") + branch="" +fi + +set_status project "$project" +set_status git_branch "$branch" + +brains="" +for brain in work-brain life-brain second-brain; do + if [[ -f "$HOME/.claude.json" ]] && jq -e --arg name "$brain" '.mcpServers[$name] // .projects[].mcpServers[$name]' "$HOME/.claude.json" >/dev/null 2>&1; then + short=${brain%-brain} + [[ "$brain" == "second-brain" ]] && short="notes" + brains="${brains:+$brains,}$short" + fi +done +set_status brains "$brains" + +if [[ -d "$target/.ai" ]]; then + spec=$(find "$target/.ai/specs" -maxdepth 1 -type f 2>/dev/null | sort | tail -n 1) + set_status sdd_spec "${spec##*/}" +else + set_status sdd_spec "" +fi diff --git a/zsh/scripts/ssh-helpers.zsh b/zsh/scripts/ssh-helpers.zsh index c7d2dd7..a8b9887 100644 --- a/zsh/scripts/ssh-helpers.zsh +++ b/zsh/scripts/ssh-helpers.zsh @@ -1,64 +1,41 @@ # ssh-helpers.zsh — Remote helpers with visible host/repo context. -_remote_herdr_command_for_path() { - local remote_path="$1" - local shell_path - - if [[ "$remote_path" == "$HOME/"* ]]; then - shell_path="~/${remote_path#$HOME/}" - else - shell_path="${(q)remote_path}" - fi - - printf 'cd %s && exec ${SHELL:-zsh}' "$shell_path" +# SSH directo, pero exportando CORTEX_SSH_TARGET para que el prompt muestre el host remoto. +sshc() { + local target="${1:?Uso: sshc [ssh args...]}" + shift + [[ "$target" == -* ]] && { echo "Target SSH inválido: $target"; return 1; } + CORTEX_SSH_TARGET="$target" ssh "$target" "$@" } -# Entrar a una workstation remota por Mosh. Si pasás path, entra directo a ese directorio. -moshx() { - local target="${1:?Uso: moshx [remote-path]}" +# Abre un workspace remoto persistente en cmux usando aliases y opciones de ~/.ssh/config. +sshx() { + local target="${1:?Uso: sshx [cmux ssh args...]}" shift + [[ "$target" == -* ]] && { echo "Target SSH inválido: $target"; return 1; } - if ! command -v mosh >/dev/null 2>&1; then - echo "mosh no está instalado localmente; fallback: sshx $target ${*}" - sshx "$target" "$@" - return + if ! command -v cmux >/dev/null 2>&1; then + echo "cmux no está disponible; usá sshc para una conexión SSH directa" + return 1 fi - local remote_path="${1:-}" - if [[ -z "$remote_path" ]]; then - CORTEX_SSH_TARGET="$target" mosh "$target" - return - fi - shift - - local remote_command - remote_command="$(_remote_herdr_command_for_path "$remote_path")" - CORTEX_SSH_TARGET="$target" mosh "$target" -- sh -lc "$remote_command" || { - echo "mosh falló; probando fallback SSH al mismo directorio remoto" - CORTEX_SSH_TARGET="$target" ssh -t "$target" sh -lc "$remote_command" - } + CORTEX_SSH_TARGET="$target" cmux ssh "$target" "$@" } -# Diagnosticar dependencias remotas para moshx/herdr sin abrir sesión interactiva. -moshx-doctor() { - local target="${1:?Uso: moshx-doctor }" +# Diagnostica el cliente cmux y la resolución SSH del host sin iniciar una sesión. +sshx-doctor() { + local target="${1:?Uso: sshx-doctor }" + local failed=0 + [[ "$target" == -* ]] && { echo "Target SSH inválido: $target"; return 1; } - echo "Local:" - command -v mosh >/dev/null 2>&1 && echo " ✓ mosh: $(command -v mosh)" || echo " ✗ mosh local no encontrado" - command -v herdr >/dev/null 2>&1 && echo " ✓ herdr: $(command -v herdr)" || echo " ✗ herdr local no encontrado" - - echo "Remote $target:" - ssh "$target" 'for cmd in mosh-server herdr git sh; do if command -v "$cmd" >/dev/null 2>&1; then printf " ✓ %s: %s\n" "$cmd" "$(command -v "$cmd")"; else printf " ✗ %s no encontrado\n" "$cmd"; fi; done' -} - -# SSH directo, pero exportando CORTEX_SSH_TARGET para que el prompt muestre el host remoto. -sshc() { - local target="${1:?Uso: sshc [ssh args...]}" - shift - CORTEX_SSH_TARGET="$target" ssh "$target" "$@" -} + command -v cmux >/dev/null 2>&1 || { echo "✗ cmux no encontrado"; failed=1; } + command -v ssh >/dev/null 2>&1 || { echo "✗ ssh no encontrado"; failed=1; } + if command -v ssh >/dev/null 2>&1 && ssh -G "$target" >/dev/null 2>&1; then + echo "✓ SSH config resuelve: $target" + else + echo "✗ SSH config no resuelve: $target" + failed=1 + fi -# SSH directo con contexto visible. Herdr se encarga de la persistencia fuera de SSH. -sshx() { - sshc "$@" + return "$failed" } diff --git a/zsh/scripts/worktree-helpers.zsh b/zsh/scripts/worktree-helpers.zsh index 6b2610e..cede88f 100644 --- a/zsh/scripts/worktree-helpers.zsh +++ b/zsh/scripts/worktree-helpers.zsh @@ -1,5 +1,5 @@ #region Worktree Helpers -# Funciones para gestión de git worktrees con integración Herdr. +# Funciones para gestión de git worktrees con integración cmux. # Detecta repos PCSoft automáticamente y bloquea la creación de worktrees en ellos. # Verifica si el repo actual contiene archivos PCSoft (Categoría B — prohibido worktree) @@ -32,7 +32,7 @@ _wt_path_for() { printf '%s/%s/%s' "$(_wt_base_dir)" "$repo_name" "$name" } -# Crea un worktree en ~/dev/worktrees// y abre el agente desde Herdr. +# Crea un worktree en ~/dev/worktrees// y abre el agente desde cmux. # Uso: wtadd [branch] # — nombre del worktree (crea ~/dev/worktrees//) # [branch] — branch existente o nueva (default: crea branch nueva con el mismo nombre) @@ -82,8 +82,7 @@ wtadd() { echo "✓ Worktree creado: $wt_path" - # Abrir el agente en el flujo Herdr si está disponible. - if [[ "${CORTEX_MULTIPLEXER:-herdr}" == "herdr" ]]; then + if [[ "${CORTEX_MULTIPLEXER:-cmux}" == "cmux" ]]; then echo "→ Abriendo agente para el worktree..." cc "$wt_path" fi diff --git a/zsh/zshrc b/zsh/zshrc index af43219..94df4da 100644 --- a/zsh/zshrc +++ b/zsh/zshrc @@ -16,6 +16,7 @@ fi #region PATH export PATH="$HOME/.local/bin:$PATH" +[[ -d "/Applications/cmux.app/Contents/Resources/bin" ]] && export PATH="/Applications/cmux.app/Contents/Resources/bin:$PATH" #endregion #region Zsh Options @@ -60,7 +61,7 @@ export CLAUDE_CODE_EFFORT_LEVEL=high export CORTEX_HOME="${CORTEX_HOME:-$HOME/.cortex}" export CORTEX_ROOT="${CORTEX_ROOT:-$BARBATDEV_DIR/cortex/cortex}" export CORTEX_DOTFILES_DIR="${CORTEX_DOTFILES_DIR:-$BARBATDEV_DIR/cortex/cortex-dotfiles}" -export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-herdr}" +export CORTEX_MULTIPLEXER="${CORTEX_MULTIPLEXER:-cmux}" # Proyectos de la organización export WORK_PROJECTS_DIR="${WORK_PROJECTS_DIR:-$BARBATDEV_DIR/innit}" From 88cd7403a7fdc13a60477259d2802c63851341a9 Mon Sep 17 00:00:00 2001 From: Juan Barbat Date: Mon, 13 Jul 2026 11:19:14 -0300 Subject: [PATCH 2/2] fix: remove unsafe Claude permission bypass --- zsh/scripts/claude-helpers.zsh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zsh/scripts/claude-helpers.zsh b/zsh/scripts/claude-helpers.zsh index 7c9dd36..3a0d857 100644 --- a/zsh/scripts/claude-helpers.zsh +++ b/zsh/scripts/claude-helpers.zsh @@ -45,7 +45,7 @@ _cortex_run_agent() { cc() { local resolved ctxfile resolved=$(_cortex_resolve_target "${1:-.}") || return - _cortex_run_agent "$resolved" claude --enable-auto-mode --dangerously-skip-permissions + _cortex_run_agent "$resolved" claude --enable-auto-mode } # Abre OpenCode en el path indicado.