dores-codex 4.0.0

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.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { spawn } from "node:child_process";
8
+
9
+ const hookType = process.argv[2];
10
+ const projectPath = process.argv[3];
11
+
12
+ if (!hookType || !projectPath) {
13
+ console.error("Usage: hook-runner.js <hook-type> <project-path>");
14
+ process.exit(1);
15
+ }
16
+
17
+ function normalizePath(filePath) {
18
+ if (os.platform() !== "win32") {
19
+ return filePath;
20
+ }
21
+
22
+ let normalized = filePath.replace(/\//g, "\\");
23
+
24
+ if (normalized.match(/^\\[a-z]\\/) || normalized.match(/^\\[a-z]\\/i)) {
25
+ normalized =
26
+ normalized.charAt(1).toUpperCase() +
27
+ ":" +
28
+ normalized.substring(2).replace(/\//g, "\\");
29
+ }
30
+
31
+ if (!normalized.match(/^[A-Z]:\\/i)) {
32
+ normalized = path.resolve(normalized);
33
+ }
34
+
35
+ return normalized;
36
+ }
37
+
38
+ async function readStdin() {
39
+ if (process.stdin.isTTY) {
40
+ return "";
41
+ }
42
+
43
+ let input = "";
44
+ process.stdin.setEncoding("utf8");
45
+ for await (const chunk of process.stdin) {
46
+ input += chunk;
47
+ }
48
+ return input;
49
+ }
50
+
51
+ const hookMap = {
52
+ SessionStart: "startup-hook.js",
53
+ UserPromptSubmit: "user-prompt-submit-hook.js",
54
+ PreToolUse: "pre-tool-use-hook.js",
55
+ PostToolUse: "post-tool-use-hook.js",
56
+ Stop: "stop-hook.js"
57
+ };
58
+
59
+ const hookFile = hookMap[hookType];
60
+ if (!hookFile) {
61
+ console.error(`Unknown hook type: ${hookType}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ const normalizedProjectPath = normalizePath(projectPath);
66
+ const hookPath = path.join(normalizedProjectPath, "lib", hookFile);
67
+
68
+ if (process.env.DEBUG_HOOKS) {
69
+ console.log(`[DEBUG] Hook type: ${hookType}`);
70
+ console.log(`[DEBUG] Project path (raw): ${projectPath}`);
71
+ console.log(`[DEBUG] Project path (normalized): ${normalizedProjectPath}`);
72
+ console.log(`[DEBUG] Hook path: ${hookPath}`);
73
+ console.log(`[DEBUG] Platform: ${os.platform()}`);
74
+ }
75
+
76
+ if (!fs.existsSync(hookPath)) {
77
+ console.error(`Hook file not found: ${hookPath}`);
78
+ console.error(`Current directory: ${process.cwd()}`);
79
+ console.error(`Platform: ${os.platform()}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ const stdinPayload = await readStdin();
84
+
85
+ const child = spawn(process.execPath, [hookPath], {
86
+ stdio: ["pipe", "inherit", "inherit"],
87
+ env: { ...process.env, PROJECT_PATH: normalizedProjectPath },
88
+ cwd: normalizedProjectPath
89
+ });
90
+
91
+ child.on("error", (error) => {
92
+ console.error(`Failed to run hook: ${error.message}`);
93
+ process.exit(1);
94
+ });
95
+
96
+ child.stdin.on("error", () => {});
97
+ child.stdin.end(stdinPayload);
98
+
99
+ child.on("exit", (code) => {
100
+ process.exit(code ?? 0);
101
+ });
@@ -0,0 +1,362 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import {
7
+ DORES_CLIENT_UNAVAILABLE_MESSAGE,
8
+ formatDoresClientErrorMessage,
9
+ normalizeDoresClientConnectionError
10
+ } from "./dores-client.js";
11
+ import { buildClientEventMessage } from "./client-event.js";
12
+
13
+ const DEFAULT_WS_URL = "ws://127.0.0.1:8765";
14
+ const DEFAULT_EVENT_IDE = "codex";
15
+ const DEFAULT_CLOSE_DELAY_MS = 500;
16
+ const DEFAULT_TIMEOUT_MS = 1500;
17
+
18
+ function shouldWriteDirectHookLog() {
19
+ return (process.env.CODEX_PLUGIN_HOOK_DIRECT_LOG ?? "0").trim() === "1";
20
+ }
21
+
22
+ function resolveHookLogFile() {
23
+ const explicit = (process.env.CODEX_PLUGIN_HOOK_LOG_FILE ?? "").trim();
24
+ if (explicit) {
25
+ return explicit;
26
+ }
27
+
28
+ const projectPath = (process.env.PROJECT_PATH ?? "").trim();
29
+ if (!projectPath) {
30
+ return null;
31
+ }
32
+
33
+ return path.join(projectPath, "runtime", "codex-plugin.log");
34
+ }
35
+
36
+ export function logHook(label, message) {
37
+ const line = `[${label}] ${message}\n`;
38
+ process.stderr.write(line);
39
+
40
+ if (!shouldWriteDirectHookLog()) {
41
+ return;
42
+ }
43
+
44
+ const logFile = resolveHookLogFile();
45
+ if (!logFile) {
46
+ return;
47
+ }
48
+
49
+ try {
50
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
51
+ fs.appendFileSync(logFile, `${new Date().toISOString()} ${line}`, "utf8");
52
+ } catch {
53
+ // Keep hook execution non-blocking if direct log writes fail.
54
+ }
55
+ }
56
+
57
+ export async function readHookInput(label) {
58
+ if (process.stdin.isTTY) {
59
+ return null;
60
+ }
61
+
62
+ let inputData = "";
63
+ process.stdin.setEncoding("utf8");
64
+
65
+ for await (const chunk of process.stdin) {
66
+ inputData += chunk;
67
+ }
68
+
69
+ if (!inputData.trim()) {
70
+ return null;
71
+ }
72
+
73
+ try {
74
+ const parsed = JSON.parse(inputData);
75
+ logHook(label, `Received hook data: ${JSON.stringify(parsed)}`);
76
+ return parsed;
77
+ } catch (error) {
78
+ logHook(label, `Failed to parse input: ${error.message}`);
79
+ return {
80
+ raw_text: inputData
81
+ };
82
+ }
83
+ }
84
+
85
+ export function resolveHookWsUrl() {
86
+ const value = (
87
+ process.env.CODEX_PLUGIN_APP_WS_URL ??
88
+ process.env.CODEX_PLUGIN_LIVE2D_WS_URL ??
89
+ DEFAULT_WS_URL
90
+ ).trim();
91
+ return value || DEFAULT_WS_URL;
92
+ }
93
+
94
+ export function resolveHookEventIde() {
95
+ const value = (
96
+ process.env.CODEX_PLUGIN_HOOK_EVENT_IDE ??
97
+ DEFAULT_EVENT_IDE
98
+ ).trim().toLowerCase();
99
+ return value || DEFAULT_EVENT_IDE;
100
+ }
101
+
102
+ export function parseHookJson(value) {
103
+ if (value == null) {
104
+ return null;
105
+ }
106
+
107
+ if (typeof value !== "string") {
108
+ return value;
109
+ }
110
+
111
+ const trimmed = value.trim();
112
+ if (!trimmed) {
113
+ return null;
114
+ }
115
+
116
+ try {
117
+ return JSON.parse(trimmed);
118
+ } catch {
119
+ return value;
120
+ }
121
+ }
122
+
123
+ function readNestedValue(value, pathParts) {
124
+ let current = value;
125
+ for (const key of pathParts) {
126
+ if (!current || typeof current !== "object") {
127
+ return undefined;
128
+ }
129
+ current = current[key];
130
+ }
131
+ return current;
132
+ }
133
+
134
+ function coerceFiniteNumber(value) {
135
+ if (typeof value === "number" && Number.isFinite(value)) {
136
+ return value;
137
+ }
138
+
139
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
140
+ return Number.parseInt(value.trim(), 10);
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ export function resolveHookEventName(hookData = null, fallbackEventName = "") {
147
+ return String(
148
+ hookData?.hook_event_name ?? hookData?.event ?? fallbackEventName ?? ""
149
+ ).trim();
150
+ }
151
+
152
+ export function normalizeCommand(value) {
153
+ if (Array.isArray(value)) {
154
+ return value.map((part) => String(part)).join(" ").trim() || null;
155
+ }
156
+
157
+ if (typeof value === "string") {
158
+ return value.trim() || null;
159
+ }
160
+
161
+ return value == null ? null : String(value);
162
+ }
163
+
164
+ export function extractCommand(hookData = null) {
165
+ return normalizeCommand(hookData?.tool_input?.command ?? hookData?.command ?? null);
166
+ }
167
+
168
+ export function parseToolResponse(hookData = null) {
169
+ return parseHookJson(hookData?.tool_response ?? null);
170
+ }
171
+
172
+ export function extractExitCode(toolResponse) {
173
+ const parsed = parseHookJson(toolResponse);
174
+ const candidatePaths = [
175
+ ["exit_code"],
176
+ ["exitCode"],
177
+ ["code"],
178
+ ["status"],
179
+ ["metadata", "exit_code"],
180
+ ["metadata", "exitCode"],
181
+ ["metadata", "code"]
182
+ ];
183
+
184
+ for (const pathParts of candidatePaths) {
185
+ const candidate = coerceFiniteNumber(readNestedValue(parsed, pathParts));
186
+ if (candidate != null) {
187
+ return candidate;
188
+ }
189
+ }
190
+
191
+ if (typeof parsed?.ok === "boolean") {
192
+ return parsed.ok ? 0 : 1;
193
+ }
194
+
195
+ if (typeof parsed?.success === "boolean") {
196
+ return parsed.success ? 0 : 1;
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ export function summarizePostToolUse(hookData = null) {
203
+ const toolResponse = parseToolResponse(hookData);
204
+ const exitCode = extractExitCode(toolResponse);
205
+ const command = extractCommand(hookData);
206
+ const ok = exitCode == null ? null : exitCode === 0;
207
+
208
+ return {
209
+ command,
210
+ exitCode,
211
+ ok,
212
+ status:
213
+ ok === false ? "failed" : ok === true ? "succeeded" : "completed",
214
+ toolResponse
215
+ };
216
+ }
217
+
218
+ export function buildHookPayload(hookData = null, extraPayload = {}) {
219
+ const payload = {
220
+ cwd: hookData?.cwd ?? process.cwd(),
221
+ model: hookData?.model ?? null,
222
+ raw: hookData ?? null,
223
+ session_id: hookData?.session_id ?? null,
224
+ source: extraPayload.source ?? "codex-native-hook",
225
+ timestamp: extraPayload.timestamp ?? new Date().toISOString(),
226
+ transcript_path: hookData?.transcript_path ?? null,
227
+ turn_id: hookData?.turn_id ?? null,
228
+ ...extraPayload
229
+ };
230
+
231
+ const hookEventName = resolveHookEventName(
232
+ {
233
+ ...hookData,
234
+ ...payload
235
+ },
236
+ extraPayload.hook_event_name
237
+ );
238
+
239
+ return {
240
+ ...payload,
241
+ event: hookEventName || null,
242
+ hook_event_name: hookEventName || null
243
+ };
244
+ }
245
+
246
+ export function buildHookMessage(hookEventName, payload) {
247
+ return buildClientEventMessage(hookEventName, payload, {
248
+ eventIde: resolveHookEventIde()
249
+ });
250
+ }
251
+
252
+ async function sendHookSocketMessage(
253
+ label,
254
+ message,
255
+ { errorHint = null, errorExitCode = 0 } = {}
256
+ ) {
257
+ const wsUrl = resolveHookWsUrl();
258
+ const timeoutMs = Number.parseInt(
259
+ process.env.CODEX_PLUGIN_HOOK_WS_TIMEOUT ?? String(DEFAULT_TIMEOUT_MS),
260
+ 10
261
+ );
262
+
263
+ if (typeof WebSocket !== "function") {
264
+ logHook(label, "WebSocket runtime unavailable in this Node.js version.");
265
+ return errorExitCode;
266
+ }
267
+
268
+ return new Promise((resolve) => {
269
+ let settled = false;
270
+ let opened = false;
271
+ const socket = new WebSocket(wsUrl);
272
+
273
+ const reportUnavailable = (error) => {
274
+ if (settled) {
275
+ return;
276
+ }
277
+
278
+ const normalized = normalizeDoresClientConnectionError(error, {
279
+ url: wsUrl
280
+ });
281
+ logHook(
282
+ label,
283
+ formatDoresClientErrorMessage(normalized, {
284
+ fallbackMessage: DORES_CLIENT_UNAVAILABLE_MESSAGE
285
+ })
286
+ );
287
+ if (errorHint && normalized === error) {
288
+ logHook(label, errorHint);
289
+ }
290
+ finish(errorExitCode);
291
+ };
292
+
293
+ const finish = (code = 0) => {
294
+ if (settled) {
295
+ return;
296
+ }
297
+
298
+ settled = true;
299
+ clearTimeout(timeoutId);
300
+ resolve(code);
301
+ };
302
+
303
+ const timeoutId = setTimeout(() => {
304
+ try {
305
+ socket.close();
306
+ } catch {
307
+ // Ignore close races.
308
+ }
309
+ reportUnavailable(new Error(`Timed out while connecting to ${wsUrl}.`));
310
+ }, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS);
311
+
312
+ socket.addEventListener("open", () => {
313
+ opened = true;
314
+ try {
315
+ socket.send(JSON.stringify(message));
316
+ logHook(label, `Sent hook websocket message: ${JSON.stringify(message)}`);
317
+ } catch (error) {
318
+ logHook(label, `Failed to send WebSocket message: ${error.message}`);
319
+ finish(errorExitCode);
320
+ return;
321
+ }
322
+
323
+ setTimeout(() => {
324
+ try {
325
+ socket.close();
326
+ } catch {
327
+ // Ignore close races.
328
+ }
329
+ finish(0);
330
+ }, DEFAULT_CLOSE_DELAY_MS);
331
+ });
332
+
333
+ socket.addEventListener("error", (event) => {
334
+ reportUnavailable(
335
+ event.error ?? new Error(`Failed to connect to dores client WebSocket at ${wsUrl}.`)
336
+ );
337
+ });
338
+
339
+ socket.addEventListener("close", () => {
340
+ if (!opened) {
341
+ reportUnavailable(
342
+ new Error(`WebSocket socket closed before connecting to ${wsUrl}.`)
343
+ );
344
+ return;
345
+ }
346
+ finish(0);
347
+ });
348
+ });
349
+ }
350
+
351
+ export async function dispatchHookEvent(
352
+ label,
353
+ hookData = null,
354
+ { errorHint = null, errorExitCode = 0, extraPayload = {} } = {}
355
+ ) {
356
+ const payload = buildHookPayload(hookData, extraPayload);
357
+
358
+ return sendHookSocketMessage(label, buildHookMessage(payload.hook_event_name, payload), {
359
+ errorExitCode,
360
+ errorHint
361
+ });
362
+ }
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ buildHookPayload,
5
+ dispatchHookEvent,
6
+ readHookInput,
7
+ summarizePostToolUse
8
+ } from "./hook-shared.js";
9
+
10
+ const label = "post-tool-use-hook";
11
+ const hookData = await readHookInput(label);
12
+ const summary = summarizePostToolUse(hookData);
13
+ const payload = buildHookPayload(hookData, {
14
+ command: summary.command,
15
+ exit_code: summary.exitCode,
16
+ ok: summary.ok,
17
+ status: summary.status,
18
+ tool_name: hookData?.tool_name ?? null,
19
+ tool_response: summary.toolResponse,
20
+ tool_use_id: hookData?.tool_use_id ?? null
21
+ });
22
+
23
+ process.exit(
24
+ await dispatchHookEvent(label, hookData, {
25
+ extraPayload: payload
26
+ })
27
+ );
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ buildHookPayload,
5
+ dispatchHookEvent,
6
+ extractCommand,
7
+ readHookInput
8
+ } from "./hook-shared.js";
9
+
10
+ const label = "pre-tool-use-hook";
11
+ const hookData = await readHookInput(label);
12
+ const payload = buildHookPayload(hookData, {
13
+ command: extractCommand(hookData),
14
+ status: "in_progress",
15
+ tool_name: hookData?.tool_name ?? null,
16
+ tool_use_id: hookData?.tool_use_id ?? null
17
+ });
18
+
19
+ process.exit(
20
+ await dispatchHookEvent(label, hookData, {
21
+ extraPayload: payload
22
+ })
23
+ );
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const MANAGED_STATUS_PREFIX = "dores-codex:";
9
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+ const DEFAULT_CODEX_HOME = path.join(os.homedir(), ".codex");
11
+ const HOOK_EVENTS = [
12
+ ["SessionStart", "startup|resume"],
13
+ ["UserPromptSubmit", null],
14
+ ["PreToolUse", "Bash"],
15
+ ["PostToolUse", "Bash"],
16
+ ["Stop", null]
17
+ ];
18
+
19
+ function resolveCodexHome() {
20
+ const explicit = (process.env.CODEX_PLUGIN_CODEX_HOME ?? "").trim();
21
+ return path.resolve(explicit || DEFAULT_CODEX_HOME);
22
+ }
23
+
24
+ function resolveConfigFile(codexHome) {
25
+ const explicit = (process.env.CODEX_PLUGIN_CODEX_CONFIG_FILE ?? "").trim();
26
+ return path.resolve(explicit || path.join(codexHome, "config.toml"));
27
+ }
28
+
29
+ function resolveHooksFile(codexHome) {
30
+ return path.join(codexHome, "hooks.json");
31
+ }
32
+
33
+ function ensureParentDirectory(filePath) {
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ }
36
+
37
+ function readJsonFile(filePath) {
38
+ if (!fs.existsSync(filePath)) {
39
+ return null;
40
+ }
41
+
42
+ const text = fs.readFileSync(filePath, "utf8");
43
+ if (!text.trim()) {
44
+ return null;
45
+ }
46
+
47
+ return JSON.parse(text);
48
+ }
49
+
50
+ function writeJsonFile(filePath, value) {
51
+ ensureParentDirectory(filePath);
52
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
53
+ }
54
+
55
+ function buildHookCommand(eventName) {
56
+ const dispatchPath = path.join(PACKAGE_ROOT, ".codex", "bin", "dores-hook-dispatch");
57
+ return `bash -lc 'exec "$1" "$2" "$3"' _ ${JSON.stringify(dispatchPath)} ${JSON.stringify(eventName)} ${JSON.stringify(PACKAGE_ROOT)}`;
58
+ }
59
+
60
+ function buildManagedGroup(eventName, matcher) {
61
+ const group = {
62
+ hooks: [
63
+ {
64
+ type: "command",
65
+ command: buildHookCommand(eventName),
66
+ statusMessage: `${MANAGED_STATUS_PREFIX} ${eventName}`
67
+ }
68
+ ]
69
+ };
70
+
71
+ if (matcher) {
72
+ group.matcher = matcher;
73
+ }
74
+
75
+ return group;
76
+ }
77
+
78
+ function isManagedGroup(group) {
79
+ return Array.isArray(group?.hooks) && group.hooks.some((hook) => {
80
+ const statusMessage = typeof hook?.statusMessage === "string" ? hook.statusMessage : "";
81
+ return statusMessage.startsWith(MANAGED_STATUS_PREFIX);
82
+ });
83
+ }
84
+
85
+ function loadHooksDocument(hooksFile) {
86
+ const existing = readJsonFile(hooksFile);
87
+ if (existing == null) {
88
+ return { hooks: {} };
89
+ }
90
+
91
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
92
+ throw new Error(`${hooksFile} must contain a JSON object.`);
93
+ }
94
+
95
+ if (existing.hooks == null) {
96
+ existing.hooks = {};
97
+ }
98
+
99
+ if (!existing.hooks || typeof existing.hooks !== "object" || Array.isArray(existing.hooks)) {
100
+ throw new Error(`${hooksFile} must contain a top-level "hooks" object.`);
101
+ }
102
+
103
+ return existing;
104
+ }
105
+
106
+ function updateHooksDocument(hooksDoc) {
107
+ for (const [eventName, matcher] of HOOK_EVENTS) {
108
+ const currentGroups = Array.isArray(hooksDoc.hooks[eventName])
109
+ ? hooksDoc.hooks[eventName]
110
+ : [];
111
+ const preservedGroups = currentGroups.filter((group) => !isManagedGroup(group));
112
+ hooksDoc.hooks[eventName] = [...preservedGroups, buildManagedGroup(eventName, matcher)];
113
+ }
114
+
115
+ return hooksDoc;
116
+ }
117
+
118
+ function ensureCodexHooksEnabled(configFile) {
119
+ const original = fs.existsSync(configFile)
120
+ ? fs.readFileSync(configFile, "utf8")
121
+ : "";
122
+
123
+ const newline = original.includes("\r\n") ? "\r\n" : "\n";
124
+ const lines = original ? original.split(/\r?\n/) : [];
125
+
126
+ let sectionStart = -1;
127
+ let sectionEnd = lines.length;
128
+
129
+ for (let index = 0; index < lines.length; index += 1) {
130
+ if (/^\s*\[features\]\s*$/.test(lines[index])) {
131
+ sectionStart = index;
132
+ sectionEnd = lines.length;
133
+ for (let inner = index + 1; inner < lines.length; inner += 1) {
134
+ if (/^\s*\[[^\]]+\]\s*$/.test(lines[inner])) {
135
+ sectionEnd = inner;
136
+ break;
137
+ }
138
+ }
139
+ break;
140
+ }
141
+ }
142
+
143
+ if (sectionStart === -1) {
144
+ const nextContent = original.trim()
145
+ ? `${original.replace(/\s*$/, "")}${newline}${newline}[features]${newline}codex_hooks = true${newline}`
146
+ : `[features]${newline}codex_hooks = true${newline}`;
147
+ ensureParentDirectory(configFile);
148
+ fs.writeFileSync(configFile, nextContent, "utf8");
149
+ return true;
150
+ }
151
+
152
+ for (let index = sectionStart + 1; index < sectionEnd; index += 1) {
153
+ if (/^\s*codex_hooks\s*=/.test(lines[index])) {
154
+ if (/^\s*codex_hooks\s*=\s*true\s*$/.test(lines[index])) {
155
+ return false;
156
+ }
157
+ lines[index] = "codex_hooks = true";
158
+ ensureParentDirectory(configFile);
159
+ fs.writeFileSync(configFile, `${lines.join(newline)}${newline}`, "utf8");
160
+ return true;
161
+ }
162
+ }
163
+
164
+ lines.splice(sectionEnd, 0, "codex_hooks = true");
165
+ ensureParentDirectory(configFile);
166
+ fs.writeFileSync(configFile, `${lines.join(newline)}${newline}`, "utf8");
167
+ return true;
168
+ }
169
+
170
+ function main() {
171
+ const codexHome = resolveCodexHome();
172
+ const hooksFile = resolveHooksFile(codexHome);
173
+ const configFile = resolveConfigFile(codexHome);
174
+ const hooksDoc = updateHooksDocument(loadHooksDocument(hooksFile));
175
+ writeJsonFile(hooksFile, hooksDoc);
176
+ const configChanged = ensureCodexHooksEnabled(configFile);
177
+
178
+ console.log(`[codex-plugin-setup] package root: ${PACKAGE_ROOT}`);
179
+ console.log(`[codex-plugin-setup] hooks file: ${hooksFile}`);
180
+ console.log(`[codex-plugin-setup] config file: ${configFile}`);
181
+ console.log(
182
+ `[codex-plugin-setup] codex_hooks feature: ${configChanged ? "updated" : "already enabled"}`
183
+ );
184
+ console.log("[codex-plugin-setup] installed native Codex hooks into the global Codex config.");
185
+ console.log(
186
+ "[codex-plugin-setup] default websocket: ws://127.0.0.1:8765"
187
+ );
188
+ console.log(
189
+ "[codex-plugin-setup] run native `codex` in any directory to trigger these global hooks."
190
+ );
191
+ }
192
+
193
+ main();