caveat-cli 0.14.0 → 0.14.2

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/caveat.js CHANGED
File without changes
package/dist/index.js CHANGED
@@ -6861,6 +6861,7 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync as readFileSync2, wri
6861
6861
  import { dirname as dirname2, join as join3 } from "node:path";
6862
6862
  var EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
6863
6863
  var EVENT_POST_TOOL_USE = "PostToolUse";
6864
+ var EVENT_POST_TOOL_USE_FAILURE = "PostToolUseFailure";
6864
6865
  var EVENT_STOP = "Stop";
6865
6866
  function quote(p) {
6866
6867
  return p.includes(" ") ? `"${p}"` : p;
@@ -6892,6 +6893,15 @@ function removeHook(settings, event, command) {
6892
6893
  settings.hooks[event] = filtered;
6893
6894
  return true;
6894
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
+ }
6895
6905
  function isSameHookCommand(actual, expected) {
6896
6906
  return actual === expected || actual.endsWith(` ${expected}`);
6897
6907
  }
@@ -6971,21 +6981,31 @@ function installClaudeIntegration(opts) {
6971
6981
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
6972
6982
  const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
6973
6983
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
6984
+ const ptInstallCmd = findExistingHookCommand(settings, EVENT_POST_TOOL_USE, ptCmd) ?? ptCmd;
6974
6985
  const userPromptSubmit = upsertHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
6975
- 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
+ );
6976
6992
  const stop = upsertHook(settings, EVENT_STOP, stopCmd);
6977
6993
  let backupPath;
6978
- const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
6994
+ const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || postToolUseFailure === "added" || stop === "added";
6979
6995
  if (!opts.dryRun && anyAdded) {
6980
6996
  const backup = writeSettings(settingsPath, settings);
6981
6997
  if (backup) backupPath = backup;
6982
6998
  } else if (opts.dryRun) {
6983
6999
  opts.logger.info(
6984
- `[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}`
6985
7001
  );
6986
7002
  }
6987
7003
  const mcp = opts.skipMcpRegistration ? { action: "skipped", detail: "skipped by caller" } : registerMcp(opts.nodePath, opts.cliScriptPath, opts.dryRun, opts.logger);
6988
- return { mcp, hooks: { userPromptSubmit, postToolUse, stop }, backupPath };
7004
+ return {
7005
+ mcp,
7006
+ hooks: { userPromptSubmit, postToolUse, postToolUseFailure, stop },
7007
+ backupPath
7008
+ };
6989
7009
  }
6990
7010
  function uninstallClaudeIntegration(opts) {
6991
7011
  const settingsPath = join3(opts.claudeDir, "settings.json");
@@ -6995,9 +7015,10 @@ function uninstallClaudeIntegration(opts) {
6995
7015
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
6996
7016
  const removedUs = removeHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
6997
7017
  const removedPt = removeHook(settings, EVENT_POST_TOOL_USE, ptCmd);
7018
+ const removedPtf = removeHook(settings, EVENT_POST_TOOL_USE_FAILURE, ptCmd);
6998
7019
  const removedStop = removeHook(settings, EVENT_STOP, stopCmd);
6999
7020
  let backupPath;
7000
- if (!opts.dryRun && (removedUs || removedPt || removedStop)) {
7021
+ if (!opts.dryRun && (removedUs || removedPt || removedPtf || removedStop)) {
7001
7022
  const backup = writeSettings(settingsPath, settings);
7002
7023
  if (backup) backupPath = backup;
7003
7024
  }
@@ -7007,6 +7028,7 @@ function uninstallClaudeIntegration(opts) {
7007
7028
  hooks: {
7008
7029
  userPromptSubmit: removedUs ? "added" : "unchanged",
7009
7030
  postToolUse: removedPt ? "added" : "unchanged",
7031
+ postToolUseFailure: removedPtf ? "added" : "unchanged",
7010
7032
  stop: removedStop ? "added" : "unchanged"
7011
7033
  },
7012
7034
  backupPath
@@ -7110,6 +7132,12 @@ function runUninstall(ctx, opts) {
7110
7132
  ctx.logger.info(
7111
7133
  `UserPromptSubmit hook: ${result.hooks.userPromptSubmit === "added" ? "removed" : "not present"}`
7112
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
+ );
7113
7141
  ctx.logger.info(
7114
7142
  `Stop hook: ${result.hooks.stop === "added" ? "removed" : "not present"}`
7115
7143
  );
@@ -7125,6 +7153,10 @@ function reportInstallResult(ctx, result, dryRun) {
7125
7153
  ctx.logger.info(
7126
7154
  `${prefix}UserPromptSubmit hook: ${result.hooks.userPromptSubmit}`
7127
7155
  );
7156
+ ctx.logger.info(`${prefix}PostToolUse hook: ${result.hooks.postToolUse}`);
7157
+ ctx.logger.info(
7158
+ `${prefix}PostToolUseFailure hook: ${result.hooks.postToolUseFailure}`
7159
+ );
7128
7160
  ctx.logger.info(`${prefix}Stop hook: ${result.hooks.stop}`);
7129
7161
  if (result.backupPath) {
7130
7162
  ctx.logger.info(`settings.json backed up: ${result.backupPath}`);
@@ -21935,11 +21967,13 @@ function extractToolResponseText(response) {
21935
21967
  return "";
21936
21968
  }
21937
21969
  function isToolError(payload) {
21970
+ if (payload.hook_event_name === "PostToolUseFailure") return true;
21938
21971
  const resp = payload.tool_response ?? payload.toolResponse;
21939
21972
  if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
21940
21973
  if (resp.is_error === true) return true;
21941
21974
  }
21942
21975
  if (payload.is_error === true) return true;
21976
+ if (typeof payload.error === "string" && payload.error.length > 0) return true;
21943
21977
  return false;
21944
21978
  }
21945
21979
  function spawnWorker(job) {
@@ -22163,7 +22197,9 @@ async function runHook(name, arg) {
22163
22197
  }
22164
22198
  if (name === "post-tool-use") {
22165
22199
  if (!isToolError(payload)) process.exit(0);
22166
- const errText = extractToolResponseText(payload.tool_response ?? payload);
22200
+ const errText = extractToolResponseText(
22201
+ payload.tool_response ?? payload.toolResponse ?? payload.error ?? payload
22202
+ );
22167
22203
  if (errText) {
22168
22204
  spawnWorker({ sessionId, searchText: errText });
22169
22205
  }
@@ -22500,9 +22536,11 @@ function extractToolResponseText2(response) {
22500
22536
  function numericExitCode(v) {
22501
22537
  return typeof v === "number" && Number.isInteger(v) ? v : null;
22502
22538
  }
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 : "";
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) {
22506
22544
  if (!transcriptPath || !toolUseId || !existsSync7(transcriptPath)) return null;
22507
22545
  let raw = "";
22508
22546
  try {
@@ -22523,12 +22561,32 @@ function transcriptExitCode(payload) {
22523
22561
  if (payloadObj === null || typeof payloadObj !== "object") continue;
22524
22562
  const p = payloadObj;
22525
22563
  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]);
22564
+ return typeof p.output === "string" ? p.output : "";
22529
22565
  }
22530
22566
  return null;
22531
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
+ }
22532
22590
  function isCodexToolError(payload) {
22533
22591
  if (payload.is_error === true) return true;
22534
22592
  const topExit = numericExitCode(payload.exit_code ?? payload.exitCode);
@@ -22540,10 +22598,37 @@ function isCodexToolError(payload) {
22540
22598
  const exit2 = numericExitCode(r.exit_code ?? r.exitCode);
22541
22599
  if (exit2 !== null) return exit2 !== 0;
22542
22600
  }
22601
+ const responseExit = processExitCodeFromText(extractToolResponseText2(resp));
22602
+ if (responseExit !== null) return responseExit !== 0;
22543
22603
  const transcriptExit = transcriptExitCode(payload);
22544
22604
  if (transcriptExit !== null) return transcriptExit !== 0;
22545
22605
  return false;
22546
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
+ }
22547
22632
  function codexContextOutput(text, eventName = "UserPromptSubmit") {
22548
22633
  return JSON.stringify({
22549
22634
  hookSpecificOutput: {
@@ -22567,37 +22652,39 @@ function drainForSession2(sessionId, eventName = "UserPromptSubmit") {
22567
22652
  `);
22568
22653
  }
22569
22654
  }
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;
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));
22582
22661
  }
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 {
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");
22599
22677
  }
22600
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
+ }
22601
22688
  }
22602
22689
  async function runCodexWorker(workFile) {
22603
22690
  let raw;
@@ -22616,15 +22703,7 @@ async function runCodexWorker(workFile) {
22616
22703
  } catch {
22617
22704
  process.exit(0);
22618
22705
  }
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
- }
22706
+ await processCodexWorkerJob(job);
22628
22707
  process.exit(0);
22629
22708
  }
22630
22709
  function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join10(homedir3(), ".codex")) {
@@ -22669,8 +22748,8 @@ async function runCodexHook(name, arg) {
22669
22748
  }
22670
22749
  const payload = parsePayload2(raw);
22671
22750
  const sessionId = codexSessionId(payload);
22672
- if (sessionId) drainForSession2(sessionId);
22673
- else process.stderr.write("[caveat:codex-hook] missing session_id; pending drain disabled\n");
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");
22674
22753
  if (name === "user-prompt-submit") {
22675
22754
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
22676
22755
  const hits = searchCaveatsFromTextSafely2(prompt);
@@ -22681,10 +22760,8 @@ async function runCodexHook(name, arg) {
22681
22760
  process.exit(0);
22682
22761
  }
22683
22762
  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 });
22763
+ const job = buildCodexPostToolUseWorkerJob(payload);
22764
+ if (job) await processCodexWorkerJob(job, { waitForTranscript: false });
22688
22765
  process.exit(0);
22689
22766
  }
22690
22767
  if (name === "stop") {