caveat-cli 0.13.0 → 0.14.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/dist/{chunk-WTHGWCPM.js → chunk-CSXD73IX.js} +180 -8
- package/dist/chunk-CSXD73IX.js.map +1 -0
- package/dist/index.js +593 -18
- package/dist/index.js.map +1 -1
- package/dist/{server-IWDZZESV.js → server-BP54BTEL.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-WTHGWCPM.js.map +0 -1
- /package/dist/{server-IWDZZESV.js.map → server-BP54BTEL.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
loadConfig,
|
|
28
28
|
markHit,
|
|
29
29
|
openDb,
|
|
30
|
+
readCodexSessionSignals,
|
|
30
31
|
readSessionSignals,
|
|
31
32
|
rebuildAll,
|
|
32
33
|
recordEntry,
|
|
@@ -39,7 +40,7 @@ import {
|
|
|
39
40
|
toolErrorReminderText,
|
|
40
41
|
updateEntry,
|
|
41
42
|
userPromptSubmitReminderText
|
|
42
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-CSXD73IX.js";
|
|
43
44
|
|
|
44
45
|
// ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
|
|
45
46
|
var require_code = __commonJS({
|
|
@@ -7312,7 +7313,7 @@ function runStats(ctx) {
|
|
|
7312
7313
|
|
|
7313
7314
|
// src/commands/serve.ts
|
|
7314
7315
|
async function runServe(opts) {
|
|
7315
|
-
const { startServer } = await import("./server-
|
|
7316
|
+
const { startServer } = await import("./server-BP54BTEL.js");
|
|
7316
7317
|
const { port, host } = startServer({ port: opts.port });
|
|
7317
7318
|
process.stdout.write(`[caveat] web portal: http://${host}:${port}/
|
|
7318
7319
|
`);
|
|
@@ -22183,11 +22184,529 @@ async function runHook(name, arg) {
|
|
|
22183
22184
|
process.exit(0);
|
|
22184
22185
|
}
|
|
22185
22186
|
|
|
22187
|
+
// src/commands/codexHookCmd.ts
|
|
22188
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
22189
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
22190
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
|
|
22191
|
+
import { join as join10 } from "node:path";
|
|
22192
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
22193
|
+
|
|
22194
|
+
// src/codexHookInstall.ts
|
|
22195
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
22196
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
22197
|
+
function quote2(p) {
|
|
22198
|
+
return p.includes(" ") ? `"${p}"` : p;
|
|
22199
|
+
}
|
|
22200
|
+
function hookCommand2(nodePath, cliScriptPath, event) {
|
|
22201
|
+
return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
|
|
22202
|
+
}
|
|
22203
|
+
function eventCommandFragment(event) {
|
|
22204
|
+
return `codex-hook ${event}`;
|
|
22205
|
+
}
|
|
22206
|
+
function isSameHookCommand2(actual, expected) {
|
|
22207
|
+
return actual === expected || actual.endsWith(` ${expected}`);
|
|
22208
|
+
}
|
|
22209
|
+
function hasCaveatHook(hooksJson, event, fragment) {
|
|
22210
|
+
return hooksJson.hooks?.[event]?.some(
|
|
22211
|
+
(entry) => entry.hooks?.some((h) => h.command.includes(fragment))
|
|
22212
|
+
) ?? false;
|
|
22213
|
+
}
|
|
22214
|
+
function readHooks(path) {
|
|
22215
|
+
if (!existsSync6(path)) return {};
|
|
22216
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
22217
|
+
}
|
|
22218
|
+
function writeJsonWithBackup(path, value) {
|
|
22219
|
+
const dir = dirname5(path);
|
|
22220
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
22221
|
+
let backupPath = "";
|
|
22222
|
+
if (existsSync6(path)) {
|
|
22223
|
+
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
22224
|
+
copyFileSync2(path, backupPath);
|
|
22225
|
+
}
|
|
22226
|
+
writeFileSync4(path, `${JSON.stringify(value, null, 2)}
|
|
22227
|
+
`, "utf-8");
|
|
22228
|
+
return backupPath;
|
|
22229
|
+
}
|
|
22230
|
+
function upsertHook2(hooksJson, event, command) {
|
|
22231
|
+
hooksJson.hooks ??= {};
|
|
22232
|
+
const list = hooksJson.hooks[event] ??= [];
|
|
22233
|
+
const alreadyPresent = list.some(
|
|
22234
|
+
(entry) => entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
|
|
22235
|
+
);
|
|
22236
|
+
if (alreadyPresent) return "unchanged";
|
|
22237
|
+
list.push({
|
|
22238
|
+
hooks: [
|
|
22239
|
+
{
|
|
22240
|
+
type: "command",
|
|
22241
|
+
command,
|
|
22242
|
+
timeoutSec: 5,
|
|
22243
|
+
async: false,
|
|
22244
|
+
statusMessage: null
|
|
22245
|
+
}
|
|
22246
|
+
]
|
|
22247
|
+
});
|
|
22248
|
+
return "added";
|
|
22249
|
+
}
|
|
22250
|
+
function removeHook2(hooksJson, event, command) {
|
|
22251
|
+
const list = hooksJson.hooks?.[event];
|
|
22252
|
+
if (!list) return false;
|
|
22253
|
+
const before = list.length;
|
|
22254
|
+
const filtered = list.filter(
|
|
22255
|
+
(entry) => !entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
|
|
22256
|
+
);
|
|
22257
|
+
if (filtered.length === before) return false;
|
|
22258
|
+
hooksJson.hooks[event] = filtered;
|
|
22259
|
+
return true;
|
|
22260
|
+
}
|
|
22261
|
+
function enableCodexHooksFeature(raw) {
|
|
22262
|
+
if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(raw)) return { text: raw, changed: false };
|
|
22263
|
+
const lines = raw.split(/\r?\n/);
|
|
22264
|
+
const featuresStart = lines.findIndex((line) => /^\s*\[features]\s*$/.test(line));
|
|
22265
|
+
if (featuresStart === -1) {
|
|
22266
|
+
const prefix = raw.trimEnd();
|
|
22267
|
+
const text = `${prefix}${prefix ? "\n\n" : ""}[features]
|
|
22268
|
+
codex_hooks = true
|
|
22269
|
+
`;
|
|
22270
|
+
return { text, changed: true };
|
|
22271
|
+
}
|
|
22272
|
+
let insertAt = featuresStart + 1;
|
|
22273
|
+
for (let i = featuresStart + 1; i < lines.length; i += 1) {
|
|
22274
|
+
if (/^\s*\[.+]\s*$/.test(lines[i])) {
|
|
22275
|
+
break;
|
|
22276
|
+
}
|
|
22277
|
+
if (/^\s*codex_hooks\s*=/.test(lines[i])) {
|
|
22278
|
+
lines[i] = "codex_hooks = true";
|
|
22279
|
+
return { text: `${lines.join("\n").trimEnd()}
|
|
22280
|
+
`, changed: true };
|
|
22281
|
+
}
|
|
22282
|
+
}
|
|
22283
|
+
lines.splice(insertAt, 0, "codex_hooks = true");
|
|
22284
|
+
return { text: `${lines.join("\n").trimEnd()}
|
|
22285
|
+
`, changed: true };
|
|
22286
|
+
}
|
|
22287
|
+
function writeConfigWithBackup(path, text) {
|
|
22288
|
+
const dir = dirname5(path);
|
|
22289
|
+
if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
|
|
22290
|
+
let backupPath = "";
|
|
22291
|
+
if (existsSync6(path)) {
|
|
22292
|
+
backupPath = `${path}.caveat-backup-${Date.now()}`;
|
|
22293
|
+
copyFileSync2(path, backupPath);
|
|
22294
|
+
}
|
|
22295
|
+
writeFileSync4(path, text, "utf-8");
|
|
22296
|
+
return backupPath;
|
|
22297
|
+
}
|
|
22298
|
+
function installCodexHooks(opts) {
|
|
22299
|
+
const hooksPath = join9(opts.codexHome, "hooks.json");
|
|
22300
|
+
const configPath = join9(opts.codexHome, "config.toml");
|
|
22301
|
+
const hooksJson = readHooks(hooksPath);
|
|
22302
|
+
const userPromptSubmit = upsertHook2(
|
|
22303
|
+
hooksJson,
|
|
22304
|
+
"UserPromptSubmit",
|
|
22305
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
|
|
22306
|
+
);
|
|
22307
|
+
const postToolUse = upsertHook2(
|
|
22308
|
+
hooksJson,
|
|
22309
|
+
"PostToolUse",
|
|
22310
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
|
|
22311
|
+
);
|
|
22312
|
+
const stop = upsertHook2(
|
|
22313
|
+
hooksJson,
|
|
22314
|
+
"Stop",
|
|
22315
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
|
|
22316
|
+
);
|
|
22317
|
+
const rawConfig = existsSync6(configPath) ? readFileSync4(configPath, "utf-8") : "";
|
|
22318
|
+
const enabled = enableCodexHooksFeature(rawConfig);
|
|
22319
|
+
const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
|
|
22320
|
+
let backupPath;
|
|
22321
|
+
let configBackupPath;
|
|
22322
|
+
if (opts.dryRun) {
|
|
22323
|
+
opts.logger.info(`[dry-run] would update ${hooksPath}`);
|
|
22324
|
+
opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
|
|
22325
|
+
} else {
|
|
22326
|
+
if (anyHookAdded) {
|
|
22327
|
+
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
22328
|
+
if (backup) backupPath = backup;
|
|
22329
|
+
}
|
|
22330
|
+
if (enabled.changed) {
|
|
22331
|
+
const backup = writeConfigWithBackup(configPath, enabled.text);
|
|
22332
|
+
if (backup) configBackupPath = backup;
|
|
22333
|
+
}
|
|
22334
|
+
}
|
|
22335
|
+
return {
|
|
22336
|
+
hooks: { userPromptSubmit, postToolUse, stop },
|
|
22337
|
+
feature: enabled.changed ? "enabled" : "unchanged",
|
|
22338
|
+
backupPath,
|
|
22339
|
+
configBackupPath
|
|
22340
|
+
};
|
|
22341
|
+
}
|
|
22342
|
+
function uninstallCodexHooks(opts) {
|
|
22343
|
+
const hooksPath = join9(opts.codexHome, "hooks.json");
|
|
22344
|
+
const hooksJson = readHooks(hooksPath);
|
|
22345
|
+
const userPromptSubmitRemoved = removeHook2(
|
|
22346
|
+
hooksJson,
|
|
22347
|
+
"UserPromptSubmit",
|
|
22348
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
|
|
22349
|
+
);
|
|
22350
|
+
const postToolUseRemoved = removeHook2(
|
|
22351
|
+
hooksJson,
|
|
22352
|
+
"PostToolUse",
|
|
22353
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
|
|
22354
|
+
);
|
|
22355
|
+
const stopRemoved = removeHook2(
|
|
22356
|
+
hooksJson,
|
|
22357
|
+
"Stop",
|
|
22358
|
+
hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
|
|
22359
|
+
);
|
|
22360
|
+
let backupPath;
|
|
22361
|
+
if (opts.dryRun) {
|
|
22362
|
+
opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
|
|
22363
|
+
} else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
|
|
22364
|
+
const backup = writeJsonWithBackup(hooksPath, hooksJson);
|
|
22365
|
+
if (backup) backupPath = backup;
|
|
22366
|
+
}
|
|
22367
|
+
return {
|
|
22368
|
+
hooks: {
|
|
22369
|
+
userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
|
|
22370
|
+
postToolUse: postToolUseRemoved ? "added" : "unchanged",
|
|
22371
|
+
stop: stopRemoved ? "added" : "unchanged"
|
|
22372
|
+
},
|
|
22373
|
+
feature: "unchanged",
|
|
22374
|
+
backupPath
|
|
22375
|
+
};
|
|
22376
|
+
}
|
|
22377
|
+
function detectCodexHookInstallation(codexHome) {
|
|
22378
|
+
const hooksPath = join9(codexHome, "hooks.json");
|
|
22379
|
+
const hooksJson = readHooks(hooksPath);
|
|
22380
|
+
const hooks = {
|
|
22381
|
+
userPromptSubmit: hasCaveatHook(
|
|
22382
|
+
hooksJson,
|
|
22383
|
+
"UserPromptSubmit",
|
|
22384
|
+
eventCommandFragment("user-prompt-submit")
|
|
22385
|
+
),
|
|
22386
|
+
postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
|
|
22387
|
+
stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
|
|
22388
|
+
};
|
|
22389
|
+
const count = Object.values(hooks).filter(Boolean).length;
|
|
22390
|
+
return {
|
|
22391
|
+
installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
|
|
22392
|
+
hooksPath,
|
|
22393
|
+
hooks
|
|
22394
|
+
};
|
|
22395
|
+
}
|
|
22396
|
+
|
|
22397
|
+
// src/commands/codexHookCmd.ts
|
|
22398
|
+
var silentLogger2 = {
|
|
22399
|
+
info: () => {
|
|
22400
|
+
},
|
|
22401
|
+
warn: () => {
|
|
22402
|
+
},
|
|
22403
|
+
error: (m) => process.stderr.write(`[caveat:codex-hook] ${m}
|
|
22404
|
+
`)
|
|
22405
|
+
};
|
|
22406
|
+
async function readStdin2() {
|
|
22407
|
+
const chunks = [];
|
|
22408
|
+
for await (const chunk of process.stdin) {
|
|
22409
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
22410
|
+
}
|
|
22411
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
22412
|
+
}
|
|
22413
|
+
function parsePayload2(raw) {
|
|
22414
|
+
if (!raw) return {};
|
|
22415
|
+
try {
|
|
22416
|
+
return JSON.parse(raw);
|
|
22417
|
+
} catch (err) {
|
|
22418
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22419
|
+
process.stderr.write(`[caveat:codex-hook] json parse error: ${msg}
|
|
22420
|
+
`);
|
|
22421
|
+
return {};
|
|
22422
|
+
}
|
|
22423
|
+
}
|
|
22424
|
+
function buildContextSafely2() {
|
|
22425
|
+
try {
|
|
22426
|
+
return buildContext(silentLogger2);
|
|
22427
|
+
} catch (err) {
|
|
22428
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22429
|
+
process.stderr.write(`[caveat:codex-hook] context error: ${msg}
|
|
22430
|
+
`);
|
|
22431
|
+
return null;
|
|
22432
|
+
}
|
|
22433
|
+
}
|
|
22434
|
+
function searchCaveatsFromTextSafely2(text) {
|
|
22435
|
+
if (!text) return [];
|
|
22436
|
+
let db;
|
|
22437
|
+
try {
|
|
22438
|
+
const ctx = buildContextSafely2();
|
|
22439
|
+
if (!ctx || !existsSync7(ctx.paths.dbPath)) return [];
|
|
22440
|
+
db = openDb({ path: ctx.paths.dbPath });
|
|
22441
|
+
const hits = findCaveatsForPrompt(db, text, {
|
|
22442
|
+
selfIdentity: defaultSelfIdentityTokens()
|
|
22443
|
+
});
|
|
22444
|
+
if (hits.length > 0) {
|
|
22445
|
+
try {
|
|
22446
|
+
markHit(db, hits);
|
|
22447
|
+
} catch (err) {
|
|
22448
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22449
|
+
process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
|
|
22450
|
+
`);
|
|
22451
|
+
}
|
|
22452
|
+
}
|
|
22453
|
+
return hits;
|
|
22454
|
+
} catch (err) {
|
|
22455
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22456
|
+
process.stderr.write(`[caveat:codex-hook] search error: ${msg}
|
|
22457
|
+
`);
|
|
22458
|
+
return [];
|
|
22459
|
+
} finally {
|
|
22460
|
+
db?.close();
|
|
22461
|
+
}
|
|
22462
|
+
}
|
|
22463
|
+
function loadSignalsSafely2(path) {
|
|
22464
|
+
try {
|
|
22465
|
+
return readCodexSessionSignals(path);
|
|
22466
|
+
} catch (err) {
|
|
22467
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22468
|
+
process.stderr.write(`[caveat:codex-hook] transcript read error: ${msg}
|
|
22469
|
+
`);
|
|
22470
|
+
return null;
|
|
22471
|
+
}
|
|
22472
|
+
}
|
|
22473
|
+
function codexSessionId(payload) {
|
|
22474
|
+
const v = payload.session_id ?? payload.sessionId;
|
|
22475
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
22476
|
+
}
|
|
22477
|
+
function extractToolResponseText2(response) {
|
|
22478
|
+
if (typeof response === "string") return response;
|
|
22479
|
+
if (Array.isArray(response)) {
|
|
22480
|
+
return response.map((item) => {
|
|
22481
|
+
if (typeof item === "string") return item;
|
|
22482
|
+
if (item !== null && typeof item === "object") {
|
|
22483
|
+
const text = item.text;
|
|
22484
|
+
return typeof text === "string" ? text : "";
|
|
22485
|
+
}
|
|
22486
|
+
return "";
|
|
22487
|
+
}).filter(Boolean).join(" ");
|
|
22488
|
+
}
|
|
22489
|
+
if (response !== null && typeof response === "object") {
|
|
22490
|
+
const r = response;
|
|
22491
|
+
if (typeof r.content === "string") return r.content;
|
|
22492
|
+
if (Array.isArray(r.content)) return extractToolResponseText2(r.content);
|
|
22493
|
+
if (typeof r.output === "string") return r.output;
|
|
22494
|
+
if (typeof r.stdout === "string" || typeof r.stderr === "string") {
|
|
22495
|
+
return [r.stdout, r.stderr].filter((x) => typeof x === "string").join(" ");
|
|
22496
|
+
}
|
|
22497
|
+
}
|
|
22498
|
+
return "";
|
|
22499
|
+
}
|
|
22500
|
+
function numericExitCode(v) {
|
|
22501
|
+
return typeof v === "number" && Number.isInteger(v) ? v : null;
|
|
22502
|
+
}
|
|
22503
|
+
function transcriptExitCode(payload) {
|
|
22504
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
22505
|
+
const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
|
|
22506
|
+
if (!transcriptPath || !toolUseId || !existsSync7(transcriptPath)) return null;
|
|
22507
|
+
let raw = "";
|
|
22508
|
+
try {
|
|
22509
|
+
raw = readFileSync5(transcriptPath, "utf-8");
|
|
22510
|
+
} catch {
|
|
22511
|
+
return null;
|
|
22512
|
+
}
|
|
22513
|
+
for (const line of raw.split("\n")) {
|
|
22514
|
+
if (!line.includes(toolUseId)) continue;
|
|
22515
|
+
let parsed;
|
|
22516
|
+
try {
|
|
22517
|
+
parsed = JSON.parse(line);
|
|
22518
|
+
} catch {
|
|
22519
|
+
continue;
|
|
22520
|
+
}
|
|
22521
|
+
if (parsed === null || typeof parsed !== "object") continue;
|
|
22522
|
+
const payloadObj = parsed.payload;
|
|
22523
|
+
if (payloadObj === null || typeof payloadObj !== "object") continue;
|
|
22524
|
+
const p = payloadObj;
|
|
22525
|
+
if (p.type !== "function_call_output" || p.call_id !== toolUseId) continue;
|
|
22526
|
+
const output = typeof p.output === "string" ? p.output : "";
|
|
22527
|
+
const m = /Process exited with code\s+(-?\d+)/.exec(output);
|
|
22528
|
+
if (m) return Number(m[1]);
|
|
22529
|
+
}
|
|
22530
|
+
return null;
|
|
22531
|
+
}
|
|
22532
|
+
function isCodexToolError(payload) {
|
|
22533
|
+
if (payload.is_error === true) return true;
|
|
22534
|
+
const topExit = numericExitCode(payload.exit_code ?? payload.exitCode);
|
|
22535
|
+
if (topExit !== null) return topExit !== 0;
|
|
22536
|
+
const resp = payload.tool_response ?? payload.toolResponse;
|
|
22537
|
+
if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
|
|
22538
|
+
const r = resp;
|
|
22539
|
+
if (r.is_error === true) return true;
|
|
22540
|
+
const exit2 = numericExitCode(r.exit_code ?? r.exitCode);
|
|
22541
|
+
if (exit2 !== null) return exit2 !== 0;
|
|
22542
|
+
}
|
|
22543
|
+
const transcriptExit = transcriptExitCode(payload);
|
|
22544
|
+
if (transcriptExit !== null) return transcriptExit !== 0;
|
|
22545
|
+
return false;
|
|
22546
|
+
}
|
|
22547
|
+
function codexContextOutput(text, eventName = "UserPromptSubmit") {
|
|
22548
|
+
return JSON.stringify({
|
|
22549
|
+
hookSpecificOutput: {
|
|
22550
|
+
hookEventName: eventName,
|
|
22551
|
+
additionalContext: text
|
|
22552
|
+
}
|
|
22553
|
+
});
|
|
22554
|
+
}
|
|
22555
|
+
function codexStopOutput(text) {
|
|
22556
|
+
return JSON.stringify({
|
|
22557
|
+
decision: "block",
|
|
22558
|
+
reason: text
|
|
22559
|
+
});
|
|
22560
|
+
}
|
|
22561
|
+
function drainForSession2(sessionId, eventName = "UserPromptSubmit") {
|
|
22562
|
+
const ctx = buildContextSafely2();
|
|
22563
|
+
if (!ctx) return;
|
|
22564
|
+
const reminders = drainPendingReminders(ctx.caveatHome, sessionId);
|
|
22565
|
+
for (const text of reminders) {
|
|
22566
|
+
process.stdout.write(`${codexContextOutput(text, eventName)}
|
|
22567
|
+
`);
|
|
22568
|
+
}
|
|
22569
|
+
}
|
|
22570
|
+
function spawnCodexWorker(job) {
|
|
22571
|
+
const workFile = join10(
|
|
22572
|
+
tmpdir2(),
|
|
22573
|
+
`caveat-codex-worker-${Date.now()}-${randomBytes2(4).toString("hex")}.json`
|
|
22574
|
+
);
|
|
22575
|
+
try {
|
|
22576
|
+
writeFileSync5(workFile, JSON.stringify(job), "utf-8");
|
|
22577
|
+
} catch (err) {
|
|
22578
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22579
|
+
process.stderr.write(`[caveat:codex-hook] worker writefile error: ${msg}
|
|
22580
|
+
`);
|
|
22581
|
+
return;
|
|
22582
|
+
}
|
|
22583
|
+
const cliScript = process.argv[1];
|
|
22584
|
+
if (!cliScript) return;
|
|
22585
|
+
try {
|
|
22586
|
+
const child = spawn2(
|
|
22587
|
+
process.execPath,
|
|
22588
|
+
["--disable-warning=ExperimentalWarning", cliScript, "codex-hook", "worker", workFile],
|
|
22589
|
+
{ detached: true, stdio: "ignore", windowsHide: true }
|
|
22590
|
+
);
|
|
22591
|
+
child.unref();
|
|
22592
|
+
} catch (err) {
|
|
22593
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22594
|
+
process.stderr.write(`[caveat:codex-hook] worker spawn error: ${msg}
|
|
22595
|
+
`);
|
|
22596
|
+
try {
|
|
22597
|
+
unlinkSync2(workFile);
|
|
22598
|
+
} catch {
|
|
22599
|
+
}
|
|
22600
|
+
}
|
|
22601
|
+
}
|
|
22602
|
+
async function runCodexWorker(workFile) {
|
|
22603
|
+
let raw;
|
|
22604
|
+
try {
|
|
22605
|
+
raw = readFileSync5(workFile, "utf-8");
|
|
22606
|
+
} catch {
|
|
22607
|
+
process.exit(0);
|
|
22608
|
+
}
|
|
22609
|
+
try {
|
|
22610
|
+
unlinkSync2(workFile);
|
|
22611
|
+
} catch {
|
|
22612
|
+
}
|
|
22613
|
+
let job;
|
|
22614
|
+
try {
|
|
22615
|
+
job = JSON.parse(raw);
|
|
22616
|
+
} catch {
|
|
22617
|
+
process.exit(0);
|
|
22618
|
+
}
|
|
22619
|
+
if (!job.searchText || !job.sessionId) process.exit(0);
|
|
22620
|
+
const hits = searchCaveatsFromTextSafely2(job.searchText);
|
|
22621
|
+
if (hits.length === 0) process.exit(0);
|
|
22622
|
+
const ctx = buildContextSafely2();
|
|
22623
|
+
if (!ctx) process.exit(0);
|
|
22624
|
+
try {
|
|
22625
|
+
appendPendingReminder(ctx.caveatHome, job.sessionId, toolErrorReminderText(hits));
|
|
22626
|
+
} catch {
|
|
22627
|
+
}
|
|
22628
|
+
process.exit(0);
|
|
22629
|
+
}
|
|
22630
|
+
function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join10(homedir3(), ".codex")) {
|
|
22631
|
+
const features = spawnSync3("codex", ["features", "list"], {
|
|
22632
|
+
encoding: "utf-8",
|
|
22633
|
+
maxBuffer: 1024 * 1024
|
|
22634
|
+
});
|
|
22635
|
+
const featureOutput = [features.stdout, features.stderr].filter(Boolean).join("\n");
|
|
22636
|
+
const hasHooks = /^codex_hooks\s+\S+\s+true\b/m.test(featureOutput);
|
|
22637
|
+
const installation = detectCodexHookInstallation(codexHome);
|
|
22638
|
+
const result = {
|
|
22639
|
+
availability: features.error || features.status !== 0 || !hasHooks ? "unavailable" : "available",
|
|
22640
|
+
codexBinary: features.error ? "missing" : "present",
|
|
22641
|
+
codexHooksFeature: hasHooks ? "enabled" : "not-enabled",
|
|
22642
|
+
installation: installation.installation,
|
|
22643
|
+
codexHome,
|
|
22644
|
+
hooksPath: installation.hooksPath,
|
|
22645
|
+
installedHooks: installation.hooks,
|
|
22646
|
+
evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("codex_hooks")) ?? null
|
|
22647
|
+
};
|
|
22648
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
22649
|
+
`);
|
|
22650
|
+
}
|
|
22651
|
+
async function runCodexHook(name, arg) {
|
|
22652
|
+
if (name === "diagnostics") {
|
|
22653
|
+
runDiagnostics(arg);
|
|
22654
|
+
return;
|
|
22655
|
+
}
|
|
22656
|
+
if (name === "worker") {
|
|
22657
|
+
if (!arg) process.exit(0);
|
|
22658
|
+
await runCodexWorker(arg);
|
|
22659
|
+
return;
|
|
22660
|
+
}
|
|
22661
|
+
let raw = "";
|
|
22662
|
+
try {
|
|
22663
|
+
raw = await readStdin2();
|
|
22664
|
+
} catch (err) {
|
|
22665
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22666
|
+
process.stderr.write(`[caveat:codex-hook] stdin read error: ${msg}
|
|
22667
|
+
`);
|
|
22668
|
+
process.exit(0);
|
|
22669
|
+
}
|
|
22670
|
+
const payload = parsePayload2(raw);
|
|
22671
|
+
const sessionId = codexSessionId(payload);
|
|
22672
|
+
if (sessionId) drainForSession2(sessionId);
|
|
22673
|
+
else process.stderr.write("[caveat:codex-hook] missing session_id; pending drain disabled\n");
|
|
22674
|
+
if (name === "user-prompt-submit") {
|
|
22675
|
+
const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
|
|
22676
|
+
const hits = searchCaveatsFromTextSafely2(prompt);
|
|
22677
|
+
if (hits.length > 0) {
|
|
22678
|
+
process.stdout.write(`${codexContextOutput(userPromptSubmitReminderText(hits))}
|
|
22679
|
+
`);
|
|
22680
|
+
}
|
|
22681
|
+
process.exit(0);
|
|
22682
|
+
}
|
|
22683
|
+
if (name === "post-tool-use") {
|
|
22684
|
+
if (!sessionId) process.exit(0);
|
|
22685
|
+
if (!isCodexToolError(payload)) process.exit(0);
|
|
22686
|
+
const errText = extractToolResponseText2(payload.tool_response ?? payload.toolResponse ?? payload);
|
|
22687
|
+
if (errText) spawnCodexWorker({ sessionId, searchText: errText });
|
|
22688
|
+
process.exit(0);
|
|
22689
|
+
}
|
|
22690
|
+
if (name === "stop") {
|
|
22691
|
+
if (payload.stop_hook_active === true) process.exit(0);
|
|
22692
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
22693
|
+
const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
|
|
22694
|
+
if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
|
|
22695
|
+
const related = searchCaveatsFromTextSafely2(struggleSearchText(signals));
|
|
22696
|
+
process.stdout.write(`${codexStopOutput(stopReminderText(signals, related))}
|
|
22697
|
+
`);
|
|
22698
|
+
process.exit(0);
|
|
22699
|
+
}
|
|
22700
|
+
process.stderr.write(`[caveat:codex-hook] unknown hook name: ${name}
|
|
22701
|
+
`);
|
|
22702
|
+
process.exit(0);
|
|
22703
|
+
}
|
|
22704
|
+
|
|
22186
22705
|
// src/commands/pull.ts
|
|
22187
|
-
import { existsSync as
|
|
22188
|
-
import { join as
|
|
22706
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4 } from "node:fs";
|
|
22707
|
+
import { join as join11 } from "node:path";
|
|
22189
22708
|
async function runPull(ctx) {
|
|
22190
|
-
if (!
|
|
22709
|
+
if (!existsSync8(ctx.paths.communityDir)) {
|
|
22191
22710
|
ctx.logger.info(
|
|
22192
22711
|
"no community repos yet \u2014 add one with `caveat community add <github-url>`."
|
|
22193
22712
|
);
|
|
@@ -22207,15 +22726,15 @@ async function runPull(ctx) {
|
|
|
22207
22726
|
const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
|
|
22208
22727
|
try {
|
|
22209
22728
|
rebuildAll(db);
|
|
22210
|
-
if (
|
|
22729
|
+
if (existsSync8(ctx.paths.entriesDir)) {
|
|
22211
22730
|
const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
|
|
22212
22731
|
ctx.logger.info(`own: +${own.added}`);
|
|
22213
22732
|
}
|
|
22214
22733
|
for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
|
|
22215
22734
|
if (!entry.isDirectory()) continue;
|
|
22216
22735
|
const source = `community/${entry.name}`;
|
|
22217
|
-
const root =
|
|
22218
|
-
if (!
|
|
22736
|
+
const root = join11(ctx.paths.communityDir, entry.name, "entries");
|
|
22737
|
+
if (!existsSync8(root)) continue;
|
|
22219
22738
|
const scan = scanSource({ db, source, entriesRoot: root });
|
|
22220
22739
|
ctx.logger.info(`${source}: +${scan.added}`);
|
|
22221
22740
|
}
|
|
@@ -22225,10 +22744,10 @@ async function runPull(ctx) {
|
|
|
22225
22744
|
}
|
|
22226
22745
|
|
|
22227
22746
|
// src/commands/codexSidecar.ts
|
|
22228
|
-
import { spawnSync as
|
|
22229
|
-
import { mkdirSync as
|
|
22230
|
-
import { tmpdir as
|
|
22231
|
-
import { dirname as
|
|
22747
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
22748
|
+
import { mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
22749
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
22750
|
+
import { dirname as dirname6, join as join12 } from "node:path";
|
|
22232
22751
|
import { cwd, exit } from "node:process";
|
|
22233
22752
|
function runCodexSidecarDiagnostics(logger, opts) {
|
|
22234
22753
|
const plan = buildCodexSidecarDiagnosticsCommand({
|
|
@@ -22260,8 +22779,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
|
|
|
22260
22779
|
process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
|
|
22261
22780
|
exit(0);
|
|
22262
22781
|
}
|
|
22263
|
-
const contextDir = mkdtempSync2(
|
|
22264
|
-
const contextFile =
|
|
22782
|
+
const contextDir = mkdtempSync2(join12(tmpdir3(), "caveat-sidecar-context-"));
|
|
22783
|
+
const contextFile = join12(contextDir, "context.json");
|
|
22265
22784
|
let status = 1;
|
|
22266
22785
|
try {
|
|
22267
22786
|
const blocks = collectCaveatContextBlocks(ctx, {
|
|
@@ -22270,7 +22789,7 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
|
|
|
22270
22789
|
source: opts.source,
|
|
22271
22790
|
visibility: opts.visibility
|
|
22272
22791
|
});
|
|
22273
|
-
|
|
22792
|
+
writeFileSync6(contextFile, JSON.stringify({ context: blocks }, null, 2) + "\n", "utf-8");
|
|
22274
22793
|
const plan = buildCodexSidecarRunCommand({
|
|
22275
22794
|
workflow,
|
|
22276
22795
|
projectRoot: opts.project ?? cwd(),
|
|
@@ -22325,7 +22844,7 @@ function defaultPreset(workflow) {
|
|
|
22325
22844
|
}
|
|
22326
22845
|
function executePlan(logger, command, args, options = {}) {
|
|
22327
22846
|
logger.info(`[codex-sidecar] ${command} ${args.map(shellDisplayQuote).join(" ")}`);
|
|
22328
|
-
const result =
|
|
22847
|
+
const result = spawnSync4(command, args, { encoding: "utf-8", stdio: "pipe" });
|
|
22329
22848
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
22330
22849
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
22331
22850
|
if (options.saveResult && result.stdout) {
|
|
@@ -22339,8 +22858,8 @@ function executePlan(logger, command, args, options = {}) {
|
|
|
22339
22858
|
}
|
|
22340
22859
|
function saveStructuredResult(path, stdout) {
|
|
22341
22860
|
const parsed = JSON.parse(stdout);
|
|
22342
|
-
|
|
22343
|
-
|
|
22861
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
22862
|
+
writeFileSync6(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
|
|
22344
22863
|
}
|
|
22345
22864
|
function shellDisplayQuote(value) {
|
|
22346
22865
|
return /[\s"'$`]/.test(value) ? JSON.stringify(value) : value;
|
|
@@ -22493,6 +23012,62 @@ program.command("hook <name> [arg]").description(
|
|
|
22493
23012
|
).action(async (name, arg) => {
|
|
22494
23013
|
await runHook(name, arg);
|
|
22495
23014
|
});
|
|
23015
|
+
var codexHook = program.command("codex-hook").description("Install or run Codex hooks for Caveat");
|
|
23016
|
+
codexHook.command("install").description("Install Caveat hooks into ~/.codex/hooks.json and enable codex_hooks").option("--dry-run", "show planned changes without writing", false).option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action((opts) => {
|
|
23017
|
+
const cliScriptPath = process.argv[1];
|
|
23018
|
+
if (!cliScriptPath) {
|
|
23019
|
+
process.stderr.write("[caveat:error] cannot determine CLI script path\n");
|
|
23020
|
+
process.exit(1);
|
|
23021
|
+
}
|
|
23022
|
+
const result = installCodexHooks({
|
|
23023
|
+
codexHome: opts.codexHome,
|
|
23024
|
+
cliScriptPath,
|
|
23025
|
+
nodePath: process.execPath,
|
|
23026
|
+
dryRun: opts.dryRun,
|
|
23027
|
+
logger: stdoutLogger
|
|
23028
|
+
});
|
|
23029
|
+
stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`);
|
|
23030
|
+
stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
|
|
23031
|
+
stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
|
|
23032
|
+
stdoutLogger.info(`codex_hooks feature: ${result.feature}`);
|
|
23033
|
+
if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
|
|
23034
|
+
if (result.configBackupPath) {
|
|
23035
|
+
stdoutLogger.info(`config.toml backed up: ${result.configBackupPath}`);
|
|
23036
|
+
}
|
|
23037
|
+
});
|
|
23038
|
+
codexHook.command("uninstall").description("Remove Caveat-owned Codex hooks from ~/.codex/hooks.json").option("--dry-run", "show planned changes without writing", false).option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action((opts) => {
|
|
23039
|
+
const cliScriptPath = process.argv[1];
|
|
23040
|
+
if (!cliScriptPath) {
|
|
23041
|
+
process.stderr.write("[caveat:error] cannot determine CLI script path\n");
|
|
23042
|
+
process.exit(1);
|
|
23043
|
+
}
|
|
23044
|
+
const result = uninstallCodexHooks({
|
|
23045
|
+
codexHome: opts.codexHome,
|
|
23046
|
+
cliScriptPath,
|
|
23047
|
+
nodePath: process.execPath,
|
|
23048
|
+
dryRun: opts.dryRun,
|
|
23049
|
+
logger: stdoutLogger
|
|
23050
|
+
});
|
|
23051
|
+
stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit === "added" ? "removed" : "not present"}`);
|
|
23052
|
+
stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse === "added" ? "removed" : "not present"}`);
|
|
23053
|
+
stdoutLogger.info(`Stop hook: ${result.hooks.stop === "added" ? "removed" : "not present"}`);
|
|
23054
|
+
if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
|
|
23055
|
+
});
|
|
23056
|
+
codexHook.command("diagnostics").description("Check local Codex hook availability").option("--codex-home <path>", "Codex home directory", process.env.CODEX_HOME ?? `${process.env.HOME}/.codex`).action(async (opts) => {
|
|
23057
|
+
await runCodexHook("diagnostics", opts.codexHome);
|
|
23058
|
+
});
|
|
23059
|
+
codexHook.command("user-prompt-submit").description("Run the Codex UserPromptSubmit hook").action(async () => {
|
|
23060
|
+
await runCodexHook("user-prompt-submit");
|
|
23061
|
+
});
|
|
23062
|
+
codexHook.command("post-tool-use").description("Run the Codex PostToolUse hook").action(async () => {
|
|
23063
|
+
await runCodexHook("post-tool-use");
|
|
23064
|
+
});
|
|
23065
|
+
codexHook.command("stop").description("Run the Codex Stop hook").action(async () => {
|
|
23066
|
+
await runCodexHook("stop");
|
|
23067
|
+
});
|
|
23068
|
+
codexHook.command("worker <workFile>").description("Run the detached Codex hook worker").action(async (workFile) => {
|
|
23069
|
+
await runCodexHook("worker", workFile);
|
|
23070
|
+
});
|
|
22496
23071
|
var codexSidecar = program.command("codex-sidecar").description("Check Codex sidecar availability for the current repository");
|
|
22497
23072
|
codexSidecar.command("diagnostics").description("Run codex-sidecar diagnostics for this repository").option("--project <path>", "repository root to check").option("--preset <preset>", "codex-sidecar preset", "review").option("--command <command>", "codex-sidecar executable", "codex-sidecar").option("--node-cli <path>", "development path to codex-sidecar CLI JS").option("--save-result <path>", "write structured SidecarResult JSON to this path").action((opts) => {
|
|
22498
23073
|
runCodexSidecarDiagnostics(stdoutLogger, opts);
|