@quantiya/codevibe-codex-plugin 2.0.11 → 2.0.14
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/dist/server.js +36 -27
- package/hooks/common.sh +21 -9
- package/libexec/codex-hook-config.js +190 -0
- package/libexec/companion-launcher +9 -158
- package/node_modules/@quantiya/codevibe-core/dist/__tests__/gate-ux-regressions.test.d.ts +1 -0
- package/node_modules/@quantiya/codevibe-core/dist/index.d.ts +1 -1
- package/node_modules/@quantiya/codevibe-core/dist/index.js +240 -240
- package/node_modules/@quantiya/codevibe-core/dist/local-executor/workspace-shadow.d.ts +7 -0
- package/node_modules/@quantiya/codevibe-core/dist/local-model/manager.d.ts +1 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/audit-runner.d.ts +2 -3
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/cli.d.ts +2 -2
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/cli.js +158 -28
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/quorum-loop.d.ts +22 -3
- package/node_modules/@quantiya/codevibe-core/dist/orchestration-shell/task-progress.d.ts +3 -0
- package/node_modules/@quantiya/codevibe-core/dist/reduced-trust-notice.d.ts +44 -0
- package/node_modules/@quantiya/codevibe-core/dist/tool-activity/consolidator.d.ts +7 -0
- package/node_modules/@quantiya/codevibe-core/dist/tool-activity/ledger.d.ts +33 -13
- package/node_modules/@quantiya/codevibe-core/package.json +1 -1
- package/node_modules/fs-ext/build/Makefile +2 -2
- package/node_modules/fs-ext/build/Release/fs_ext.node +0 -0
- package/node_modules/fs-ext/build/Release/obj.target/fs_ext/fs-ext.o +0 -0
- package/node_modules/fs-ext/build/config.gypi +1 -1
- package/package.json +5 -2
package/hooks/common.sh
CHANGED
|
@@ -117,9 +117,9 @@ is_codevibe_codex_daemon() {
|
|
|
117
117
|
esac
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
# (#638 G5/H2/H3) THE identity-guarded reap:
|
|
120
|
+
# (#638 G5/H2/H3) THE identity-guarded reap: graceful signal → bounded wait → identity
|
|
121
121
|
# re-check → KILL → settle. Identity (is_codevibe_codex_daemon) is re-verified
|
|
122
|
-
# immediately before EVERY signal — the PID can die after the
|
|
122
|
+
# immediately before EVERY signal — the PID can die after the graceful signal and be
|
|
123
123
|
# recycled to an unrelated process during the settle poll, so signaling blind
|
|
124
124
|
# could kill a foreign process. Works for BOTH our own background child (`wait`
|
|
125
125
|
# collects its status instantly, closing the PID-reuse window) and a non-child
|
|
@@ -127,13 +127,15 @@ is_codevibe_codex_daemon() {
|
|
|
127
127
|
# provides the settle). Returns 0 when the daemon is provably gone, 1 when the
|
|
128
128
|
# PID was not (or stopped being) our daemon and was left alone.
|
|
129
129
|
_reap_codevibe_codex_daemon_pid() {
|
|
130
|
-
local pid="$1" why="$2"
|
|
130
|
+
local pid="$1" why="$2" graceful_signal="${3:-TERM}"
|
|
131
131
|
[ -n "$pid" ] || return 1
|
|
132
132
|
if ! is_codevibe_codex_daemon "$pid"; then
|
|
133
133
|
log "WARN" "reap($why): pid $pid is not our daemon (recycled/replaced) — not signaling"
|
|
134
134
|
return 1
|
|
135
135
|
fi
|
|
136
|
-
|
|
136
|
+
# TERM is a real terminal/session shutdown. USR2 is a daemon handoff: the
|
|
137
|
+
# replacement must reclaim the still-ACTIVE hosted row and its history.
|
|
138
|
+
kill -s "$graceful_signal" "$pid" 2>/dev/null
|
|
137
139
|
local reap=0
|
|
138
140
|
while [ $reap -lt 10 ] && ps -p "$pid" > /dev/null 2>&1; do
|
|
139
141
|
sleep 0.2
|
|
@@ -439,7 +441,7 @@ ensure_codex_daemon() {
|
|
|
439
441
|
;;
|
|
440
442
|
*)
|
|
441
443
|
log "WARN" "ensure_codex_daemon: reaping wedged daemon pid $wedged_pid on excluded port $exclude_port (via $wedged_src) before relaunch (#638 H2)"
|
|
442
|
-
_reap_codevibe_codex_daemon_pid "$wedged_pid" "wedged /event on port $exclude_port"
|
|
444
|
+
_reap_codevibe_codex_daemon_pid "$wedged_pid" "wedged /event on port $exclude_port" USR2
|
|
443
445
|
;;
|
|
444
446
|
esac
|
|
445
447
|
fi
|
|
@@ -528,10 +530,13 @@ send_to_server() {
|
|
|
528
530
|
# (accepts TCP, never responds) must not hang the hook indefinitely — hooks
|
|
529
531
|
# block the agent UI. --connect-timeout 2 bounds connection establishment;
|
|
530
532
|
# --max-time 10 bounds the whole POST (generous for a slow-network POST).
|
|
531
|
-
|
|
533
|
+
# Stream the payload over stdin. Passing a screenshot-bearing PostToolUse
|
|
534
|
+
# body through `-d "$json_data"` puts it in argv and can fail locally with
|
|
535
|
+
# E2BIG before curl reaches the healthy daemon.
|
|
536
|
+
response=$(printf '%s' "$json_data" | curl -s --connect-timeout 2 --max-time 10 -w "\n%{http_code}" \
|
|
532
537
|
-X POST \
|
|
533
538
|
-H "Content-Type: application/json" \
|
|
534
|
-
-
|
|
539
|
+
--data-binary @- \
|
|
535
540
|
"$server_url/$endpoint" 2>&1)
|
|
536
541
|
curl_exit=$?
|
|
537
542
|
http_code=$(echo "$response" | tail -n 1)
|
|
@@ -551,6 +556,13 @@ send_to_server() {
|
|
|
551
556
|
log "WARN" "No server URL (port file missing) — will attempt daemon relaunch"
|
|
552
557
|
fi
|
|
553
558
|
|
|
559
|
+
# A local exec failure means curl never contacted the daemon. Replacing a
|
|
560
|
+
# health-passing process in response would retire or disrupt a valid session.
|
|
561
|
+
if [ "$curl_exit" -eq 126 ] || [ "$curl_exit" -eq 127 ]; then
|
|
562
|
+
log "ERROR" "Local curl invocation failed (exit $curl_exit) — not replacing the Codex daemon"
|
|
563
|
+
return 1
|
|
564
|
+
fi
|
|
565
|
+
|
|
554
566
|
# (#638) TRANSPORT failure = curl non-zero OR HTTP 000 (dead daemon / missing
|
|
555
567
|
# port file). Relaunch the daemon ONCE and retry the POST once (one relaunch
|
|
556
568
|
# max, no loops). NOT for app 4xx/5xx — the daemon is alive and rejected the
|
|
@@ -574,10 +586,10 @@ send_to_server() {
|
|
|
574
586
|
local new_port retry
|
|
575
587
|
if new_port=$(ensure_codex_daemon "$failed_port" "$failed_pid"); then
|
|
576
588
|
log "INFO" "Relaunched Codex daemon on port $new_port, retrying POST /$endpoint"
|
|
577
|
-
retry=$(curl -s --connect-timeout 2 --max-time 10 -w "\n%{http_code}" \
|
|
589
|
+
retry=$(printf '%s' "$json_data" | curl -s --connect-timeout 2 --max-time 10 -w "\n%{http_code}" \
|
|
578
590
|
-X POST \
|
|
579
591
|
-H "Content-Type: application/json" \
|
|
580
|
-
-
|
|
592
|
+
--data-binary @- \
|
|
581
593
|
"http://localhost:${new_port}/$endpoint" 2>&1)
|
|
582
594
|
curl_exit=$?
|
|
583
595
|
http_code=$(echo "$retry" | tail -n 1)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const OUR_HOOK_FILES = new Set([
|
|
8
|
+
'session-start.sh',
|
|
9
|
+
'user-prompt.sh',
|
|
10
|
+
'pre-tool-use.sh',
|
|
11
|
+
'permission-request.sh',
|
|
12
|
+
'post-tool-use.sh',
|
|
13
|
+
'stop.sh',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function hookScriptPath(command) {
|
|
17
|
+
if (typeof command !== 'string') return null;
|
|
18
|
+
const match = command.trim().match(/^bash\s+(?:"([^"]+)"|'([^']+)'|(\S+))$/);
|
|
19
|
+
return match ? (match[1] || match[2] || match[3]) : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isOurCommand(command) {
|
|
23
|
+
const scriptPath = hookScriptPath(command);
|
|
24
|
+
if (!scriptPath) return false;
|
|
25
|
+
|
|
26
|
+
const filename = scriptPath.slice(scriptPath.lastIndexOf('/') + 1);
|
|
27
|
+
if (!OUR_HOOK_FILES.has(filename) || !scriptPath.endsWith(`/hooks/${filename}`)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (scriptPath.includes('/codevibe-codex-plugin/hooks/')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Development links can use task-specific repository names rather than the
|
|
36
|
+
// canonical package directory. Keep this deliberately limited to CodeVibe's
|
|
37
|
+
// agent-worktree layout so an unrelated hook with the same filename survives.
|
|
38
|
+
return /\/\.agent-worktrees\/[^/]+\/codevibe-codex-[^/]+\/hooks\/[^/]+$/.test(scriptPath);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripOurHooksFromEntry(entry) {
|
|
42
|
+
if (!entry || typeof entry !== 'object' || !Array.isArray(entry.hooks)) {
|
|
43
|
+
return entry;
|
|
44
|
+
}
|
|
45
|
+
const filtered = entry.hooks.filter((hook) => !(hook && isOurCommand(hook.command)));
|
|
46
|
+
if (filtered.length === entry.hooks.length) return entry;
|
|
47
|
+
if (filtered.length === 0) return null;
|
|
48
|
+
return { ...entry, hooks: filtered };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ownedCommands(entries) {
|
|
52
|
+
return entries
|
|
53
|
+
.flatMap((entry) => entry && typeof entry === 'object' && Array.isArray(entry.hooks)
|
|
54
|
+
? entry.hooks
|
|
55
|
+
: [])
|
|
56
|
+
.map((hook) => hook && hook.command)
|
|
57
|
+
.filter(isOurCommand)
|
|
58
|
+
.sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function mergeHookConfig(existingObj, nextObj, existed = true) {
|
|
62
|
+
const existingHooks = existingObj && typeof existingObj === 'object'
|
|
63
|
+
&& existingObj.hooks && typeof existingObj.hooks === 'object'
|
|
64
|
+
? existingObj.hooks
|
|
65
|
+
: {};
|
|
66
|
+
const nextHooks = nextObj && typeof nextObj === 'object'
|
|
67
|
+
&& nextObj.hooks && typeof nextObj.hooks === 'object'
|
|
68
|
+
? nextObj.hooks
|
|
69
|
+
: {};
|
|
70
|
+
|
|
71
|
+
let allPresent = existed;
|
|
72
|
+
for (const key of Object.keys(nextHooks)) {
|
|
73
|
+
const existingEntries = Array.isArray(existingHooks[key]) ? existingHooks[key] : [];
|
|
74
|
+
const nextEntries = Array.isArray(nextHooks[key]) ? nextHooks[key] : [];
|
|
75
|
+
if (JSON.stringify(ownedCommands(existingEntries)) !== JSON.stringify(ownedCommands(nextEntries))) {
|
|
76
|
+
allPresent = false;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (allPresent) {
|
|
81
|
+
return { outcome: 'already_installed', config: existingObj };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mergedHooks = { ...existingHooks };
|
|
85
|
+
for (const key of Object.keys(nextHooks)) {
|
|
86
|
+
const existingEntries = Array.isArray(existingHooks[key]) ? existingHooks[key] : [];
|
|
87
|
+
const cleanedExisting = existingEntries
|
|
88
|
+
.map(stripOurHooksFromEntry)
|
|
89
|
+
.filter((entry) => entry !== null && entry !== undefined);
|
|
90
|
+
const nextEntries = Array.isArray(nextHooks[key]) ? nextHooks[key] : [];
|
|
91
|
+
mergedHooks[key] = [...cleanedExisting, ...nextEntries];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
outcome: existed ? 'merged' : 'fresh_install',
|
|
96
|
+
config: existingObj && typeof existingObj === 'object'
|
|
97
|
+
? { ...existingObj, hooks: mergedHooks }
|
|
98
|
+
: { hooks: mergedHooks },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanHookConfig(existingObj) {
|
|
103
|
+
if (!existingObj || typeof existingObj !== 'object'
|
|
104
|
+
|| !existingObj.hooks || typeof existingObj.hooks !== 'object') {
|
|
105
|
+
return existingObj;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hooks = { ...existingObj.hooks };
|
|
109
|
+
for (const key of Object.keys(hooks)) {
|
|
110
|
+
if (!Array.isArray(hooks[key])) continue;
|
|
111
|
+
hooks[key] = hooks[key]
|
|
112
|
+
.map(stripOurHooksFromEntry)
|
|
113
|
+
.filter((entry) => entry !== null && entry !== undefined);
|
|
114
|
+
}
|
|
115
|
+
return { ...existingObj, hooks };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function writeAtomic(target, value) {
|
|
119
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
120
|
+
try {
|
|
121
|
+
fs.writeFileSync(temporary, JSON.stringify(value, null, 2));
|
|
122
|
+
fs.renameSync(temporary, target);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
try { fs.unlinkSync(temporary); } catch {}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function runMerge() {
|
|
130
|
+
const target = process.env.CV_EXISTING;
|
|
131
|
+
let existingObj = null;
|
|
132
|
+
let existed = false;
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(target)) {
|
|
135
|
+
existed = true;
|
|
136
|
+
existingObj = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
process.stderr.write(`PARSE_EXISTING_FAILED:${error.message || String(error)}`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let nextObj;
|
|
144
|
+
try {
|
|
145
|
+
nextObj = JSON.parse(process.env.CV_NEW);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
process.stderr.write(`PARSE_NEW_FAILED:${error.message || String(error)}`);
|
|
148
|
+
return 3;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const result = mergeHookConfig(existingObj, nextObj, existed);
|
|
152
|
+
if (result.outcome !== 'already_installed') {
|
|
153
|
+
try {
|
|
154
|
+
writeAtomic(target, result.config);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
process.stderr.write(`WRITE_FAILED:${error.message || String(error)}`);
|
|
157
|
+
return 4;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
process.stdout.write(`OUTCOME:${result.outcome}\n`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function runCleanup() {
|
|
165
|
+
const target = process.env.CV_HOOKS_FILE;
|
|
166
|
+
try {
|
|
167
|
+
const existingObj = JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
168
|
+
writeAtomic(target, cleanHookConfig(existingObj));
|
|
169
|
+
return 0;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
process.stderr.write(`CLEAN_FAILED:${error.message || String(error)}`);
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (require.main === module) {
|
|
177
|
+
const action = process.argv[2];
|
|
178
|
+
const exitCode = action === 'merge'
|
|
179
|
+
? runMerge()
|
|
180
|
+
: action === 'cleanup'
|
|
181
|
+
? runCleanup()
|
|
182
|
+
: 64;
|
|
183
|
+
process.exit(exitCode);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
cleanHookConfig,
|
|
188
|
+
isOurCommand,
|
|
189
|
+
mergeHookConfig,
|
|
190
|
+
};
|
|
@@ -259,55 +259,10 @@ cleanup() {
|
|
|
259
259
|
# Only remove if no other codevibe-codex sessions are running
|
|
260
260
|
if [ "$(pgrep -f 'codevibe-codex' | wc -l)" -le 1 ]; then
|
|
261
261
|
log "Removing CodeVibe hooks from $HOME/.codex/hooks.json"
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if CV_HOOKS_FILE="$HOME/.codex/hooks.json" node -e '
|
|
267
|
-
const fs = require("fs");
|
|
268
|
-
const OUR_HOOK_FILES = new Set([
|
|
269
|
-
"session-start.sh",
|
|
270
|
-
"user-prompt.sh",
|
|
271
|
-
"pre-tool-use.sh",
|
|
272
|
-
"post-tool-use.sh",
|
|
273
|
-
"stop.sh",
|
|
274
|
-
]);
|
|
275
|
-
const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
|
|
276
|
-
const isOurCommand = (command) => {
|
|
277
|
-
if (typeof command !== "string") return false;
|
|
278
|
-
if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
|
|
279
|
-
for (const f of OUR_HOOK_FILES) {
|
|
280
|
-
if (command.endsWith("/" + f)) return true;
|
|
281
|
-
}
|
|
282
|
-
return false;
|
|
283
|
-
};
|
|
284
|
-
const stripOurHooksFromEntry = (entry) => {
|
|
285
|
-
if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) return entry;
|
|
286
|
-
const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
|
|
287
|
-
if (filtered.length === entry.hooks.length) return entry;
|
|
288
|
-
if (filtered.length === 0) return null;
|
|
289
|
-
return { ...entry, hooks: filtered };
|
|
290
|
-
};
|
|
291
|
-
try {
|
|
292
|
-
const target = process.env.CV_HOOKS_FILE;
|
|
293
|
-
const data = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
294
|
-
if (data && typeof data === "object" && data.hooks && typeof data.hooks === "object") {
|
|
295
|
-
for (const k of Object.keys(data.hooks)) {
|
|
296
|
-
if (Array.isArray(data.hooks[k])) {
|
|
297
|
-
data.hooks[k] = data.hooks[k]
|
|
298
|
-
.map(stripOurHooksFromEntry)
|
|
299
|
-
.filter((entry) => entry !== null && entry !== undefined);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
|
|
304
|
-
fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
|
|
305
|
-
fs.renameSync(tmp, target);
|
|
306
|
-
} catch (e) {
|
|
307
|
-
process.stderr.write("CLEAN_FAILED:" + (e && e.message ? e.message : String(e)));
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
' 2>>"$LOG_FILE"; then
|
|
262
|
+
# The shared helper keeps install and cleanup ownership rules in
|
|
263
|
+
# lockstep, including task-named CodeVibe agent worktrees.
|
|
264
|
+
if CV_HOOKS_FILE="$HOME/.codex/hooks.json" \
|
|
265
|
+
node "$PLUGIN_DIR/libexec/codex-hook-config.js" cleanup 2>>"$LOG_FILE"; then
|
|
311
266
|
:
|
|
312
267
|
else
|
|
313
268
|
log "WARN: failed to clean CodeVibe hooks from hooks.json (left as-is)"
|
|
@@ -539,9 +494,9 @@ _CV_HOOKS_REASON=""
|
|
|
539
494
|
# keeps the user hook (only the CodeVibe hooks get stripped, not
|
|
540
495
|
# the surrounding entry).
|
|
541
496
|
#
|
|
542
|
-
# - Ownership predicate:
|
|
543
|
-
#
|
|
544
|
-
# Rejects user paths
|
|
497
|
+
# - Ownership predicate: canonical package paths or CodeVibe's strict
|
|
498
|
+
# `.agent-worktrees/<agent>/codevibe-codex-*/hooks/` layout, plus a
|
|
499
|
+
# filename allowlist. Rejects unrelated user hook paths.
|
|
545
500
|
#
|
|
546
501
|
# - Per-process tmp file (.<pid>.<time>.tmp): two concurrent installs
|
|
547
502
|
# won't clobber each other's intermediate state. Final rename is
|
|
@@ -555,112 +510,8 @@ _CV_HOOKS_REASON=""
|
|
|
555
510
|
GENERATED_HOOKS=$(generate_hooks_json)
|
|
556
511
|
if INSTALLER_OUTPUT=$(CV_EXISTING="$CODEX_HOOKS_FILE" \
|
|
557
512
|
CV_NEW="$GENERATED_HOOKS" \
|
|
558
|
-
node -
|
|
559
|
-
|
|
560
|
-
const OUR_HOOK_FILES = new Set([
|
|
561
|
-
"session-start.sh",
|
|
562
|
-
"user-prompt.sh",
|
|
563
|
-
"pre-tool-use.sh",
|
|
564
|
-
"permission-request.sh",
|
|
565
|
-
"post-tool-use.sh",
|
|
566
|
-
"stop.sh",
|
|
567
|
-
]);
|
|
568
|
-
const OWN_PATH_MARKER = "codevibe-codex-plugin/hooks/";
|
|
569
|
-
const isOurCommand = (command) => {
|
|
570
|
-
if (typeof command !== "string") return false;
|
|
571
|
-
if (command.indexOf(OWN_PATH_MARKER) === -1) return false;
|
|
572
|
-
for (const f of OUR_HOOK_FILES) {
|
|
573
|
-
if (command.endsWith("/" + f)) return true;
|
|
574
|
-
}
|
|
575
|
-
return false;
|
|
576
|
-
};
|
|
577
|
-
// Strip our hooks from a single matcher entrys inner hooks[] array.
|
|
578
|
-
// Returns null if NO non-owned hooks remain (whole entry should be
|
|
579
|
-
// dropped); otherwise returns a new entry with only the user-owned
|
|
580
|
-
// hooks. Returns the original entry unchanged if it contains none of
|
|
581
|
-
// ours (preserves identity for unrelated entries).
|
|
582
|
-
const stripOurHooksFromEntry = (entry) => {
|
|
583
|
-
if (!entry || typeof entry !== "object" || !Array.isArray(entry.hooks)) {
|
|
584
|
-
return entry;
|
|
585
|
-
}
|
|
586
|
-
const filtered = entry.hooks.filter((h) => !(h && isOurCommand(h.command)));
|
|
587
|
-
if (filtered.length === entry.hooks.length) return entry;
|
|
588
|
-
if (filtered.length === 0) return null;
|
|
589
|
-
return { ...entry, hooks: filtered };
|
|
590
|
-
};
|
|
591
|
-
const ownedCommands = (entries) => entries
|
|
592
|
-
.flatMap((entry) => entry && typeof entry === "object" && Array.isArray(entry.hooks)
|
|
593
|
-
? entry.hooks
|
|
594
|
-
: [])
|
|
595
|
-
.map((hook) => hook && hook.command)
|
|
596
|
-
.filter(isOurCommand)
|
|
597
|
-
.sort();
|
|
598
|
-
|
|
599
|
-
let existingObj = null;
|
|
600
|
-
let existed = false;
|
|
601
|
-
try {
|
|
602
|
-
if (fs.existsSync(process.env.CV_EXISTING)) {
|
|
603
|
-
existed = true;
|
|
604
|
-
existingObj = JSON.parse(fs.readFileSync(process.env.CV_EXISTING, "utf8"));
|
|
605
|
-
}
|
|
606
|
-
} catch (e) {
|
|
607
|
-
process.stderr.write("PARSE_EXISTING_FAILED:" + (e && e.message ? e.message : String(e)));
|
|
608
|
-
process.exit(2);
|
|
609
|
-
}
|
|
610
|
-
let nextObj;
|
|
611
|
-
try {
|
|
612
|
-
nextObj = JSON.parse(process.env.CV_NEW);
|
|
613
|
-
} catch (e) {
|
|
614
|
-
process.stderr.write("PARSE_NEW_FAILED:" + (e && e.message ? e.message : String(e)));
|
|
615
|
-
process.exit(3);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const existingHooks = (existingObj && typeof existingObj === "object" &&
|
|
619
|
-
existingObj.hooks && typeof existingObj.hooks === "object") ? existingObj.hooks : {};
|
|
620
|
-
const nextHooks = (nextObj && typeof nextObj === "object" &&
|
|
621
|
-
nextObj.hooks && typeof nextObj.hooks === "object") ? nextObj.hooks : {};
|
|
622
|
-
let allPresent = existed;
|
|
623
|
-
for (const k of Object.keys(nextHooks)) {
|
|
624
|
-
const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
|
|
625
|
-
const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
|
|
626
|
-
if (JSON.stringify(ownedCommands(existingArr)) !== JSON.stringify(ownedCommands(nextArr))) {
|
|
627
|
-
allPresent = false;
|
|
628
|
-
break;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (allPresent) {
|
|
632
|
-
process.stdout.write("OUTCOME:already_installed\n");
|
|
633
|
-
process.exit(0);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const mergedHooks = { ...existingHooks };
|
|
637
|
-
for (const k of Object.keys(nextHooks)) {
|
|
638
|
-
const existingArr = Array.isArray(existingHooks[k]) ? existingHooks[k] : [];
|
|
639
|
-
const cleanedExisting = existingArr
|
|
640
|
-
.map(stripOurHooksFromEntry)
|
|
641
|
-
.filter((entry) => entry !== null && entry !== undefined);
|
|
642
|
-
const nextArr = Array.isArray(nextHooks[k]) ? nextHooks[k] : [];
|
|
643
|
-
mergedHooks[k] = [...cleanedExisting, ...nextArr];
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const out = (existingObj && typeof existingObj === "object")
|
|
647
|
-
? { ...existingObj, hooks: mergedHooks }
|
|
648
|
-
: { hooks: mergedHooks };
|
|
649
|
-
const outcome = existed ? "merged" : "fresh_install";
|
|
650
|
-
const target = process.env.CV_EXISTING;
|
|
651
|
-
// Per-process tmp suffix avoids two concurrent installs both writing
|
|
652
|
-
// ${target}.tmp and one mvs the others half-written file.
|
|
653
|
-
const tmp = target + "." + process.pid + "." + Date.now() + ".tmp";
|
|
654
|
-
try {
|
|
655
|
-
fs.writeFileSync(tmp, JSON.stringify(out, null, 2));
|
|
656
|
-
fs.renameSync(tmp, target);
|
|
657
|
-
} catch (e) {
|
|
658
|
-
try { fs.unlinkSync(tmp); } catch {}
|
|
659
|
-
process.stderr.write("WRITE_FAILED:" + (e && e.message ? e.message : String(e)));
|
|
660
|
-
process.exit(4);
|
|
661
|
-
}
|
|
662
|
-
process.stdout.write("OUTCOME:" + outcome + "\n");
|
|
663
|
-
' 2>>"$LOG_FILE"); then
|
|
513
|
+
node "$PLUGIN_DIR/libexec/codex-hook-config.js" merge \
|
|
514
|
+
2>>"$LOG_FILE"); then
|
|
664
515
|
INSTALLER_RC=0
|
|
665
516
|
else
|
|
666
517
|
INSTALLER_RC=$?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -43,7 +43,7 @@ export { ToolActivityOutbox, type ToolActivityOutboxOptions, type ListPendingRes
|
|
|
43
43
|
export { ToolActivityLedger, LedgerEntrySchema, LedgerFileSchema, LegacyOwnedFileSchema, type LedgerEntry, type LedgerFile, type LegacyOwnedFile, type OccurrenceRef, type ToolActivityLedgerOptions, } from './tool-activity';
|
|
44
44
|
export { ToolReplayCursor, ReplayFrameTooLargeError, ReplayCursorStateSchema, DEFAULT_REPLAY_READ_BYTES, DEFAULT_REPLAY_MAX_FRAME_BYTES, type ReplayCursorState, type ReplayFrame, type ReadFramesResult, type ToolReplayCursorOptions, } from './tool-activity';
|
|
45
45
|
export { ToolActivitySessionMap, InProcessRolloutLock, SessionMapRecordSchema, type RolloutLock, type RolloutOwnership, type SessionMapRecord, type ToolActivitySessionMapOptions, } from './tool-activity';
|
|
46
|
-
export { ToolActivityConsolidator, isSessionKeyStaleError, type ObservedOccurrence, type ObserveDisposition, type ToolActivityTransport, type ToolActivityConsolidatorOptions, } from './tool-activity';
|
|
46
|
+
export { ToolActivityConsolidator, isSessionKeyStaleError, isTerminalSessionError, type ObservedOccurrence, type ObserveDisposition, type ToolActivityTransport, type ToolActivityConsolidatorOptions, } from './tool-activity';
|
|
47
47
|
export { SocketRolloutLock, type SocketRolloutLockOptions, FlockRolloutLock, type FlockRolloutLockOptions, type FlockSyncFn, type FlockFlag, createRolloutLock, type CreateRolloutLockOptions, } from './tool-activity';
|
|
48
48
|
export { ToolActivityOccurrenceSchema, ToolActivityDetailSchema, ToolActivityGroupSchema, ToolActivityManifestSchema, ToolActivityOutboxEnvelopeSchema, type ToolActivityOccurrence, type ToolActivityDetail, type ToolActivityGroup, type ToolActivityManifest, type ToolActivityOutboxEnvelope, } from './tool-activity';
|
|
49
49
|
export { digestOf, stableStringify } from './tool-activity';
|