tmuxclihook 0.5.11 → 0.5.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmuxclihook",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "交互式 TUI 安装器:环境侦测、AI CLI 侦测、tmux-ai-status hooks 装/卸/修、TPM 软链校验",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,15 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
18
18
  source "${_LIB_DIR}/lib-install-hooks.sh"
19
19
  source "${_LIB_DIR}/adapters/claude.sh"
20
20
 
21
+ # 子命令白名单:必须在取锁前校验——拼错的子命令不应先白等最多 10s 锁超时
22
+ case "${1:-install}" in
23
+ install|uninstall|check|auto) ;;
24
+ *)
25
+ echo "Usage: $0 [install|uninstall|check [--json]|auto]" >&2
26
+ exit 2
27
+ ;;
28
+ esac
29
+
21
30
  # 写路径互斥(check 只读不锁):临界区覆盖 adapter 调用 + suppress 标记读-改-写,
22
31
  # 串行化 auto 与显式 uninstall 的标记 TOCTOU。失败语义见 _install_acquire_lock:
23
32
  # 降级无锁(rc=2)继续;等待超时(rc=1)退出。
@@ -15,6 +15,15 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
15
15
  source "${_LIB_DIR}/lib-install-hooks.sh"
16
16
  source "${_LIB_DIR}/adapters/codex.sh"
17
17
 
18
+ # 子命令白名单:必须在取锁前校验——拼错的子命令不应先白等最多 10s 锁超时
19
+ case "${1:-install}" in
20
+ install|uninstall|check) ;;
21
+ *)
22
+ echo "Usage: $0 [install|uninstall|check [--json]]" >&2
23
+ exit 2
24
+ ;;
25
+ esac
26
+
18
27
  # 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
19
28
  case "${1:-install}" in
20
29
  check) ;;
@@ -15,6 +15,15 @@ _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
15
15
  source "${_LIB_DIR}/lib-install-hooks.sh"
16
16
  source "${_LIB_DIR}/adapters/opencode.sh"
17
17
 
18
+ # 子命令白名单:必须在取锁前校验——拼错的子命令不应先白等最多 10s 锁超时
19
+ case "${1:-install}" in
20
+ install|uninstall|check) ;;
21
+ *)
22
+ echo "Usage: $0 [install|uninstall|check [--json]]" >&2
23
+ exit 2
24
+ ;;
25
+ esac
26
+
18
27
  # 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
19
28
  case "${1:-install}" in
20
29
  check) ;;
@@ -666,6 +666,23 @@ _clear_pending() {
666
666
  _release_lock
667
667
  }
668
668
 
669
+ # pending 集合中是否有 N 秒内的条目(每行 "id tool ts")。用于区分「工具确实在
670
+ # 跑」与「泄漏/中断残留的旧条目」——并发 hook 抢锁超时会让 _remove_pending_id
671
+ # 静默放弃,集合可能长期残留陈旧 id(实测存在),不能只按非空判定活跃。
672
+ _pending_recent() { # $1=秒数 → 0=有新鲜条目
673
+ local max="${1:-10}" now f
674
+ now=$(date +%s)
675
+ for f in "$(_pending_pre_path)" "$(_pending_perm_path)"; do
676
+ [ -s "$f" ] || continue
677
+ if awk -v now="$now" -v max="$max" '
678
+ $3 ~ /^[0-9]+$/ && now - $3 <= max { found = 1; exit }
679
+ END { exit !found }' "$f" 2>/dev/null; then
680
+ return 0
681
+ fi
682
+ done
683
+ return 1
684
+ }
685
+
669
686
  # 根据当前 pending-perm / pending-pre 派生 STATUS。结果写入全局 DERIVED
670
687
  # return 0 = 派生出非空 STATUS, return 1 = 都空(事件需自行决定)
671
688
  _derive_status_from_pending() {
@@ -942,13 +959,17 @@ _cleanup_stale_panes() {
942
959
  # (tr -c 'A-Za-z0-9._-' '_'),无法从 key 反推 client → 反向枚举存活集比对。
943
960
  # 实测关注:长期运行的 server 会累积已 detach 的 client 键(8 option / 4 存活)。
944
961
  _prune_prev_sess_options() {
945
- local alive=" " name key opt
962
+ local alive=" " name key opt clients opts
963
+ # 两次 tmux 查询都必须成功才剪枝:tmux 抖动(server 重启/连接失败)时
964
+ # list-clients 返回空会被误判为「无存活 client」→ 把全部键删光。
965
+ clients=$(tmux list-clients -F '#{client_name}' 2>/dev/null) || return 0
966
+ opts=$(tmux show-options -g 2>/dev/null) || return 0
946
967
  while IFS= read -r name; do
947
968
  [ -n "$name" ] || continue
948
969
  key=$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_')
949
970
  alive="$alive$key "
950
- done < <(tmux list-clients -F '#{client_name}' 2>/dev/null)
951
- for opt in $(tmux show-options -g 2>/dev/null | awk '/^@ai_prev_sess_/{print $1}'); do
971
+ done <<< "$clients"
972
+ for opt in $(printf '%s\n' "$opts" | awk '/^@ai_prev_sess_/{print $1}'); do
952
973
  key="${opt#@ai_prev_sess_}"
953
974
  case "$alive" in
954
975
  *" $key "*) ;;
@@ -80,20 +80,28 @@ _current_pane_status() {
80
80
 
81
81
  # Notification「隐式」路径(idle_prompt / waiting for your input)的状态解析:
82
82
  # 通知可能乱序晚到(async hook 无顺序保证),不能无条件降级为 idle '-'。
83
- # 优先级:pending 派生(工具确实在跑 → '>')> live 审批态(!/?;claude 不写
84
- # pending-perm,纯派生看不见审批态)> idle '-'。
83
+ # 判定顺序(⚠️ 不能颠倒):
84
+ # 1. live 审批态 !/? → 保持。审批等待期间 pending-pre 非空(PreToolUse 先于
85
+ # PermissionRequest),若先看 pending 会派生出 '>' 把 '!' 降级——违反
86
+ # 「审批态永不覆盖」
87
+ # 2. pending 派生出 !/? → 采用(codex/opencode 的审批集合;claude 不写 perm)
88
+ # 3. pending 派生出 '>' → **仅当存在 10s 内的新条目才可信**(并发 hook 抢锁
89
+ # 超时会让清理静默放弃,集合可能残留陈旧 id),否则视为残留
90
+ # 4. 其余 → '-'
85
91
  # 显式重置消息(denied/cancelled/rejected)不走本函数——denial 携带「等待已结束」
86
92
  # 语义,应无条件置 '-'。
87
93
  _notification_implicit_status() {
88
- if _derive_status_from_pending; then
89
- printf '%s' "$DERIVED"
90
- return
91
- fi
92
94
  local live
93
95
  live=$(_current_pane_status)
94
96
  case "$live" in
95
97
  "!"|"?") printf '%s' "$live"; return ;;
96
98
  esac
99
+ if _derive_status_from_pending; then
100
+ case "$DERIVED" in
101
+ "!"|"?") printf '%s' "$DERIVED"; return ;;
102
+ esac
103
+ if _pending_recent 10; then printf '%s' ">"; return; fi
104
+ fi
97
105
  printf '%s' "-"
98
106
  }
99
107
 
@@ -128,7 +128,7 @@ fi
128
128
  # 全局 root 表绑定会覆盖用户既有 Escape 绑定——覆盖前保存 + 一次性告警
129
129
  # (无法安全链式调用任意已有绑定,恢复需手动):
130
130
  # tmux show-option -gv @claude_hooks_saved_bind_escape
131
- _prev_esc=$(tmux list-keys -n Escape 2>/dev/null || true)
131
+ _prev_esc=$(tmux list-keys -T root Escape 2>/dev/null || true)
132
132
  case "$_prev_esc" in
133
133
  ""|*tmux-ai-esc*) ;;
134
134
  *)