caveat-cli 0.13.0 → 0.14.1

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/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-WTHGWCPM.js";
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({
@@ -6860,6 +6861,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync as readFileSync2, wri
6860
6861
  import { dirname as dirname2, join as join3 } from "node:path";
6861
6862
  var EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
6862
6863
  var EVENT_POST_TOOL_USE = "PostToolUse";
6864
+ var EVENT_POST_TOOL_USE_FAILURE = "PostToolUseFailure";
6863
6865
  var EVENT_STOP = "Stop";
6864
6866
  function quote(p) {
6865
6867
  return p.includes(" ") ? `"${p}"` : p;
@@ -6891,6 +6893,15 @@ function removeHook(settings, event, command) {
6891
6893
  settings.hooks[event] = filtered;
6892
6894
  return true;
6893
6895
  }
6896
+ function findExistingHookCommand(settings, event, command) {
6897
+ const list = settings.hooks?.[event];
6898
+ if (!list) return void 0;
6899
+ for (const entry of list) {
6900
+ const found = entry.hooks?.find((h) => isSameHookCommand(h.command, command));
6901
+ if (found) return found.command;
6902
+ }
6903
+ return void 0;
6904
+ }
6894
6905
  function isSameHookCommand(actual, expected) {
6895
6906
  return actual === expected || actual.endsWith(` ${expected}`);
6896
6907
  }
@@ -6970,21 +6981,31 @@ function installClaudeIntegration(opts) {
6970
6981
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
6971
6982
  const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
6972
6983
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
6984
+ const ptInstallCmd = findExistingHookCommand(settings, EVENT_POST_TOOL_USE, ptCmd) ?? ptCmd;
6973
6985
  const userPromptSubmit = upsertHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
6974
- const postToolUse = upsertHook(settings, EVENT_POST_TOOL_USE, ptCmd);
6986
+ const postToolUse = upsertHook(settings, EVENT_POST_TOOL_USE, ptInstallCmd);
6987
+ const postToolUseFailure = upsertHook(
6988
+ settings,
6989
+ EVENT_POST_TOOL_USE_FAILURE,
6990
+ ptInstallCmd
6991
+ );
6975
6992
  const stop = upsertHook(settings, EVENT_STOP, stopCmd);
6976
6993
  let backupPath;
6977
- const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
6994
+ const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || postToolUseFailure === "added" || stop === "added";
6978
6995
  if (!opts.dryRun && anyAdded) {
6979
6996
  const backup = writeSettings(settingsPath, settings);
6980
6997
  if (backup) backupPath = backup;
6981
6998
  } else if (opts.dryRun) {
6982
6999
  opts.logger.info(
6983
- `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit, ${postToolUse === "added" ? "add" : "keep"} PostToolUse, ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
7000
+ `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit, ${postToolUse === "added" ? "add" : "keep"} PostToolUse, ${postToolUseFailure === "added" ? "add" : "keep"} PostToolUseFailure, ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
6984
7001
  );
6985
7002
  }
6986
7003
  const mcp = opts.skipMcpRegistration ? { action: "skipped", detail: "skipped by caller" } : registerMcp(opts.nodePath, opts.cliScriptPath, opts.dryRun, opts.logger);
6987
- return { mcp, hooks: { userPromptSubmit, postToolUse, stop }, backupPath };
7004
+ return {
7005
+ mcp,
7006
+ hooks: { userPromptSubmit, postToolUse, postToolUseFailure, stop },
7007
+ backupPath
7008
+ };
6988
7009
  }
6989
7010
  function uninstallClaudeIntegration(opts) {
6990
7011
  const settingsPath = join3(opts.claudeDir, "settings.json");
@@ -6994,9 +7015,10 @@ function uninstallClaudeIntegration(opts) {
6994
7015
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
6995
7016
  const removedUs = removeHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
6996
7017
  const removedPt = removeHook(settings, EVENT_POST_TOOL_USE, ptCmd);
7018
+ const removedPtf = removeHook(settings, EVENT_POST_TOOL_USE_FAILURE, ptCmd);
6997
7019
  const removedStop = removeHook(settings, EVENT_STOP, stopCmd);
6998
7020
  let backupPath;
6999
- if (!opts.dryRun && (removedUs || removedPt || removedStop)) {
7021
+ if (!opts.dryRun && (removedUs || removedPt || removedPtf || removedStop)) {
7000
7022
  const backup = writeSettings(settingsPath, settings);
7001
7023
  if (backup) backupPath = backup;
7002
7024
  }
@@ -7006,6 +7028,7 @@ function uninstallClaudeIntegration(opts) {
7006
7028
  hooks: {
7007
7029
  userPromptSubmit: removedUs ? "added" : "unchanged",
7008
7030
  postToolUse: removedPt ? "added" : "unchanged",
7031
+ postToolUseFailure: removedPtf ? "added" : "unchanged",
7009
7032
  stop: removedStop ? "added" : "unchanged"
7010
7033
  },
7011
7034
  backupPath
@@ -7109,6 +7132,12 @@ function runUninstall(ctx, opts) {
7109
7132
  ctx.logger.info(
7110
7133
  `UserPromptSubmit hook: ${result.hooks.userPromptSubmit === "added" ? "removed" : "not present"}`
7111
7134
  );
7135
+ ctx.logger.info(
7136
+ `PostToolUse hook: ${result.hooks.postToolUse === "added" ? "removed" : "not present"}`
7137
+ );
7138
+ ctx.logger.info(
7139
+ `PostToolUseFailure hook: ${result.hooks.postToolUseFailure === "added" ? "removed" : "not present"}`
7140
+ );
7112
7141
  ctx.logger.info(
7113
7142
  `Stop hook: ${result.hooks.stop === "added" ? "removed" : "not present"}`
7114
7143
  );
@@ -7124,6 +7153,10 @@ function reportInstallResult(ctx, result, dryRun) {
7124
7153
  ctx.logger.info(
7125
7154
  `${prefix}UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`
7126
7155
  );
7156
+ ctx.logger.info(`${prefix}PostToolUse hook: ${result.hooks.postToolUse}`);
7157
+ ctx.logger.info(
7158
+ `${prefix}PostToolUseFailure hook: ${result.hooks.postToolUseFailure}`
7159
+ );
7127
7160
  ctx.logger.info(`${prefix}Stop hook: ${result.hooks.stop}`);
7128
7161
  if (result.backupPath) {
7129
7162
  ctx.logger.info(`settings.json backed up: ${result.backupPath}`);
@@ -7312,7 +7345,7 @@ function runStats(ctx) {
7312
7345
 
7313
7346
  // src/commands/serve.ts
7314
7347
  async function runServe(opts) {
7315
- const { startServer } = await import("./server-IWDZZESV.js");
7348
+ const { startServer } = await import("./server-BP54BTEL.js");
7316
7349
  const { port, host } = startServer({ port: opts.port });
7317
7350
  process.stdout.write(`[caveat] web portal: http://${host}:${port}/
7318
7351
  `);
@@ -21934,11 +21967,13 @@ function extractToolResponseText(response) {
21934
21967
  return "";
21935
21968
  }
21936
21969
  function isToolError(payload) {
21970
+ if (payload.hook_event_name === "PostToolUseFailure") return true;
21937
21971
  const resp = payload.tool_response ?? payload.toolResponse;
21938
21972
  if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
21939
21973
  if (resp.is_error === true) return true;
21940
21974
  }
21941
21975
  if (payload.is_error === true) return true;
21976
+ if (typeof payload.error === "string" && payload.error.length > 0) return true;
21942
21977
  return false;
21943
21978
  }
21944
21979
  function spawnWorker(job) {
@@ -22162,7 +22197,9 @@ async function runHook(name, arg) {
22162
22197
  }
22163
22198
  if (name === "post-tool-use") {
22164
22199
  if (!isToolError(payload)) process.exit(0);
22165
- const errText = extractToolResponseText(payload.tool_response ?? payload);
22200
+ const errText = extractToolResponseText(
22201
+ payload.tool_response ?? payload.toolResponse ?? payload.error ?? payload
22202
+ );
22166
22203
  if (errText) {
22167
22204
  spawnWorker({ sessionId, searchText: errText });
22168
22205
  }
@@ -22183,11 +22220,570 @@ async function runHook(name, arg) {
22183
22220
  process.exit(0);
22184
22221
  }
22185
22222
 
22223
+ // src/commands/codexHookCmd.ts
22224
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
22225
+ import { existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
22226
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
22227
+ import { join as join10 } from "node:path";
22228
+ import { randomBytes as randomBytes2 } from "node:crypto";
22229
+
22230
+ // src/codexHookInstall.ts
22231
+ import { copyFileSync as copyFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
22232
+ import { dirname as dirname5, join as join9 } from "node:path";
22233
+ function quote2(p) {
22234
+ return p.includes(" ") ? `"${p}"` : p;
22235
+ }
22236
+ function hookCommand2(nodePath, cliScriptPath, event) {
22237
+ return `${quote2(nodePath)} ${quote2(cliScriptPath)} codex-hook ${event}`;
22238
+ }
22239
+ function eventCommandFragment(event) {
22240
+ return `codex-hook ${event}`;
22241
+ }
22242
+ function isSameHookCommand2(actual, expected) {
22243
+ return actual === expected || actual.endsWith(` ${expected}`);
22244
+ }
22245
+ function hasCaveatHook(hooksJson, event, fragment) {
22246
+ return hooksJson.hooks?.[event]?.some(
22247
+ (entry) => entry.hooks?.some((h) => h.command.includes(fragment))
22248
+ ) ?? false;
22249
+ }
22250
+ function readHooks(path) {
22251
+ if (!existsSync6(path)) return {};
22252
+ return JSON.parse(readFileSync4(path, "utf-8"));
22253
+ }
22254
+ function writeJsonWithBackup(path, value) {
22255
+ const dir = dirname5(path);
22256
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
22257
+ let backupPath = "";
22258
+ if (existsSync6(path)) {
22259
+ backupPath = `${path}.caveat-backup-${Date.now()}`;
22260
+ copyFileSync2(path, backupPath);
22261
+ }
22262
+ writeFileSync4(path, `${JSON.stringify(value, null, 2)}
22263
+ `, "utf-8");
22264
+ return backupPath;
22265
+ }
22266
+ function upsertHook2(hooksJson, event, command) {
22267
+ hooksJson.hooks ??= {};
22268
+ const list = hooksJson.hooks[event] ??= [];
22269
+ const alreadyPresent = list.some(
22270
+ (entry) => entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
22271
+ );
22272
+ if (alreadyPresent) return "unchanged";
22273
+ list.push({
22274
+ hooks: [
22275
+ {
22276
+ type: "command",
22277
+ command,
22278
+ timeoutSec: 5,
22279
+ async: false,
22280
+ statusMessage: null
22281
+ }
22282
+ ]
22283
+ });
22284
+ return "added";
22285
+ }
22286
+ function removeHook2(hooksJson, event, command) {
22287
+ const list = hooksJson.hooks?.[event];
22288
+ if (!list) return false;
22289
+ const before = list.length;
22290
+ const filtered = list.filter(
22291
+ (entry) => !entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
22292
+ );
22293
+ if (filtered.length === before) return false;
22294
+ hooksJson.hooks[event] = filtered;
22295
+ return true;
22296
+ }
22297
+ function enableCodexHooksFeature(raw) {
22298
+ if (/^\s*codex_hooks\s*=\s*true\s*$/m.test(raw)) return { text: raw, changed: false };
22299
+ const lines = raw.split(/\r?\n/);
22300
+ const featuresStart = lines.findIndex((line) => /^\s*\[features]\s*$/.test(line));
22301
+ if (featuresStart === -1) {
22302
+ const prefix = raw.trimEnd();
22303
+ const text = `${prefix}${prefix ? "\n\n" : ""}[features]
22304
+ codex_hooks = true
22305
+ `;
22306
+ return { text, changed: true };
22307
+ }
22308
+ let insertAt = featuresStart + 1;
22309
+ for (let i = featuresStart + 1; i < lines.length; i += 1) {
22310
+ if (/^\s*\[.+]\s*$/.test(lines[i])) {
22311
+ break;
22312
+ }
22313
+ if (/^\s*codex_hooks\s*=/.test(lines[i])) {
22314
+ lines[i] = "codex_hooks = true";
22315
+ return { text: `${lines.join("\n").trimEnd()}
22316
+ `, changed: true };
22317
+ }
22318
+ }
22319
+ lines.splice(insertAt, 0, "codex_hooks = true");
22320
+ return { text: `${lines.join("\n").trimEnd()}
22321
+ `, changed: true };
22322
+ }
22323
+ function writeConfigWithBackup(path, text) {
22324
+ const dir = dirname5(path);
22325
+ if (!existsSync6(dir)) mkdirSync4(dir, { recursive: true });
22326
+ let backupPath = "";
22327
+ if (existsSync6(path)) {
22328
+ backupPath = `${path}.caveat-backup-${Date.now()}`;
22329
+ copyFileSync2(path, backupPath);
22330
+ }
22331
+ writeFileSync4(path, text, "utf-8");
22332
+ return backupPath;
22333
+ }
22334
+ function installCodexHooks(opts) {
22335
+ const hooksPath = join9(opts.codexHome, "hooks.json");
22336
+ const configPath = join9(opts.codexHome, "config.toml");
22337
+ const hooksJson = readHooks(hooksPath);
22338
+ const userPromptSubmit = upsertHook2(
22339
+ hooksJson,
22340
+ "UserPromptSubmit",
22341
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
22342
+ );
22343
+ const postToolUse = upsertHook2(
22344
+ hooksJson,
22345
+ "PostToolUse",
22346
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
22347
+ );
22348
+ const stop = upsertHook2(
22349
+ hooksJson,
22350
+ "Stop",
22351
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
22352
+ );
22353
+ const rawConfig = existsSync6(configPath) ? readFileSync4(configPath, "utf-8") : "";
22354
+ const enabled = enableCodexHooksFeature(rawConfig);
22355
+ const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
22356
+ let backupPath;
22357
+ let configBackupPath;
22358
+ if (opts.dryRun) {
22359
+ opts.logger.info(`[dry-run] would update ${hooksPath}`);
22360
+ opts.logger.info(`[dry-run] would ensure [features].codex_hooks = true in ${configPath}`);
22361
+ } else {
22362
+ if (anyHookAdded) {
22363
+ const backup = writeJsonWithBackup(hooksPath, hooksJson);
22364
+ if (backup) backupPath = backup;
22365
+ }
22366
+ if (enabled.changed) {
22367
+ const backup = writeConfigWithBackup(configPath, enabled.text);
22368
+ if (backup) configBackupPath = backup;
22369
+ }
22370
+ }
22371
+ return {
22372
+ hooks: { userPromptSubmit, postToolUse, stop },
22373
+ feature: enabled.changed ? "enabled" : "unchanged",
22374
+ backupPath,
22375
+ configBackupPath
22376
+ };
22377
+ }
22378
+ function uninstallCodexHooks(opts) {
22379
+ const hooksPath = join9(opts.codexHome, "hooks.json");
22380
+ const hooksJson = readHooks(hooksPath);
22381
+ const userPromptSubmitRemoved = removeHook2(
22382
+ hooksJson,
22383
+ "UserPromptSubmit",
22384
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
22385
+ );
22386
+ const postToolUseRemoved = removeHook2(
22387
+ hooksJson,
22388
+ "PostToolUse",
22389
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
22390
+ );
22391
+ const stopRemoved = removeHook2(
22392
+ hooksJson,
22393
+ "Stop",
22394
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
22395
+ );
22396
+ let backupPath;
22397
+ if (opts.dryRun) {
22398
+ opts.logger.info(`[dry-run] would remove Caveat Codex hooks from ${hooksPath}`);
22399
+ } else if (userPromptSubmitRemoved || postToolUseRemoved || stopRemoved) {
22400
+ const backup = writeJsonWithBackup(hooksPath, hooksJson);
22401
+ if (backup) backupPath = backup;
22402
+ }
22403
+ return {
22404
+ hooks: {
22405
+ userPromptSubmit: userPromptSubmitRemoved ? "added" : "unchanged",
22406
+ postToolUse: postToolUseRemoved ? "added" : "unchanged",
22407
+ stop: stopRemoved ? "added" : "unchanged"
22408
+ },
22409
+ feature: "unchanged",
22410
+ backupPath
22411
+ };
22412
+ }
22413
+ function detectCodexHookInstallation(codexHome) {
22414
+ const hooksPath = join9(codexHome, "hooks.json");
22415
+ const hooksJson = readHooks(hooksPath);
22416
+ const hooks = {
22417
+ userPromptSubmit: hasCaveatHook(
22418
+ hooksJson,
22419
+ "UserPromptSubmit",
22420
+ eventCommandFragment("user-prompt-submit")
22421
+ ),
22422
+ postToolUse: hasCaveatHook(hooksJson, "PostToolUse", eventCommandFragment("post-tool-use")),
22423
+ stop: hasCaveatHook(hooksJson, "Stop", eventCommandFragment("stop"))
22424
+ };
22425
+ const count = Object.values(hooks).filter(Boolean).length;
22426
+ return {
22427
+ installation: count === 0 ? "not-installed" : count === 3 ? "installed" : "partial",
22428
+ hooksPath,
22429
+ hooks
22430
+ };
22431
+ }
22432
+
22433
+ // src/commands/codexHookCmd.ts
22434
+ var silentLogger2 = {
22435
+ info: () => {
22436
+ },
22437
+ warn: () => {
22438
+ },
22439
+ error: (m) => process.stderr.write(`[caveat:codex-hook] ${m}
22440
+ `)
22441
+ };
22442
+ async function readStdin2() {
22443
+ const chunks = [];
22444
+ for await (const chunk of process.stdin) {
22445
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
22446
+ }
22447
+ return Buffer.concat(chunks).toString("utf-8");
22448
+ }
22449
+ function parsePayload2(raw) {
22450
+ if (!raw) return {};
22451
+ try {
22452
+ return JSON.parse(raw);
22453
+ } catch (err) {
22454
+ const msg = err instanceof Error ? err.message : String(err);
22455
+ process.stderr.write(`[caveat:codex-hook] json parse error: ${msg}
22456
+ `);
22457
+ return {};
22458
+ }
22459
+ }
22460
+ function buildContextSafely2() {
22461
+ try {
22462
+ return buildContext(silentLogger2);
22463
+ } catch (err) {
22464
+ const msg = err instanceof Error ? err.message : String(err);
22465
+ process.stderr.write(`[caveat:codex-hook] context error: ${msg}
22466
+ `);
22467
+ return null;
22468
+ }
22469
+ }
22470
+ function searchCaveatsFromTextSafely2(text) {
22471
+ if (!text) return [];
22472
+ let db;
22473
+ try {
22474
+ const ctx = buildContextSafely2();
22475
+ if (!ctx || !existsSync7(ctx.paths.dbPath)) return [];
22476
+ db = openDb({ path: ctx.paths.dbPath });
22477
+ const hits = findCaveatsForPrompt(db, text, {
22478
+ selfIdentity: defaultSelfIdentityTokens()
22479
+ });
22480
+ if (hits.length > 0) {
22481
+ try {
22482
+ markHit(db, hits);
22483
+ } catch (err) {
22484
+ const msg = err instanceof Error ? err.message : String(err);
22485
+ process.stderr.write(`[caveat:codex-hook] markHit error: ${msg}
22486
+ `);
22487
+ }
22488
+ }
22489
+ return hits;
22490
+ } catch (err) {
22491
+ const msg = err instanceof Error ? err.message : String(err);
22492
+ process.stderr.write(`[caveat:codex-hook] search error: ${msg}
22493
+ `);
22494
+ return [];
22495
+ } finally {
22496
+ db?.close();
22497
+ }
22498
+ }
22499
+ function loadSignalsSafely2(path) {
22500
+ try {
22501
+ return readCodexSessionSignals(path);
22502
+ } catch (err) {
22503
+ const msg = err instanceof Error ? err.message : String(err);
22504
+ process.stderr.write(`[caveat:codex-hook] transcript read error: ${msg}
22505
+ `);
22506
+ return null;
22507
+ }
22508
+ }
22509
+ function codexSessionId(payload) {
22510
+ const v = payload.session_id ?? payload.sessionId;
22511
+ return typeof v === "string" && v.length > 0 ? v : null;
22512
+ }
22513
+ function extractToolResponseText2(response) {
22514
+ if (typeof response === "string") return response;
22515
+ if (Array.isArray(response)) {
22516
+ return response.map((item) => {
22517
+ if (typeof item === "string") return item;
22518
+ if (item !== null && typeof item === "object") {
22519
+ const text = item.text;
22520
+ return typeof text === "string" ? text : "";
22521
+ }
22522
+ return "";
22523
+ }).filter(Boolean).join(" ");
22524
+ }
22525
+ if (response !== null && typeof response === "object") {
22526
+ const r = response;
22527
+ if (typeof r.content === "string") return r.content;
22528
+ if (Array.isArray(r.content)) return extractToolResponseText2(r.content);
22529
+ if (typeof r.output === "string") return r.output;
22530
+ if (typeof r.stdout === "string" || typeof r.stderr === "string") {
22531
+ return [r.stdout, r.stderr].filter((x) => typeof x === "string").join(" ");
22532
+ }
22533
+ }
22534
+ return "";
22535
+ }
22536
+ function numericExitCode(v) {
22537
+ return typeof v === "number" && Number.isInteger(v) ? v : null;
22538
+ }
22539
+ function processExitCodeFromText(text) {
22540
+ const m = /Process exited with code\s+(-?\d+)/.exec(text);
22541
+ return m ? Number(m[1]) : null;
22542
+ }
22543
+ function transcriptToolOutput(transcriptPath, toolUseId) {
22544
+ if (!transcriptPath || !toolUseId || !existsSync7(transcriptPath)) return null;
22545
+ let raw = "";
22546
+ try {
22547
+ raw = readFileSync5(transcriptPath, "utf-8");
22548
+ } catch {
22549
+ return null;
22550
+ }
22551
+ for (const line of raw.split("\n")) {
22552
+ if (!line.includes(toolUseId)) continue;
22553
+ let parsed;
22554
+ try {
22555
+ parsed = JSON.parse(line);
22556
+ } catch {
22557
+ continue;
22558
+ }
22559
+ if (parsed === null || typeof parsed !== "object") continue;
22560
+ const payloadObj = parsed.payload;
22561
+ if (payloadObj === null || typeof payloadObj !== "object") continue;
22562
+ const p = payloadObj;
22563
+ if (p.type !== "function_call_output" || p.call_id !== toolUseId) continue;
22564
+ return typeof p.output === "string" ? p.output : "";
22565
+ }
22566
+ return null;
22567
+ }
22568
+ function transcriptExitCode(payload) {
22569
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
22570
+ const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : "";
22571
+ const output = transcriptToolOutput(transcriptPath, toolUseId);
22572
+ return output === null ? null : processExitCodeFromText(output);
22573
+ }
22574
+ function isShellLikeTool(payload) {
22575
+ const name = typeof payload.tool_name === "string" ? payload.tool_name : "";
22576
+ if (name === "Bash" || name === "exec_command") return true;
22577
+ const input = payload.tool_input;
22578
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
22579
+ const r = input;
22580
+ return typeof r.command === "string" || typeof r.cmd === "string";
22581
+ }
22582
+ return false;
22583
+ }
22584
+ function toolInputText(input) {
22585
+ if (input === null || typeof input !== "object" || Array.isArray(input)) return "";
22586
+ const r = input;
22587
+ const command = r.command ?? r.cmd;
22588
+ return typeof command === "string" ? command : "";
22589
+ }
22590
+ function isCodexToolError(payload) {
22591
+ if (payload.is_error === true) return true;
22592
+ const topExit = numericExitCode(payload.exit_code ?? payload.exitCode);
22593
+ if (topExit !== null) return topExit !== 0;
22594
+ const resp = payload.tool_response ?? payload.toolResponse;
22595
+ if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
22596
+ const r = resp;
22597
+ if (r.is_error === true) return true;
22598
+ const exit2 = numericExitCode(r.exit_code ?? r.exitCode);
22599
+ if (exit2 !== null) return exit2 !== 0;
22600
+ }
22601
+ const responseExit = processExitCodeFromText(extractToolResponseText2(resp));
22602
+ if (responseExit !== null) return responseExit !== 0;
22603
+ const transcriptExit = transcriptExitCode(payload);
22604
+ if (transcriptExit !== null) return transcriptExit !== 0;
22605
+ return false;
22606
+ }
22607
+ function buildCodexPostToolUseWorkerJob(payload) {
22608
+ const sessionId = codexSessionId(payload);
22609
+ if (!sessionId) return null;
22610
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
22611
+ const toolUseId = typeof payload.tool_use_id === "string" ? payload.tool_use_id : void 0;
22612
+ const responseText = extractToolResponseText2(payload.tool_response ?? payload.toolResponse);
22613
+ const inputText = toolInputText(payload.tool_input);
22614
+ const transcriptOutput = transcriptPath && toolUseId ? transcriptToolOutput(transcriptPath, toolUseId) : null;
22615
+ const searchText = [inputText, responseText, transcriptOutput ?? ""].map((s) => s.trim()).filter(Boolean).join("\n");
22616
+ const knownError = isCodexToolError(payload);
22617
+ if (knownError) {
22618
+ return { sessionId, searchText, knownError: true, transcriptPath, toolUseId };
22619
+ }
22620
+ if (transcriptPath && toolUseId && isShellLikeTool(payload) && searchText) {
22621
+ return {
22622
+ sessionId,
22623
+ searchText,
22624
+ knownError: false,
22625
+ allowSymptomOnly: true,
22626
+ transcriptPath,
22627
+ toolUseId
22628
+ };
22629
+ }
22630
+ return null;
22631
+ }
22632
+ function codexContextOutput(text, eventName = "UserPromptSubmit") {
22633
+ return JSON.stringify({
22634
+ hookSpecificOutput: {
22635
+ hookEventName: eventName,
22636
+ additionalContext: text
22637
+ }
22638
+ });
22639
+ }
22640
+ function codexStopOutput(text) {
22641
+ return JSON.stringify({
22642
+ decision: "block",
22643
+ reason: text
22644
+ });
22645
+ }
22646
+ function drainForSession2(sessionId, eventName = "UserPromptSubmit") {
22647
+ const ctx = buildContextSafely2();
22648
+ if (!ctx) return;
22649
+ const reminders = drainPendingReminders(ctx.caveatHome, sessionId);
22650
+ for (const text of reminders) {
22651
+ process.stdout.write(`${codexContextOutput(text, eventName)}
22652
+ `);
22653
+ }
22654
+ }
22655
+ async function waitForTranscriptOutput(transcriptPath, toolUseId) {
22656
+ const deadline = Date.now() + 2e3;
22657
+ while (Date.now() <= deadline) {
22658
+ const output = transcriptToolOutput(transcriptPath, toolUseId);
22659
+ if (output !== null) return output;
22660
+ await new Promise((resolve) => setTimeout(resolve, 100));
22661
+ }
22662
+ return null;
22663
+ }
22664
+ async function processCodexWorkerJob(job, opts = { waitForTranscript: true }) {
22665
+ if (!job.searchText || !job.sessionId) return;
22666
+ let searchText = job.searchText;
22667
+ let knownError = job.knownError === true;
22668
+ if (opts.waitForTranscript && job.transcriptPath && job.toolUseId) {
22669
+ const transcriptOutput = await waitForTranscriptOutput(job.transcriptPath, job.toolUseId);
22670
+ if (transcriptOutput) {
22671
+ const exit2 = processExitCodeFromText(transcriptOutput);
22672
+ if (exit2 !== null) {
22673
+ if (exit2 === 0) return;
22674
+ knownError = true;
22675
+ }
22676
+ searchText = [searchText, transcriptOutput].filter(Boolean).join("\n");
22677
+ }
22678
+ }
22679
+ if (!knownError && job.allowSymptomOnly !== true) return;
22680
+ const hits = searchCaveatsFromTextSafely2(searchText);
22681
+ if (hits.length === 0) return;
22682
+ const ctx = buildContextSafely2();
22683
+ if (!ctx) return;
22684
+ try {
22685
+ appendPendingReminder(ctx.caveatHome, job.sessionId, toolErrorReminderText(hits));
22686
+ } catch {
22687
+ }
22688
+ }
22689
+ async function runCodexWorker(workFile) {
22690
+ let raw;
22691
+ try {
22692
+ raw = readFileSync5(workFile, "utf-8");
22693
+ } catch {
22694
+ process.exit(0);
22695
+ }
22696
+ try {
22697
+ unlinkSync2(workFile);
22698
+ } catch {
22699
+ }
22700
+ let job;
22701
+ try {
22702
+ job = JSON.parse(raw);
22703
+ } catch {
22704
+ process.exit(0);
22705
+ }
22706
+ await processCodexWorkerJob(job);
22707
+ process.exit(0);
22708
+ }
22709
+ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join10(homedir3(), ".codex")) {
22710
+ const features = spawnSync3("codex", ["features", "list"], {
22711
+ encoding: "utf-8",
22712
+ maxBuffer: 1024 * 1024
22713
+ });
22714
+ const featureOutput = [features.stdout, features.stderr].filter(Boolean).join("\n");
22715
+ const hasHooks = /^codex_hooks\s+\S+\s+true\b/m.test(featureOutput);
22716
+ const installation = detectCodexHookInstallation(codexHome);
22717
+ const result = {
22718
+ availability: features.error || features.status !== 0 || !hasHooks ? "unavailable" : "available",
22719
+ codexBinary: features.error ? "missing" : "present",
22720
+ codexHooksFeature: hasHooks ? "enabled" : "not-enabled",
22721
+ installation: installation.installation,
22722
+ codexHome,
22723
+ hooksPath: installation.hooksPath,
22724
+ installedHooks: installation.hooks,
22725
+ evidence: featureOutput.split("\n").find((line) => line.trim().startsWith("codex_hooks")) ?? null
22726
+ };
22727
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
22728
+ `);
22729
+ }
22730
+ async function runCodexHook(name, arg) {
22731
+ if (name === "diagnostics") {
22732
+ runDiagnostics(arg);
22733
+ return;
22734
+ }
22735
+ if (name === "worker") {
22736
+ if (!arg) process.exit(0);
22737
+ await runCodexWorker(arg);
22738
+ return;
22739
+ }
22740
+ let raw = "";
22741
+ try {
22742
+ raw = await readStdin2();
22743
+ } catch (err) {
22744
+ const msg = err instanceof Error ? err.message : String(err);
22745
+ process.stderr.write(`[caveat:codex-hook] stdin read error: ${msg}
22746
+ `);
22747
+ process.exit(0);
22748
+ }
22749
+ const payload = parsePayload2(raw);
22750
+ const sessionId = codexSessionId(payload);
22751
+ if (sessionId && name === "user-prompt-submit") drainForSession2(sessionId);
22752
+ else if (!sessionId) process.stderr.write("[caveat:codex-hook] missing session_id; pending drain disabled\n");
22753
+ if (name === "user-prompt-submit") {
22754
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
22755
+ const hits = searchCaveatsFromTextSafely2(prompt);
22756
+ if (hits.length > 0) {
22757
+ process.stdout.write(`${codexContextOutput(userPromptSubmitReminderText(hits))}
22758
+ `);
22759
+ }
22760
+ process.exit(0);
22761
+ }
22762
+ if (name === "post-tool-use") {
22763
+ const job = buildCodexPostToolUseWorkerJob(payload);
22764
+ if (job) await processCodexWorkerJob(job, { waitForTranscript: false });
22765
+ process.exit(0);
22766
+ }
22767
+ if (name === "stop") {
22768
+ if (payload.stop_hook_active === true) process.exit(0);
22769
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
22770
+ const signals = transcriptPath ? loadSignalsSafely2(transcriptPath) : null;
22771
+ if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
22772
+ const related = searchCaveatsFromTextSafely2(struggleSearchText(signals));
22773
+ process.stdout.write(`${codexStopOutput(stopReminderText(signals, related))}
22774
+ `);
22775
+ process.exit(0);
22776
+ }
22777
+ process.stderr.write(`[caveat:codex-hook] unknown hook name: ${name}
22778
+ `);
22779
+ process.exit(0);
22780
+ }
22781
+
22186
22782
  // src/commands/pull.ts
22187
- import { existsSync as existsSync6, readdirSync as readdirSync4 } from "node:fs";
22188
- import { join as join9 } from "node:path";
22783
+ import { existsSync as existsSync8, readdirSync as readdirSync4 } from "node:fs";
22784
+ import { join as join11 } from "node:path";
22189
22785
  async function runPull(ctx) {
22190
- if (!existsSync6(ctx.paths.communityDir)) {
22786
+ if (!existsSync8(ctx.paths.communityDir)) {
22191
22787
  ctx.logger.info(
22192
22788
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
22193
22789
  );
@@ -22207,15 +22803,15 @@ async function runPull(ctx) {
22207
22803
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
22208
22804
  try {
22209
22805
  rebuildAll(db);
22210
- if (existsSync6(ctx.paths.entriesDir)) {
22806
+ if (existsSync8(ctx.paths.entriesDir)) {
22211
22807
  const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
22212
22808
  ctx.logger.info(`own: +${own.added}`);
22213
22809
  }
22214
22810
  for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
22215
22811
  if (!entry.isDirectory()) continue;
22216
22812
  const source = `community/${entry.name}`;
22217
- const root = join9(ctx.paths.communityDir, entry.name, "entries");
22218
- if (!existsSync6(root)) continue;
22813
+ const root = join11(ctx.paths.communityDir, entry.name, "entries");
22814
+ if (!existsSync8(root)) continue;
22219
22815
  const scan = scanSource({ db, source, entriesRoot: root });
22220
22816
  ctx.logger.info(`${source}: +${scan.added}`);
22221
22817
  }
@@ -22225,10 +22821,10 @@ async function runPull(ctx) {
22225
22821
  }
22226
22822
 
22227
22823
  // src/commands/codexSidecar.ts
22228
- import { spawnSync as spawnSync3 } from "node:child_process";
22229
- import { mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
22230
- import { tmpdir as tmpdir2 } from "node:os";
22231
- import { dirname as dirname5, join as join10 } from "node:path";
22824
+ import { spawnSync as spawnSync4 } from "node:child_process";
22825
+ import { mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
22826
+ import { tmpdir as tmpdir3 } from "node:os";
22827
+ import { dirname as dirname6, join as join12 } from "node:path";
22232
22828
  import { cwd, exit } from "node:process";
22233
22829
  function runCodexSidecarDiagnostics(logger, opts) {
22234
22830
  const plan = buildCodexSidecarDiagnosticsCommand({
@@ -22260,8 +22856,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
22260
22856
  process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
22261
22857
  exit(0);
22262
22858
  }
22263
- const contextDir = mkdtempSync2(join10(tmpdir2(), "caveat-sidecar-context-"));
22264
- const contextFile = join10(contextDir, "context.json");
22859
+ const contextDir = mkdtempSync2(join12(tmpdir3(), "caveat-sidecar-context-"));
22860
+ const contextFile = join12(contextDir, "context.json");
22265
22861
  let status = 1;
22266
22862
  try {
22267
22863
  const blocks = collectCaveatContextBlocks(ctx, {
@@ -22270,7 +22866,7 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
22270
22866
  source: opts.source,
22271
22867
  visibility: opts.visibility
22272
22868
  });
22273
- writeFileSync4(contextFile, JSON.stringify({ context: blocks }, null, 2) + "\n", "utf-8");
22869
+ writeFileSync6(contextFile, JSON.stringify({ context: blocks }, null, 2) + "\n", "utf-8");
22274
22870
  const plan = buildCodexSidecarRunCommand({
22275
22871
  workflow,
22276
22872
  projectRoot: opts.project ?? cwd(),
@@ -22325,7 +22921,7 @@ function defaultPreset(workflow) {
22325
22921
  }
22326
22922
  function executePlan(logger, command, args, options = {}) {
22327
22923
  logger.info(`[codex-sidecar] ${command} ${args.map(shellDisplayQuote).join(" ")}`);
22328
- const result = spawnSync3(command, args, { encoding: "utf-8", stdio: "pipe" });
22924
+ const result = spawnSync4(command, args, { encoding: "utf-8", stdio: "pipe" });
22329
22925
  if (result.stdout) process.stdout.write(result.stdout);
22330
22926
  if (result.stderr) process.stderr.write(result.stderr);
22331
22927
  if (options.saveResult && result.stdout) {
@@ -22339,8 +22935,8 @@ function executePlan(logger, command, args, options = {}) {
22339
22935
  }
22340
22936
  function saveStructuredResult(path, stdout) {
22341
22937
  const parsed = JSON.parse(stdout);
22342
- mkdirSync4(dirname5(path), { recursive: true });
22343
- writeFileSync4(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
22938
+ mkdirSync5(dirname6(path), { recursive: true });
22939
+ writeFileSync6(path, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
22344
22940
  }
22345
22941
  function shellDisplayQuote(value) {
22346
22942
  return /[\s"'$`]/.test(value) ? JSON.stringify(value) : value;
@@ -22493,6 +23089,62 @@ program.command("hook <name> [arg]").description(
22493
23089
  ).action(async (name, arg) => {
22494
23090
  await runHook(name, arg);
22495
23091
  });
23092
+ var codexHook = program.command("codex-hook").description("Install or run Codex hooks for Caveat");
23093
+ 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) => {
23094
+ const cliScriptPath = process.argv[1];
23095
+ if (!cliScriptPath) {
23096
+ process.stderr.write("[caveat:error] cannot determine CLI script path\n");
23097
+ process.exit(1);
23098
+ }
23099
+ const result = installCodexHooks({
23100
+ codexHome: opts.codexHome,
23101
+ cliScriptPath,
23102
+ nodePath: process.execPath,
23103
+ dryRun: opts.dryRun,
23104
+ logger: stdoutLogger
23105
+ });
23106
+ stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`);
23107
+ stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse}`);
23108
+ stdoutLogger.info(`Stop hook: ${result.hooks.stop}`);
23109
+ stdoutLogger.info(`codex_hooks feature: ${result.feature}`);
23110
+ if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
23111
+ if (result.configBackupPath) {
23112
+ stdoutLogger.info(`config.toml backed up: ${result.configBackupPath}`);
23113
+ }
23114
+ });
23115
+ 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) => {
23116
+ const cliScriptPath = process.argv[1];
23117
+ if (!cliScriptPath) {
23118
+ process.stderr.write("[caveat:error] cannot determine CLI script path\n");
23119
+ process.exit(1);
23120
+ }
23121
+ const result = uninstallCodexHooks({
23122
+ codexHome: opts.codexHome,
23123
+ cliScriptPath,
23124
+ nodePath: process.execPath,
23125
+ dryRun: opts.dryRun,
23126
+ logger: stdoutLogger
23127
+ });
23128
+ stdoutLogger.info(`UserPromptSubmit hook: ${result.hooks.userPromptSubmit === "added" ? "removed" : "not present"}`);
23129
+ stdoutLogger.info(`PostToolUse hook: ${result.hooks.postToolUse === "added" ? "removed" : "not present"}`);
23130
+ stdoutLogger.info(`Stop hook: ${result.hooks.stop === "added" ? "removed" : "not present"}`);
23131
+ if (result.backupPath) stdoutLogger.info(`hooks.json backed up: ${result.backupPath}`);
23132
+ });
23133
+ 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) => {
23134
+ await runCodexHook("diagnostics", opts.codexHome);
23135
+ });
23136
+ codexHook.command("user-prompt-submit").description("Run the Codex UserPromptSubmit hook").action(async () => {
23137
+ await runCodexHook("user-prompt-submit");
23138
+ });
23139
+ codexHook.command("post-tool-use").description("Run the Codex PostToolUse hook").action(async () => {
23140
+ await runCodexHook("post-tool-use");
23141
+ });
23142
+ codexHook.command("stop").description("Run the Codex Stop hook").action(async () => {
23143
+ await runCodexHook("stop");
23144
+ });
23145
+ codexHook.command("worker <workFile>").description("Run the detached Codex hook worker").action(async (workFile) => {
23146
+ await runCodexHook("worker", workFile);
23147
+ });
22496
23148
  var codexSidecar = program.command("codex-sidecar").description("Check Codex sidecar availability for the current repository");
22497
23149
  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
23150
  runCodexSidecarDiagnostics(stdoutLogger, opts);