diff --git a/home-manager/modules/local-scripts/default.nix b/home-manager/modules/local-scripts/default.nix index 954f698df..8d7ede06b 100644 --- a/home-manager/modules/local-scripts/default.nix +++ b/home-manager/modules/local-scripts/default.nix @@ -14,4 +14,9 @@ _: { force = true; source = ./pushover-notify.sh; }; + home.file.".local/scripts/tmux-bridge" = { + executable = true; + force = true; + source = ./tmux-bridge.sh; + }; } diff --git a/home-manager/modules/local-scripts/tmux-bridge.sh b/home-manager/modules/local-scripts/tmux-bridge.sh new file mode 100755 index 000000000..602429030 --- /dev/null +++ b/home-manager/modules/local-scripts/tmux-bridge.sh @@ -0,0 +1,404 @@ +#!/usr/bin/env bash +# tmux-bridge — Agent-agnostic CLI for cross-pane communication in tmux. +# Any tool that can run bash can use this to talk to other panes. +# From: https://github.com/ShawnPana/smux/blob/main/scripts/tmux-bridge +set -euo pipefail + +VERSION="2.0.0" + +# --- Helpers --- + +die() { echo "error: $*" >&2; exit 1; } + +# --- Read Guard --- +# Enforces read-before-act: agents must read a pane before typing/keys. + +read_guard_path() { + local pane_id="$1" + # Sanitize: %66 → _66 + echo "/tmp/tmux-bridge-read-${pane_id//%/_}" +} + +mark_read() { + touch "$(read_guard_path "$1")" +} + +require_read() { + local guard + guard=$(read_guard_path "$1") + if [[ ! -f "$guard" ]]; then + die "must read the pane before interacting. Run: tmux-bridge read $1" + fi +} + +clear_read() { + rm -f "$(read_guard_path "$1")" +} + +# Detect the correct tmux server socket. +# Priority: TMUX_BRIDGE_SOCKET env > $TMUX (if socket alive) > scan for pane owner. +detect_socket() { + # Explicit override + if [[ -n "${TMUX_BRIDGE_SOCKET:-}" ]]; then + if [[ -S "$TMUX_BRIDGE_SOCKET" ]]; then + echo "$TMUX_BRIDGE_SOCKET" + return + fi + die "TMUX_BRIDGE_SOCKET=$TMUX_BRIDGE_SOCKET is not a valid socket" + fi + + # Extract from $TMUX (format: "socket_path,pid,session_index") + if [[ -n "${TMUX:-}" ]]; then + local socket="${TMUX%%,*}" + if [[ -S "$socket" ]]; then + # Verify the socket is actually reachable + if tmux -S "$socket" list-sessions &>/dev/null; then + echo "$socket" + return + fi + fi + fi + + # Fallback: scan tmux server sockets for one that owns $TMUX_PANE + local pane="${TMUX_PANE:-}" + if [[ -n "$pane" ]]; then + local uid + uid=$(id -u) + # Check both /tmp and /private/tmp (macOS symlinks /tmp → /private/tmp, + # but some sandboxed environments only see one or the other) + local sock_dirs=("/tmp/tmux-${uid}" "/private/tmp/tmux-${uid}") + for sock_dir in "${sock_dirs[@]}"; do + [[ -d "$sock_dir" ]] || continue + for sock in "$sock_dir"/*; do + [[ -S "$sock" ]] || continue + if tmux -S "$sock" display-message -t "$pane" -p '#{pane_id}' &>/dev/null; then + echo "$sock" + return + fi + done + done + # Also try the default socket (no -S flag) + if tmux display-message -t "$pane" -p '#{pane_id}' &>/dev/null; then + echo "__default__" + return + fi + fi + + # Last resort: try default tmux server + if tmux list-sessions &>/dev/null; then + echo "__default__" + return + fi + + die "cannot find a reachable tmux server (TMUX=${TMUX:-}, TMUX_PANE=${TMUX_PANE:-})" +} + +# Resolved once at startup, used by tmx() for all tmux calls. +TMUX_SOCKET="" + +init_socket() { + TMUX_SOCKET=$(detect_socket) +} + +# Wrapper: run tmux with the detected socket. +tmx() { + if [[ "$TMUX_SOCKET" == "__default__" || -z "$TMUX_SOCKET" ]]; then + tmux "$@" + else + tmux -S "$TMUX_SOCKET" "$@" + fi +} + +usage() { + cat <<'EOF' +tmux-bridge — cross-pane communication for AI agents + +Usage: tmux-bridge [args...] + +Commands: + list Show all panes (target, pid, command, size, label) + type Type text without pressing Enter + message Type text with auto-prepended sender info and reply target + read [lines] Read last N lines from pane (default: 50) + keys ... Send special keys (Enter, Escape, C-c, etc.) + name