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.
- package/.codex/bin/dores-hook-dispatch +43 -0
- package/.codex/hooks.json +62 -0
- package/INSTALL.md +37 -0
- package/README.md +231 -0
- package/codex-plugin-setup +14 -0
- package/codex-plugin-uninstall +14 -0
- package/install.sh +5 -0
- package/lib/client-event.js +144 -0
- package/lib/dores-client.js +76 -0
- package/lib/hook-runner.js +101 -0
- package/lib/hook-shared.js +362 -0
- package/lib/post-tool-use-hook.js +27 -0
- package/lib/pre-tool-use-hook.js +23 -0
- package/lib/setup-hooks.js +193 -0
- package/lib/startup-hook.js +21 -0
- package/lib/stop-hook.js +38 -0
- package/lib/uninstall-hooks.js +88 -0
- package/lib/user-prompt-submit-hook.js +20 -0
- package/package.json +39 -0
- package/uninstall.sh +5 -0
|
@@ -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();
|