caveat-cli 0.14.7 → 0.14.9

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
@@ -10,6 +10,7 @@ import {
10
10
  buildCodexSidecarReadOnlySmokeCommand,
11
11
  buildCodexSidecarRunCommand,
12
12
  caveatEntriesToSidecarContextBlocks,
13
+ cleanupStalePendingDirs,
13
14
  communityAdd,
14
15
  communityList,
15
16
  communityPull,
@@ -26,6 +27,7 @@ import {
26
27
  listStale,
27
28
  loadConfig,
28
29
  markHit,
30
+ maybeSweepPendingDirs,
29
31
  openDb,
30
32
  readCodexSessionSignals,
31
33
  readSessionSignals,
@@ -40,7 +42,7 @@ import {
40
42
  toolErrorReminderText,
41
43
  updateEntry,
42
44
  userPromptSubmitReminderText
43
- } from "./chunk-EH3S6X6X.js";
45
+ } from "./chunk-ECC5LJZF.js";
44
46
 
45
47
  // ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/compile/codegen/code.js
46
48
  var require_code = __commonJS({
@@ -6852,8 +6854,8 @@ var stdoutLogger = {
6852
6854
  };
6853
6855
 
6854
6856
  // src/commands/init.ts
6855
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
6856
- import { dirname as dirname3, join as join4 } from "node:path";
6857
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, renameSync, rmdirSync, writeFileSync as writeFileSync2 } from "node:fs";
6858
+ import { dirname as dirname3, join as join5 } from "node:path";
6857
6859
 
6858
6860
  // src/claudeInstall.ts
6859
6861
  import { spawnSync } from "node:child_process";
@@ -6875,10 +6877,16 @@ function hookCommand(nodePath, cliScriptPath, event) {
6875
6877
  function upsertHook(settings, event, command) {
6876
6878
  settings.hooks ??= {};
6877
6879
  const list = settings.hooks[event] ??= [];
6878
- const alreadyPresent = list.some(
6879
- (entry) => entry.hooks?.some((h) => isSameHookCommand(h.command, command))
6880
- );
6881
- if (alreadyPresent) return "unchanged";
6880
+ const subcommand = command.includes("hook user-prompt-submit") ? "user-prompt-submit" : command.includes("hook post-tool-use") ? "post-tool-use" : command.includes("hook stop") ? "stop" : null;
6881
+ for (const entry of list) {
6882
+ for (const hook of entry.hooks ?? []) {
6883
+ if (isSameHookCommand(hook.command, command)) return "unchanged";
6884
+ if (subcommand && isCaveatClaudeHookCommand(hook.command, subcommand)) {
6885
+ hook.command = command;
6886
+ return "added";
6887
+ }
6888
+ }
6889
+ }
6882
6890
  list.push({ hooks: [{ type: "command", command }] });
6883
6891
  return "added";
6884
6892
  }
@@ -6905,6 +6913,13 @@ function findExistingHookCommand(settings, event, command) {
6905
6913
  function isSameHookCommand(actual, expected) {
6906
6914
  return actual === expected || actual.endsWith(` ${expected}`);
6907
6915
  }
6916
+ function isEnvPrefixedCommand(command) {
6917
+ return /^[A-Z_][A-Z0-9_]*=/.test(command.trim());
6918
+ }
6919
+ function isCaveatClaudeHookCommand(actual, event) {
6920
+ const lower = actual.toLowerCase();
6921
+ return !isEnvPrefixedCommand(actual) && lower.includes("caveat") && actual.includes(`hook ${event}`);
6922
+ }
6908
6923
  function readSettings(path) {
6909
6924
  if (!existsSync(path)) return {};
6910
6925
  return JSON.parse(readFileSync2(path, "utf-8"));
@@ -7035,6 +7050,40 @@ function uninstallClaudeIntegration(opts) {
7035
7050
  };
7036
7051
  }
7037
7052
 
7053
+ // src/nodePath.ts
7054
+ import { existsSync as existsSync2, realpathSync } from "node:fs";
7055
+ import { delimiter, join as join4 } from "node:path";
7056
+ var defaultRealpath = (path) => realpathSync.native(path);
7057
+ function safeRealpath(path, realpath = defaultRealpath) {
7058
+ try {
7059
+ return realpath(path);
7060
+ } catch {
7061
+ return null;
7062
+ }
7063
+ }
7064
+ function resolveHookNodePath({
7065
+ env = process.env,
7066
+ execPath = process.execPath,
7067
+ platform = process.platform,
7068
+ exists = existsSync2,
7069
+ realpath = defaultRealpath
7070
+ } = {}) {
7071
+ const execRealpath = safeRealpath(execPath, realpath);
7072
+ const pathEnv = env.PATH ?? env.Path ?? "";
7073
+ const names = platform === "win32" ? ["node.exe", "node.cmd", "node.bat", "node"] : ["node"];
7074
+ for (const dir of pathEnv.split(delimiter).filter(Boolean)) {
7075
+ for (const name of names) {
7076
+ const candidate = join4(dir, name);
7077
+ if (!exists(candidate)) continue;
7078
+ const candidateRealpath = safeRealpath(candidate, realpath);
7079
+ if (execRealpath && candidateRealpath && candidateRealpath === execRealpath) {
7080
+ return candidate;
7081
+ }
7082
+ }
7083
+ }
7084
+ return execPath;
7085
+ }
7086
+
7038
7087
  // src/commands/init.ts
7039
7088
  var KNOWLEDGE_GITIGNORE = [
7040
7089
  "# Never commit entries flagged private (visibility: private is enforced by the",
@@ -7048,7 +7097,7 @@ var KNOWLEDGE_GITIGNORE = [
7048
7097
  async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7049
7098
  ensureUserConfig(ctx.userConfigPath);
7050
7099
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
7051
- if (!existsSync2(ctx.paths.knowledgeRepo)) {
7100
+ if (!existsSync3(ctx.paths.knowledgeRepo)) {
7052
7101
  mkdirSync2(ctx.paths.knowledgeRepo, { recursive: true });
7053
7102
  mkdirSync2(ctx.paths.entriesDir, { recursive: true });
7054
7103
  ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
@@ -7056,20 +7105,31 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7056
7105
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
7057
7106
  }
7058
7107
  migrateLegacyCommunityDir(ctx);
7059
- const gitignorePath = join4(ctx.paths.knowledgeRepo, ".gitignore");
7060
- if (!existsSync2(gitignorePath)) {
7108
+ const gitignorePath = join5(ctx.paths.knowledgeRepo, ".gitignore");
7109
+ if (!existsSync3(gitignorePath)) {
7061
7110
  writeFileSync2(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
7062
7111
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
7063
7112
  }
7064
7113
  if (!opts.dryRun) {
7065
7114
  const dbDir = dirname3(ctx.paths.dbPath);
7066
- if (!existsSync2(dbDir)) mkdirSync2(dbDir, { recursive: true });
7115
+ if (!existsSync3(dbDir)) mkdirSync2(dbDir, { recursive: true });
7067
7116
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7068
7117
  db.close();
7069
7118
  ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
7070
7119
  } else {
7071
7120
  ctx.logger.info(`[dry-run] db path: ${ctx.paths.dbPath}`);
7072
7121
  }
7122
+ const staleDays = opts.pendingStaleDays ?? 7;
7123
+ if (opts.dryRun) {
7124
+ ctx.logger.info(`[dry-run] would sweep pending dirs older than ${staleDays}d`);
7125
+ } else {
7126
+ const swept = cleanupStalePendingDirs(ctx.caveatHome, { staleDays });
7127
+ if (swept.removed.length > 0) {
7128
+ ctx.logger.info(
7129
+ `pending dirs swept: removed ${swept.removed.length} (kept ${swept.kept}, threshold ${staleDays}d)`
7130
+ );
7131
+ }
7132
+ }
7073
7133
  ctx.logger.info(
7074
7134
  "tip: subscribe to a group repo with `caveat community add <github-url>`, then `caveat pull`."
7075
7135
  );
@@ -7083,20 +7143,20 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
7083
7143
  return;
7084
7144
  }
7085
7145
  const result = installClaudeIntegration({
7086
- claudeDir: join4(ctx.userHome, ".claude"),
7146
+ claudeDir: join5(ctx.userHome, ".claude"),
7087
7147
  cliScriptPath,
7088
- nodePath: process.execPath,
7148
+ nodePath: resolveHookNodePath(),
7089
7149
  dryRun: opts.dryRun,
7090
7150
  logger: ctx.logger
7091
7151
  });
7092
7152
  reportInstallResult(ctx, result, opts.dryRun);
7093
7153
  }
7094
7154
  function migrateLegacyCommunityDir(ctx) {
7095
- const legacy = join4(ctx.paths.knowledgeRepo, "community");
7155
+ const legacy = join5(ctx.paths.knowledgeRepo, "community");
7096
7156
  const current = ctx.paths.communityDir;
7097
7157
  if (legacy === current) return;
7098
- if (!existsSync2(legacy)) return;
7099
- if (existsSync2(current)) {
7158
+ if (!existsSync3(legacy)) return;
7159
+ if (existsSync3(current)) {
7100
7160
  ctx.logger.warn(
7101
7161
  `legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
7102
7162
  );
@@ -7105,7 +7165,7 @@ function migrateLegacyCommunityDir(ctx) {
7105
7165
  mkdirSync2(current, { recursive: true });
7106
7166
  for (const entry of readdirSync(legacy, { withFileTypes: true })) {
7107
7167
  if (!entry.isDirectory()) continue;
7108
- renameSync(join4(legacy, entry.name), join4(current, entry.name));
7168
+ renameSync(join5(legacy, entry.name), join5(current, entry.name));
7109
7169
  }
7110
7170
  try {
7111
7171
  rmdirSync(legacy);
@@ -7120,9 +7180,9 @@ function runUninstall(ctx, opts) {
7120
7180
  process.exit(1);
7121
7181
  }
7122
7182
  const result = uninstallClaudeIntegration({
7123
- claudeDir: join4(ctx.userHome, ".claude"),
7183
+ claudeDir: join5(ctx.userHome, ".claude"),
7124
7184
  cliScriptPath,
7125
- nodePath: process.execPath,
7185
+ nodePath: resolveHookNodePath(),
7126
7186
  dryRun: opts.dryRun,
7127
7187
  logger: ctx.logger
7128
7188
  });
@@ -7167,29 +7227,29 @@ function reportInstallResult(ctx, result, dryRun) {
7167
7227
  }
7168
7228
 
7169
7229
  // src/commands/indexCmd.ts
7170
- import { existsSync as existsSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "node:fs";
7171
- import { dirname as dirname4, join as join5 } from "node:path";
7230
+ import { existsSync as existsSync4, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "node:fs";
7231
+ import { dirname as dirname4, join as join6 } from "node:path";
7172
7232
  function runIndex(ctx, opts) {
7173
7233
  const dbDir = dirname4(ctx.paths.dbPath);
7174
- if (!existsSync3(dbDir)) mkdirSync3(dbDir, { recursive: true });
7234
+ if (!existsSync4(dbDir)) mkdirSync3(dbDir, { recursive: true });
7175
7235
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
7176
7236
  try {
7177
7237
  if (opts.full) {
7178
7238
  ctx.logger.info("full rebuild: DELETE FROM entries");
7179
7239
  rebuildAll(db);
7180
7240
  }
7181
- if (existsSync3(ctx.paths.entriesDir)) {
7241
+ if (existsSync4(ctx.paths.entriesDir)) {
7182
7242
  const result = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
7183
7243
  ctx.logger.info(`own: +${result.added} ~${result.updated} -${result.deleted}`);
7184
7244
  } else {
7185
7245
  ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
7186
7246
  }
7187
- if (existsSync3(ctx.paths.communityDir)) {
7247
+ if (existsSync4(ctx.paths.communityDir)) {
7188
7248
  for (const entry of readdirSync2(ctx.paths.communityDir, { withFileTypes: true })) {
7189
7249
  if (!entry.isDirectory()) continue;
7190
7250
  const source = `community/${entry.name}`;
7191
- const root = join5(ctx.paths.communityDir, entry.name, "entries");
7192
- if (!existsSync3(root)) continue;
7251
+ const root = join6(ctx.paths.communityDir, entry.name, "entries");
7252
+ if (!existsSync4(root)) continue;
7193
7253
  const result = scanSource({ db, source, entriesRoot: root });
7194
7254
  ctx.logger.info(`${source}: +${result.added} ~${result.updated} -${result.deleted}`);
7195
7255
  }
@@ -7345,7 +7405,7 @@ function runStats(ctx) {
7345
7405
 
7346
7406
  // src/commands/serve.ts
7347
7407
  async function runServe(opts) {
7348
- const { startServer } = await import("./server-RWVXGVXK.js");
7408
+ const { startServer } = await import("./server-YQ4DJ2H7.js");
7349
7409
  const { port, host } = startServer({ port: opts.port });
7350
7410
  process.stdout.write(`[caveat] web portal: http://${host}:${port}/
7351
7411
  `);
@@ -12599,8 +12659,8 @@ function emoji() {
12599
12659
  }
12600
12660
  var ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
12601
12661
  var ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
12602
- var mac = (delimiter) => {
12603
- const escapedDelim = escapeRegex(delimiter ?? ":");
12662
+ var mac = (delimiter2) => {
12663
+ const escapedDelim = escapeRegex(delimiter2 ?? ":");
12604
12664
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
12605
12665
  };
12606
12666
  var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -31417,11 +31477,11 @@ var StdioServerTransport = class {
31417
31477
 
31418
31478
  // ../mcp/dist/context.js
31419
31479
  import { homedir as homedir2 } from "node:os";
31420
- import { join as join6 } from "node:path";
31480
+ import { join as join7 } from "node:path";
31421
31481
  function buildMcpContext(overrides = {}) {
31422
31482
  const userHome = overrides.userHome ?? homedir2();
31423
31483
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
31424
- const userConfigPath = join6(userHome, ".caveatrc.json");
31484
+ const userConfigPath = join7(userHome, ".caveatrc.json");
31425
31485
  const logger = overrides.logger ?? stderrLogger;
31426
31486
  const config2 = loadConfig(userConfigPath);
31427
31487
  const paths = resolvePaths(caveatHome, config2.knowledgeRepo, userHome);
@@ -31556,13 +31616,13 @@ function handleListRecent(ctx, args) {
31556
31616
  }
31557
31617
 
31558
31618
  // ../mcp/dist/tools/pull.js
31559
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
31560
- import { join as join7 } from "node:path";
31619
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "node:fs";
31620
+ import { join as join8 } from "node:path";
31561
31621
  var pullInputShape = {};
31562
31622
  async function handlePull(ctx, _args = {}) {
31563
31623
  const pulled = [];
31564
31624
  const indexed = [];
31565
- if (existsSync4(ctx.paths.communityDir)) {
31625
+ if (existsSync5(ctx.paths.communityDir)) {
31566
31626
  const results = await communityPull({
31567
31627
  communityDir: ctx.paths.communityDir,
31568
31628
  logger: ctx.logger
@@ -31572,16 +31632,16 @@ async function handlePull(ctx, _args = {}) {
31572
31632
  }
31573
31633
  }
31574
31634
  rebuildAll(ctx.db);
31575
- if (existsSync4(ctx.paths.entriesDir)) {
31635
+ if (existsSync5(ctx.paths.entriesDir)) {
31576
31636
  const own = scanSource({ db: ctx.db, source: "own", entriesRoot: ctx.paths.entriesDir });
31577
31637
  indexed.push({ source: "own", ...own });
31578
31638
  }
31579
- if (existsSync4(ctx.paths.communityDir)) {
31639
+ if (existsSync5(ctx.paths.communityDir)) {
31580
31640
  for (const entry of readdirSync3(ctx.paths.communityDir, { withFileTypes: true })) {
31581
31641
  if (!entry.isDirectory()) continue;
31582
31642
  const source = `community/${entry.name}`;
31583
- const root = join7(ctx.paths.communityDir, entry.name, "entries");
31584
- if (!existsSync4(root)) continue;
31643
+ const root = join8(ctx.paths.communityDir, entry.name, "entries");
31644
+ if (!existsSync5(root)) continue;
31585
31645
  const scan = scanSource({ db: ctx.db, source, entriesRoot: root });
31586
31646
  indexed.push({ source, ...scan });
31587
31647
  }
@@ -31696,7 +31756,7 @@ async function runMcpServer() {
31696
31756
  // src/commands/hookCmd.ts
31697
31757
  import { spawn, spawnSync as spawnSync2 } from "node:child_process";
31698
31758
  import {
31699
- existsSync as existsSync5,
31759
+ existsSync as existsSync6,
31700
31760
  mkdirSync as mkdirSync4,
31701
31761
  mkdtempSync,
31702
31762
  readFileSync as readFileSync3,
@@ -31705,7 +31765,7 @@ import {
31705
31765
  writeFileSync as writeFileSync3
31706
31766
  } from "node:fs";
31707
31767
  import { tmpdir } from "node:os";
31708
- import { join as join8 } from "node:path";
31768
+ import { join as join9 } from "node:path";
31709
31769
  import { createHash, randomBytes } from "node:crypto";
31710
31770
  var silentLogger = {
31711
31771
  info: () => {
@@ -31755,7 +31815,7 @@ function searchCaveatsFromTextSafely(text) {
31755
31815
  let db;
31756
31816
  try {
31757
31817
  const ctx = buildContextSafely();
31758
- if (!ctx || !existsSync5(ctx.paths.dbPath)) return [];
31818
+ if (!ctx || !existsSync6(ctx.paths.dbPath)) return [];
31759
31819
  db = openDb({ path: ctx.paths.dbPath });
31760
31820
  const hits = findCaveatsForPrompt(db, text, {
31761
31821
  selfIdentity: defaultSelfIdentityTokens()
@@ -31839,7 +31899,7 @@ function stopSignalKey(signals, related) {
31839
31899
  return createHash("sha256").update(body).digest("hex");
31840
31900
  }
31841
31901
  function stopStatePath(caveatHome, sessionId) {
31842
- return join8(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
31902
+ return join9(caveatHome, CLAUDE_STOP_STATE_DIR, `${sanitizeClaudeStateId(sessionId)}.txt`);
31843
31903
  }
31844
31904
  function wasStopReminderQueued(caveatHome, sessionId, key) {
31845
31905
  const path = stopStatePath(caveatHome, sessionId);
@@ -31851,7 +31911,7 @@ function wasStopReminderQueued(caveatHome, sessionId, key) {
31851
31911
  }
31852
31912
  function markStopReminderQueued(caveatHome, sessionId, key) {
31853
31913
  const path = stopStatePath(caveatHome, sessionId);
31854
- mkdirSync4(join8(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
31914
+ mkdirSync4(join9(caveatHome, CLAUDE_STOP_STATE_DIR), { recursive: true });
31855
31915
  writeFileSync3(path, key, "utf-8");
31856
31916
  }
31857
31917
  function queueStopForSession(sessionId, signals, related) {
@@ -31902,7 +31962,7 @@ function isToolError(payload) {
31902
31962
  return false;
31903
31963
  }
31904
31964
  function spawnWorker(job) {
31905
- const workFile = join8(
31965
+ const workFile = join9(
31906
31966
  tmpdir(),
31907
31967
  `caveat-worker-${Date.now()}-${randomBytes(4).toString("hex")}.json`
31908
31968
  );
@@ -31966,7 +32026,7 @@ function buildToolErrorReminder(job, hits) {
31966
32026
  const mode = hookCodexSidecarMode();
31967
32027
  if (mode === "off") return base;
31968
32028
  const projectRoot = process.cwd();
31969
- const hasSidecarConfig = existsSync5(join8(projectRoot, ".codex-sidecar.yml"));
32029
+ const hasSidecarConfig = existsSync6(join9(projectRoot, ".codex-sidecar.yml"));
31970
32030
  if (mode === "auto" && !hasSidecarConfig) return base;
31971
32031
  const advisory = runCodexSidecarAdvisory({
31972
32032
  searchText: job.searchText,
@@ -32003,7 +32063,7 @@ function buildStopReminder(signals, related) {
32003
32063
  const mode = hookCodexSidecarMode();
32004
32064
  if (mode === "off") return base;
32005
32065
  const projectRoot = process.cwd();
32006
- const hasSidecarConfig = existsSync5(join8(projectRoot, ".codex-sidecar.yml"));
32066
+ const hasSidecarConfig = existsSync6(join9(projectRoot, ".codex-sidecar.yml"));
32007
32067
  if (mode === "auto" && !hasSidecarConfig) return base;
32008
32068
  const advisory = runCodexSidecarAdvisory({
32009
32069
  searchText: struggleSearchText(signals),
@@ -32068,8 +32128,8 @@ function runCodexSidecarAdvisory(input) {
32068
32128
  "--availability",
32069
32129
  "operational"
32070
32130
  ];
32071
- const resultDir = mkdtempSync(join8(tmpdir(), "caveat-codex-advisory-"));
32072
- const resultFile = join8(resultDir, "result.json");
32131
+ const resultDir = mkdtempSync(join9(tmpdir(), "caveat-codex-advisory-"));
32132
+ const resultFile = join9(resultDir, "result.json");
32073
32133
  args.push("--save-result", resultFile);
32074
32134
  const nodeCli = process.env.CAVEAT_CODEX_SIDECAR_NODE_CLI;
32075
32135
  if (nodeCli) {
@@ -32164,6 +32224,11 @@ async function runHook(name, arg) {
32164
32224
  process.exit(0);
32165
32225
  }
32166
32226
  if (name === "stop") {
32227
+ try {
32228
+ const ctx = buildContextSafely();
32229
+ if (ctx) maybeSweepPendingDirs(ctx.caveatHome);
32230
+ } catch {
32231
+ }
32167
32232
  if (payload.stop_hook_active === true) process.exit(0);
32168
32233
  const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
32169
32234
  const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
@@ -32179,14 +32244,14 @@ async function runHook(name, arg) {
32179
32244
 
32180
32245
  // src/commands/codexHookCmd.ts
32181
32246
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
32182
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
32247
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
32183
32248
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
32184
- import { join as join10 } from "node:path";
32249
+ import { join as join11 } from "node:path";
32185
32250
  import { createHash as createHash2, randomBytes as randomBytes2 } from "node:crypto";
32186
32251
 
32187
32252
  // src/codexHookInstall.ts
32188
- import { copyFileSync as copyFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
32189
- import { dirname as dirname5, join as join9 } from "node:path";
32253
+ import { copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
32254
+ import { dirname as dirname5, join as join10 } from "node:path";
32190
32255
  function quote2(p) {
32191
32256
  return p.includes(" ") ? `"${p}"` : p;
32192
32257
  }
@@ -32199,20 +32264,24 @@ function eventCommandFragment(event) {
32199
32264
  function isSameHookCommand2(actual, expected) {
32200
32265
  return actual === expected || actual.endsWith(` ${expected}`);
32201
32266
  }
32267
+ function isCaveatCodexHookCommand(actual, event) {
32268
+ const lower = actual.toLowerCase();
32269
+ return lower.includes("caveat") && actual.includes(eventCommandFragment(event));
32270
+ }
32202
32271
  function hasCaveatHook(hooksJson, event, fragment) {
32203
32272
  return hooksJson.hooks?.[event]?.some(
32204
32273
  (entry) => entry.hooks?.some((h) => h.command.includes(fragment))
32205
32274
  ) ?? false;
32206
32275
  }
32207
32276
  function readHooks(path) {
32208
- if (!existsSync6(path)) return {};
32277
+ if (!existsSync7(path)) return {};
32209
32278
  return JSON.parse(readFileSync4(path, "utf-8"));
32210
32279
  }
32211
32280
  function writeJsonWithBackup(path, value) {
32212
32281
  const dir = dirname5(path);
32213
- if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
32282
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
32214
32283
  let backupPath = "";
32215
- if (existsSync6(path)) {
32284
+ if (existsSync7(path)) {
32216
32285
  backupPath = `${path}.caveat-backup-${Date.now()}`;
32217
32286
  copyFileSync2(path, backupPath);
32218
32287
  }
@@ -32220,13 +32289,21 @@ function writeJsonWithBackup(path, value) {
32220
32289
  `, "utf-8");
32221
32290
  return backupPath;
32222
32291
  }
32223
- function upsertHook2(hooksJson, event, command) {
32292
+ function upsertHook2(hooksJson, event, command, subcommand) {
32224
32293
  hooksJson.hooks ??= {};
32225
32294
  const list = hooksJson.hooks[event] ??= [];
32226
- const alreadyPresent = list.some(
32227
- (entry) => entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
32228
- );
32229
- if (alreadyPresent) return "unchanged";
32295
+ for (const entry of list) {
32296
+ for (const hook of entry.hooks ?? []) {
32297
+ if (isSameHookCommand2(hook.command, command)) return "unchanged";
32298
+ if (isCaveatCodexHookCommand(hook.command, subcommand)) {
32299
+ hook.command = command;
32300
+ hook.timeoutSec = 5;
32301
+ hook.async = false;
32302
+ hook.statusMessage = null;
32303
+ return "added";
32304
+ }
32305
+ }
32306
+ }
32230
32307
  list.push({
32231
32308
  hooks: [
32232
32309
  {
@@ -32240,12 +32317,14 @@ function upsertHook2(hooksJson, event, command) {
32240
32317
  });
32241
32318
  return "added";
32242
32319
  }
32243
- function removeHook2(hooksJson, event, command) {
32320
+ function removeHook2(hooksJson, event, command, subcommand) {
32244
32321
  const list = hooksJson.hooks?.[event];
32245
32322
  if (!list) return false;
32246
32323
  const before = list.length;
32247
32324
  const filtered = list.filter(
32248
- (entry) => !entry.hooks?.some((h) => isSameHookCommand2(h.command, command))
32325
+ (entry) => !entry.hooks?.some(
32326
+ (h) => isSameHookCommand2(h.command, command) || isCaveatCodexHookCommand(h.command, subcommand)
32327
+ )
32249
32328
  );
32250
32329
  if (filtered.length === before) return false;
32251
32330
  hooksJson.hooks[event] = filtered;
@@ -32279,9 +32358,9 @@ codex_hooks = true
32279
32358
  }
32280
32359
  function writeConfigWithBackup(path, text) {
32281
32360
  const dir = dirname5(path);
32282
- if (!existsSync6(dir)) mkdirSync5(dir, { recursive: true });
32361
+ if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
32283
32362
  let backupPath = "";
32284
- if (existsSync6(path)) {
32363
+ if (existsSync7(path)) {
32285
32364
  backupPath = `${path}.caveat-backup-${Date.now()}`;
32286
32365
  copyFileSync2(path, backupPath);
32287
32366
  }
@@ -32289,25 +32368,28 @@ function writeConfigWithBackup(path, text) {
32289
32368
  return backupPath;
32290
32369
  }
32291
32370
  function installCodexHooks(opts) {
32292
- const hooksPath = join9(opts.codexHome, "hooks.json");
32293
- const configPath = join9(opts.codexHome, "config.toml");
32371
+ const hooksPath = join10(opts.codexHome, "hooks.json");
32372
+ const configPath = join10(opts.codexHome, "config.toml");
32294
32373
  const hooksJson = readHooks(hooksPath);
32295
32374
  const userPromptSubmit = upsertHook2(
32296
32375
  hooksJson,
32297
32376
  "UserPromptSubmit",
32298
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
32377
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32378
+ "user-prompt-submit"
32299
32379
  );
32300
32380
  const postToolUse = upsertHook2(
32301
32381
  hooksJson,
32302
32382
  "PostToolUse",
32303
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
32383
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32384
+ "post-tool-use"
32304
32385
  );
32305
32386
  const stop = upsertHook2(
32306
32387
  hooksJson,
32307
32388
  "Stop",
32308
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
32389
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32390
+ "stop"
32309
32391
  );
32310
- const rawConfig = existsSync6(configPath) ? readFileSync4(configPath, "utf-8") : "";
32392
+ const rawConfig = existsSync7(configPath) ? readFileSync4(configPath, "utf-8") : "";
32311
32393
  const enabled = enableCodexHooksFeature(rawConfig);
32312
32394
  const anyHookAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
32313
32395
  let backupPath;
@@ -32333,22 +32415,25 @@ function installCodexHooks(opts) {
32333
32415
  };
32334
32416
  }
32335
32417
  function uninstallCodexHooks(opts) {
32336
- const hooksPath = join9(opts.codexHome, "hooks.json");
32418
+ const hooksPath = join10(opts.codexHome, "hooks.json");
32337
32419
  const hooksJson = readHooks(hooksPath);
32338
32420
  const userPromptSubmitRemoved = removeHook2(
32339
32421
  hooksJson,
32340
32422
  "UserPromptSubmit",
32341
- hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit")
32423
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "user-prompt-submit"),
32424
+ "user-prompt-submit"
32342
32425
  );
32343
32426
  const postToolUseRemoved = removeHook2(
32344
32427
  hooksJson,
32345
32428
  "PostToolUse",
32346
- hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use")
32429
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "post-tool-use"),
32430
+ "post-tool-use"
32347
32431
  );
32348
32432
  const stopRemoved = removeHook2(
32349
32433
  hooksJson,
32350
32434
  "Stop",
32351
- hookCommand2(opts.nodePath, opts.cliScriptPath, "stop")
32435
+ hookCommand2(opts.nodePath, opts.cliScriptPath, "stop"),
32436
+ "stop"
32352
32437
  );
32353
32438
  let backupPath;
32354
32439
  if (opts.dryRun) {
@@ -32368,7 +32453,7 @@ function uninstallCodexHooks(opts) {
32368
32453
  };
32369
32454
  }
32370
32455
  function detectCodexHookInstallation(codexHome) {
32371
- const hooksPath = join9(codexHome, "hooks.json");
32456
+ const hooksPath = join10(codexHome, "hooks.json");
32372
32457
  const hooksJson = readHooks(hooksPath);
32373
32458
  const hooks = {
32374
32459
  userPromptSubmit: hasCaveatHook(
@@ -32432,7 +32517,7 @@ function searchCaveatsFromTextSafely2(text) {
32432
32517
  let db;
32433
32518
  try {
32434
32519
  const ctx = buildContextSafely2();
32435
- if (!ctx || !existsSync7(ctx.paths.dbPath)) return [];
32520
+ if (!ctx || !existsSync8(ctx.paths.dbPath)) return [];
32436
32521
  db = openDb({ path: ctx.paths.dbPath });
32437
32522
  const hits = findCaveatsForPrompt(db, text, {
32438
32523
  selfIdentity: defaultSelfIdentityTokens()
@@ -32501,7 +32586,7 @@ function processExitCodeFromText(text) {
32501
32586
  return m ? Number(m[1]) : null;
32502
32587
  }
32503
32588
  function transcriptToolOutput(transcriptPath, toolUseId) {
32504
- if (!transcriptPath || !toolUseId || !existsSync7(transcriptPath)) return null;
32589
+ if (!transcriptPath || !toolUseId || !existsSync8(transcriptPath)) return null;
32505
32590
  let raw = "";
32506
32591
  try {
32507
32592
  raw = readFileSync5(transcriptPath, "utf-8");
@@ -32642,7 +32727,7 @@ function stopSignalKey2(signals, related) {
32642
32727
  return createHash2("sha256").update(body).digest("hex");
32643
32728
  }
32644
32729
  function stopStatePath2(caveatHome, sessionId) {
32645
- return join10(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
32730
+ return join11(caveatHome, CODEX_STOP_STATE_DIR, `${sanitizeCodexStateId(sessionId)}.txt`);
32646
32731
  }
32647
32732
  function wasStopReminderQueued2(caveatHome, sessionId, key) {
32648
32733
  const path = stopStatePath2(caveatHome, sessionId);
@@ -32654,7 +32739,7 @@ function wasStopReminderQueued2(caveatHome, sessionId, key) {
32654
32739
  }
32655
32740
  function markStopReminderQueued2(caveatHome, sessionId, key) {
32656
32741
  const path = stopStatePath2(caveatHome, sessionId);
32657
- mkdirSync6(join10(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
32742
+ mkdirSync6(join11(caveatHome, CODEX_STOP_STATE_DIR), { recursive: true });
32658
32743
  writeFileSync5(path, key, "utf-8");
32659
32744
  }
32660
32745
  function queueStopForSession2(sessionId, signals, related) {
@@ -32725,7 +32810,7 @@ async function runCodexWorker(workFile) {
32725
32810
  await processCodexWorkerJob(job);
32726
32811
  process.exit(0);
32727
32812
  }
32728
- function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join10(homedir3(), ".codex")) {
32813
+ function runDiagnostics(codexHome = process.env.CODEX_HOME ?? join11(homedir3(), ".codex")) {
32729
32814
  const features = spawnSync3("codex", ["features", "list"], {
32730
32815
  encoding: "utf-8",
32731
32816
  maxBuffer: 1024 * 1024
@@ -32802,10 +32887,10 @@ async function runCodexHook(name, arg) {
32802
32887
  }
32803
32888
 
32804
32889
  // src/commands/pull.ts
32805
- import { existsSync as existsSync8, readdirSync as readdirSync4 } from "node:fs";
32806
- import { join as join11 } from "node:path";
32890
+ import { existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
32891
+ import { join as join12 } from "node:path";
32807
32892
  async function runPull(ctx) {
32808
- if (!existsSync8(ctx.paths.communityDir)) {
32893
+ if (!existsSync9(ctx.paths.communityDir)) {
32809
32894
  ctx.logger.info(
32810
32895
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
32811
32896
  );
@@ -32825,15 +32910,15 @@ async function runPull(ctx) {
32825
32910
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
32826
32911
  try {
32827
32912
  rebuildAll(db);
32828
- if (existsSync8(ctx.paths.entriesDir)) {
32913
+ if (existsSync9(ctx.paths.entriesDir)) {
32829
32914
  const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
32830
32915
  ctx.logger.info(`own: +${own.added}`);
32831
32916
  }
32832
32917
  for (const entry of readdirSync4(ctx.paths.communityDir, { withFileTypes: true })) {
32833
32918
  if (!entry.isDirectory()) continue;
32834
32919
  const source = `community/${entry.name}`;
32835
- const root = join11(ctx.paths.communityDir, entry.name, "entries");
32836
- if (!existsSync8(root)) continue;
32920
+ const root = join12(ctx.paths.communityDir, entry.name, "entries");
32921
+ if (!existsSync9(root)) continue;
32837
32922
  const scan = scanSource({ db, source, entriesRoot: root });
32838
32923
  ctx.logger.info(`${source}: +${scan.added}`);
32839
32924
  }
@@ -32846,7 +32931,7 @@ async function runPull(ctx) {
32846
32931
  import { spawnSync as spawnSync4 } from "node:child_process";
32847
32932
  import { mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
32848
32933
  import { tmpdir as tmpdir3 } from "node:os";
32849
- import { dirname as dirname6, join as join12 } from "node:path";
32934
+ import { dirname as dirname6, join as join13 } from "node:path";
32850
32935
  import { cwd, exit } from "node:process";
32851
32936
  function runCodexSidecarDiagnostics(logger, opts) {
32852
32937
  const plan = buildCodexSidecarDiagnosticsCommand({
@@ -32878,8 +32963,8 @@ function runCodexSidecarWithCaveats(ctx, workflow, prompt, opts) {
32878
32963
  process.stdout.write(JSON.stringify({ status: "skipped", decision }, null, 2) + "\n");
32879
32964
  exit(0);
32880
32965
  }
32881
- const contextDir = mkdtempSync2(join12(tmpdir3(), "caveat-sidecar-context-"));
32882
- const contextFile = join12(contextDir, "context.json");
32966
+ const contextDir = mkdtempSync2(join13(tmpdir3(), "caveat-sidecar-context-"));
32967
+ const contextFile = join13(contextDir, "context.json");
32883
32968
  let status = 1;
32884
32969
  try {
32885
32970
  const blocks = collectCaveatContextBlocks(ctx, {
@@ -33049,13 +33134,26 @@ var program = new Command();
33049
33134
  program.name("caveat").description("External spec gotcha knowledge base CLI").version(CAVEAT_VERSION);
33050
33135
  program.command("init").description(
33051
33136
  "Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration. Add knowledge sources later with `caveat community add <github-url>`."
33052
- ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).action(async (opts) => {
33053
- const ctx = buildContext(stdoutLogger);
33054
- await runInit(ctx, {
33055
- skipClaude: opts.skipClaude,
33056
- dryRun: opts.dryRun
33057
- });
33058
- });
33137
+ ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).option(
33138
+ "--pending-stale-days <days>",
33139
+ "sweep `<caveatHome>/pending/<sessionId>/` whose newest entry is older than this (default 7)",
33140
+ (raw) => {
33141
+ const n = Number.parseInt(raw, 10);
33142
+ if (!Number.isFinite(n) || n < 0) {
33143
+ throw new Error(`--pending-stale-days expects a non-negative integer (got "${raw}")`);
33144
+ }
33145
+ return n;
33146
+ }
33147
+ ).action(
33148
+ async (opts) => {
33149
+ const ctx = buildContext(stdoutLogger);
33150
+ await runInit(ctx, {
33151
+ skipClaude: opts.skipClaude,
33152
+ dryRun: opts.dryRun,
33153
+ pendingStaleDays: opts.pendingStaleDays
33154
+ });
33155
+ }
33156
+ );
33059
33157
  program.command("uninstall").description("Remove Claude Code MCP server and hooks registered by `caveat init`").option("--dry-run", "show planned changes without writing", false).action((opts) => {
33060
33158
  const ctx = buildContext(stdoutLogger);
33061
33159
  runUninstall(ctx, { dryRun: opts.dryRun });
@@ -33121,7 +33219,7 @@ codexHook.command("install").description("Install Caveat hooks into ~/.codex/hoo
33121
33219
  const result = installCodexHooks({
33122
33220
  codexHome: opts.codexHome,
33123
33221
  cliScriptPath,
33124
- nodePath: process.execPath,
33222
+ nodePath: resolveHookNodePath(),
33125
33223
  dryRun: opts.dryRun,
33126
33224
  logger: stdoutLogger
33127
33225
  });
@@ -33143,7 +33241,7 @@ codexHook.command("uninstall").description("Remove Caveat-owned Codex hooks from
33143
33241
  const result = uninstallCodexHooks({
33144
33242
  codexHome: opts.codexHome,
33145
33243
  cliScriptPath,
33146
- nodePath: process.execPath,
33244
+ nodePath: resolveHookNodePath(),
33147
33245
  dryRun: opts.dryRun,
33148
33246
  logger: stdoutLogger
33149
33247
  });