chore: import zh skill tmux
This commit is contained in:
@@ -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 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -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)
|
||||
Executable
+112
@@ -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}"
|
||||
Executable
+83
@@ -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
|
||||
Reference in New Issue
Block a user