tmuxclihook 0.5.9 → 0.5.11
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 +1 -1
- package/scripts/adapters/claude.sh +87 -0
- package/scripts/install-claude-hooks.sh +30 -3
- package/scripts/install-codex-hooks.sh +19 -2
- package/scripts/install-opencode-hooks.sh +19 -2
- package/scripts/lib-install-hooks.sh +74 -3
- package/scripts/lib-tmux-ai-status.sh +35 -10
- package/scripts/tmux-ai-reload +29 -0
- package/scripts/tmux-ai-status +37 -12
- package/tmux-ai-hooks-status.tmux +35 -13
package/package.json
CHANGED
|
@@ -175,6 +175,24 @@ _ccswitch_warn_gui() {
|
|
|
175
175
|
fi
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
# live 模式的真源是 $HOME/.claude/settings.json(cc-switch 硬编码,不随
|
|
179
|
+
# CLAUDE_CONFIG_DIR 变)。若当前环境的 CLAUDE_CONFIG_DIR 指向非 cc-switch
|
|
180
|
+
# 实例目录(用户自定义目录裸跑 claude),该进程读的是自定义目录,本插件写入
|
|
181
|
+
# 的真源可能不生效——纯诊断告警,不改写入行为(不做双写,理由同 2026-10-13
|
|
182
|
+
# 决策:外部覆盖拉锯风险大于收益)。
|
|
183
|
+
_ccswitch_warn_config_dir_mismatch() {
|
|
184
|
+
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || return 0
|
|
185
|
+
local inst="${HOME}/.cc-switch/instances/claude" resolved
|
|
186
|
+
resolved=$(cd "$CLAUDE_CONFIG_DIR" 2>/dev/null && pwd -P) || resolved="$CLAUDE_CONFIG_DIR"
|
|
187
|
+
case "$resolved" in
|
|
188
|
+
"$inst"|"$inst"/*) ;;
|
|
189
|
+
*)
|
|
190
|
+
echo "WARN: CLAUDE_CONFIG_DIR=$CLAUDE_CONFIG_DIR 不是 cc-switch 实例目录;" \
|
|
191
|
+
"live 模式写入 $(_ccswitch_live_file) 可能不被该进程读取" >&2
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
# 读通用配置 value → mktemp 文件,路径写入 $1 命名的变量。
|
|
179
197
|
# 行缺失/空白 → "{}";非空但非法 JSON → 明确报错 return 1(绝不覆盖用户数据)。
|
|
180
198
|
# 注意:内部临时变量名不得与调用方常用名(tmp 等)同名——bash 动态作用域下
|
|
@@ -255,6 +273,51 @@ adapter_check_integrity() {
|
|
|
255
273
|
return 0
|
|
256
274
|
}
|
|
257
275
|
|
|
276
|
+
# 已收敛判定(仅供 wrapper 的 auto 分支使用——⚠️ 不得被事件路径复用)。
|
|
277
|
+
# 四条件全过 = auto 可安全跳过重装:integrity 过 + 全部条目指向当前源(无漂移/
|
|
278
|
+
# 死条目)+ 每事件本插件条目 ≤1(无重复)。
|
|
279
|
+
# 参照系:wrapper 的 _LIB_DIR 是 pwd -P 物理路径;写入(install/auto/修复全部经
|
|
280
|
+
# wrapper)与比较同参照系自洽。事件路径的 _LIB_DIR 是 pwd 逻辑路径(软链场景
|
|
281
|
+
# 两者不等),事件路径复用会误判漂移。
|
|
282
|
+
# 取源逻辑与 adapter_check_integrity 的三模式 case 保持同步(不抽共享以控制
|
|
283
|
+
# integrity 的回归面)。
|
|
284
|
+
adapter_check_converged() {
|
|
285
|
+
adapter_check_integrity || return 1
|
|
286
|
+
local mode src="" tmp="" stats
|
|
287
|
+
mode=$(_ccswitch_mode)
|
|
288
|
+
case "$mode" in
|
|
289
|
+
live)
|
|
290
|
+
src=$(_ccswitch_live_file)
|
|
291
|
+
[ -f "$src" ] || return 1
|
|
292
|
+
;;
|
|
293
|
+
legacy)
|
|
294
|
+
_ccswitch_dump_common tmp || return 1
|
|
295
|
+
src="$tmp"
|
|
296
|
+
;;
|
|
297
|
+
*)
|
|
298
|
+
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
299
|
+
src="$ADAPTER_HOOKS_FILE"
|
|
300
|
+
;;
|
|
301
|
+
esac
|
|
302
|
+
# 一次 jq 同时产出漂移计数(≠ 当前源,含死路径)与重复计数(每事件条目 >1)
|
|
303
|
+
stats=$(jq -r --arg cur "${_LIB_DIR}/tmux-ai-status" '
|
|
304
|
+
.hooks as $h |
|
|
305
|
+
["SessionStart","SessionEnd","UserPromptSubmit","PreToolUse","PostToolUse",
|
|
306
|
+
"PostToolUseFailure","PermissionRequest","Notification","Stop","StopFailure"] as $events |
|
|
307
|
+
[$events[] as $e |
|
|
308
|
+
[$h[$e][]?.hooks[]?.command // empty
|
|
309
|
+
| select(contains("tmux-ai-status")) | split(" ")[0]] as $scripts |
|
|
310
|
+
select(($scripts | length) > 0) |
|
|
311
|
+
{bad: ([$scripts[] | select(. != $cur)] | length),
|
|
312
|
+
n: ($scripts | length)}]
|
|
313
|
+
| "bad=\([.[].bad] | add // 0) dup=\([.[].n | select(. > 1)] | length)"' "$src" 2>/dev/null)
|
|
314
|
+
[ -z "$tmp" ] || rm -f "$tmp"
|
|
315
|
+
case "$stats" in
|
|
316
|
+
"bad=0 dup=0") return 0 ;;
|
|
317
|
+
*) return 1 ;;
|
|
318
|
+
esac
|
|
319
|
+
}
|
|
320
|
+
|
|
258
321
|
# 明细检查(仅 install wrapper 的 check --json 调用;事件热路径不触达)。
|
|
259
322
|
# stdout 输出单行紧凑 JSON 片段 {"mode":...,"events":{...},"plugin":null,"registeredPaths":[...]},
|
|
260
323
|
# 由 lib-install-hooks.sh 的 _wrapper_check_json 拼装 tool/status/suppress/drift。
|
|
@@ -342,6 +405,16 @@ adapter_install_hooks() {
|
|
|
342
405
|
_ccswitch_warn_gui
|
|
343
406
|
_ccswitch_dump_common tmp || return 1
|
|
344
407
|
merged=$(_claude_apply_merge "$tmp")
|
|
408
|
+
# 语义不变跳写:merge 只动 .hooks,顶层其他字段天然继承 tmp——
|
|
409
|
+
# 两边 jq -S 规范化对比,相同则跳过 upsert(GUI 冲掉 hooks 时
|
|
410
|
+
# 库内现值 ≠ merged,恒不跳,对抗不弱化;行缺失时 tmp={},
|
|
411
|
+
# merged 恒含 10 事件,同样恒不跳)
|
|
412
|
+
if [ "$(jq -S . "$tmp" 2>/dev/null)" = "$(jq -S . <<<"$merged" 2>/dev/null)" ]; then
|
|
413
|
+
rm -f "$tmp"
|
|
414
|
+
echo "Claude hooks already up to date in cc-switch common config (skip write)"
|
|
415
|
+
echo "Events: $_CLAUDE_EVENTS"
|
|
416
|
+
return 0
|
|
417
|
+
fi
|
|
345
418
|
rm -f "$tmp"
|
|
346
419
|
hooks_patch=$(jq -c '{hooks: (.hooks // {})}' <<<"$merged")
|
|
347
420
|
full_doc=$(jq -c . <<<"$merged")
|
|
@@ -352,6 +425,7 @@ adapter_install_hooks() {
|
|
|
352
425
|
;;
|
|
353
426
|
live)
|
|
354
427
|
_ccswitch_warn_gui
|
|
428
|
+
_ccswitch_warn_config_dir_mismatch
|
|
355
429
|
target=$(_ccswitch_live_file)
|
|
356
430
|
;;
|
|
357
431
|
*)
|
|
@@ -360,6 +434,14 @@ adapter_install_hooks() {
|
|
|
360
434
|
esac
|
|
361
435
|
[ -f "$target" ] || _install_atomic_write '{}' "$target"
|
|
362
436
|
updated=$(_claude_apply_merge "$target")
|
|
437
|
+
# 语义不变跳写:jq 输出格式与手写文件不同,字节 cmp 不可用——两边 jq -S
|
|
438
|
+
# 规范化后比较(命令替换对称剥尾换行)。跳写不触碰 mtime,避免触发 Claude
|
|
439
|
+
# Code file watcher 的无谓 hooks 重载。
|
|
440
|
+
if [ "$(jq -S . "$target" 2>/dev/null)" = "$(jq -S . <<<"$updated" 2>/dev/null)" ]; then
|
|
441
|
+
echo "Claude hooks already up to date at $target (skip write)"
|
|
442
|
+
echo "Events: $_CLAUDE_EVENTS"
|
|
443
|
+
return 0
|
|
444
|
+
fi
|
|
363
445
|
_install_atomic_write "$updated" "$target"
|
|
364
446
|
echo "Claude hooks installed to $target"
|
|
365
447
|
echo "Events: $_CLAUDE_EVENTS"
|
|
@@ -412,6 +494,11 @@ adapter_uninstall_hooks() {
|
|
|
412
494
|
esac
|
|
413
495
|
[ -f "$target" ] || { echo "Claude hooks: nothing to uninstall"; return 0; }
|
|
414
496
|
updated=$(_claude_apply_strip "$target")
|
|
497
|
+
# 无本插件条目时 strip 无变化 → 跳写(不触碰 mtime)
|
|
498
|
+
if [ "$(jq -S . "$target" 2>/dev/null)" = "$(jq -S . <<<"$updated" 2>/dev/null)" ]; then
|
|
499
|
+
echo "Claude hooks: nothing to uninstall from $target (skip write)"
|
|
500
|
+
return 0
|
|
501
|
+
fi
|
|
415
502
|
_install_atomic_write "$updated" "$target"
|
|
416
503
|
echo "Claude hooks uninstalled from $target"
|
|
417
504
|
}
|
|
@@ -18,6 +18,21 @@ _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
|
+
# 写路径互斥(check 只读不锁):临界区覆盖 adapter 调用 + suppress 标记读-改-写,
|
|
22
|
+
# 串行化 auto 与显式 uninstall 的标记 TOCTOU。失败语义见 _install_acquire_lock:
|
|
23
|
+
# 降级无锁(rc=2)继续;等待超时(rc=1)退出。
|
|
24
|
+
case "${1:-install}" in
|
|
25
|
+
check) ;;
|
|
26
|
+
*)
|
|
27
|
+
_lock_rc=0
|
|
28
|
+
_install_acquire_lock claude || _lock_rc=$?
|
|
29
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
30
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
31
|
+
trap '_install_release_lock claude' EXIT
|
|
32
|
+
fi
|
|
33
|
+
;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
21
36
|
case "${1:-install}" in
|
|
22
37
|
uninstall)
|
|
23
38
|
adapter_uninstall_hooks
|
|
@@ -37,11 +52,23 @@ case "${1:-install}" in
|
|
|
37
52
|
if [ -f "$(_hooks_suppress_file claude)" ]; then
|
|
38
53
|
exit 0
|
|
39
54
|
fi
|
|
55
|
+
# 已收敛(integrity + 无漂移/死条目/重复)→ 跳过重装:tmux 每次 reload
|
|
56
|
+
# 不再全量重写真源。强收敛保留——漂移/死条目/缺失/重复任一存在都走
|
|
57
|
+
# install 收敛。结构漂移(async/matcher 被外部改)不在判定内,auto 跳过
|
|
58
|
+
# 不修复,显式 install 仍强收敛。
|
|
59
|
+
if adapter_check_converged; then
|
|
60
|
+
echo "claude hooks already converged (skip install)"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
40
63
|
adapter_install_hooks
|
|
41
|
-
_hooks_suppress_clear claude
|
|
64
|
+
_hooks_suppress_clear claude || true
|
|
42
65
|
;;
|
|
43
|
-
|
|
66
|
+
install)
|
|
44
67
|
adapter_install_hooks
|
|
45
|
-
_hooks_suppress_clear claude
|
|
68
|
+
_hooks_suppress_clear claude || true
|
|
69
|
+
;;
|
|
70
|
+
*)
|
|
71
|
+
echo "Usage: $0 [install|uninstall|check [--json]|auto]" >&2
|
|
72
|
+
exit 2
|
|
46
73
|
;;
|
|
47
74
|
esac
|
|
@@ -15,6 +15,19 @@ _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
|
+
# 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
|
|
19
|
+
case "${1:-install}" in
|
|
20
|
+
check) ;;
|
|
21
|
+
*)
|
|
22
|
+
_lock_rc=0
|
|
23
|
+
_install_acquire_lock codex || _lock_rc=$?
|
|
24
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
25
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
26
|
+
trap '_install_release_lock codex' EXIT
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
18
31
|
case "${1:-install}" in
|
|
19
32
|
uninstall)
|
|
20
33
|
adapter_uninstall_hooks
|
|
@@ -30,8 +43,12 @@ case "${1:-install}" in
|
|
|
30
43
|
exit 1
|
|
31
44
|
fi
|
|
32
45
|
;;
|
|
33
|
-
|
|
46
|
+
install)
|
|
34
47
|
adapter_install_hooks
|
|
35
|
-
_hooks_suppress_clear codex
|
|
48
|
+
_hooks_suppress_clear codex || true
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "Usage: $0 [install|uninstall|check [--json]]" >&2
|
|
52
|
+
exit 2
|
|
36
53
|
;;
|
|
37
54
|
esac
|
|
@@ -15,6 +15,19 @@ _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
|
+
# 写路径互斥(check 只读不锁):见 _install_acquire_lock 头注释。
|
|
19
|
+
case "${1:-install}" in
|
|
20
|
+
check) ;;
|
|
21
|
+
*)
|
|
22
|
+
_lock_rc=0
|
|
23
|
+
_install_acquire_lock opencode || _lock_rc=$?
|
|
24
|
+
if [ "$_lock_rc" = 1 ]; then exit 1; fi
|
|
25
|
+
if [ "$_lock_rc" = 0 ]; then
|
|
26
|
+
trap '_install_release_lock opencode' EXIT
|
|
27
|
+
fi
|
|
28
|
+
;;
|
|
29
|
+
esac
|
|
30
|
+
|
|
18
31
|
case "${1:-install}" in
|
|
19
32
|
uninstall)
|
|
20
33
|
adapter_uninstall_hooks
|
|
@@ -30,8 +43,12 @@ case "${1:-install}" in
|
|
|
30
43
|
exit 1
|
|
31
44
|
fi
|
|
32
45
|
;;
|
|
33
|
-
|
|
46
|
+
install)
|
|
34
47
|
adapter_install_hooks
|
|
35
|
-
_hooks_suppress_clear opencode
|
|
48
|
+
_hooks_suppress_clear opencode || true
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "Usage: $0 [install|uninstall|check [--json]]" >&2
|
|
52
|
+
exit 2
|
|
36
53
|
;;
|
|
37
54
|
esac
|
|
@@ -29,14 +29,85 @@ _install_atomic_write() {
|
|
|
29
29
|
# 显式 uninstall 成功 → 写 suppressed-<tool>;显式 install 成功 → 清。
|
|
30
30
|
# 自动修复路径(_maybe_repair_hooks / 入口 auto / SessionStart 重装)见标记一律不装。
|
|
31
31
|
# ⚠️ _SUPPRESS_DIR 与 lib-tmux-ai-status.sh 中的定义保持一致(两处同步改)。
|
|
32
|
+
#
|
|
33
|
+
# 失败语义(两个方向都告警,不对称):
|
|
34
|
+
# - write 失败:hooks 已剥离但标记没落 → auto 会静默装回(用户显式卸载被反转)
|
|
35
|
+
# → return 1,wrapper uninstall 分支 errexit 使其以非 0 退出,用户可见。
|
|
36
|
+
# - clear 失败:install 已成功但残留标记会把 auto 永久挡住(比 write 失败更隐蔽)
|
|
37
|
+
# → 告警但 return 0(主操作已成功,调用方 wrapper 的 clear 调用需 || true 容忍)。
|
|
32
38
|
_SUPPRESS_DIR="${TMUX_AI_SUPPRESS_DIR:-${XDG_CONFIG_HOME:-${HOME}/.config}/tmuxclihook}"
|
|
33
39
|
|
|
34
40
|
_hooks_suppress_file() { printf '%s/suppressed-%s' "$_SUPPRESS_DIR" "$1"; }
|
|
35
41
|
_hooks_suppress_write() {
|
|
36
|
-
mkdir -p "$_SUPPRESS_DIR" 2>/dev/null
|
|
37
|
-
|
|
42
|
+
if ! mkdir -p "$_SUPPRESS_DIR" 2>/dev/null \
|
|
43
|
+
|| ! date +%s > "$(_hooks_suppress_file "$1")" 2>/dev/null; then
|
|
44
|
+
echo "WARN: 抑制标记写入失败——hooks 已卸载,但 tmux 重载/新会话可能自动装回;" \
|
|
45
|
+
"请检查 $_SUPPRESS_DIR 可写性" >&2
|
|
46
|
+
return 1
|
|
47
|
+
fi
|
|
48
|
+
}
|
|
49
|
+
_hooks_suppress_clear() {
|
|
50
|
+
if [ -e "$(_hooks_suppress_file "$1")" ] \
|
|
51
|
+
&& ! rm -f "$(_hooks_suppress_file "$1")" 2>/dev/null; then
|
|
52
|
+
echo "WARN: 卸载意图标记清除失败——tmux 自动安装将持续跳过;" \
|
|
53
|
+
"请检查 $_SUPPRESS_DIR 可写性" >&2
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# --- 安装/卸载读-改-写互斥锁 ---
|
|
59
|
+
# 写路径的并发方:TUI 显式装 / tmux 入口 auto / SessionStart 自修复 / _maybe_repair_hooks /
|
|
60
|
+
# drift 收敛——五方都经 wrapper dispatch,锁在此层一处覆盖三 adapter。
|
|
61
|
+
# mkdir mutex(项目风格同 .lock/.event-lock;macOS 无 flock(1))。两种失败语义区分:
|
|
62
|
+
# - 锁目录建不出来(权限/ENOTDIR)→ 降级无锁继续 + 告警(历史行为是无锁写;install
|
|
63
|
+
# 真正写目标与锁目录不同权限域,锁失败不代表写不了)
|
|
64
|
+
# - 他人持锁 → 等待(0.2s × 50 ≈ 10s)→ stale(10s)强夺 → 超时报错 return 1(显式操作可见)。
|
|
65
|
+
# ⚠️ codex SessionStart 是同步 hook(timeout:5s):极端并发下锁等待可能超过 5s 使该次
|
|
66
|
+
# hook 超时——后果轻(该次 hook 不执行、codex 继续、无阻塞),概率极低(repair 60s 一次)。
|
|
67
|
+
# 边界:锁只收敛「我们自己的多方」;Claude Code 自身写 settings.json 不遵守本锁(外部
|
|
68
|
+
# 写者无法防)——这正是 60s 自修复存在的原因。写路径互斥由 wrapper 层负责,adapter
|
|
69
|
+
# 函数不含锁——直接 source adapter 调 install 会绕过锁。
|
|
70
|
+
_install_lock_dir() { printf '%s/.install-lock-%s' "$_SUPPRESS_DIR" "$1"; }
|
|
71
|
+
_install_acquire_lock() { # $1=tool id → 0=持锁 1=超时(2=降级无锁,调用方继续)
|
|
72
|
+
local lock; lock=$(_install_lock_dir "$1")
|
|
73
|
+
if ! mkdir -p "$_SUPPRESS_DIR" 2>/dev/null; then
|
|
74
|
+
echo "WARN: 锁目录不可创建($_SUPPRESS_DIR),降级无锁继续安装" >&2
|
|
75
|
+
return 2
|
|
76
|
+
fi
|
|
77
|
+
local _start=$SECONDS i
|
|
78
|
+
for i in {1..50}; do
|
|
79
|
+
if mkdir "$lock" 2>/dev/null; then
|
|
80
|
+
echo "$$" > "$lock/owner"
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
83
|
+
# stale 强夺:持锁者进程已死 → 立即夺;锁 mtime 超 10s → 强夺。
|
|
84
|
+
# 外部命令(stat/sleep/rmdir)逐一防御退出码——ccswitch 用例 8 的 stubbin
|
|
85
|
+
# 是最小 PATH 沙箱(无 stat/rmdir/sleep),127 会在 errexit 下炸掉 install。
|
|
86
|
+
local owner mtime
|
|
87
|
+
owner=$(cat "$lock/owner" 2>/dev/null)
|
|
88
|
+
if [ -n "$owner" ] && ! kill -0 "$owner" 2>/dev/null; then
|
|
89
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
90
|
+
rmdir "$lock" 2>/dev/null || true
|
|
91
|
+
continue
|
|
92
|
+
fi
|
|
93
|
+
mtime=""
|
|
94
|
+
mtime=$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null || true)
|
|
95
|
+
if [ -n "$mtime" ] && [ $(( _start + SECONDS - mtime )) -ge 10 ]; then
|
|
96
|
+
echo "WARN: 强夺超时的安装锁(owner=${owner:-?})" >&2
|
|
97
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
98
|
+
rmdir "$lock" 2>/dev/null || true
|
|
99
|
+
continue
|
|
100
|
+
fi
|
|
101
|
+
sleep 0.2 2>/dev/null || :
|
|
102
|
+
done
|
|
103
|
+
echo "ERROR: 安装锁获取超时($lock)——另一安装操作可能仍在进行,稍后重试" >&2
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
_install_release_lock() { # $1=tool id
|
|
107
|
+
local lock; lock=$(_install_lock_dir "$1")
|
|
108
|
+
rm -f "$lock/owner" 2>/dev/null || true
|
|
109
|
+
rmdir "$lock" 2>/dev/null || true
|
|
38
110
|
}
|
|
39
|
-
_hooks_suppress_clear() { rm -f "$(_hooks_suppress_file "$1")" 2>/dev/null || true; }
|
|
40
111
|
|
|
41
112
|
# --- check --json:完整性明细 JSON(wrapper 专用,事件热路径不触达)---
|
|
42
113
|
# stdout = 单行紧凑 JSON;exit code 与默认 check 一致(ok→0 / missing→1)。
|
|
@@ -94,7 +94,7 @@ _load_pane_snapshot() {
|
|
|
94
94
|
_CURRENT_SERVER_PID=""
|
|
95
95
|
command -v tmux >/dev/null 2>&1 || return 1
|
|
96
96
|
_PANE_SNAPSHOT=$(tmux list-panes -a -F \
|
|
97
|
-
'#{pane_id}|#{pane_current_path}|#{pane_current_command}|#{pane_pid}|#{pid}|#{session_attached}|#{pane_title}' \
|
|
97
|
+
'#{pane_id}|#{s/[|]/_/:pane_current_path}|#{s/[|]/_/:pane_current_command}|#{pane_pid}|#{pid}|#{session_attached}|#{pane_title}' \
|
|
98
98
|
2>/dev/null)
|
|
99
99
|
[ -n "$_PANE_SNAPSHOT" ] || return 1
|
|
100
100
|
_PANE_SNAPSHOT_OK=1
|
|
@@ -184,7 +184,8 @@ _narrow_panes_by_adapter() {
|
|
|
184
184
|
_narrow_panes_by_cwd() {
|
|
185
185
|
local base="$1" want="" matches="" id path cmd pid server attached title
|
|
186
186
|
[ -n "${HOOK_CWD:-}" ] || { printf '%s\n' "$base"; return; }
|
|
187
|
-
|
|
187
|
+
# _normalize_path 对非空输入恒返回 0(cd 失败时返回去尾斜杠原串),无需兜底分支
|
|
188
|
+
want=$(_normalize_path "$HOOK_CWD")
|
|
188
189
|
while IFS='|' read -r id path cmd pid server attached title; do
|
|
189
190
|
[ -n "$id" ] || continue
|
|
190
191
|
[ "$(_normalize_path "$path")" = "$want" ] && matches="$matches $id"
|
|
@@ -410,7 +411,10 @@ build_all_status() {
|
|
|
410
411
|
_ai_names=$(_collect_ai_process_names)
|
|
411
412
|
# 动态追加上 monitor 的 seen 标记(无 SessionEnd 工具的进程树探测结果)。
|
|
412
413
|
# switch/volta-shim 启动的 pane 前台命令不是 codex,必须靠该标记补 idle `-`。
|
|
413
|
-
|
|
414
|
+
# 自由文本字段就地消毒:tmux format 的 s/[|]/_/ 替换(`|` 是 ERE 交替符,
|
|
415
|
+
# 必须写成 [|] 或 \|;裸 s/|/…/ 不生效)。session 名/provider 名含 `|` 时
|
|
416
|
+
# 会劈裂字段——attached 位变状态字符使 $6>0 误判、该 session 整行消失。
|
|
417
|
+
_fmt="#{pane_id}|#{s/[|]/_/:session_name}|#{window_index}|#{pane_index}|#{@ai_pane_status}|#{session_attached}|#{session_created}|#{@ai_entered_time}|#{s/[|]/_/:pane_current_command}|#{s/[|]/_/:@ai_provider}|#{s/[|]/_/:@ai_provider_id}"
|
|
414
418
|
for _n in $_ai_names; do
|
|
415
419
|
_fmt="$_fmt|#{@ai_monitor_seen_$_n}"
|
|
416
420
|
done
|
|
@@ -874,7 +878,7 @@ _is_pane_session_dead() {
|
|
|
874
878
|
# 有对应文件但文件过期且进程已死(Stop 未触发的崩溃场景),也清除。
|
|
875
879
|
_cleanup_stale_panes() {
|
|
876
880
|
local active_panes=""
|
|
877
|
-
local f pane_id
|
|
881
|
+
local f pane_id pane_status pane_dir pid pid_file
|
|
878
882
|
for f in "${_STATUS_DIR}"/*/pane-*; do
|
|
879
883
|
[ -f "$f" ] || continue
|
|
880
884
|
pane_id=$(cat "$f" 2>/dev/null)
|
|
@@ -925,7 +929,6 @@ _cleanup_stale_panes() {
|
|
|
925
929
|
# 清理孤儿 poll 进程
|
|
926
930
|
for pid_file in "${pane_dir}"*-poll-pid; do
|
|
927
931
|
[ -f "$pid_file" ] || continue
|
|
928
|
-
local pid
|
|
929
932
|
pid=$(cat "$pid_file" 2>/dev/null)
|
|
930
933
|
[ -n "$pid" ] && kill "$pid" 2>/dev/null
|
|
931
934
|
done
|
|
@@ -935,6 +938,25 @@ _cleanup_stale_panes() {
|
|
|
935
938
|
done
|
|
936
939
|
}
|
|
937
940
|
|
|
941
|
+
# 清理已断开 client 的 @ai_prev_sess_* 残留。key 是写入端有损 sanitize
|
|
942
|
+
# (tr -c 'A-Za-z0-9._-' '_'),无法从 key 反推 client → 反向枚举存活集比对。
|
|
943
|
+
# 实测关注:长期运行的 server 会累积已 detach 的 client 键(8 option / 4 存活)。
|
|
944
|
+
_prune_prev_sess_options() {
|
|
945
|
+
local alive=" " name key opt
|
|
946
|
+
while IFS= read -r name; do
|
|
947
|
+
[ -n "$name" ] || continue
|
|
948
|
+
key=$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '_')
|
|
949
|
+
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
|
|
952
|
+
key="${opt#@ai_prev_sess_}"
|
|
953
|
+
case "$alive" in
|
|
954
|
+
*" $key "*) ;;
|
|
955
|
+
*) tmux set-option -gu "$opt" 2>/dev/null || true ;;
|
|
956
|
+
esac
|
|
957
|
+
done
|
|
958
|
+
}
|
|
959
|
+
|
|
938
960
|
# 频率限制的孤儿清理(每次 hook 调用,最多每 60s 执行一次)
|
|
939
961
|
_maybe_cleanup_stale() {
|
|
940
962
|
local marker="${_STATUS_DIR}/.stale-cleanup-ts"
|
|
@@ -947,6 +969,7 @@ _maybe_cleanup_stale() {
|
|
|
947
969
|
[ $((now - ${last:-0})) -lt 60 ] && return
|
|
948
970
|
fi
|
|
949
971
|
_cleanup_stale_panes
|
|
972
|
+
_prune_prev_sess_options
|
|
950
973
|
echo "$now" > "$marker"
|
|
951
974
|
}
|
|
952
975
|
|
|
@@ -962,12 +985,14 @@ _check_hooks_integrity() {
|
|
|
962
985
|
fi
|
|
963
986
|
}
|
|
964
987
|
|
|
965
|
-
# 兜底自修复:tmux
|
|
966
|
-
# 不依赖 SESSION_ID/EVENT,可被 _refresh 触发。60s
|
|
988
|
+
# 兜底自修复:tmux 端事件路径与 SessionStart hook 共用,配置被外部覆盖时自动重装。
|
|
989
|
+
# 不依赖 SESSION_ID/EVENT,可被 _refresh 与 SessionStart 触发。60s 节流退避
|
|
990
|
+
# (窗口内不重复 integrity 检查;修复失败 60s 后才重试)。
|
|
967
991
|
# 安装器由 adapter 声明(ADAPTER_INSTALLER)。
|
|
968
992
|
_maybe_repair_hooks() {
|
|
969
993
|
# 用户显式卸载意图(suppressed-<tool>)→ 不自动修复,尊重到显式安装为止。
|
|
970
994
|
if [ -f "${_SUPPRESS_DIR}/suppressed-${TOOL_ID:-claude}" ]; then
|
|
995
|
+
_ai_log "REPAIR: suppressed by uninstall marker, skip (${TOOL_ID:-claude})"
|
|
971
996
|
return 0
|
|
972
997
|
fi
|
|
973
998
|
local marker="${_STATUS_DIR}/.hooks-repair-${TOOL_ID:-claude}-ts"
|
|
@@ -983,9 +1008,9 @@ _maybe_repair_hooks() {
|
|
|
983
1008
|
local installer="${ADAPTER_INSTALLER}"
|
|
984
1009
|
if ! _check_hooks_integrity; then
|
|
985
1010
|
_ai_log "REPAIR: hooks integrity check failed, reinstalling (${TOOL_ID:-claude})"
|
|
986
|
-
#
|
|
987
|
-
#
|
|
988
|
-
#
|
|
1011
|
+
# 浮出提示(成败区分):SessionStart(Claude Code hook 进程内)与
|
|
1012
|
+
# _run_refresh(tmux run-shell)两个上下文都会到这里;hook 进程继承
|
|
1013
|
+
# TMUX env,display-message 无 -p 写 client tty 而非调用方 stdout。
|
|
989
1014
|
if [ -x "$installer" ] && "$installer" >/dev/null 2>&1; then
|
|
990
1015
|
tmux display-message "✓ ${TOOL_ID:-claude} hooks 已自动修复(检测到缺失)" 2>/dev/null || true
|
|
991
1016
|
else
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# tmux-ai-reload — prefix+r 重载:source 实际存在的配置文件后重跑插件入口
|
|
3
|
+
#
|
|
4
|
+
# 背景:入口曾用 mktemp conf 绑定 `source-file ~/.tmux.conf \; run-shell 入口`:
|
|
5
|
+
# 1) 硬编码 ~/.tmux.conf(XDG / tmux -f 用户 reload 读错文件)
|
|
6
|
+
# 2) 一次性哨兵:source-file 内用户自己的 `bind r` 会把绑定重置回原命令,
|
|
7
|
+
# 哨兵已置位 → 纯 TPM 模式(配置无 run-shell 行)从第二次重载起插件初始化
|
|
8
|
+
# 静默停止
|
|
9
|
+
# 本 helper 每次由绑定直接调用(入口每次加载都重绑,自愈):
|
|
10
|
+
# - 按 tmux 的 #{config_files} 候选列表逐个过滤存在性后 source(顺序同启动)
|
|
11
|
+
# - 再显式跑一次入口(纯 TPM 模式也能重新初始化;配置里的 run-shell 行会让
|
|
12
|
+
# 入口再跑一次——幂等,仅一次多余启动)
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
_ENTRY="$(cd "${_SELF_DIR}/.." && pwd)/tmux-ai-hooks-status.tmux"
|
|
17
|
+
|
|
18
|
+
_cfg=$(tmux display-message -p '#{config_files}' 2>/dev/null || true)
|
|
19
|
+
# config_files 实测是「候选列表」而非已加载列表(含不存在路径)→ 逐个 [ -f ] 过滤;
|
|
20
|
+
# 老版本 tmux 无该 format 时回落 ~/.tmux.conf。
|
|
21
|
+
[ -n "$_cfg" ] || _cfg="$HOME/.tmux.conf"
|
|
22
|
+
# 注意用 printf '%s\n':末项无换行符时 read 返回非零,循环体会漏掉最后一项
|
|
23
|
+
printf '%s\n' "$_cfg" | tr ',' '\n' | while IFS= read -r _f; do
|
|
24
|
+
[ -n "$_f" ] && [ -f "$_f" ] || continue
|
|
25
|
+
tmux source-file "$_f" 2>/dev/null || true
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
[ -f "$_ENTRY" ] && tmux run-shell "$_ENTRY" 2>/dev/null || true
|
|
29
|
+
tmux display-message "配置已重载" 2>/dev/null || true
|
package/scripts/tmux-ai-status
CHANGED
|
@@ -78,6 +78,25 @@ _current_pane_status() {
|
|
|
78
78
|
tmux display-message -pt "$TMUX_PANE" -p "#{@ai_pane_status}" 2>/dev/null
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
# Notification「隐式」路径(idle_prompt / waiting for your input)的状态解析:
|
|
82
|
+
# 通知可能乱序晚到(async hook 无顺序保证),不能无条件降级为 idle '-'。
|
|
83
|
+
# 优先级:pending 派生(工具确实在跑 → '>')> live 审批态(!/?;claude 不写
|
|
84
|
+
# pending-perm,纯派生看不见审批态)> idle '-'。
|
|
85
|
+
# 显式重置消息(denied/cancelled/rejected)不走本函数——denial 携带「等待已结束」
|
|
86
|
+
# 语义,应无条件置 '-'。
|
|
87
|
+
_notification_implicit_status() {
|
|
88
|
+
if _derive_status_from_pending; then
|
|
89
|
+
printf '%s' "$DERIVED"
|
|
90
|
+
return
|
|
91
|
+
fi
|
|
92
|
+
local live
|
|
93
|
+
live=$(_current_pane_status)
|
|
94
|
+
case "$live" in
|
|
95
|
+
"!"|"?") printf '%s' "$live"; return ;;
|
|
96
|
+
esac
|
|
97
|
+
printf '%s' "-"
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
# 重建聚合状态(_refresh / _entered 共用)
|
|
82
101
|
_run_refresh() {
|
|
83
102
|
_maybe_repair_hooks
|
|
@@ -94,6 +113,7 @@ PREV_STATUS=$(_current_pane_status)
|
|
|
94
113
|
TOOL=""
|
|
95
114
|
TOOL_USE_ID=""
|
|
96
115
|
INPUT_DIGEST=""
|
|
116
|
+
_log_flags=""
|
|
97
117
|
|
|
98
118
|
case "$EVENT" in
|
|
99
119
|
Notification)
|
|
@@ -102,14 +122,15 @@ case "$EVENT" in
|
|
|
102
122
|
INPUT_DIGEST=$(echo "$INPUT" | jq -c '{notification_type, message}' 2>/dev/null)
|
|
103
123
|
case "$NOTIF_TYPE" in
|
|
104
124
|
idle_prompt)
|
|
105
|
-
STATUS
|
|
125
|
+
STATUS=$(_notification_implicit_status)
|
|
106
126
|
;;
|
|
107
127
|
*)
|
|
108
128
|
case "$MSG" in
|
|
109
129
|
*"waiting for your input"*)
|
|
110
|
-
STATUS
|
|
130
|
+
STATUS=$(_notification_implicit_status)
|
|
111
131
|
;;
|
|
112
132
|
*"denied"*|*"cancelled"*|*"canceled"*|*"rejected"*)
|
|
133
|
+
# 显式重置:denial 表示等待已结束,无条件 idle(即便有 pending 残项)
|
|
113
134
|
STATUS="-"
|
|
114
135
|
;;
|
|
115
136
|
*)
|
|
@@ -118,6 +139,8 @@ case "$EVENT" in
|
|
|
118
139
|
esac
|
|
119
140
|
;;
|
|
120
141
|
esac
|
|
142
|
+
# 写入循环内不再对 Notification 重复 re-derive:事件锁(tmux-ai-status 头部)
|
|
143
|
+
# 已串行化同 pane 事件,分支内判定即最终判定。
|
|
121
144
|
;;
|
|
122
145
|
PermissionRequest)
|
|
123
146
|
# 一次 @tsv 提取 tool_name/tool_use_id(digest 单独一次;stdin 只能读一次,
|
|
@@ -209,8 +232,12 @@ case "$EVENT" in
|
|
|
209
232
|
else
|
|
210
233
|
STATUS="✓"
|
|
211
234
|
fi
|
|
212
|
-
# fallback: TMUX_PANE 未解析时,精准清理孤儿状态(不碰其他活跃 pane
|
|
235
|
+
# fallback: TMUX_PANE 未解析时,精准清理孤儿状态(不碰其他活跃 pane)。
|
|
236
|
+
# 此时事件无处写入——不要伪造 ✓ 写入日志(真实 pane 可能仍是 !):
|
|
237
|
+
# STATUS 置空 + flags 标注,日志显示「无状态转移」。
|
|
213
238
|
if [ -n "$TMUX" ] && [ -z "$TMUX_PANE" ]; then
|
|
239
|
+
STATUS=""
|
|
240
|
+
_log_flags="PANE=unresolved"
|
|
214
241
|
_cleanup_stale_panes
|
|
215
242
|
build_all_status
|
|
216
243
|
tmux set-option -g @ai_all_status "$ALL" 2>/dev/null || true
|
|
@@ -221,14 +248,12 @@ case "$EVENT" in
|
|
|
221
248
|
INPUT_DIGEST=$(echo "$INPUT" | jq -c '{source, model}' 2>/dev/null)
|
|
222
249
|
STATUS="-"
|
|
223
250
|
_clear_pending
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
fi
|
|
231
|
-
fi
|
|
251
|
+
# 自修复归并到 _maybe_repair_hooks:60s 节流退避(旧实现每次 SessionStart——
|
|
252
|
+
# 含 resume/clear/fork——都全量 integrity + 失败即重装且无 exit code 检查),
|
|
253
|
+
# 并统一 suppress 检查与成功/失败区分的 display-message。节流语义:窗口内
|
|
254
|
+
# 新会话不重复检查 integrity(外部改 settings 后最多延迟 60s 才被发现);
|
|
255
|
+
# 修复失败 60s 后由 _run_refresh 或下一次 SessionStart 重试。
|
|
256
|
+
_maybe_repair_hooks
|
|
232
257
|
;;
|
|
233
258
|
SessionEnd)
|
|
234
259
|
INPUT_DIGEST=$(echo "$INPUT" | jq -c '{reason}' 2>/dev/null)
|
|
@@ -296,7 +321,7 @@ if [ -n "$INPUT_DIGEST" ] && [ "$INPUT_DIGEST" != "null" ]; then
|
|
|
296
321
|
_log_input_line="input: $(_ai_log_truncate_input "$INPUT_DIGEST")"
|
|
297
322
|
fi
|
|
298
323
|
fi
|
|
299
|
-
_ai_log_event "$PREV_STATUS" "$STATUS" "" "$_log_tool_line" "$_log_input_line"
|
|
324
|
+
_ai_log_event "$PREV_STATUS" "$STATUS" "$_log_flags" "$_log_tool_line" "$_log_input_line"
|
|
300
325
|
|
|
301
326
|
if [ -n "$TMUX_PANE" ] && { [ -n "$TMUX" ] || command -v tmux &>/dev/null; }; then
|
|
302
327
|
# 写入前重新读取 pending,避免并发事件用过期结果覆盖仍有效的审批态。
|
|
@@ -43,19 +43,25 @@ tmux set-option -g "status-format[${CLAUDE_ROW}]" "#[align=centre]#{?#{@ai_all_s
|
|
|
43
43
|
# 不动 status-right,让用户自行管理第 1 行内容
|
|
44
44
|
# Claude 状态通过多行 status-format 的独立行显示,不修改 status-right
|
|
45
45
|
|
|
46
|
-
# --- Reload
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
tmux
|
|
58
|
-
|
|
46
|
+
# --- Reload 保护:prefix+r = source 实际配置文件 + 重跑本插件初始化 ---
|
|
47
|
+
# 绑定单命令指向 scripts/tmux-ai-reload(不再需要 mktemp conf 规避 \;)。
|
|
48
|
+
# ⚠️ 每次加载都重绑(bind-key 是 upsert,幂等):旧实现用一次性哨兵
|
|
49
|
+
# @claude_hooks_reload_registered,但 source-file 配置内用户自己的 `bind r`
|
|
50
|
+
# 会把绑定重置回原命令,哨兵已置位 → 纯 TPM 模式从第二次重载起插件初始化
|
|
51
|
+
# 静默停止。每次重绑使其自愈。
|
|
52
|
+
# 原绑定保存:覆盖前捕获(排除本插件新旧两种命令形态),供手动恢复:
|
|
53
|
+
# tmux show-option -gv @claude_hooks_saved_bind_r # 查看
|
|
54
|
+
# eval "$(tmux show-option -gv @claude_hooks_saved_bind_r)" # 回放
|
|
55
|
+
_prev_r=$(tmux list-keys -T prefix r 2>/dev/null || true)
|
|
56
|
+
case "$_prev_r" in
|
|
57
|
+
""|*tmux-ai-reload*|*tmux-ai-hooks-status.tmux*) ;;
|
|
58
|
+
*)
|
|
59
|
+
if [ -z "$(tmux show-option -gv @claude_hooks_saved_bind_r 2>/dev/null)" ]; then
|
|
60
|
+
tmux set-option -g @claude_hooks_saved_bind_r "$_prev_r" 2>/dev/null || true
|
|
61
|
+
fi
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
tmux bind-key r run-shell "'${CURRENT_DIR}/scripts/tmux-ai-reload'" 2>/dev/null || true
|
|
59
65
|
|
|
60
66
|
# --- 自动注册 Claude hooks(幂等,每次插件加载时确保 hooks 存在)---
|
|
61
67
|
# auto 模式:用户显式卸载过(suppressed-claude 标记在)时静默跳过,尊重卸载意图;
|
|
@@ -119,4 +125,20 @@ else
|
|
|
119
125
|
fi
|
|
120
126
|
|
|
121
127
|
# Esc 拒绝检测:! / ? 状态下 Esc → -
|
|
128
|
+
# 全局 root 表绑定会覆盖用户既有 Escape 绑定——覆盖前保存 + 一次性告警
|
|
129
|
+
# (无法安全链式调用任意已有绑定,恢复需手动):
|
|
130
|
+
# tmux show-option -gv @claude_hooks_saved_bind_escape
|
|
131
|
+
_prev_esc=$(tmux list-keys -n Escape 2>/dev/null || true)
|
|
132
|
+
case "$_prev_esc" in
|
|
133
|
+
""|*tmux-ai-esc*) ;;
|
|
134
|
+
*)
|
|
135
|
+
if [ -z "$(tmux show-option -gv @claude_hooks_saved_bind_escape 2>/dev/null)" ]; then
|
|
136
|
+
tmux set-option -g @claude_hooks_saved_bind_escape "$_prev_esc" 2>/dev/null || true
|
|
137
|
+
fi
|
|
138
|
+
if [ -z "$(tmux show-option -gv @claude_hooks_esc_warned 2>/dev/null)" ]; then
|
|
139
|
+
tmux display-message "⚠ tmux-ai-hooks-status 覆盖了全局 Esc 绑定;原绑定已存 @claude_hooks_saved_bind_escape" 2>/dev/null || true
|
|
140
|
+
tmux set-option -g @claude_hooks_esc_warned "1" 2>/dev/null || true
|
|
141
|
+
fi
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
122
144
|
tmux bind-key -n Escape run-shell "${CURRENT_DIR}/scripts/tmux-ai-esc"
|