From b288d7bad0788fdc6906f77b8b7ee85a2dbcdae2 Mon Sep 17 00:00:00 2001 From: wehub-skill-sync Date: Mon, 13 Jul 2026 21:36:33 +0800 Subject: [PATCH] chore: import zh skill tmux --- README.wehub.md | 9 +++ SKILL.md | 121 +++++++++++++++++++++++++++++++++++++++ scripts/find-sessions.sh | 112 ++++++++++++++++++++++++++++++++++++ scripts/wait-for-text.sh | 83 +++++++++++++++++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 README.wehub.md create mode 100644 SKILL.md create mode 100755 scripts/find-sessions.sh create mode 100755 scripts/wait-for-text.sh diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..1500b86 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,9 @@ +# WeHub 来源说明 + +- Skill 名称:`tmux` +- 中文类目:tmux 会话编排 +- 上游仓库:`hkuds__nanobot` +- 上游路径:`nanobot/skills/tmux/SKILL.md` +- 上游链接:https://github.com/hkuds/nanobot/blob/HEAD/nanobot/skills/tmux/SKILL.md +- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理 +- 原作者、版权和许可证信息以上游仓库为准 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..ff61f3a --- /dev/null +++ b/SKILL.md @@ -0,0 +1,121 @@ +--- +name: tmux +description: 通过发送按键和抓取窗格输出来远程控制 tmux 会话,用于交互式 CLI。 +metadata: {"nanobot":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins":["tmux"]}}} +--- + +# tmux 技能 + +仅当你需要交互式 TTY 时才使用 tmux。对于长期运行的非交互式任务,优先使用 exec 后台模式。 + +## 快速开始(独立 socket,exec 工具) + +```bash +SOCKET_DIR="${NANOBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nanobot-tmux-sockets}" +mkdir -p "$SOCKET_DIR" +SOCKET="$SOCKET_DIR/nanobot.sock" +SESSION=nanobot-python + +tmux -S "$SOCKET" new -d -s "$SESSION" -n shell +tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter +tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 +``` + +启动会话后,始终输出监控命令: + +``` +To monitor: + tmux -S "$SOCKET" attach -t "$SESSION" + tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 +``` + +## Socket 约定 + +- 使用 `NANOBOT_TMUX_SOCKET_DIR` 环境变量。 +- 默认 socket 路径:`"$NANOBOT_TMUX_SOCKET_DIR/nanobot.sock"`。 + +## 定位窗格与命名 + +- 目标格式:`session:window.pane`(默认为 `:0.0`)。 +- 名称保持简短;避免空格。 +- 查看:`tmux -S "$SOCKET" list-sessions`,`tmux -S "$SOCKET" list-panes -a`。 + +## 查找会话 + +- 列出你的 socket 上的会话:`{baseDir}/scripts/find-sessions.sh -S "$SOCKET"`。 +- 扫描所有 socket:`{baseDir}/scripts/find-sessions.sh --all`(使用 `NANOBOT_TMUX_SOCKET_DIR`)。 + +## 安全发送输入 + +- 优先使用逐字发送:`tmux -S "$SOCKET" send-keys -t target -l -- "$cmd"`。 +- 控制键:`tmux -S "$SOCKET" send-keys -t target C-c`。 + +## 查看输出 + +- 抓取最近的历史记录:`tmux -S "$SOCKET" capture-pane -p -J -t target -S -200`。 +- 等待提示符:`{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern'`。 +- 允许附加;用 `Ctrl+b d` 分离。 + +## 生成进程 + +- 对于 Python REPL,设置 `PYTHON_BASIC_REPL=1`(非基础 REPL 会破坏 send-keys 流程)。 + +## Windows / WSL + +- tmux 在 macOS/Linux 上受支持。在 Windows 上,请使用 WSL 并在 WSL 内安装 tmux。 +- 本技能限定在 `darwin`/`linux` 系统上运行,并且要求 `tmux` 在 PATH 中可用。 + +## 编排编码 Agent(Codex、Claude Code) + +tmux 擅长并行运行多个编码 agent: + +```bash +SOCKET="${TMPDIR:-/tmp}/codex-army.sock" + +# 创建多个会话 +for i in 1 2 3 4 5; do + tmux -S "$SOCKET" new-session -d -s "agent-$i" +done + +# 在不同工作目录中启动 agent +tmux -S "$SOCKET" send-keys -t agent-1 "cd /tmp/project1 && codex --yolo 'Fix bug X'" Enter +tmux -S "$SOCKET" send-keys -t agent-2 "cd /tmp/project2 && codex --yolo 'Fix bug Y'" Enter + +# 轮询完成状态(检查提示符是否返回) +for sess in agent-1 agent-2; do + if tmux -S "$SOCKET" capture-pane -p -t "$sess" -S -3 | grep -q "❯"; then + echo "$sess: DONE" + else + echo "$sess: Running..." + fi +done + +# 获取已完成会话的完整输出 +tmux -S "$SOCKET" capture-pane -p -t agent-1 -S -500 +``` + +**提示:** +- 对并行的修复任务使用独立的 git worktree(避免分支冲突) +- 在全新克隆中运行 codex 之前先执行 `pnpm install` +- 检查 shell 提示符(`❯` 或 `$`)以判断是否完成 +- Codex 需要 `--yolo` 或 `--full-auto` 参数才能进行非交互式修复 + +## 清理 + +- 终止一个会话:`tmux -S "$SOCKET" kill-session -t "$SESSION"`。 +- 终止某个 socket 上的所有会话:`tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`。 +- 移除私有 socket 上的所有内容:`tmux -S "$SOCKET" kill-server`。 + +## 辅助工具:wait-for-text.sh + +`{baseDir}/scripts/wait-for-text.sh` 以带超时的形式轮询窗格,匹配正则表达式(或固定字符串)。 + +```bash +{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000] +``` + +- `-t`/`--target` 窗格目标(必填) +- `-p`/`--pattern` 要匹配的正则表达式(必填);加 `-F` 表示固定字符串 +- `-T` 超时秒数(整数,默认 15) +- `-i` 轮询间隔秒数(默认 0.5) +- `-l` 要搜索的历史行数(整数,默认 1000) diff --git a/scripts/find-sessions.sh b/scripts/find-sessions.sh new file mode 100755 index 0000000..00552c6 --- /dev/null +++ b/scripts/find-sessions.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern] + +List tmux sessions on a socket (default tmux socket if none provided). + +Options: + -L, --socket tmux socket name (passed to tmux -L) + -S, --socket-path tmux socket path (passed to tmux -S) + -A, --all scan all sockets under NANOBOT_TMUX_SOCKET_DIR + -q, --query case-insensitive substring to filter session names + -h, --help show this help +USAGE +} + +socket_name="" +socket_path="" +query="" +scan_all=false +socket_dir="${NANOBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nanobot-tmux-sockets}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -L|--socket) socket_name="${2-}"; shift 2 ;; + -S|--socket-path) socket_path="${2-}"; shift 2 ;; + -A|--all) scan_all=true; shift ;; + -q|--query) query="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then + echo "Cannot combine --all with -L or -S" >&2 + exit 1 +fi + +if [[ -n "$socket_name" && -n "$socket_path" ]]; then + echo "Use either -L or -S, not both" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +list_sessions() { + local label="$1"; shift + local tmux_cmd=(tmux "$@") + + if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then + echo "No tmux server found on $label" >&2 + return 1 + fi + + if [[ -n "$query" ]]; then + sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)" + fi + + if [[ -z "$sessions" ]]; then + echo "No sessions found on $label" + return 0 + fi + + echo "Sessions on $label:" + printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do + attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached") + printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created" + done +} + +if [[ "$scan_all" == true ]]; then + if [[ ! -d "$socket_dir" ]]; then + echo "Socket directory not found: $socket_dir" >&2 + exit 1 + fi + + shopt -s nullglob + sockets=("$socket_dir"/*) + shopt -u nullglob + + if [[ "${#sockets[@]}" -eq 0 ]]; then + echo "No sockets found under $socket_dir" >&2 + exit 1 + fi + + exit_code=0 + for sock in "${sockets[@]}"; do + if [[ ! -S "$sock" ]]; then + continue + fi + list_sessions "socket path '$sock'" -S "$sock" || exit_code=$? + done + exit "$exit_code" +fi + +tmux_cmd=(tmux) +socket_label="default socket" + +if [[ -n "$socket_name" ]]; then + tmux_cmd+=(-L "$socket_name") + socket_label="socket name '$socket_name'" +elif [[ -n "$socket_path" ]]; then + tmux_cmd+=(-S "$socket_path") + socket_label="socket path '$socket_path'" +fi + +list_sessions "$socket_label" "${tmux_cmd[@]:1}" diff --git a/scripts/wait-for-text.sh b/scripts/wait-for-text.sh new file mode 100755 index 0000000..56354be --- /dev/null +++ b/scripts/wait-for-text.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: wait-for-text.sh -t target -p pattern [options] + +Poll a tmux pane for text and exit when found. + +Options: + -t, --target tmux target (session:window.pane), required + -p, --pattern regex pattern to look for, required + -F, --fixed treat pattern as a fixed string (grep -F) + -T, --timeout seconds to wait (integer, default: 15) + -i, --interval poll interval in seconds (default: 0.5) + -l, --lines number of history lines to inspect (integer, default: 1000) + -h, --help show this help +USAGE +} + +target="" +pattern="" +grep_flag="-E" +timeout=15 +interval=0.5 +lines=1000 + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2-}"; shift 2 ;; + -p|--pattern) pattern="${2-}"; shift 2 ;; + -F|--fixed) grep_flag="-F"; shift ;; + -T|--timeout) timeout="${2-}"; shift 2 ;; + -i|--interval) interval="${2-}"; shift 2 ;; + -l|--lines) lines="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ -z "$target" || -z "$pattern" ]]; then + echo "target and pattern are required" >&2 + usage + exit 1 +fi + +if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then + echo "timeout must be an integer number of seconds" >&2 + exit 1 +fi + +if ! [[ "$lines" =~ ^[0-9]+$ ]]; then + echo "lines must be an integer" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +# End time in epoch seconds (integer, good enough for polling) +start_epoch=$(date +%s) +deadline=$((start_epoch + timeout)) + +while true; do + # -J joins wrapped lines, -S uses negative index to read last N lines + pane_text="$(tmux capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)" + + if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then + exit 0 + fi + + now=$(date +%s) + if (( now >= deadline )); then + echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2 + echo "Last ${lines} lines from $target:" >&2 + printf '%s\n' "$pane_text" >&2 + exit 1 + fi + + sleep "$interval" +done