tmuxclihook 0.5.16 → 0.5.17
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/bin/cli.js +26 -11
- package/package.json +1 -1
- package/scripts/adapters/codex.sh +26 -16
- package/scripts/lib-tmux-ai-status.sh +94 -33
- package/scripts/tmux-ai-status +27 -20
- package/src/check-detail.js +19 -0
package/bin/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import { checkEnv, detectClis, interactiveShell } from '../src/detect.js';
|
|
|
14
14
|
import { planBrewRemediation, runBrewAction } from '../src/env.js';
|
|
15
15
|
import { installHooks, uninstallHooks, repairHooks } from '../src/hooks.js';
|
|
16
16
|
import { checkSymlink, checkTmuxConf } from '../src/symlink.js';
|
|
17
|
-
import { checkToolDetail, renderToolDetail, summarizeDetails } from '../src/check-detail.js';
|
|
17
|
+
import { checkToolDetail, renderToolDetail, summarizeDetails, findDuplicateEvents } from '../src/check-detail.js';
|
|
18
18
|
import { ensureTmuxIntegration, reloadTmux } from '../src/tmuxconf.js';
|
|
19
19
|
import { stopMonitor, clearAggregateStatus, removeSymlinks } from '../src/purge.js';
|
|
20
20
|
import {
|
|
@@ -165,26 +165,41 @@ async function detectCliMenu() {
|
|
|
165
165
|
await offerDriftConvergence(details);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// 失效,必须显式确认;opencode 重写无信任代价,确认一次即可。
|
|
168
|
+
// 漂移/重复收敛入口:检测(check --json 的 drift 字段 + .tmux.conf 的 pointsToCurrent
|
|
169
|
+
// + codex 的 registered>1)与收敛分离。收敛策略不对称——claude 无代价直接收敛;
|
|
170
|
+
// codex 重写即 trusted_hash 失效,必须显式确认;opencode 重写无信任代价,确认一次即可。
|
|
171
171
|
async function offerDriftConvergence(details) {
|
|
172
|
-
const
|
|
172
|
+
const candidates = details.filter((d) => {
|
|
173
|
+
if (d.status === 'unknown') return false;
|
|
174
|
+
if (d.drift?.drifted) return true;
|
|
175
|
+
// codex 重复注册:drift 检测不到(同路径),但 force install 可收敛
|
|
176
|
+
return d.tool === 'codex' && findDuplicateEvents(d).length > 0;
|
|
177
|
+
});
|
|
173
178
|
const conf = checkTmuxConf();
|
|
174
179
|
const confStale = conf.exists && conf.referenced && !conf.pointsToCurrent;
|
|
175
180
|
|
|
176
|
-
const options =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
const options = candidates.map((d) => {
|
|
182
|
+
const hints = [];
|
|
183
|
+
if (d.drift?.drifted) hints.push(`旧路径 ${(d.drift.stale || []).join(', ')}`);
|
|
184
|
+
if (d.tool === 'codex') {
|
|
185
|
+
const dups = findDuplicateEvents(d);
|
|
186
|
+
if (dups.length > 0) {
|
|
187
|
+
hints.push(`重复:${dups.map((x) => `${x.event}×${x.registered}`).join('、')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
value: `tool:${d.tool}`,
|
|
192
|
+
label: `${d.label} hooks 收敛到当前 source`,
|
|
193
|
+
hint: hints.join(';'),
|
|
194
|
+
};
|
|
195
|
+
});
|
|
181
196
|
if (confStale) {
|
|
182
197
|
options.push({ value: 'tmuxconf', label: '.tmux.conf 加载声明更新为当前 source' });
|
|
183
198
|
}
|
|
184
199
|
if (options.length === 0) return;
|
|
185
200
|
|
|
186
201
|
options.push({ value: '__skip__', label: '← 暂不处理' });
|
|
187
|
-
p.log.warn('
|
|
202
|
+
p.log.warn('检测到配置漂移或 codex hooks 重复注册。功能仍可用,但收敛会重写 hooks(codex 需重新 Trust)。');
|
|
188
203
|
const sel = await p.select({
|
|
189
204
|
message: '选择要收敛的项(Esc 跳过)',
|
|
190
205
|
options,
|
package/package.json
CHANGED
|
@@ -36,12 +36,17 @@ ADAPTER_PERMISSION_PRE_TOOL_NAMES="request_permissions"
|
|
|
36
36
|
# 自修复/TUI 调用的安装脚本
|
|
37
37
|
ADAPTER_INSTALLER="${_LIB_DIR}/install-codex-hooks.sh"
|
|
38
38
|
|
|
39
|
-
#
|
|
39
|
+
# 本插件条目的唯一 marker(归属判定路径无关:contains 子串匹配)。
|
|
40
|
+
# 统一用于 count/integrity/detail/strip——避免历史实现里 strip 用宽串
|
|
41
|
+
# `tmux-ai-status`、check 用 `tmux-ai-status codex` 的发散。
|
|
42
|
+
_CODEX_MARKER="tmux-ai-status codex"
|
|
43
|
+
|
|
44
|
+
# 完整性计数:6 个事件中注册了含 _CODEX_MARKER 的 command 的事件数(按事件,
|
|
40
45
|
# 非扁平总数——重复+缺失的组合在扁平计数下会假阳性)。
|
|
41
46
|
_CODEX_JQ_COUNT='
|
|
42
47
|
.hooks as $h |
|
|
43
48
|
["SessionStart","UserPromptSubmit","PreToolUse","PermissionRequest","PostToolUse","Stop"]
|
|
44
|
-
| map(select([$h[.][]?.hooks[]?.command // empty] | any(contains(
|
|
49
|
+
| map(select([$h[.][]?.hooks[]?.command // empty] | any(contains($marker))))
|
|
45
50
|
| length
|
|
46
51
|
'
|
|
47
52
|
|
|
@@ -51,17 +56,17 @@ _CODEX_JQ_COUNT='
|
|
|
51
56
|
adapter_check_integrity() {
|
|
52
57
|
[ -f "$ADAPTER_HOOKS_FILE" ] || return 1
|
|
53
58
|
local n ev p ok_events=""
|
|
54
|
-
n=$(jq "$_CODEX_JQ_COUNT" "$ADAPTER_HOOKS_FILE" 2>/dev/null)
|
|
59
|
+
n=$(jq --arg marker "$_CODEX_MARKER" "$_CODEX_JQ_COUNT" "$ADAPTER_HOOKS_FILE" 2>/dev/null)
|
|
55
60
|
[ "${n:-0}" -ge 6 ] || return 1
|
|
56
61
|
while IFS=$'\t' read -r ev p; do
|
|
57
62
|
if [ -n "$ev" ] && [ -x "$p" ]; then
|
|
58
63
|
case ",$ok_events," in *",$ev,"*) ;; *) ok_events="$ok_events,$ev" ;; esac
|
|
59
64
|
fi
|
|
60
|
-
done < <(jq -r '.hooks as $h |
|
|
65
|
+
done < <(jq -r --arg marker "$_CODEX_MARKER" '.hooks as $h |
|
|
61
66
|
["SessionStart","UserPromptSubmit","PreToolUse","PermissionRequest","PostToolUse","Stop"] as $events |
|
|
62
67
|
$events[] as $e |
|
|
63
68
|
[$h[$e][]?.hooks[]?.command // empty
|
|
64
|
-
| select(contains(
|
|
69
|
+
| select(contains($marker)) | split(" ")[0]] as $scripts |
|
|
65
70
|
select(($scripts | length) > 0) |
|
|
66
71
|
$scripts[] | "\($e)\t\(.)"' "$ADAPTER_HOOKS_FILE" 2>/dev/null)
|
|
67
72
|
for ev in SessionStart UserPromptSubmit PreToolUse PermissionRequest PostToolUse Stop; do
|
|
@@ -135,11 +140,11 @@ adapter_check_detail() {
|
|
|
135
140
|
printf '{"mode":"file","events":{},"plugin":null,"registeredPaths":[],"trust":%s}\n' "$trust_json"
|
|
136
141
|
return 0
|
|
137
142
|
fi
|
|
138
|
-
pairs=$(jq -r '.hooks as $h |
|
|
143
|
+
pairs=$(jq -r --arg marker "$_CODEX_MARKER" '.hooks as $h |
|
|
139
144
|
["SessionStart","UserPromptSubmit","PreToolUse","PermissionRequest","PostToolUse","Stop"] as $events |
|
|
140
145
|
$events[] as $e |
|
|
141
146
|
$h[$e][]?.hooks[]?.command // empty |
|
|
142
|
-
select(contains(
|
|
147
|
+
select(contains($marker)) | split(" ")[0] | "\($e)\t\(.)"' "$src" 2>/dev/null)
|
|
143
148
|
rows=""
|
|
144
149
|
while IFS= read -r line; do
|
|
145
150
|
[ -n "$line" ] || continue
|
|
@@ -208,17 +213,22 @@ EOF
|
|
|
208
213
|
printf '%s\n' "${out# }" | tr ' ' '\n'
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
#
|
|
216
|
+
# 命令级剥离:逐 hook 删除含 _CODEX_MARKER 的 command(不看 type,缺 type 的
|
|
217
|
+
# 本插件条目同样清除);group 仍有剩余 hook 则保留,空 group/事件删除。
|
|
218
|
+
# 与 claude 的 _CLAUDE_JQ_STRIP 同构;防御缺 hooks 键 / null / 非数组的 group。
|
|
212
219
|
_codex_strip_managed() {
|
|
213
|
-
jq '
|
|
214
|
-
def
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
+
jq --arg marker "$_CODEX_MARKER" '
|
|
221
|
+
def trim_group:
|
|
222
|
+
if (.hooks | type) == "array" then
|
|
223
|
+
.hooks |= [ .[] | select(((.command // "") | tostring | contains($marker)) | not) ]
|
|
224
|
+
| select((.hooks | length) > 0)
|
|
225
|
+
else . end;
|
|
226
|
+
def trim_event:
|
|
227
|
+
if type == "array" then [ .[] | trim_group ] else . end;
|
|
228
|
+
if (.hooks | type) == "object" then
|
|
229
|
+
.hooks |= (with_entries(.value |= trim_event) | with_entries(select((.value | length) > 0)))
|
|
220
230
|
else . end
|
|
221
|
-
| if (.hooks
|
|
231
|
+
| if (.hooks | type) == "object" and ((.hooks | length) == 0) then del(.hooks) else . end
|
|
222
232
|
'
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -479,17 +479,23 @@ build_all_status() {
|
|
|
479
479
|
}
|
|
480
480
|
|
|
481
481
|
# --- Pending 集合管理(PreToolUse / PermissionRequest 配对)---
|
|
482
|
-
# 状态判定基于两个 per-pane
|
|
483
|
-
# pending-pre : 已 PreToolUse 未 PostToolUse 的 tool_use_id
|
|
484
|
-
# pending-perm : 已 PermissionRequest 未配对的 tool_use_id
|
|
482
|
+
# 状态判定基于两个 per-pane 集合(另有一个乱序辅助集合):
|
|
483
|
+
# pending-pre : 已 PreToolUse 未 PostToolUse 的 tool_use_id(行 "id tool ts")
|
|
484
|
+
# pending-perm : 已 PermissionRequest 未配对的 tool_use_id(行 "id mark tool ts")
|
|
485
|
+
# recent-post : 已 PostToolUse 完成的 tool_use_id(行 "id ts")
|
|
486
|
+
# Post 先于 Pre 到达时,迟到的 Pre 据此跳过写入,避免状态卡 ">"
|
|
485
487
|
# 派生规则:
|
|
486
488
|
# perm 含 "!" → STATUS=!
|
|
487
489
|
# perm 含 "?" → STATUS=?
|
|
488
490
|
# pre 非空 → STATUS=>
|
|
489
491
|
# 都空 → STATUS=""(事件自行决定)
|
|
490
492
|
|
|
493
|
+
_PENDING_DONE_WINDOW=60 # 迟到 Pre 的拦截窗口(tool_use_id 单次唯一,10x 余量)
|
|
494
|
+
_PENDING_DONE_RETAIN=300 # recent-post 保留时长(写时修剪)
|
|
495
|
+
|
|
491
496
|
_pending_pre_path() { echo "$(_pane_dir)/pending-pre"; }
|
|
492
497
|
_pending_perm_path() { echo "$(_pane_dir)/pending-perm"; }
|
|
498
|
+
_pending_done_path() { echo "$(_pane_dir)/recent-post"; }
|
|
493
499
|
_lock_dir() { echo "$(_pane_dir)/.lock"; }
|
|
494
500
|
_event_lock_dir() { echo "$(_pane_dir)/.event-lock"; }
|
|
495
501
|
|
|
@@ -586,17 +592,73 @@ _release_lock() {
|
|
|
586
592
|
rmdir "$(_lock_dir)" 2>/dev/null
|
|
587
593
|
}
|
|
588
594
|
|
|
595
|
+
# --- recent-post(已完成 id 的 tombstone)---
|
|
596
|
+
# 调用方必须持 .lock。只读:id 是否在窗口内已完成。
|
|
597
|
+
_recent_done_has() { # $1=id
|
|
598
|
+
local id="$1" f now
|
|
599
|
+
[ -n "$id" ] || return 1
|
|
600
|
+
f=$(_pending_done_path)
|
|
601
|
+
[ -s "$f" ] || return 1
|
|
602
|
+
now=$(date +%s)
|
|
603
|
+
awk -v id="$id" -v now="$now" -v max="$_PENDING_DONE_WINDOW" '
|
|
604
|
+
$1 == id && $2 ~ /^[0-9]+$/ && now - $2 <= max { found = 1; exit }
|
|
605
|
+
END { exit !found }' "$f" 2>/dev/null
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
# 调用方必须持 .lock。一次 awk 同时完成「同 id 去重 + 过期修剪 + 追加」。
|
|
609
|
+
_record_pending_done() { # $1=id
|
|
610
|
+
local id="$1" f now tmp
|
|
611
|
+
[ -n "$id" ] || return 0
|
|
612
|
+
f=$(_pending_done_path)
|
|
613
|
+
now=$(date +%s)
|
|
614
|
+
tmp="${f}.tmp.$$"
|
|
615
|
+
if [ -f "$f" ]; then
|
|
616
|
+
awk -v id="$id" -v cutoff="$((now - _PENDING_DONE_RETAIN))" '
|
|
617
|
+
$1 != id && $2 ~ /^[0-9]+$/ && $2 >= cutoff { print }' "$f" > "$tmp" 2>/dev/null
|
|
618
|
+
else
|
|
619
|
+
: > "$tmp"
|
|
620
|
+
fi
|
|
621
|
+
printf '%s %s\n' "$id" "$now" >> "$tmp"
|
|
622
|
+
mv -f "$tmp" "$f" 2>/dev/null || rm -f "$tmp"
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
# 调用方必须持 .lock。按首列精确比较(id 可能含 . [ 等 BRE 元字符)。
|
|
626
|
+
_remove_pending_id_locked() { # $1=id
|
|
627
|
+
local id="$1" pre perm
|
|
628
|
+
[ -n "$id" ] || return 0
|
|
629
|
+
pre=$(_pending_pre_path)
|
|
630
|
+
perm=$(_pending_perm_path)
|
|
631
|
+
if [ -f "$pre" ]; then
|
|
632
|
+
awk -v id="$id" '$1 != id' "$pre" > "${pre}.tmp" 2>/dev/null
|
|
633
|
+
mv -f "${pre}.tmp" "$pre" 2>/dev/null || rm -f "${pre}.tmp"
|
|
634
|
+
fi
|
|
635
|
+
if [ -f "$perm" ]; then
|
|
636
|
+
awk -v id="$id" '$1 != id' "$perm" > "${perm}.tmp" 2>/dev/null
|
|
637
|
+
mv -f "${perm}.tmp" "$perm" 2>/dev/null || rm -f "${perm}.tmp"
|
|
638
|
+
fi
|
|
639
|
+
}
|
|
640
|
+
|
|
589
641
|
# 在锁内追加一行到 pending-pre。$1=tool_use_id $2=tool_name
|
|
642
|
+
# Post 已先到达的同 id(recent-post 窗口内)→ 跳过并置 _pending_pre_skipped=1,
|
|
643
|
+
# 调用方据此保持已完成状态(不能把迟到的 Pre 重新抬成处理中/审批态)。
|
|
644
|
+
_pending_pre_skipped=0
|
|
590
645
|
_add_pending_pre() {
|
|
591
|
-
local id="$1" tool="$2"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if
|
|
597
|
-
|
|
646
|
+
local id="$1" tool="$2" pre
|
|
647
|
+
_pending_pre_skipped=0
|
|
648
|
+
[ -z "$id" ] && return 0
|
|
649
|
+
_acquire_lock || return 0
|
|
650
|
+
pre=$(_pending_pre_path)
|
|
651
|
+
if _recent_done_has "$id"; then
|
|
652
|
+
_pending_pre_skipped=1
|
|
653
|
+
# 防御:Post 的清理曾因 IO/锁失败残留同 id 时,顺手清掉
|
|
654
|
+
_remove_pending_id_locked "$id"
|
|
655
|
+
elif ! awk -v id="$id" '$1 == id { found = 1; exit } END { exit !found }' "$pre" 2>/dev/null; then
|
|
656
|
+
printf '%s %s %s\n' "$id" "${tool:-?}" "$(date +%s)" >> "$pre"
|
|
598
657
|
fi
|
|
599
658
|
_release_lock
|
|
659
|
+
if [ "$_pending_pre_skipped" = "1" ]; then
|
|
660
|
+
_ai_log "PENDING: SKIP stale Pre id=${id} tool=${tool:-?} (completed within ${_PENDING_DONE_WINDOW}s)"
|
|
661
|
+
fi
|
|
600
662
|
}
|
|
601
663
|
|
|
602
664
|
# 在锁内追加一行到 pending-perm。$1=tool_use_id $2=! 或 ? $3=tool_name
|
|
@@ -605,8 +667,9 @@ _add_pending_perm() {
|
|
|
605
667
|
[ -z "$id" ] && return
|
|
606
668
|
_acquire_lock || return
|
|
607
669
|
local f; f=$(_pending_perm_path)
|
|
608
|
-
|
|
609
|
-
|
|
670
|
+
# 首列精确比较(id 可能含 BRE 元字符)
|
|
671
|
+
if ! awk -v id="$id" '$1 == id { found = 1; exit } END { exit !found }' "$f" 2>/dev/null; then
|
|
672
|
+
printf '%s %s %s %s\n' "$id" "$mark" "${tool:-?}" "$(date +%s)" >> "$f"
|
|
610
673
|
fi
|
|
611
674
|
_release_lock
|
|
612
675
|
}
|
|
@@ -636,40 +699,36 @@ _bind_pending_perm_to_pre() {
|
|
|
636
699
|
[ -n "$BOUND_PERMISSION_ID" ]
|
|
637
700
|
}
|
|
638
701
|
|
|
639
|
-
# 从 pending-pre 和 pending-perm 同时移除 id
|
|
702
|
+
# 从 pending-pre 和 pending-perm 同时移除 id(配对清除)。
|
|
703
|
+
# 无论 pending 文件是否存在都记录 tombstone:Post 先于 Pre 到达时,后者是唯一
|
|
704
|
+
# 能拦住迟到 Pre 的证据(见 _add_pending_pre)。
|
|
640
705
|
_remove_pending_id() {
|
|
641
706
|
local id="$1"
|
|
642
|
-
[ -z "$id" ] && return
|
|
643
|
-
_acquire_lock || return
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
# 注意: grep -v 无匹配行时退出 1,不能用 && 串接 mv;改用无条件 mv
|
|
647
|
-
if [ -f "$pre" ]; then
|
|
648
|
-
grep -v "^${id} " "$pre" > "${pre}.tmp" 2>/dev/null
|
|
649
|
-
mv "${pre}.tmp" "$pre" 2>/dev/null || rm -f "${pre}.tmp"
|
|
650
|
-
fi
|
|
651
|
-
if [ -f "$perm" ]; then
|
|
652
|
-
grep -v "^${id} " "$perm" > "${perm}.tmp" 2>/dev/null
|
|
653
|
-
mv "${perm}.tmp" "$perm" 2>/dev/null || rm -f "${perm}.tmp"
|
|
654
|
-
fi
|
|
707
|
+
[ -z "$id" ] && return 0
|
|
708
|
+
_acquire_lock || return 0
|
|
709
|
+
_record_pending_done "$id"
|
|
710
|
+
_remove_pending_id_locked "$id"
|
|
655
711
|
_release_lock
|
|
656
712
|
}
|
|
657
713
|
|
|
658
714
|
# 仅移除已回复的权限请求。OpenCode 的 request ID 与 tool call ID 独立,不能同时清
|
|
659
715
|
# pending-pre;后续 PostToolUse 会用 call ID 清理工具执行项。
|
|
716
|
+
# 不记 tombstone:request id 与 callID 命名空间不同,记了也拦不到迟到的 Pre。
|
|
660
717
|
_remove_pending_perm_id() {
|
|
661
718
|
local id="$1"
|
|
662
719
|
[ -z "$id" ] && return
|
|
663
720
|
_acquire_lock || return
|
|
664
721
|
local perm; perm=$(_pending_perm_path)
|
|
665
722
|
if [ -f "$perm" ]; then
|
|
666
|
-
|
|
667
|
-
mv "${perm}.tmp" "$perm" 2>/dev/null || rm -f "${perm}.tmp"
|
|
723
|
+
awk -v id="$id" '$1 != id' "$perm" > "${perm}.tmp" 2>/dev/null
|
|
724
|
+
mv -f "${perm}.tmp" "$perm" 2>/dev/null || rm -f "${perm}.tmp"
|
|
668
725
|
fi
|
|
669
726
|
_release_lock
|
|
670
727
|
}
|
|
671
728
|
|
|
672
|
-
# 清空两个 pending 文件(Stop/SessionStart/SessionEnd/UserPromptSubmit/Esc
|
|
729
|
+
# 清空两个 pending 文件(Stop/SessionStart/SessionEnd/UserPromptSubmit/Esc 用)。
|
|
730
|
+
# recent-post 刻意不清:turn/session 边界之后仍可能收到同 turn 迟到的 Pre,
|
|
731
|
+
# 删掉 tombstone 会在边界重新引入残留;其生命周期由 300s 修剪与 pane 目录销毁负责。
|
|
673
732
|
_clear_pending() {
|
|
674
733
|
[ -n "$TMUX_PANE" ] || return
|
|
675
734
|
_acquire_lock || return
|
|
@@ -677,16 +736,18 @@ _clear_pending() {
|
|
|
677
736
|
_release_lock
|
|
678
737
|
}
|
|
679
738
|
|
|
680
|
-
# pending 集合中是否有 N
|
|
681
|
-
#
|
|
682
|
-
#
|
|
739
|
+
# pending 集合中是否有 N 秒内的条目。用于区分「工具确实在跑」与「泄漏/中断残留
|
|
740
|
+
# 的旧条目」——并发 hook 抢锁超时会让 _remove_pending_id 静默放弃,集合可能长期
|
|
741
|
+
# 残留陈旧 id(实测存在),不能只按非空判定活跃。
|
|
742
|
+
# 时间戳取末列:pending-pre 是 "id tool ts"(3 列),pending-perm 是
|
|
743
|
+
# "id mark tool ts"(4 列)——固定 $3 对 perm 永远不命中(历史 bug)。
|
|
683
744
|
_pending_recent() { # $1=秒数 → 0=有新鲜条目
|
|
684
745
|
local max="${1:-10}" now f
|
|
685
746
|
now=$(date +%s)
|
|
686
747
|
for f in "$(_pending_pre_path)" "$(_pending_perm_path)"; do
|
|
687
748
|
[ -s "$f" ] || continue
|
|
688
749
|
if awk -v now="$now" -v max="$max" '
|
|
689
|
-
$
|
|
750
|
+
$NF ~ /^[0-9]+$/ && now - $NF <= max { found = 1; exit }
|
|
690
751
|
END { exit !found }' "$f" 2>/dev/null; then
|
|
691
752
|
return 0
|
|
692
753
|
fi
|
package/scripts/tmux-ai-status
CHANGED
|
@@ -192,27 +192,34 @@ case "$EVENT" in
|
|
|
192
192
|
IFS=$'\t' read -r TOOL TOOL_USE_ID <<< "$_tool_fields"
|
|
193
193
|
INPUT_DIGEST=$(printf '%s' "$INPUT" | jq -c '{tool_name, tool_input}' 2>/dev/null)
|
|
194
194
|
_add_pending_pre "$TOOL_USE_ID" "$TOOL"
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
STATUS="!"
|
|
201
|
-
_add_pending_perm "$TOOL_USE_ID" "$STATUS" "$TOOL"
|
|
195
|
+
if [ "$_pending_pre_skipped" = "1" ]; then
|
|
196
|
+
# Post/PostToolUseFailure 已处理同 id(乱序到达):不重建 pending、不进入
|
|
197
|
+
# 审批态(尤其 codex request_permissions 不得复活 !),保持已完成状态。
|
|
198
|
+
STATUS="$PREV_STATUS"
|
|
199
|
+
_log_flags="STALE_PRE=skip"
|
|
202
200
|
else
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
_permission_pre_tool=0
|
|
202
|
+
for _permission_tool in ${ADAPTER_PERMISSION_PRE_TOOL_NAMES:-}; do
|
|
203
|
+
[ "$TOOL" = "$_permission_tool" ] && { _permission_pre_tool=1; break; }
|
|
204
|
+
done
|
|
205
|
+
if [ "$_permission_pre_tool" = "1" ]; then
|
|
206
|
+
STATUS="!"
|
|
207
|
+
_add_pending_perm "$TOOL_USE_ID" "$STATUS" "$TOOL"
|
|
208
|
+
else
|
|
209
|
+
# Claude 的 PermReq 不带 tool_use_id,派生看不到审批态;用 LIVE_STATUS 兜底。
|
|
210
|
+
# Codex sentinel 和 OpenCode request ID 则会直接从 pending 集合派生。
|
|
211
|
+
LIVE_STATUS=$(_current_pane_status)
|
|
212
|
+
case "$LIVE_STATUS" in
|
|
213
|
+
"!"|"?") STATUS="$LIVE_STATUS" ;;
|
|
214
|
+
*)
|
|
215
|
+
if _derive_status_from_pending; then
|
|
216
|
+
STATUS="$DERIVED"
|
|
217
|
+
else
|
|
218
|
+
STATUS=">"
|
|
219
|
+
fi
|
|
220
|
+
;;
|
|
221
|
+
esac
|
|
222
|
+
fi
|
|
216
223
|
fi
|
|
217
224
|
;;
|
|
218
225
|
PostToolUse|PostToolUseFailure)
|
package/src/check-detail.js
CHANGED
|
@@ -95,6 +95,14 @@ export async function checkToolDetail(tool, { run } = {}) {
|
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
// 重复注册检测:同事件下 registered > 1 表示有多条本插件 command(跨 group 或多 group)。
|
|
99
|
+
// 只用于提示——codex 重写 hooks.json 会使 trusted_hash 失效,因此不自动收敛。
|
|
100
|
+
export function findDuplicateEvents(detail) {
|
|
101
|
+
return Object.entries(detail?.events || {})
|
|
102
|
+
.filter(([, e]) => Number(e?.registered) > 1)
|
|
103
|
+
.map(([event, e]) => ({ event, registered: Number(e.registered) }));
|
|
104
|
+
}
|
|
105
|
+
|
|
98
106
|
// 渲染单工具明细为人类可读行。drift 行用 ⚠ 前缀(脚本仍可用,功能未坏,
|
|
99
107
|
// 但指向的安装源不是当前 source——收敛入口在 TUI 侧)。
|
|
100
108
|
export function renderToolDetail(detail) {
|
|
@@ -108,6 +116,17 @@ export function renderToolDetail(detail) {
|
|
|
108
116
|
lines.push(` ${NO} 无法判定:${detail.error || '未知错误'}`);
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
// codex 重复注册:功能性提示(status 仍 ok、exit code 不变),收敛需显式操作
|
|
120
|
+
if (detail.tool === 'codex') {
|
|
121
|
+
const dups = findDuplicateEvents(detail);
|
|
122
|
+
if (dups.length > 0) {
|
|
123
|
+
const list = dups.map((d) => `${d.event}×${d.registered}`).join('、');
|
|
124
|
+
lines.push(
|
|
125
|
+
` ${WARN} codex hooks 重复注册:${list}——codex 不会自动收敛(重写 hooks.json 会使 trusted_hash 失效,需要重新 Trust all);可用 TMUX_AI_FORCE_INSTALL=1 重跑 install-codex-hooks.sh,或使用 TUI 收敛入口(会先确认 re-trust 代价)`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
111
130
|
if (detail.suppress) {
|
|
112
131
|
lines.push(` ${WARN} 存在卸载意图标记(suppressed-${detail.tool}):自动修复已被抑制`);
|
|
113
132
|
}
|