diff --git a/AGENTS.md b/AGENTS.md index c022a13..5bfdee0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -166,7 +166,7 @@ git push origin develop │ ├── README.md # skills 总览与索引 │ ├── AGENTS.md # skills/ 目录规则 │ ├── auto-skill/ # 元技能核心 -│ ├── auto-tmux/ # tmux 自动化操控与多终端协作 +│ ├── auto-tmux/ # tmux 自动化脚本、pane 巡检、救援与多终端协作 │ └── claude-official-skills/ # Claude 官方 skills 软链接入口 │ ├── assets/ # 静态资产与外部资源入口 @@ -239,7 +239,7 @@ git push origin develop - `docs/references/README.md#reference-engineering-practice` - 项目架构、代码组织、开发经验、底层程序逻辑、AI 编程质量门禁与常见坑的统一入口 - `docs/references/README.md#reference-technology-stack` - 常见软件系统技术栈、选型维度、组合案例与初学者学习路径 - `skills/auto-skill/` - Skills 生成、重构与校验的元技能 -- `skills/auto-tmux/` - tmux 自动化操控、pane 巡检、按键注入与多终端协作技能 +- `skills/auto-tmux/` - tmux 自动化操控、脚本化 pane 巡检、按键注入、日志录制与多终端协作技能 ### docs README 结构契约 diff --git a/README.md b/README.md index 1a7809d..d1487d9 100644 --- a/README.md +++ b/README.md @@ -501,7 +501,7 @@ pip install -r tools/prompts-library/scripts/requirements.txt * [**Skills 制作器**](https://github.com/yusufkaraaslan/Skill_Seekers): 可根据需求生成定制化 Skills 的工具。 * [**元提示词**](https://docs.google.com/spreadsheets/d/1Ifk_dLF25ULSxcfGem1hXzJsi7_RBUNAki8SBCuvkJA/edit?gid=1254297203#gid=1254297203): 用于生成提示词的高级提示词。 * [**元技能:Auto Skill**](./skills/auto-skill/SKILL.md): 用于生成、重构与校验 Skills 的元技能。 -* [**auto-tmux**](./skills/auto-tmux/SKILL.md): tmux 自动化操控、pane 巡检、按键注入与多终端协作技能。 +* [**auto-tmux**](./skills/auto-tmux/SKILL.md): tmux 自动化操控、脚本化 pane 巡检、按键注入、日志录制与多终端协作技能。 * [**Mermaid Chart**](https://www.mermaidchart.com/): 用于将文本描述转换为架构图、序列图等可视化图表。 * [**NotebookLM**](https://notebooklm.google.com/): 一款用于 AI 解读资料、音频和生成思维导图的工具。 * [**Zread**](https://zread.ai/): AI 驱动的 GitHub 仓库阅读工具,有助于快速理解项目代码。 @@ -587,7 +587,7 @@ pip install -r tools/prompts-library/scripts/requirements.txt ├── prompts/ # 提示词库入口(指向云端表格) ├── skills/ # 技能库入口 │ ├── auto-skill/ # 元技能核心 -│ ├── auto-tmux/ # tmux 自动化操控与多终端协作 +│ ├── auto-tmux/ # tmux 自动化脚本、pane 巡检、救援与多终端协作 │ └── claude-official-skills/ # Claude 官方 skills 软链接入口 ├── tools/ # 辅助工具、外部仓库与工具配置 ├── scripts/ # 自动化脚本 diff --git a/skills/AGENTS.md b/skills/AGENTS.md index f5ac5c1..5ca921b 100644 --- a/skills/AGENTS.md +++ b/skills/AGENTS.md @@ -15,7 +15,7 @@ skills/ │ ├── assets/ # (可选) 模板/样例/静态资源 │ └── agents/ # (可选) Agent 元数据(如 openai.yaml) ├── auto-skill/ # 元技能:生成/校验/脚手架化其它技能 -└── auto-tmux/ # tmux 自动化操控、pane 巡检与多终端协作 +└── auto-tmux/ # tmux 自动化脚本、pane 巡检、救援与多终端协作 ``` ## 外部仓库引用(约定) @@ -47,6 +47,6 @@ skills/ ## 快速定位(常用技能) - `skills/auto-skill/`:元技能(技能生成/校验/脚手架) -- `skills/auto-tmux/`:tmux 自动化技能;通过 `assets/` 软链接复用 `tools/external/.tmux` 与 `tools/external/tmux` +- `skills/auto-tmux/`:tmux 自动化技能;通过 `scripts/auto-tmux.sh` 提供安全执行层,并通过 `assets/` 软链接复用 `tools/external/.tmux` 与 `tools/external/tmux` - `skills/claude-official-skills/`:Claude 官方 skills 仓库(Anthropic)的软链接入口 - `skills/auto-skill/scripts/Skill_Seekers-development`:Skill Seekers submodule 的软链接入口 diff --git a/skills/README.md b/skills/README.md index c00af86..8e811cb 100644 --- a/skills/README.md +++ b/skills/README.md @@ -7,13 +7,13 @@ | 技能 | 说明 | |:---|:---| | [auto-skill](./auto-skill/SKILL.md) | 生成、重构与校验 Skills 的元技能 | -| [auto-tmux](./auto-tmux/SKILL.md) | tmux 自动化操控、pane 巡检、按键注入、蜂群协作和 oh-my-tmux 参考入口 | +| [auto-tmux](./auto-tmux/SKILL.md) | tmux 自动化操控、脚本化 pane 巡检、按键注入、日志录制、蜂群协作和 oh-my-tmux 参考入口 | | [claude-official-skills](./claude-official-skills/) | Claude 官方 skills 仓库的软链接入口,事实来源在 `tools/external/claude-official-skills/` | ## 维护口径 - `auto-skill/` 是本仓库的 Skill 生产与升级入口,继续保留。 -- `auto-tmux/` 是 tmux 自动化与多 AI 终端协作入口,通过软链接复用 `tools/external/.tmux` 与 `tools/external/tmux`。 +- `auto-tmux/` 是 tmux 自动化与多 AI 终端协作入口,包含 `scripts/auto-tmux.sh` 执行层,并通过软链接复用 `tools/external/.tmux` 与 `tools/external/tmux`。 - `claude-official-skills/` 是外部官方仓库展示入口,通过 submodule 管理事实来源。 - 领域型/工具型 Skill 如交易、数据库、代理、Telegram、EPUB 等不在主仓库维护;如需引入,优先使用独立仓库、submodule 或重新生成的方式。 @@ -51,6 +51,6 @@ git submodule update --init --recursive ## 相关资源 - [元技能文件](./auto-skill/SKILL.md) - 生成、重构与校验 Skills -- [auto-tmux](./auto-tmux/SKILL.md) - tmux 自动化操控与多终端协作 +- [auto-tmux](./auto-tmux/SKILL.md) - tmux 自动化操控、脚本化巡检与多终端协作 - [提示词库](../prompts/) - 更细粒度的提示词集合 - [文档库](../docs/) - 方法论与开发经验 diff --git a/skills/auto-tmux/AGENTS.md b/skills/auto-tmux/AGENTS.md index 79f7a34..61b5085 100644 --- a/skills/auto-tmux/AGENTS.md +++ b/skills/auto-tmux/AGENTS.md @@ -11,10 +11,15 @@ skills/auto-tmux/ ├── assets/ # 外部 tmux 相关仓库的只读软链接入口 │ ├── oh-my-tmux -> ../../../tools/external/.tmux │ └── tmux-src -> ../../../tools/external/tmux +├── scripts/ # 安全封装的 tmux 自动化脚本 +│ ├── auto-tmux.sh +│ ├── README.md +│ └── AGENTS.md └── references/ # 长文档、示例与故障排查 ├── index.md ├── getting_started.md ├── api.md + ├── automation.md ├── examples.md └── troubleshooting.md ``` @@ -23,6 +28,7 @@ skills/auto-tmux/ - `assets/oh-my-tmux` 只暴露 gpakosz/oh-my-tmux submodule,不在技能目录内复制配置源码。 - `assets/tmux-src` 只暴露 tmux/tmux submodule,供需要查看源码或上游文档时定位。 +- `scripts/auto-tmux.sh` 是技能执行层,封装 topology/capture/send/scan/rescue/record/hub/wait。 - 技能文档可以引用软链接入口;更新上游内容必须通过 `tools/external/` 下的 submodule 指针完成。 - 不在本目录直接修改 submodule 内容;如需改造,先 fork 上游并更新 submodule 来源。 @@ -31,3 +37,4 @@ skills/auto-tmux/ - 上游来源:`tools/external/.tmux` 与 `tools/external/tmux`。 - 下游使用:`SKILL.md` 和 `references/` 中的命令示例。 - 验证入口:`skills/auto-skill/scripts/validate-skill.sh skills/auto-tmux --strict`。 +- 脚本验证:`bash -n skills/auto-tmux/scripts/auto-tmux.sh`。 diff --git a/skills/auto-tmux/README.md b/skills/auto-tmux/README.md index fe1202f..a84687b 100644 --- a/skills/auto-tmux/README.md +++ b/skills/auto-tmux/README.md @@ -5,6 +5,7 @@ ## 入口 - [SKILL.md](./SKILL.md) - 技能触发条件、命令片段、规则和质量门禁。 +- [scripts/](./scripts/) - 安全封装的 tmux 自动化脚本。 - [references/](./references/) - tmux / oh-my-tmux 参考、示例和故障排查。 - [assets/](./assets/) - 指向仓库外部 submodule 的只读软链接入口。 @@ -18,12 +19,15 @@ ## 快速验证 ```bash +bash -n skills/auto-tmux/scripts/auto-tmux.sh +skills/auto-tmux/scripts/auto-tmux.sh help skills/auto-skill/scripts/validate-skill.sh skills/auto-tmux --strict ``` ## 使用边界 -- 发送按键前必须先 `capture-pane` 复核目标上下文。 +- 优先使用 `scripts/auto-tmux.sh`,需要低层调试时再直接使用 tmux 原生命令。 +- 发送按键前必须先 `capture-pane` 复核目标上下文;脚本会默认打印最近上下文。 - 批量操作前必须先列出 session/window/pane 白名单。 - 不在未知 pane 中发送破坏性命令。 - 不在本目录复制或修改上游 tmux / oh-my-tmux 源码。 diff --git a/skills/auto-tmux/SKILL.md b/skills/auto-tmux/SKILL.md index f1d909b..b92e7e8 100644 --- a/skills/auto-tmux/SKILL.md +++ b/skills/auto-tmux/SKILL.md @@ -1,11 +1,11 @@ --- name: auto-tmux -description: "tmux 自动化操控:读取/广播/救援 session|window|pane,基于 oh-my-tmux 配置安全地让 AI 自控/互控终端。触发:需要 capture-pane/send-keys、批量巡检、蜂群 AI 协作、卡死救援或快速查看快捷键。" +description: "tmux 自动化操控:用 scripts/auto-tmux.sh 安全读取、发送、巡检、救援、录制 session|window|pane,并基于 oh-my-tmux 组织多 AI 终端协作。触发:capture-pane、send-keys、批量巡检、蜂群 AI 协作、卡死救援、tmux 工作台初始化。" --- # auto-tmux Skill -让 AI 像熟练运维一样操作 tmux:读取终端输出、发送按键、批量巡检、协作/救援其他终端,默认兼容 `tools/external/.tmux`(gpakosz/oh-my-tmux),并在本技能内通过 `assets/oh-my-tmux` 与 `assets/tmux-src` 暴露上游 submodule。 +让 AI 像熟练运维一样操作 tmux:优先使用 `scripts/auto-tmux.sh` 安全封装读取终端输出、发送按键、批量巡检、协作/救援其他终端;必要时再回退到 tmux 原生命令。默认兼容 `tools/external/.tmux`(gpakosz/oh-my-tmux),并在本技能内通过 `assets/oh-my-tmux` 与 `assets/tmux-src` 暴露上游 submodule。 ## When to Use This Skill @@ -13,6 +13,7 @@ description: "tmux 自动化操控:读取/广播/救援 session|window|pane, - 需要远程读取/复制某个 tmux pane 的最新输出(日志、提示、错误)。 - 需要向指定 pane 发送按键/命令(确认 `y`、`Enter`、`Ctrl+C`、广播同一窗口)。 - 需要批量巡检/接管多 AI 终端(蜂群协作、自动救援卡死任务)。 +- 需要初始化 AI 多终端工作台、等待输出 pattern、录制 pane 日志或保存巡检证据。 - 需要快速回忆 oh-my-tmux 快捷键、前缀或同步面板操作。 - 需要在当前仓库复用 `tools/external/.tmux` 配置并避免修改主配置。 - 需要从技能目录内快速查看 oh-my-tmux 配置或 tmux 上游源码入口。 @@ -22,30 +23,54 @@ description: "tmux 自动化操控:读取/广播/救援 session|window|pane, - 不用于执行非 tmux 环境(目标不存在时先创建 session/window/pane)。 - 不直接编辑 `~/.tmux.conf` / `.tmux.conf`(遵循 oh-my-tmux:修改请放到 `.tmux.conf.local`)。 - 不盲发破坏性按键:`kill-server`、`rm -rf` 等必须先 capture-pane 确认目标上下文。 +- 不把脚本当成权限绕过器;敏感会话、密码输入、生产数据库操作仍需人工确认。 - 如果 tmux 版本 < 2.6,需先升级再使用本技能的命令组合。 ## Quick Reference ### 常用模式(≤20 条,直接复制即用) +**脚本帮助与语法检查** +```bash +bash -n skills/auto-tmux/scripts/auto-tmux.sh +skills/auto-tmux/scripts/auto-tmux.sh help +``` + **查看快捷键帮助**(oh-my-tmux 双前缀:主 `C-a`,备用 `C-b`,前缀后按 `?`) ```text [按] ? ``` -**枚举拓扑(session/window/pane)** +**枚举拓扑(脚本优先)** +```bash +skills/auto-tmux/scripts/auto-tmux.sh topology +skills/auto-tmux/scripts/auto-tmux.sh topology --session ai-hub +``` + +**枚举拓扑(原生命令兜底)** ```bash tmux list-sessions tmux list-windows -a -F '#S:#I:#W#F' tmux list-panes -a -F '#S:#I.#P #{pane_current_command} #{pane_title}' ``` -**读取指定 pane 最近 120 行** +**读取指定 pane 最近输出** +```bash +skills/auto-tmux/scripts/auto-tmux.sh capture -t :. -n 120 +``` + +**原生读取指定 pane 最近 120 行** ```bash tmux capture-pane -t :. -p -S -120 ``` -**向指定 pane 发送确认键** +**向指定 pane 发送命令或确认键** +```bash +skills/auto-tmux/scripts/auto-tmux.sh send -t :. --text "make test" --enter +skills/auto-tmux/scripts/auto-tmux.sh rescue -t :. --pattern "(y/n)" --reply y +``` + +**原生发送确认键** ```bash tmux send-keys -t :. "y" Enter ``` @@ -61,27 +86,23 @@ tmux set-window-option synchronize-panes on tmux set-window-option synchronize-panes off ``` -**一次扫描全部窗口并截取最近输出(蜂群巡检骨架)** +**一次扫描全部窗口并截取最近输出(蜂群巡检)** ```bash -for w in $(tmux list-windows -a -F '#S:#I'); do - tmux capture-pane -t "$w" -p -S -80 | sed "s/^/[$w] /"; -done +skills/auto-tmux/scripts/auto-tmux.sh scan --session ai-hub -n 80 +skills/auto-tmux/scripts/auto-tmux.sh scan --session ai-hub --pattern "ERROR|Traceback" --save-dir /tmp/auto-tmux-scan ``` **远程救援:发现等待输入即发送 y** ```bash -target="0:3.0" # 示例 -tmux capture-pane -t "$target" -p -S -50 | grep -qi "(y/n)" && \ - tmux send-keys -t "$target" "y" Enter +target="$(tmux list-panes -t ai-hub:worker1 -F '#S:#I.#P' | head -n 1)" +skills/auto-tmux/scripts/auto-tmux.sh rescue -t "$target" --pattern "(y/n)" --reply y --dry-run +skills/auto-tmux/scripts/auto-tmux.sh rescue -t "$target" --pattern "(y/n)" --reply y ``` **新建 AI 蜂群工作台(1 指挥 + 3 工人)** ```bash -tmux new-session -d -s ai-hub -n commander 'bash' -tmux new-window -t ai-hub -n worker1 'kiro-cli chat' -tmux new-window -t ai-hub -n worker2 'kiro-cli chat' -tmux new-window -t ai-hub -n worker3 'kiro-cli chat' -tmux attach -t ai-hub +skills/auto-tmux/scripts/auto-tmux.sh hub --session ai-hub --workers 3 --cmd "codex" +skills/auto-tmux/scripts/auto-tmux.sh topology --session ai-hub ``` **启用 oh-my-tmux 配置(仓库内版本)** @@ -93,11 +114,18 @@ cp -n "$repo_root/skills/auto-tmux/assets/oh-my-tmux/.tmux.conf.local" ~/.tmux.c **记录 pane 输出到文件** ```bash -tmux pipe-pane -t :. -o 'cat >> /tmp/tmux---.log' +skills/auto-tmux/scripts/auto-tmux.sh record start -t :. --dir /tmp/auto-tmux-records +skills/auto-tmux/scripts/auto-tmux.sh record stop -t :. +``` + +**等待 pane 出现完成信号** +```bash +skills/auto-tmux/scripts/auto-tmux.sh wait -t :. --pattern "Tests passed|BUILD SUCCESS" --timeout 300 ``` ## Rules & Constraints +- MUST:优先使用 `scripts/auto-tmux.sh`;直接使用 tmux 原生命令时必须保留同等安全约束。 - MUST:在发送按键前用 `capture-pane` 复核目标上下文;按键操作必须带 `:.` 绝对定位。 - MUST:遵循 oh-my-tmux 约定,不修改主配置文件;自定义写入 `~/.tmux.conf.local`。 - MUST:批量操作前先 `list-windows`/`list-panes` 建立白名单,避免误控用户窗口。 @@ -110,17 +138,18 @@ tmux pipe-pane -t :. -o 'cat >> /tmp/tmux--:.`。 + 2) `skills/auto-tmux/scripts/auto-tmux.sh rescue -t --pattern "(y/n)" --reply y --dry-run` 确认命中。 + 3) `skills/auto-tmux/scripts/auto-tmux.sh rescue -t --pattern "(y/n)" --reply y` 发送确认。 - 验收:脚本继续运行,无额外多余输入。 ### Example 2:蜂群巡检 + 自动救援 - 输入:4 个 AI 终端分布在 session `ai-hub` 的多个窗口。 - 步骤: - 1) 运行 Quick Reference 巡检循环收集最近 80 行并观察状态。 - 2) 对匹配 `"(y/n)"` 的 pane 触发「远程救援」片段自动发 `y`。 + 1) `skills/auto-tmux/scripts/auto-tmux.sh scan --session ai-hub -n 80 --save-dir /tmp/auto-tmux-scan` 收集状态。 + 2) 对匹配 `"(y/n)"` 的 pane 运行 `rescue --dry-run`,确认后再真实发送。 3) 对输出包含 `Traceback` 或 `ERROR` 的 pane,先 `capture-pane` 记录再人工介入。 - 验收:等待确认的终端恢复,未误伤正常任务;巡检日志保存在本地。 @@ -128,8 +157,8 @@ tmux pipe-pane -t :. -o 'cat >> /tmp/tmux--:. -o 'cat >> /tmp/tmux--:. -o 'cat >> /tmp/tmux--:. + +Examples: + auto-tmux.sh topology + auto-tmux.sh capture -t -n 80 + auto-tmux.sh send -t --text "make test" --enter + auto-tmux.sh rescue -t --pattern "(y/n)" --reply y + auto-tmux.sh scan --session ai-hub --pattern "ERROR|Traceback" + auto-tmux.sh hub --session ai-hub --workers 3 --cmd "codex" +EOF +} + +die() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +warn() { + printf 'warn: %s\n' "$*" >&2 +} + +require_tmux() { + command -v tmux >/dev/null 2>&1 || die "tmux is not installed or not in PATH" + tmux list-sessions >/dev/null 2>&1 || die "no tmux server/session found" +} + +target_exists() { + local target="$1" + tmux display-message -pt "$target" '#{pane_id}' >/dev/null 2>&1 +} + +require_target() { + local target="$1" + [[ -n "$target" ]] || die "missing -t TARGET" + target_exists "$target" || die "tmux target not found: $target" +} + +redact() { + sed -E \ + -e 's/(OPENAI_API_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN|GH_TOKEN|API_KEY|TOKEN|PASSWORD|SECRET)=([^[:space:]]+)/\1=/g' \ + -e 's/(Bearer[[:space:]]+)[A-Za-z0-9._~+\/=-]+/\1/g' \ + -e 's/(sk-[A-Za-z0-9_-]{12,})//g' +} + +capture_raw() { + local target="$1" + local lines="$2" + tmux capture-pane -t "$target" -p -S "-$lines" +} + +capture_print() { + local target="$1" + local lines="$2" + local no_redact="$3" + if [[ "$no_redact" == "1" ]]; then + capture_raw "$target" "$lines" + else + capture_raw "$target" "$lines" | redact + fi +} + +is_dangerous_text() { + local text="$1" + echo "$text" | grep -Eiq "$DANGEROUS_PATTERN" +} + +cancel_copy_mode_if_needed() { + local target="$1" + local in_mode + in_mode="$(tmux display-message -pt "$target" '#{pane_in_mode}' 2>/dev/null || echo 0)" + if [[ "$in_mode" == "1" ]]; then + tmux send-keys -t "$target" -X cancel + fi +} + +pane_list() { + local session="$1" + if [[ -n "$session" ]]; then + tmux list-panes -a -F '#S:#I.#P' | awk -F: -v s="$session" '$1 == s {print}' + else + tmux list-panes -a -F '#S:#I.#P' + fi +} + +cmd_topology() { + local session="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown topology option: $1" ;; + esac + done + + require_tmux + if [[ -n "$session" ]]; then + tmux has-session -t "$session" 2>/dev/null || die "session not found: $session" + printf 'session: %s\n' "$session" + tmux list-windows -t "$session" -F ' window #{window_index}: #{window_name} #{window_flags}' + tmux list-panes -a -F '#S #S:#{window_index}.#{pane_index} #{pane_current_command} #{pane_current_path}' | + awk -v s="$session" '$1 == s {print " pane target=" $2 " cmd=" $3 " path=" $4}' + else + tmux list-sessions -F 'session: #{session_name} windows=#{session_windows} attached=#{session_attached}' + tmux list-windows -a -F ' window #S:#{window_index}: #{window_name} #{window_flags}' + tmux list-panes -a -F ' pane #S:#{window_index}.#{pane_index} cmd=#{pane_current_command} path=#{pane_current_path}' + fi +} + +cmd_capture() { + local target="" + local lines="$DEFAULT_LINES" + local save_file="" + local no_redact="0" + while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2:-}"; shift 2 ;; + -n|--lines) lines="${2:-}"; shift 2 ;; + --save) save_file="${2:-}"; shift 2 ;; + --no-redact) no_redact="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown capture option: $1" ;; + esac + done + + require_tmux + require_target "$target" + if [[ -n "$save_file" ]]; then + mkdir -p "$(dirname "$save_file")" + capture_print "$target" "$lines" "$no_redact" | tee "$save_file" + else + capture_print "$target" "$lines" "$no_redact" + fi +} + +cmd_send() { + local target="" + local text="" + local key="" + local enter="0" + local clear="0" + local force="0" + local dry_run="0" + while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2:-}"; shift 2 ;; + --text) text="${2:-}"; shift 2 ;; + --key) key="${2:-}"; shift 2 ;; + --enter) enter="1"; shift ;; + --clear) clear="1"; shift ;; + --force) force="1"; shift ;; + --dry-run) dry_run="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown send option: $1" ;; + esac + done + + require_tmux + require_target "$target" + [[ -n "$text" || -n "$key" ]] || die "send requires --text TEXT or --key KEY" + [[ -z "$text" || -z "$key" ]] || die "send accepts only one of --text or --key" + + printf -- '--- current context: %s ---\n' "$target" >&2 + capture_print "$target" 40 0 >&2 || true + + if [[ -n "$text" && "$force" != "1" ]] && is_dangerous_text "$text"; then + die "refusing dangerous text without --force: $text" + fi + + if [[ "$dry_run" == "1" ]]; then + printf '[dry-run] target=%s clear=%s text=%q key=%q enter=%s\n' "$target" "$clear" "$text" "$key" "$enter" + return 0 + fi + + cancel_copy_mode_if_needed "$target" + if [[ "$clear" == "1" ]]; then + tmux send-keys -t "$target" C-u + fi + if [[ -n "$text" ]]; then + tmux send-keys -t "$target" -l "$text" + else + tmux send-keys -t "$target" "$key" + fi + if [[ "$enter" == "1" ]]; then + tmux send-keys -t "$target" Enter + fi +} + +cmd_rescue() { + local target="" + local pattern="$DEFAULT_RESCUE_PATTERN" + local reply="y" + local lines=80 + local force="0" + local dry_run="0" + while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2:-}"; shift 2 ;; + --pattern) pattern="${2:-}"; shift 2 ;; + --reply) reply="${2:-}"; shift 2 ;; + -n|--lines) lines="${2:-}"; shift 2 ;; + --force) force="1"; shift ;; + --dry-run) dry_run="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown rescue option: $1" ;; + esac + done + + require_tmux + require_target "$target" + local output + output="$(capture_raw "$target" "$lines")" + printf '%s\n' "$output" | redact + if printf '%s\n' "$output" | grep -Eiq "$pattern"; then + printf '[match] target=%s pattern=%s reply=%q\n' "$target" "$pattern" "$reply" >&2 + if [[ "$dry_run" == "1" ]]; then + printf '[dry-run] would send reply to %s\n' "$target" + else + if [[ "$force" == "1" ]]; then + cmd_send -t "$target" --text "$reply" --enter --force + else + cmd_send -t "$target" --text "$reply" --enter + fi + fi + else + printf '[skip] no rescue pattern matched for %s\n' "$target" >&2 + fi +} + +cmd_scan() { + local session="" + local lines=80 + local pattern="" + local rescue="0" + local reply="y" + local save_dir="" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="${2:-}"; shift 2 ;; + -n|--lines) lines="${2:-}"; shift 2 ;; + --pattern) pattern="${2:-}"; shift 2 ;; + --rescue) rescue="1"; shift ;; + --reply) reply="${2:-}"; shift 2 ;; + --save-dir) save_dir="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown scan option: $1" ;; + esac + done + + require_tmux + if [[ -n "$save_dir" ]]; then + mkdir -p "$save_dir" + fi + + local pane output safe_name + while IFS= read -r pane; do + [[ -n "$pane" ]] || continue + printf '\n===== [%s] =====\n' "$pane" + output="$(capture_raw "$pane" "$lines")" + printf '%s\n' "$output" | redact + if [[ -n "$save_dir" ]]; then + safe_name="${pane//[:.]/-}.log" + printf '%s\n' "$output" | redact > "$save_dir/$safe_name" + fi + if [[ -n "$pattern" ]] && printf '%s\n' "$output" | grep -Eiq "$pattern"; then + printf '[match] %s matched %s\n' "$pane" "$pattern" >&2 + if [[ "$rescue" == "1" ]]; then + cmd_send -t "$pane" --text "$reply" --enter + fi + fi + done < <(pane_list "$session") +} + +cmd_record() { + local action="${1:-}" + [[ -n "$action" ]] || die "record requires start or stop" + shift || true + + local target="" + local dir="/tmp/auto-tmux-records" + while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2:-}"; shift 2 ;; + --dir) dir="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown record option: $1" ;; + esac + done + + require_tmux + require_target "$target" + case "$action" in + start) + mkdir -p "$dir" + local safe_name="${target//[:.]/-}.log" + local file="$dir/$safe_name" + tmux pipe-pane -t "$target" -o "cat >> '$file'" + printf 'recording %s -> %s\n' "$target" "$file" + ;; + stop) + tmux pipe-pane -t "$target" + printf 'recording stopped for %s\n' "$target" + ;; + *) + die "record action must be start or stop" + ;; + esac +} + +cmd_hub() { + local session="ai-hub" + local workers=3 + local worker_cmd="bash" + local commander_cmd="bash" + local attach="0" + while [[ $# -gt 0 ]]; do + case "$1" in + --session) session="${2:-}"; shift 2 ;; + --workers) workers="${2:-}"; shift 2 ;; + --cmd) worker_cmd="${2:-}"; shift 2 ;; + --commander-cmd) commander_cmd="${2:-}"; shift 2 ;; + --attach) attach="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown hub option: $1" ;; + esac + done + + command -v tmux >/dev/null 2>&1 || die "tmux is not installed or not in PATH" + if tmux has-session -t "$session" 2>/dev/null; then + warn "session already exists: $session" + tmux list-windows -t "$session" -F '#S:#I #{window_name}' + else + tmux new-session -d -s "$session" -n commander "$commander_cmd" + local i + for i in $(seq 1 "$workers"); do + tmux new-window -t "$session" -n "worker$i" "$worker_cmd" + done + tmux select-window -t "$session:commander" + printf 'created tmux AI hub: %s workers=%s cmd=%q\n' "$session" "$workers" "$worker_cmd" + fi + if [[ "$attach" == "1" ]]; then + tmux attach -t "$session" + fi +} + +cmd_wait() { + local target="" + local pattern="" + local timeout=60 + local interval=2 + local lines=80 + while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2:-}"; shift 2 ;; + --pattern) pattern="${2:-}"; shift 2 ;; + --timeout) timeout="${2:-}"; shift 2 ;; + --interval) interval="${2:-}"; shift 2 ;; + -n|--lines) lines="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown wait option: $1" ;; + esac + done + + require_tmux + require_target "$target" + [[ -n "$pattern" ]] || die "wait requires --pattern REGEX" + + local start now output + start="$(date +%s)" + while true; do + output="$(capture_raw "$target" "$lines")" + if printf '%s\n' "$output" | grep -Eiq "$pattern"; then + printf '%s\n' "$output" | redact + printf '[matched] target=%s pattern=%s\n' "$target" "$pattern" >&2 + return 0 + fi + now="$(date +%s)" + if (( now - start >= timeout )); then + printf '%s\n' "$output" | redact + die "timeout waiting for pattern: $pattern" + fi + sleep "$interval" + done +} + +main() { + local cmd="${1:-help}" + shift || true + case "$cmd" in + help|-h|--help) usage ;; + topology) cmd_topology "$@" ;; + capture) cmd_capture "$@" ;; + send) cmd_send "$@" ;; + rescue) cmd_rescue "$@" ;; + scan) cmd_scan "$@" ;; + record) cmd_record "$@" ;; + hub) cmd_hub "$@" ;; + wait) cmd_wait "$@" ;; + *) die "unknown command: $cmd" ;; + esac +} + +main "$@"