tmuxclihook 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,6 +88,11 @@ clack 0.7 只把 ctrl+c 映射为取消(Esc 默认无效果),安装器在
88
88
 
89
89
  live 模式以 `~/.claude/settings.json` 为共享配置真源(cc-switch 4.0.2 起不再读库里的 `common_config_claude`),写入用与文件模式相同的原子合并,他人 hook 与配置其他字段保留;legacy 模式用与 cc-switch 自身一致的 `json_patch` 更新 db。注意:cc-switch GUI 运行中会把内存缓存写回真源,安装前建议先退出 GUI(脚本会警告);legacy 模式下不经 `switch` 直接运行的 `claude` 读不到 db 里的 hooks。
90
90
 
91
+ **codex / opencode 的 cc-switch 真源**
92
+
93
+ - **codex**:`switch` 为每个 provider 创建独立 `CODEX_HOME`(`~/.cc-switch/instances/codex/<providerId>`),并把 `~/.codex/hooks.json` 符号链接进实例,因此插件写默认(或 `$CODEX_HOME`)下的 `hooks.json` 即被所有实例共享。但 `[hooks.state]` 信任记录在实例自己的 `config.toml` 里、cc-switch 不复制——**每个新 provider 实例首次启动需在 codex TUI 内 Trust all 一次**,否则 hooks 不执行(trusted_hash 无法在 bash 侧复现,插件不预写)。
94
+ - **opencode**:cc-switch 对它是 additive live config(无 per-provider 实例目录),provider 条目直接写进配置目录的 `opencode.json(c)`。插件目录解析为 `OPENCODE_CONFIG_DIR` > cc-switch `settings.json` 的 `opencodeConfigDir` > `${XDG_CONFIG_HOME:-~/.config}/opencode`;旧版 cc-switch 无该键时自动回落默认目录。
95
+
91
96
  ## Source 管理(生产默认 / 本地调试切换)
92
97
 
93
98
  `npm i -g` 装完后,hooks 安装与 tmux 集成默认使用包内自带的 `scripts/`(由 `npm publish` 前的 `prepack` 步骤从仓库根复制打包进来,见「架构」一节),开箱即用,无需仓库 checkout。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmuxclihook",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "交互式 TUI 安装器:环境侦测、AI CLI 侦测、tmux-ai-status hooks 装/卸/修、TPM 软链校验",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,8 +24,37 @@ ADAPTER_EVENTS="SessionStart UserPromptSubmit PreToolUse PermissionRequest Permi
24
24
  # 前台进程名:opencode 二进制无 arch 后缀(区别于 codex 的 codex-aarch64-* 等)
25
25
  ADAPTER_PROCESS_NAMES="opencode"
26
26
 
27
- # plugin 文件路径(XDG 全局目录,OPENCODE_CONFIG_DIR 可覆盖)
28
- ADAPTER_HOOKS_FILE="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/plugins/tmux-ai-status.ts"
27
+ # 配置目录解析(与 opencode 原生规则 + cc-switch 4.0 的设备级目录覆盖一致):
28
+ # 1. OPENCODE_CONFIG_DIR 环境变量——opencode 原生支持;cc-switch GUI(模型探测/WSL
29
+ # 启动)会注入该变量
30
+ # 2. cc-switch `~/.cc-switch/settings.json` 的 `opencodeConfigDir`(cc-switch 的
31
+ # 「目录覆盖」真源;旧版无此键 → 自动落到 3,向后兼容)
32
+ # 3. XDG 默认:`$XDG_CONFIG_HOME/opencode`(通常 ~/.config/opencode)
33
+ # 注意:opencode 不像 claude/codex 那样有 per-provider 实例目录——cc-switch 对它是
34
+ # 「additive live config」,provider 条目直接写进该目录下的 opencode.json(c);
35
+ # 本插件的 plugin 文件与它同目录共存,互不覆盖。
36
+ _opencode_config_dir() {
37
+ if [ -n "${OPENCODE_CONFIG_DIR:-}" ]; then
38
+ printf '%s\n' "$OPENCODE_CONFIG_DIR"
39
+ return 0
40
+ fi
41
+ local settings="$HOME/.cc-switch/settings.json" dir=""
42
+ if [ -f "$settings" ]; then
43
+ dir=$(sed -n 's/.*"opencodeConfigDir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$settings" 2>/dev/null | head -n 1)
44
+ case "$dir" in
45
+ "~") dir="$HOME" ;;
46
+ "~/"*) dir="${HOME}/${dir:2}" ;;
47
+ esac
48
+ fi
49
+ if [ -n "$dir" ]; then
50
+ printf '%s\n' "$dir"
51
+ else
52
+ printf '%s\n' "${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
53
+ fi
54
+ }
55
+
56
+ # plugin 文件路径(配置目录解析见 _opencode_config_dir)
57
+ ADAPTER_HOOKS_FILE="$(_opencode_config_dir)/plugins/tmux-ai-status.ts"
29
58
 
30
59
  # opencode 无 SessionEnd → 退出由 tmux-ai-monitor 做进程检测
31
60
  ADAPTER_HAS_SESSION_END="false"
@@ -168,13 +168,20 @@ _provider_tag_colour() {
168
168
  build_all_status() {
169
169
  ALL=""
170
170
  cur_sess=""
171
- local _ai_names
171
+ local _ai_names _fmt _n
172
172
  _ai_names=$(_collect_ai_process_names)
173
- while IFS='|' read -r pane_id session_name win_idx pane_idx claude_status _attached _created _entered cur_cmd prov_name prov_id; do
173
+ # 动态追加上 monitor 的 seen 标记(无 SessionEnd 工具的进程树探测结果)。
174
+ # switch/volta-shim 启动的 pane 前台命令不是 codex,必须靠该标记补 idle `-`。
175
+ _fmt="#{pane_id}|#{session_name}|#{window_index}|#{pane_index}|#{@ai_pane_status}|#{session_attached}|#{session_created}|#{@ai_entered_time}|#{pane_current_command}|#{@ai_provider}|#{@ai_provider_id}"
176
+ for _n in $_ai_names; do
177
+ _fmt="$_fmt|#{@ai_monitor_seen_$_n}"
178
+ done
179
+ while IFS='|' read -r pane_id session_name win_idx pane_idx claude_status _attached _created _entered cur_cmd prov_name prov_id _seen_flags; do
174
180
  local pane_status="$claude_status"
175
181
  if [ -z "$pane_status" ]; then
176
182
  # 无显式状态:若前台是 AI 进程(codex 首个 turn 前空窗期),补 idle。
177
183
  # 进程名集合从所有 adapter 汇总,前缀匹配(codex-aarch64-* 等)。
184
+ # 前台命令是 switch/volta-shim 时改为读 monitor 的进程树标记。
178
185
  local _cmd_base _n _is_ai=0
179
186
  _cmd_base=$(basename "$cur_cmd" 2>/dev/null)
180
187
  for _n in $_ai_names; do
@@ -182,6 +189,9 @@ build_all_status() {
182
189
  "$_n"|"$_n"-*|"$_n"_*) _is_ai=1; break ;;
183
190
  esac
184
191
  done
192
+ if [ "$_is_ai" = "0" ]; then
193
+ case "$_seen_flags" in *1*) _is_ai=1 ;; esac
194
+ fi
185
195
  [ "$_is_ai" = "1" ] && pane_status="-" || continue
186
196
  fi
187
197
  local session_block="#[bg=#6272A4,fg=#F8F8F2] ${session_name} #[bg=default,fg=default]"
@@ -212,7 +222,7 @@ build_all_status() {
212
222
  # 自指 switch 由 handler 的 per-client prev 对比滤除)
213
223
  # ② session_created(无进入记录时回退创建序)。不用 session_last_attached——
214
224
  # tmux 的 server_client_set_session 无条件刷新该时间戳(自指 switch 也算),污染排序。
215
- done < <(tmux list-panes -a -F "#{pane_id}|#{session_name}|#{window_index}|#{pane_index}|#{@ai_pane_status}|#{session_attached}|#{session_created}|#{@ai_entered_time}|#{pane_current_command}|#{@ai_provider}|#{@ai_provider_id}" 2>/dev/null | awk -F'|' -v OFS='|' '$6>0{if($8=="")$8=$7; print}' | sort -t'|' -k8,8n -k7,7n -k2,2 -k3,3n -k4,4n)
225
+ done < <(tmux list-panes -a -F "$_fmt" 2>/dev/null | awk -F'|' -v OFS='|' '$6>0{if($8=="")$8=$7; print}' | sort -t'|' -k8,8n -k7,7n -k2,2 -k3,3n -k4,4n)
216
226
  }
217
227
 
218
228
  # --- Pending 集合管理(PreToolUse / PermissionRequest 配对)---
@@ -2,9 +2,11 @@
2
2
  # tmux-ai-monitor — tmux server 级单例监控器
3
3
  #
4
4
  # 部分 AI CLI(codex、opencode)没有 SessionEnd hook,退出无法主动清空状态;codex 的
5
- # SessionStart 还会延迟到首个 turn。本监控器每秒根据 pane_current_command / 进程树刷新:
5
+ # SessionStart 还会延迟到首个 turn。本监控器每秒刷新:
6
6
  # - 前台启动此类工具、尚无 hook 状态时,显示 idle `-`
7
- # - 退出返回 shell 后,清除残留 pane 状态
7
+ # - cc-switch 的 `switch` 启动时前台命令是 volta-shim/node 而非工具名,改为
8
+ # (每 N 个 tick)探测 launcher pane 的进程树来识别,并写 seen 标记供聚合补 `-`
9
+ # - 退出返回 shell(或进程树已无该工具)后,清除残留 pane 状态
8
10
  # 泛化实现:扫描 adapters/*.sh 找出 ADAPTER_HAS_SESSION_END="false" 的工具及其
9
11
  # ADAPTER_PROCESS_NAMES,逐个工具做生命周期检测——新增同类工具的 adapter 无需改本文件。
10
12
  # `/new`(codex)等同工具内会话切换不改变前台进程,因此不会改写已有 `✓`。
@@ -15,6 +17,18 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
17
  _ADAPTER_DIR="${SCRIPT_DIR}/adapters"
16
18
  MODE="${1:-start}"
17
19
  MONITOR_INTERVAL="${TMUX_AI_MONITOR_INTERVAL:-1}"
20
+ # 进程树探测节流:launcher pane 每 N 个 tick 做一次 BFS(run 循环内 _TICK 持续累加)。
21
+ _MONITOR_PROBE_EVERY=5
22
+ _MONITOR_TICK=0
23
+
24
+ # 常见 launcher:switch/volta/npm 系脚本在前台时,目标 CLI 位于其子进程树中,
25
+ # pane_current_command 不会直接等于 codex/opencode(cc-switch 的 switch 启动即如此)。
26
+ _is_launcher_command() {
27
+ case "$(basename "${1:-}" 2>/dev/null)" in
28
+ switch|volta|volta-shim|node|npx|pnpx|pnpm|yarn|bun|bunx|deno) return 0 ;;
29
+ esac
30
+ return 1
31
+ }
18
32
 
19
33
  _tmux_server_pid() {
20
34
  tmux display-message -p '#{pid}' 2>/dev/null
@@ -86,15 +100,16 @@ _pane_has_tool_process() {
86
100
  return 1
87
101
  }
88
102
 
89
- # 对每个无 SessionEnd 的工具名做生命周期检测:前台命令匹配 → 标记见过;
90
- # 曾标记但前台已切走且进程树也无该工具 → 清空 pane 状态(该工具已退出)。
91
- # seen 标记按工具名区分(@ai_monitor_seen_<name>),避免多工具互相踩踏清理语义。
103
+ # 对每个无 SessionEnd 的工具名做生命周期检测:前台命令匹配或 launcher 进程树命中
104
+ # → 标记见过;曾标记但前台已切走且进程树也无该工具 → 清空 pane 状态(该工具已退出)。
105
+ # seen 标记按工具名区分(@ai_monitor_seen_<name>),避免多工具互相踩踏清理语义;
106
+ # 该标记同时是 build_all_status 给 launcher pane 补 idle `-` 的依据。
92
107
  _monitor_no_session_end_lifecycle() {
93
108
  local tools name pane_id current_command sanitized pane_dir
94
109
  tools=$(_collect_no_session_end_tools)
95
110
  [ -n "$tools" ] || return 0
96
111
  for name in $tools; do
97
- while IFS='|' read -r pane_id current_command seen; do
112
+ while IFS='|' read -r pane_id current_command seen attached; do
98
113
  if [ -n "$current_command" ] && _is_tool_command "$(basename "$current_command" 2>/dev/null)" "$name"; then
99
114
  [ "$seen" = "1" ] || tmux set-option -pt "$pane_id" "@ai_monitor_seen_${name}" 1 2>/dev/null || true
100
115
  elif [ "$seen" = "1" ] && ! _pane_has_tool_process "$pane_id" "$name"; then
@@ -103,8 +118,17 @@ _monitor_no_session_end_lifecycle() {
103
118
  sanitized="${pane_id//[^a-zA-Z0-9]/_}"
104
119
  pane_dir="${_STATUS_DIR}/${sanitized}"
105
120
  rm -rf "$pane_dir" 2>/dev/null
121
+ elif [ "$seen" != "1" ] \
122
+ && [ "${attached:-0}" -gt 0 ] \
123
+ && [ $((_MONITOR_TICK % _MONITOR_PROBE_EVERY)) -eq 0 ] \
124
+ && _is_launcher_command "$current_command" \
125
+ && _pane_has_tool_process "$pane_id" "$name"; then
126
+ # launcher(switch/volta-shim/node)启动:前台命令不是工具名,
127
+ # 但进程树里有。标记 seen 后 build_all_status 才会补 idle `-`,
128
+ # 工具退出时也才会走上面的清理分支(否则残留状态永不清)。
129
+ tmux set-option -pt "$pane_id" "@ai_monitor_seen_${name}" 1 2>/dev/null || true
106
130
  fi
107
- done < <(tmux list-panes -a -F "#{pane_id}|#{pane_current_command}|#{@ai_monitor_seen_${name}}" 2>/dev/null)
131
+ done < <(tmux list-panes -a -F "#{pane_id}|#{pane_current_command}|#{@ai_monitor_seen_${name}}|#{session_attached}" 2>/dev/null)
108
132
  done
109
133
  }
110
134
 
@@ -118,6 +142,7 @@ _monitor_tick() {
118
142
  tmux set-option -g @ai_all_status "$ALL" 2>/dev/null || return 1
119
143
  tmux refresh-client -S 2>/dev/null || true
120
144
  fi
145
+ _MONITOR_TICK=$((_MONITOR_TICK + 1))
121
146
  return 0
122
147
  }
123
148
 
@@ -57,7 +57,9 @@ function buildTools(scriptsDir) {
57
57
  versionArgs: ['--version'],
58
58
  versionRe: /(\d+\.\d+\.\d+)/,
59
59
  minVersion: null, // plugin 系统本地文件跳过版本兼容检查
60
- // hooksFile 实为 JS/TS plugin 文件(非 JSON hooks 配置),install/uninstall 整文件写入/删除
60
+ // hooksFile 实为 JS/TS plugin 文件(非 JSON hooks 配置),install/uninstall 整文件写入/删除。
61
+ // 权威路径由 bash adapter 解析(OPENCODE_CONFIG_DIR > cc-switch opencodeConfigDir > XDG),
62
+ // 此处仅静态近似,判定一律走 wrapper check,避免两处真源发散。
61
63
  hooksFile: join(
62
64
  process.env.OPENCODE_CONFIG_DIR || join(process.env.XDG_CONFIG_HOME || join(HOME, '.config'), 'opencode'),
63
65
  'plugins',