caveat-cli 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12976,7 +12976,7 @@ import { Command } from "commander";
12976
12976
 
12977
12977
  // src/context.ts
12978
12978
  import { homedir } from "node:os";
12979
- import { join as join7 } from "node:path";
12979
+ import { join as join8 } from "node:path";
12980
12980
 
12981
12981
  // ../../packages/core/dist/db.js
12982
12982
  import { DatabaseSync } from "node:sqlite";
@@ -21203,56 +21203,334 @@ function communityRemove(opts) {
21203
21203
  }
21204
21204
 
21205
21205
  // ../../packages/core/dist/claudeHooks.js
21206
- var CAVEAT_TRIGGERS = [
21207
- /\b(gpu|cuda|nvidia|amd|rtx|nvenc|nvdec|blackwell|ada|ampere)\b/i,
21208
- /\bdriver\b/i,
21209
- /ドライバ/,
21210
- /\b(vscode|vs\s?code|jetbrains|intellij|pycharm)\b/i,
21211
- /\bclaude\s?code\b/i,
21212
- /\b(flaky|intermittent|reproducib|reproduce|repro)\b/i,
21213
- /再現(しない|性|できない)/,
21214
- /挙動が(違|おかしい|変)/,
21215
- /\b(node|python|cuda|cudnn)\s*\d+(\.\d+)?/i,
21216
- /バージョン(依存|違い|差)/,
21217
- /\b(prebuild|native\s+module|msvc|gyp)\b/i
21218
- ];
21219
- function detectCaveatTrigger(prompt) {
21220
- if (typeof prompt !== "string" || prompt.length === 0) return false;
21221
- return CAVEAT_TRIGGERS.some((re) => re.test(prompt));
21206
+ var PROMPT_TOKEN_MIN_LENGTH = 3;
21207
+ var PROMPT_MAX_CANDIDATE_TOKENS = 50;
21208
+ var DEFAULT_REMINDER_HIT_LIMIT = 5;
21209
+ var SYMPTOM_EXCERPT_LENGTH2 = 200;
21210
+ var SYMPTOM_LINE_MAX = 120;
21211
+ var MIN_DISTINCT_TOKEN_MATCHES_CEILING = 2;
21212
+ var CJK_CHAR = /[぀-ゟ゠-ヿ一-鿿ヲ-゚]/;
21213
+ function isCjkDominated(token) {
21214
+ return CJK_CHAR.test(token);
21215
+ }
21216
+ function expandToken(token, out) {
21217
+ if (isCjkDominated(token)) {
21218
+ if (token.length < PROMPT_TOKEN_MIN_LENGTH) return;
21219
+ for (let i2 = 0; i2 <= token.length - PROMPT_TOKEN_MIN_LENGTH; i2++) {
21220
+ out.push(token.slice(i2, i2 + PROMPT_TOKEN_MIN_LENGTH));
21221
+ }
21222
+ } else if (token.length >= PROMPT_TOKEN_MIN_LENGTH) {
21223
+ out.push(token);
21224
+ }
21225
+ }
21226
+ function extractPromptCandidates(prompt) {
21227
+ if (typeof prompt !== "string" || prompt.length === 0) return [];
21228
+ const cleaned = prompt.replace(/[^\p{L}\p{N}\s]/gu, " ");
21229
+ const rawTokens = cleaned.split(/\s+/).filter((t2) => t2.length > 0);
21230
+ const expanded = [];
21231
+ for (const t2 of rawTokens) expandToken(t2, expanded);
21232
+ const seen = /* @__PURE__ */ new Set();
21233
+ const unique = [];
21234
+ for (const t2 of expanded) {
21235
+ const key = t2.toLowerCase();
21236
+ if (seen.has(key)) continue;
21237
+ seen.add(key);
21238
+ unique.push(t2);
21239
+ }
21240
+ return unique.slice(0, PROMPT_MAX_CANDIDATE_TOKENS);
21241
+ }
21242
+ function toSearchResult2(row) {
21243
+ const fm = JSON.parse(row.frontmatter_json);
21244
+ const symptomMatch = /##\s+Symptom\s*\n([\s\S]*?)(?=\n##|\n*$)/.exec(row.body);
21245
+ const symptom = symptomMatch?.[1]?.trim() ?? row.body;
21246
+ return {
21247
+ id: row.id,
21248
+ source: row.source,
21249
+ title: row.title,
21250
+ symptomExcerpt: symptom.slice(0, SYMPTOM_EXCERPT_LENGTH2),
21251
+ confidence: row.confidence,
21252
+ visibility: row.visibility ?? "public",
21253
+ environment: fm.environment ?? {}
21254
+ };
21255
+ }
21256
+ function findCaveatsForPrompt(db, prompt, opts = {}) {
21257
+ const tokens = extractPromptCandidates(prompt);
21258
+ if (tokens.length === 0) return [];
21259
+ const minMatches = Math.min(MIN_DISTINCT_TOKEN_MATCHES_CEILING, tokens.length);
21260
+ const perEntry = /* @__PURE__ */ new Map();
21261
+ const stmt = db.prepare(
21262
+ "SELECT e.* FROM entries_fts f JOIN entries e ON e.rowid = f.rowid WHERE entries_fts MATCH ?"
21263
+ );
21264
+ for (const tok of tokens) {
21265
+ let rows = [];
21266
+ try {
21267
+ rows = stmt.all(`"${tok}"`);
21268
+ } catch {
21269
+ continue;
21270
+ }
21271
+ for (const row of rows) {
21272
+ const existing = perEntry.get(row.rowid);
21273
+ if (existing) existing.count += 1;
21274
+ else perEntry.set(row.rowid, { count: 1, row });
21275
+ }
21276
+ }
21277
+ const limit = opts.limit ?? DEFAULT_REMINDER_HIT_LIMIT;
21278
+ return [...perEntry.values()].filter(({ count }) => count >= minMatches).sort((a, b2) => b2.count - a.count).slice(0, limit).map(({ row }) => toSearchResult2(row));
21279
+ }
21280
+ function toolErrorReminderText(hits) {
21281
+ const lines = [];
21282
+ lines.push(
21283
+ `[caveat] \u76F4\u524D\u306E\u30A8\u30E9\u30FC\u306B\u4E00\u81F4\u3059\u308B\u53EF\u80FD\u6027\u306E\u3042\u308B\u65E2\u77E5\u306E\u7F60\u304C ${hits.length} \u4EF6\u3042\u308A\u307E\u3059:`
21284
+ );
21285
+ lines.push("");
21286
+ hits.forEach((h2, i2) => {
21287
+ lines.push(`${i2 + 1}. ${h2.id} (${h2.source}) \u2014 ${h2.title}`);
21288
+ const excerpt = h2.symptomExcerpt.replace(/\s+/g, " ").trim().slice(0, SYMPTOM_LINE_MAX);
21289
+ if (excerpt) lines.push(` \u75C7\u72B6: ${excerpt}`);
21290
+ });
21291
+ lines.push("");
21292
+ lines.push(
21293
+ "mcp__caveat__caveat_get \u3067\u8A73\u7D30\u3092\u78BA\u8A8D\u3057\u3001documented \u306A\u5BFE\u51E6\u304C\u3042\u308C\u3070\u9069\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u7121\u95A2\u4FC2\u3068\u5224\u65AD\u3057\u305F\u3089\u7121\u8996\u3057\u3066\u7D9A\u884C\u3067 OK\u3002"
21294
+ );
21295
+ return lines.join("\n");
21296
+ }
21297
+ function userPromptSubmitReminderText(hits) {
21298
+ const lines = [];
21299
+ lines.push(
21300
+ `[caveat] \u3053\u306E\u30D7\u30ED\u30F3\u30D7\u30C8\u306B\u95A2\u9023\u3059\u308B\u53EF\u80FD\u6027\u306E\u3042\u308B\u65E2\u77E5\u306E\u7F60\u304C ${hits.length} \u4EF6\u3042\u308A\u307E\u3059:`
21301
+ );
21302
+ lines.push("");
21303
+ hits.forEach((h2, i2) => {
21304
+ lines.push(`${i2 + 1}. ${h2.id} (${h2.source}) \u2014 ${h2.title}`);
21305
+ const excerpt = h2.symptomExcerpt.replace(/\s+/g, " ").trim().slice(0, SYMPTOM_LINE_MAX);
21306
+ if (excerpt) lines.push(` \u75C7\u72B6: ${excerpt}`);
21307
+ });
21308
+ lines.push("");
21309
+ lines.push(
21310
+ "\u8A73\u7D30\u306F mcp__caveat__caveat_get \u306B id + source \u3092\u6E21\u3057\u3066\u53D6\u5F97\u3002environment \u304C\u4E00\u81F4\u3059\u308B\u304B\u78BA\u8A8D\u3057\u3066\u304B\u3089\u9069\u7528\u5224\u65AD\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u7121\u95A2\u4FC2\u3068\u5224\u65AD\u3057\u305F\u3089\u7121\u8996\u3057\u3066\u7D9A\u884C\u3067 OK\u3002"
21311
+ );
21312
+ return lines.join("\n");
21313
+ }
21314
+ function shortPath(p2) {
21315
+ const parts = p2.split(/[\\/]/);
21316
+ return parts[parts.length - 1] ?? p2;
21317
+ }
21318
+ function stopReminderText(signals, related) {
21319
+ const lines = [];
21320
+ lines.push("[caveat] \u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u3067\u5916\u90E8\u4ED5\u69D8\u306E\u7F60\u306B\u5F53\u305F\u3063\u305F\u53EF\u80FD\u6027\u3092\u793A\u3059\u30B7\u30B0\u30CA\u30EB:");
21321
+ if (signals.toolFailureCount > 0) {
21322
+ lines.push(`- tool failure: ${signals.toolFailureCount} \u4EF6`);
21323
+ }
21324
+ if (signals.fileEditCounts.length > 0) {
21325
+ const top = signals.fileEditCounts.slice(0, 3).map((e) => `${shortPath(e.path)} \xD7 ${e.count}`).join(", ");
21326
+ lines.push(`- \u540C\u4E00\u30D5\u30A1\u30A4\u30EB\u8907\u6570\u7DE8\u96C6: ${top}`);
21327
+ }
21328
+ if (signals.webSearchCount > 0) {
21329
+ const sample = signals.searchQueries[0];
21330
+ const note = sample ? ` (\u4F8B: "${sample.slice(0, 60)}")` : "";
21331
+ lines.push(`- WebSearch: ${signals.webSearchCount} \u56DE${note}`);
21332
+ }
21333
+ if (signals.webFetchCount > 0) {
21334
+ lines.push(`- WebFetch: ${signals.webFetchCount} \u56DE`);
21335
+ }
21336
+ if (signals.bashRetryCount > 0) {
21337
+ lines.push(`- \u540C\u4E00 Bash \u30B3\u30DE\u30F3\u30C9\u306E\u518D\u5B9F\u884C: ${signals.bashRetryCount} \u7A2E`);
21338
+ }
21339
+ if (signals.durationMinutes > 0) {
21340
+ lines.push(`- \u7D4C\u904E\u6642\u9593: ${signals.durationMinutes} \u5206`);
21341
+ }
21342
+ lines.push("");
21343
+ if (related.length > 0) {
21344
+ lines.push(
21345
+ `\u30BB\u30C3\u30B7\u30E7\u30F3\u5185\u5BB9\u3068\u5171\u8D77\u3059\u308B\u65E2\u5B58\u7F60 ${related.length} \u4EF6\uFF08\u95A2\u9023\u304C\u3042\u308C\u3070 mcp__caveat__caveat_update \u3067 last_verified \u3092\u66F4\u65B0 or \u8FFD\u8A18\uFF09:`
21346
+ );
21347
+ related.forEach((h2, i2) => {
21348
+ lines.push(`${i2 + 1}. ${h2.id} (${h2.source}) \u2014 ${h2.title}`);
21349
+ });
21350
+ lines.push("");
21351
+ lines.push(
21352
+ "\u4E0A\u8A18\u3068\u7570\u306A\u308B\u65B0\u898F\u306E\u7F60\u3092\u8E0F\u3093\u3067\u3044\u305F\u3089 mcp__caveat__caveat_record \u3067\u767B\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002outcome: impossible\uFF08\u73FE\u72B6\u306E\u5236\u7D04\u3067\u306F\u4E0D\u53EF\u80FD\u3068\u5224\u5B9A\u3057\u305F\u7D50\u8AD6\uFF09\u3082\u8A18\u9332\u5BFE\u8C61\u3002"
21353
+ );
21354
+ } else {
21355
+ lines.push(
21356
+ "\u65E2\u5B58\u7F60\u306B\u8A72\u5F53\u306A\u3057\u3002\u5916\u90E8\u4ED5\u69D8\u306E\u7F60\u306B\u82E6\u6226\u3057\u3066\u3044\u305F\u306A\u3089 mcp__caveat__caveat_record \u3067\u767B\u9332\u3057\u3066\u304F\u3060\u3055\u3044\u3002outcome: impossible \u3082\u8A18\u9332\u5BFE\u8C61\u3002"
21357
+ );
21358
+ }
21359
+ return lines.join("\n");
21222
21360
  }
21223
- function userPromptSubmitReminderText() {
21224
- return [
21225
- "[caveat] \u3053\u306E\u30D7\u30ED\u30F3\u30D7\u30C8\u306F\u5916\u90E8\u4ED5\u69D8\u306E\u7F60\uFF08GPU/\u30C9\u30E9\u30A4\u30D0/\u30C4\u30FC\u30EB\u30D0\u30FC\u30B8\u30E7\u30F3/IDE/\u518D\u73FE\u6027\u306E\u4F4E\u3044\u6319\u52D5\uFF09\u306B\u89E6\u308C\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002",
21226
- "\u5B9F\u88C5\u3092\u59CB\u3081\u308B\u524D\u306B mcp__caveat__caveat_search \u3092\u547C\u3093\u3067\u65E2\u5B58\u306E\u7F60\u30E1\u30E2\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21227
- "3 \u6587\u5B57\u4EE5\u4E0A\u306E\u30AF\u30A8\u30EA\u3067\u691C\u7D22\u3001\u65E5\u672C\u8A9E\u306F trigram \u4E00\u81F4\u3002\u8A72\u5F53\u306A\u3057\u306A\u3089\u7D9A\u884C\u3001\u8A72\u5F53\u3042\u308C\u3070 environment \u4E00\u81F4\u3092\u898B\u3066\u9069\u7528\u5224\u65AD\u3092\u3002"
21228
- ].join("\n");
21361
+
21362
+ // ../../packages/core/dist/transcriptSignals.js
21363
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
21364
+ var MAX_ERROR_SNIPPETS = 10;
21365
+ var MAX_ERROR_SNIPPET_LENGTH = 300;
21366
+ var MAX_SEARCH_QUERIES = 10;
21367
+ var MAX_SEARCH_QUERY_LENGTH = 200;
21368
+ var MAX_FILE_EDIT_ENTRIES = 20;
21369
+ function isRecord(v) {
21370
+ return typeof v === "object" && v !== null && !Array.isArray(v);
21371
+ }
21372
+ function extractResultText(content) {
21373
+ if (typeof content === "string") return content;
21374
+ if (Array.isArray(content)) {
21375
+ const parts = [];
21376
+ for (const c3 of content) {
21377
+ if (isRecord(c3) && typeof c3.text === "string") parts.push(c3.text);
21378
+ }
21379
+ return parts.join(" ");
21380
+ }
21381
+ return "";
21382
+ }
21383
+ function parseTimestamp(raw2) {
21384
+ if (typeof raw2 !== "string") return void 0;
21385
+ const ms = Date.parse(raw2);
21386
+ return Number.isNaN(ms) ? void 0 : ms;
21387
+ }
21388
+ function readSessionSignals(transcriptPath) {
21389
+ if (!transcriptPath || !existsSync7(transcriptPath)) return null;
21390
+ let raw2;
21391
+ try {
21392
+ raw2 = readFileSync5(transcriptPath, "utf-8");
21393
+ } catch {
21394
+ return null;
21395
+ }
21396
+ const editCounts = /* @__PURE__ */ new Map();
21397
+ const bashCounts = /* @__PURE__ */ new Map();
21398
+ const errorSnippets = [];
21399
+ const searchQueries = [];
21400
+ let toolFailureCount = 0;
21401
+ let webSearchCount = 0;
21402
+ let webFetchCount = 0;
21403
+ let firstTs;
21404
+ let lastTs;
21405
+ for (const line of raw2.split("\n")) {
21406
+ if (line.length === 0) continue;
21407
+ let parsed;
21408
+ try {
21409
+ parsed = JSON.parse(line);
21410
+ } catch {
21411
+ continue;
21412
+ }
21413
+ const ts = parseTimestamp(parsed.timestamp);
21414
+ if (ts !== void 0) {
21415
+ if (firstTs === void 0 || ts < firstTs) firstTs = ts;
21416
+ if (lastTs === void 0 || ts > lastTs) lastTs = ts;
21417
+ }
21418
+ if (parsed.type !== "assistant" && parsed.type !== "user") continue;
21419
+ const content = parsed.message?.content;
21420
+ if (!Array.isArray(content)) continue;
21421
+ for (const item of content) {
21422
+ if (!isRecord(item)) continue;
21423
+ if (item.type === "tool_use") {
21424
+ const name = item.name;
21425
+ const input = isRecord(item.input) ? item.input : {};
21426
+ if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
21427
+ const p2 = typeof input.file_path === "string" ? input.file_path : "";
21428
+ if (p2) editCounts.set(p2, (editCounts.get(p2) ?? 0) + 1);
21429
+ } else if (name === "WebSearch") {
21430
+ webSearchCount += 1;
21431
+ if (typeof input.query === "string" && searchQueries.length < MAX_SEARCH_QUERIES) {
21432
+ searchQueries.push(input.query.slice(0, MAX_SEARCH_QUERY_LENGTH));
21433
+ }
21434
+ } else if (name === "WebFetch") {
21435
+ webFetchCount += 1;
21436
+ } else if (name === "Bash") {
21437
+ const cmd = typeof input.command === "string" ? input.command : "";
21438
+ if (cmd) bashCounts.set(cmd, (bashCounts.get(cmd) ?? 0) + 1);
21439
+ }
21440
+ } else if (item.type === "tool_result") {
21441
+ if (item.is_error === true) {
21442
+ toolFailureCount += 1;
21443
+ const text2 = extractResultText(item.content).replace(/\s+/g, " ").trim();
21444
+ if (text2 && errorSnippets.length < MAX_ERROR_SNIPPETS) {
21445
+ errorSnippets.push(text2.slice(0, MAX_ERROR_SNIPPET_LENGTH));
21446
+ }
21447
+ }
21448
+ }
21449
+ }
21450
+ }
21451
+ const fileEditCounts = [...editCounts.entries()].filter(([, c3]) => c3 > 1).map(([path, count]) => ({ path, count })).sort((a, b2) => b2.count - a.count).slice(0, MAX_FILE_EDIT_ENTRIES);
21452
+ const bashRetryCount = [...bashCounts.values()].filter((c3) => c3 > 1).length;
21453
+ const durationMinutes = firstTs !== void 0 && lastTs !== void 0 ? Math.max(0, Math.round((lastTs - firstTs) / 6e4)) : 0;
21454
+ return {
21455
+ toolFailureCount,
21456
+ fileEditCounts,
21457
+ webSearchCount,
21458
+ webFetchCount,
21459
+ bashRetryCount,
21460
+ durationMinutes,
21461
+ errorSnippets,
21462
+ searchQueries
21463
+ };
21229
21464
  }
21230
- function stopReminderText() {
21231
- return [
21232
- "[caveat] \u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u3067\u5916\u90E8\u4ED5\u69D8\u306E\u7F60\uFF08GPU/\u30C9\u30E9\u30A4\u30D0/\u30C4\u30FC\u30EB\u30D0\u30FC\u30B8\u30E7\u30F3/IDE/\u518D\u73FE\u6027\uFF09\u3092\u8E0F\u307F\u3001\u6B21\u56DE\u306E\u81EA\u5206\u306E\u8ABF\u67FB\u304C\u77ED\u7E2E\u3067\u304D\u305D\u3046\u306A\u3089 mcp__caveat__caveat_record \u3067\u767B\u9332\u3092\u691C\u8A0E\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
21233
- "\u89E3\u6C7A\u3057\u305F\u30B1\u30FC\u30B9\u3060\u3051\u3067\u306A\u304F\u3001\u300C\u73FE\u72B6\u306E\u5236\u7D04\u3067\u306F\u4E0D\u53EF\u80FD\u3068\u5224\u5B9A\u3057\u305F\u300D\u7D50\u8AD6\u3082 outcome: impossible \u3068\u3057\u3066\u8A18\u9332\u5BFE\u8C61\u3002\u6B21\u56DE\u540C\u3058 3 \u6642\u9593\u3092\u6EB6\u304B\u3055\u306A\u3044\u305F\u3081\u306B\u3002",
21234
- "\u65E2\u306B\u540C\u7B49\u306E caveat \u304C\u3042\u308B\u5834\u5408\u306F caveat_update \u3067\u4E0A\u66F8\u304D\u3001\u307E\u305F\u306F last_verified \u3060\u3051\u66F4\u65B0\u3002"
21235
- ].join("\n");
21465
+ function hasAnyStruggleSignal(s) {
21466
+ return s.toolFailureCount > 0 || s.fileEditCounts.length > 0 || s.webSearchCount > 0 || s.webFetchCount > 0 || s.bashRetryCount > 0;
21467
+ }
21468
+ function struggleSearchText(s) {
21469
+ return [...s.errorSnippets, ...s.searchQueries].join(" ");
21470
+ }
21471
+
21472
+ // ../../packages/core/dist/pendingReminders.js
21473
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
21474
+ import { join as join7 } from "node:path";
21475
+ import { randomBytes as randomBytes2 } from "node:crypto";
21476
+ function sanitizeSessionId(raw2) {
21477
+ const clean = raw2.replace(/[^A-Za-z0-9_-]/g, "");
21478
+ return clean.length > 0 ? clean : "_unknown";
21479
+ }
21480
+ function pendingDirFor(caveatHome, sessionId) {
21481
+ return join7(caveatHome, "pending", sanitizeSessionId(sessionId));
21482
+ }
21483
+ function appendPendingReminder(caveatHome, sessionId, text2) {
21484
+ const dir = pendingDirFor(caveatHome, sessionId);
21485
+ mkdirSync3(dir, { recursive: true });
21486
+ const name = `${Date.now()}-${randomBytes2(4).toString("hex")}.txt`;
21487
+ const path = join7(dir, name);
21488
+ writeFileSync4(path, text2, "utf-8");
21489
+ return path;
21490
+ }
21491
+ function drainPendingReminders(caveatHome, sessionId) {
21492
+ const dir = pendingDirFor(caveatHome, sessionId);
21493
+ if (!existsSync8(dir)) return [];
21494
+ let entries;
21495
+ try {
21496
+ entries = readdirSync4(dir).filter((f) => f.endsWith(".txt")).sort();
21497
+ } catch {
21498
+ return [];
21499
+ }
21500
+ const out = [];
21501
+ for (const entry of entries) {
21502
+ const path = join7(dir, entry);
21503
+ try {
21504
+ out.push(readFileSync6(path, "utf-8"));
21505
+ } catch {
21506
+ continue;
21507
+ }
21508
+ try {
21509
+ unlinkSync(path);
21510
+ } catch {
21511
+ }
21512
+ }
21513
+ return out;
21236
21514
  }
21237
21515
 
21238
21516
  // src/context.ts
21239
21517
  function buildContext(logger, overrides = {}) {
21240
21518
  const userHome = overrides.userHome ?? homedir();
21241
21519
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
21242
- const userConfigPath = join7(userHome, ".caveatrc.json");
21520
+ const userConfigPath = join8(userHome, ".caveatrc.json");
21243
21521
  const config3 = loadConfig(userConfigPath);
21244
21522
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
21245
21523
  return { caveatHome, userHome, userConfigPath, config: config3, paths, logger };
21246
21524
  }
21247
21525
 
21248
21526
  // src/version.ts
21249
- import { readFileSync as readFileSync5 } from "node:fs";
21250
- import { dirname as dirname4, join as join8 } from "node:path";
21527
+ import { readFileSync as readFileSync7 } from "node:fs";
21528
+ import { dirname as dirname4, join as join9 } from "node:path";
21251
21529
  import { fileURLToPath as fileURLToPath3 } from "node:url";
21252
21530
  function resolveVersion() {
21253
21531
  try {
21254
21532
  const here2 = dirname4(fileURLToPath3(import.meta.url));
21255
- const pkg = JSON.parse(readFileSync5(join8(here2, "..", "package.json"), "utf-8"));
21533
+ const pkg = JSON.parse(readFileSync7(join9(here2, "..", "package.json"), "utf-8"));
21256
21534
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
21257
21535
  } catch {
21258
21536
  return "0.0.0";
@@ -21271,14 +21549,15 @@ var stdoutLogger = {
21271
21549
  };
21272
21550
 
21273
21551
  // src/commands/init.ts
21274
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
21275
- import { dirname as dirname6, join as join10 } from "node:path";
21552
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, renameSync, rmdirSync, writeFileSync as writeFileSync6 } from "node:fs";
21553
+ import { dirname as dirname6, join as join11 } from "node:path";
21276
21554
 
21277
21555
  // src/claudeInstall.ts
21278
21556
  import { spawnSync } from "node:child_process";
21279
- import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
21280
- import { dirname as dirname5, join as join9 } from "node:path";
21557
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
21558
+ import { dirname as dirname5, join as join10 } from "node:path";
21281
21559
  var EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
21560
+ var EVENT_POST_TOOL_USE = "PostToolUse";
21282
21561
  var EVENT_STOP = "Stop";
21283
21562
  function quote(p2) {
21284
21563
  return p2.includes(" ") ? `"${p2}"` : p2;
@@ -21311,18 +21590,18 @@ function removeHook(settings, event, command) {
21311
21590
  return true;
21312
21591
  }
21313
21592
  function readSettings(path) {
21314
- if (!existsSync7(path)) return {};
21315
- return JSON.parse(readFileSync6(path, "utf-8"));
21593
+ if (!existsSync9(path)) return {};
21594
+ return JSON.parse(readFileSync8(path, "utf-8"));
21316
21595
  }
21317
21596
  function writeSettings(path, settings) {
21318
21597
  const dir = dirname5(path);
21319
- if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
21598
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
21320
21599
  let backupPath = "";
21321
- if (existsSync7(path)) {
21600
+ if (existsSync9(path)) {
21322
21601
  backupPath = `${path}.caveat-backup-${Date.now()}`;
21323
21602
  copyFileSync(path, backupPath);
21324
21603
  }
21325
- writeFileSync4(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21604
+ writeFileSync5(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21326
21605
  return backupPath;
21327
21606
  }
21328
21607
  var CLAUDE_BIN = "claude";
@@ -21381,33 +21660,38 @@ function unregisterMcp(dryRun, logger) {
21381
21660
  return { action: "skipped", detail: "not registered or removal failed" };
21382
21661
  }
21383
21662
  function installClaudeIntegration(opts) {
21384
- const settingsPath = join9(opts.claudeDir, "settings.json");
21663
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21385
21664
  const settings = readSettings(settingsPath);
21386
21665
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21666
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21387
21667
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21388
21668
  const userPromptSubmit = upsertHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21669
+ const postToolUse = upsertHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21389
21670
  const stop = upsertHook(settings, EVENT_STOP, stopCmd);
21390
21671
  let backupPath;
21391
- if (!opts.dryRun && (userPromptSubmit === "added" || stop === "added")) {
21672
+ const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
21673
+ if (!opts.dryRun && anyAdded) {
21392
21674
  const backup = writeSettings(settingsPath, settings);
21393
21675
  if (backup) backupPath = backup;
21394
21676
  } else if (opts.dryRun) {
21395
21677
  opts.logger.info(
21396
- `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit hook and ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
21678
+ `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit, ${postToolUse === "added" ? "add" : "keep"} PostToolUse, ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
21397
21679
  );
21398
21680
  }
21399
21681
  const mcp = opts.skipMcpRegistration ? { action: "skipped", detail: "skipped by caller" } : registerMcp(opts.nodePath, opts.cliScriptPath, opts.dryRun, opts.logger);
21400
- return { mcp, hooks: { userPromptSubmit, stop }, backupPath };
21682
+ return { mcp, hooks: { userPromptSubmit, postToolUse, stop }, backupPath };
21401
21683
  }
21402
21684
  function uninstallClaudeIntegration(opts) {
21403
- const settingsPath = join9(opts.claudeDir, "settings.json");
21685
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21404
21686
  const settings = readSettings(settingsPath);
21405
21687
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21688
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21406
21689
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21407
21690
  const removedUs = removeHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21691
+ const removedPt = removeHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21408
21692
  const removedStop = removeHook(settings, EVENT_STOP, stopCmd);
21409
21693
  let backupPath;
21410
- if (!opts.dryRun && (removedUs || removedStop)) {
21694
+ if (!opts.dryRun && (removedUs || removedPt || removedStop)) {
21411
21695
  const backup = writeSettings(settingsPath, settings);
21412
21696
  if (backup) backupPath = backup;
21413
21697
  }
@@ -21416,6 +21700,7 @@ function uninstallClaudeIntegration(opts) {
21416
21700
  mcp,
21417
21701
  hooks: {
21418
21702
  userPromptSubmit: removedUs ? "added" : "unchanged",
21703
+ postToolUse: removedPt ? "added" : "unchanged",
21419
21704
  stop: removedStop ? "added" : "unchanged"
21420
21705
  },
21421
21706
  backupPath
@@ -21435,22 +21720,22 @@ var KNOWLEDGE_GITIGNORE = [
21435
21720
  async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21436
21721
  ensureUserConfig(ctx.userConfigPath);
21437
21722
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
21438
- if (!existsSync8(ctx.paths.knowledgeRepo)) {
21439
- mkdirSync4(ctx.paths.knowledgeRepo, { recursive: true });
21440
- mkdirSync4(ctx.paths.entriesDir, { recursive: true });
21723
+ if (!existsSync10(ctx.paths.knowledgeRepo)) {
21724
+ mkdirSync5(ctx.paths.knowledgeRepo, { recursive: true });
21725
+ mkdirSync5(ctx.paths.entriesDir, { recursive: true });
21441
21726
  ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
21442
21727
  } else {
21443
21728
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
21444
21729
  }
21445
21730
  migrateLegacyCommunityDir(ctx);
21446
- const gitignorePath = join10(ctx.paths.knowledgeRepo, ".gitignore");
21447
- if (!existsSync8(gitignorePath)) {
21448
- writeFileSync5(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21731
+ const gitignorePath = join11(ctx.paths.knowledgeRepo, ".gitignore");
21732
+ if (!existsSync10(gitignorePath)) {
21733
+ writeFileSync6(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21449
21734
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
21450
21735
  }
21451
21736
  if (!opts.dryRun) {
21452
21737
  const dbDir = dirname6(ctx.paths.dbPath);
21453
- if (!existsSync8(dbDir)) mkdirSync4(dbDir, { recursive: true });
21738
+ if (!existsSync10(dbDir)) mkdirSync5(dbDir, { recursive: true });
21454
21739
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21455
21740
  db.close();
21456
21741
  ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
@@ -21470,7 +21755,7 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21470
21755
  return;
21471
21756
  }
21472
21757
  const result = installClaudeIntegration({
21473
- claudeDir: join10(ctx.userHome, ".claude"),
21758
+ claudeDir: join11(ctx.userHome, ".claude"),
21474
21759
  cliScriptPath,
21475
21760
  nodePath: process.execPath,
21476
21761
  dryRun: opts.dryRun,
@@ -21479,20 +21764,20 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21479
21764
  reportInstallResult(ctx, result, opts.dryRun);
21480
21765
  }
21481
21766
  function migrateLegacyCommunityDir(ctx) {
21482
- const legacy = join10(ctx.paths.knowledgeRepo, "community");
21767
+ const legacy = join11(ctx.paths.knowledgeRepo, "community");
21483
21768
  const current = ctx.paths.communityDir;
21484
21769
  if (legacy === current) return;
21485
- if (!existsSync8(legacy)) return;
21486
- if (existsSync8(current)) {
21770
+ if (!existsSync10(legacy)) return;
21771
+ if (existsSync10(current)) {
21487
21772
  ctx.logger.warn(
21488
21773
  `legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
21489
21774
  );
21490
21775
  return;
21491
21776
  }
21492
- mkdirSync4(current, { recursive: true });
21493
- for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
21777
+ mkdirSync5(current, { recursive: true });
21778
+ for (const entry of readdirSync5(legacy, { withFileTypes: true })) {
21494
21779
  if (!entry.isDirectory()) continue;
21495
- renameSync(join10(legacy, entry.name), join10(current, entry.name));
21780
+ renameSync(join11(legacy, entry.name), join11(current, entry.name));
21496
21781
  }
21497
21782
  try {
21498
21783
  rmdirSync(legacy);
@@ -21507,7 +21792,7 @@ function runUninstall(ctx, opts) {
21507
21792
  process.exit(1);
21508
21793
  }
21509
21794
  const result = uninstallClaudeIntegration({
21510
- claudeDir: join10(ctx.userHome, ".claude"),
21795
+ claudeDir: join11(ctx.userHome, ".claude"),
21511
21796
  cliScriptPath,
21512
21797
  nodePath: process.execPath,
21513
21798
  dryRun: opts.dryRun,
@@ -21544,29 +21829,29 @@ function reportInstallResult(ctx, result, dryRun) {
21544
21829
  }
21545
21830
 
21546
21831
  // src/commands/indexCmd.ts
21547
- import { existsSync as existsSync9, readdirSync as readdirSync5, mkdirSync as mkdirSync5 } from "node:fs";
21548
- import { dirname as dirname7, join as join11 } from "node:path";
21832
+ import { existsSync as existsSync11, readdirSync as readdirSync6, mkdirSync as mkdirSync6 } from "node:fs";
21833
+ import { dirname as dirname7, join as join12 } from "node:path";
21549
21834
  function runIndex(ctx, opts) {
21550
21835
  const dbDir = dirname7(ctx.paths.dbPath);
21551
- if (!existsSync9(dbDir)) mkdirSync5(dbDir, { recursive: true });
21836
+ if (!existsSync11(dbDir)) mkdirSync6(dbDir, { recursive: true });
21552
21837
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21553
21838
  try {
21554
21839
  if (opts.full) {
21555
21840
  ctx.logger.info("full rebuild: DELETE FROM entries");
21556
21841
  rebuildAll(db);
21557
21842
  }
21558
- if (existsSync9(ctx.paths.entriesDir)) {
21843
+ if (existsSync11(ctx.paths.entriesDir)) {
21559
21844
  const result = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
21560
21845
  ctx.logger.info(`own: +${result.added} ~${result.updated} -${result.deleted}`);
21561
21846
  } else {
21562
21847
  ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
21563
21848
  }
21564
- if (existsSync9(ctx.paths.communityDir)) {
21565
- for (const entry of readdirSync5(ctx.paths.communityDir, { withFileTypes: true })) {
21849
+ if (existsSync11(ctx.paths.communityDir)) {
21850
+ for (const entry of readdirSync6(ctx.paths.communityDir, { withFileTypes: true })) {
21566
21851
  if (!entry.isDirectory()) continue;
21567
21852
  const source = `community/${entry.name}`;
21568
- const root = join11(ctx.paths.communityDir, entry.name, "entries");
21569
- if (!existsSync9(root)) continue;
21853
+ const root = join12(ctx.paths.communityDir, entry.name, "entries");
21854
+ if (!existsSync11(root)) continue;
21570
21855
  const result = scanSource({ db, source, entriesRoot: root });
21571
21856
  ctx.logger.info(`${source}: +${result.added} ~${result.updated} -${result.deleted}`);
21572
21857
  }
@@ -22340,11 +22625,11 @@ var serve = (options2, listeningListener) => {
22340
22625
 
22341
22626
  // ../web/dist/context.js
22342
22627
  import { homedir as homedir2 } from "node:os";
22343
- import { join as join12 } from "node:path";
22628
+ import { join as join13 } from "node:path";
22344
22629
  function buildWebContext(overrides = {}) {
22345
22630
  const userHome = overrides.userHome ?? homedir2();
22346
22631
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
22347
- const userConfigPath = join12(userHome, ".caveatrc.json");
22632
+ const userConfigPath = join13(userHome, ".caveatrc.json");
22348
22633
  const logger = overrides.logger ?? stderrLogger;
22349
22634
  const config3 = loadConfig(userConfigPath);
22350
22635
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -30089,14 +30374,14 @@ function createDetailRoute(ctx) {
30089
30374
  }
30090
30375
 
30091
30376
  // ../web/dist/routes/community.js
30092
- import { existsSync as existsSync10, readdirSync as readdirSync6, statSync as statSync5 } from "node:fs";
30093
- import { join as join13 } from "node:path";
30377
+ import { existsSync as existsSync12, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
30378
+ import { join as join14 } from "node:path";
30094
30379
  function listCommunity(communityDir, db) {
30095
- if (!existsSync10(communityDir)) return [];
30380
+ if (!existsSync12(communityDir)) return [];
30096
30381
  const handles = [];
30097
- for (const entry of readdirSync6(communityDir, { withFileTypes: true })) {
30382
+ for (const entry of readdirSync7(communityDir, { withFileTypes: true })) {
30098
30383
  if (!entry.isDirectory()) continue;
30099
- const handlePath = join13(communityDir, entry.name);
30384
+ const handlePath = join14(communityDir, entry.name);
30100
30385
  const countRow = db.prepare("SELECT COUNT(*) AS n FROM entries WHERE source = ?").get(`community/${entry.name}`);
30101
30386
  handles.push({
30102
30387
  handle: entry.name,
@@ -44394,11 +44679,11 @@ var StdioServerTransport = class {
44394
44679
 
44395
44680
  // ../mcp/dist/context.js
44396
44681
  import { homedir as homedir3 } from "node:os";
44397
- import { join as join14 } from "node:path";
44682
+ import { join as join15 } from "node:path";
44398
44683
  function buildMcpContext(overrides = {}) {
44399
44684
  const userHome = overrides.userHome ?? homedir3();
44400
44685
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
44401
- const userConfigPath = join14(userHome, ".caveatrc.json");
44686
+ const userConfigPath = join15(userHome, ".caveatrc.json");
44402
44687
  const logger = overrides.logger ?? stderrLogger;
44403
44688
  const config3 = loadConfig(userConfigPath);
44404
44689
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -44507,13 +44792,13 @@ function handleListRecent(ctx, args) {
44507
44792
  }
44508
44793
 
44509
44794
  // ../mcp/dist/tools/pull.js
44510
- import { existsSync as existsSync11, readdirSync as readdirSync7 } from "node:fs";
44511
- import { join as join15 } from "node:path";
44795
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
44796
+ import { join as join16 } from "node:path";
44512
44797
  var pullInputShape = {};
44513
44798
  async function handlePull(ctx, _args = {}) {
44514
44799
  const pulled = [];
44515
44800
  const indexed = [];
44516
- if (existsSync11(ctx.paths.communityDir)) {
44801
+ if (existsSync13(ctx.paths.communityDir)) {
44517
44802
  const results = await communityPull({
44518
44803
  communityDir: ctx.paths.communityDir,
44519
44804
  logger: ctx.logger
@@ -44523,16 +44808,16 @@ async function handlePull(ctx, _args = {}) {
44523
44808
  }
44524
44809
  }
44525
44810
  rebuildAll(ctx.db);
44526
- if (existsSync11(ctx.paths.entriesDir)) {
44811
+ if (existsSync13(ctx.paths.entriesDir)) {
44527
44812
  const own = scanSource({ db: ctx.db, source: "own", entriesRoot: ctx.paths.entriesDir });
44528
44813
  indexed.push({ source: "own", ...own });
44529
44814
  }
44530
- if (existsSync11(ctx.paths.communityDir)) {
44531
- for (const entry of readdirSync7(ctx.paths.communityDir, { withFileTypes: true })) {
44815
+ if (existsSync13(ctx.paths.communityDir)) {
44816
+ for (const entry of readdirSync8(ctx.paths.communityDir, { withFileTypes: true })) {
44532
44817
  if (!entry.isDirectory()) continue;
44533
44818
  const source = `community/${entry.name}`;
44534
- const root = join15(ctx.paths.communityDir, entry.name, "entries");
44535
- if (!existsSync11(root)) continue;
44819
+ const root = join16(ctx.paths.communityDir, entry.name, "entries");
44820
+ if (!existsSync13(root)) continue;
44536
44821
  const scan = scanSource({ db: ctx.db, source, entriesRoot: root });
44537
44822
  indexed.push({ source, ...scan });
44538
44823
  }
@@ -44645,6 +44930,19 @@ async function runMcpServer() {
44645
44930
  }
44646
44931
 
44647
44932
  // src/commands/hookCmd.ts
44933
+ import { spawn as spawn2 } from "node:child_process";
44934
+ import { existsSync as existsSync14, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
44935
+ import { tmpdir } from "node:os";
44936
+ import { join as join17 } from "node:path";
44937
+ import { randomBytes as randomBytes3 } from "node:crypto";
44938
+ var silentLogger = {
44939
+ info: () => {
44940
+ },
44941
+ warn: () => {
44942
+ },
44943
+ error: (m) => process.stderr.write(`[caveat:hook] ${m}
44944
+ `)
44945
+ };
44648
44946
  async function readStdin() {
44649
44947
  const chunks = [];
44650
44948
  for await (const chunk of process.stdin) {
@@ -44663,7 +44961,153 @@ function parsePayload(raw2) {
44663
44961
  return {};
44664
44962
  }
44665
44963
  }
44666
- async function runHook(name) {
44964
+ function getSessionId(payload) {
44965
+ const v = payload.session_id ?? payload.sessionId;
44966
+ return typeof v === "string" && v.length > 0 ? v : "_unknown";
44967
+ }
44968
+ function buildContextSafely() {
44969
+ try {
44970
+ return buildContext(silentLogger);
44971
+ } catch (err) {
44972
+ const msg = err instanceof Error ? err.message : String(err);
44973
+ process.stderr.write(`[caveat:hook] context error: ${msg}
44974
+ `);
44975
+ return null;
44976
+ }
44977
+ }
44978
+ function searchCaveatsFromTextSafely(text2) {
44979
+ if (!text2) return [];
44980
+ let db;
44981
+ try {
44982
+ const ctx = buildContextSafely();
44983
+ if (!ctx || !existsSync14(ctx.paths.dbPath)) return [];
44984
+ db = openDb({ path: ctx.paths.dbPath });
44985
+ return findCaveatsForPrompt(db, text2);
44986
+ } catch (err) {
44987
+ const msg = err instanceof Error ? err.message : String(err);
44988
+ process.stderr.write(`[caveat:hook] search error: ${msg}
44989
+ `);
44990
+ return [];
44991
+ } finally {
44992
+ db?.close();
44993
+ }
44994
+ }
44995
+ function loadSignalsSafely(path) {
44996
+ try {
44997
+ return readSessionSignals(path);
44998
+ } catch (err) {
44999
+ const msg = err instanceof Error ? err.message : String(err);
45000
+ process.stderr.write(`[caveat:hook] transcript read error: ${msg}
45001
+ `);
45002
+ return null;
45003
+ }
45004
+ }
45005
+ function drainForSession(sessionId) {
45006
+ const ctx = buildContextSafely();
45007
+ if (!ctx) return;
45008
+ const reminders = drainPendingReminders(ctx.caveatHome, sessionId);
45009
+ for (const text2 of reminders) {
45010
+ process.stdout.write(`<system-reminder>${text2}</system-reminder>
45011
+ `);
45012
+ }
45013
+ }
45014
+ function extractToolResponseText(response) {
45015
+ if (typeof response === "string") return response;
45016
+ if (Array.isArray(response)) {
45017
+ const parts = [];
45018
+ for (const item of response) {
45019
+ if (typeof item === "string") parts.push(item);
45020
+ else if (item !== null && typeof item === "object" && typeof item.text === "string") {
45021
+ parts.push(item.text);
45022
+ }
45023
+ }
45024
+ return parts.join(" ");
45025
+ }
45026
+ if (response !== null && typeof response === "object") {
45027
+ const r2 = response;
45028
+ if (typeof r2.content === "string") return r2.content;
45029
+ if (Array.isArray(r2.content)) return extractToolResponseText(r2.content);
45030
+ if (typeof r2.output === "string") return r2.output;
45031
+ if (typeof r2.stdout === "string" || typeof r2.stderr === "string") {
45032
+ return [r2.stdout, r2.stderr].filter((x2) => typeof x2 === "string").join(" ");
45033
+ }
45034
+ }
45035
+ return "";
45036
+ }
45037
+ function isToolError(payload) {
45038
+ const resp = payload.tool_response ?? payload.toolResponse;
45039
+ if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
45040
+ if (resp.is_error === true) return true;
45041
+ }
45042
+ if (payload.is_error === true) return true;
45043
+ return false;
45044
+ }
45045
+ function spawnWorker(job) {
45046
+ const workFile = join17(
45047
+ tmpdir(),
45048
+ `caveat-worker-${Date.now()}-${randomBytes3(4).toString("hex")}.json`
45049
+ );
45050
+ try {
45051
+ writeFileSync7(workFile, JSON.stringify(job), "utf-8");
45052
+ } catch (err) {
45053
+ const msg = err instanceof Error ? err.message : String(err);
45054
+ process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
45055
+ `);
45056
+ return;
45057
+ }
45058
+ const cliScript = process.argv[1];
45059
+ if (!cliScript) return;
45060
+ try {
45061
+ const child = spawn2(
45062
+ process.execPath,
45063
+ ["--disable-warning=ExperimentalWarning", cliScript, "hook", "worker", workFile],
45064
+ { detached: true, stdio: "ignore", windowsHide: true }
45065
+ );
45066
+ child.unref();
45067
+ } catch (err) {
45068
+ const msg = err instanceof Error ? err.message : String(err);
45069
+ process.stderr.write(`[caveat:hook] worker spawn error: ${msg}
45070
+ `);
45071
+ try {
45072
+ unlinkSync2(workFile);
45073
+ } catch {
45074
+ }
45075
+ }
45076
+ }
45077
+ async function runWorker(workFile) {
45078
+ let raw2;
45079
+ try {
45080
+ raw2 = readFileSync9(workFile, "utf-8");
45081
+ } catch {
45082
+ process.exit(0);
45083
+ }
45084
+ try {
45085
+ unlinkSync2(workFile);
45086
+ } catch {
45087
+ }
45088
+ let job;
45089
+ try {
45090
+ job = JSON.parse(raw2);
45091
+ } catch {
45092
+ process.exit(0);
45093
+ }
45094
+ if (!job.searchText || !job.sessionId) process.exit(0);
45095
+ const hits = searchCaveatsFromTextSafely(job.searchText);
45096
+ if (hits.length === 0) process.exit(0);
45097
+ const ctx = buildContextSafely();
45098
+ if (!ctx) process.exit(0);
45099
+ try {
45100
+ appendPendingReminder(ctx.caveatHome, job.sessionId, toolErrorReminderText(hits));
45101
+ } catch {
45102
+ }
45103
+ process.exit(0);
45104
+ }
45105
+ async function runHook(name, arg) {
45106
+ if (name === "worker") {
45107
+ if (!arg) process.exit(0);
45108
+ await runWorker(arg);
45109
+ return;
45110
+ }
44667
45111
  let raw2 = "";
44668
45112
  try {
44669
45113
  raw2 = await readStdin();
@@ -44674,22 +45118,37 @@ async function runHook(name) {
44674
45118
  process.exit(0);
44675
45119
  }
44676
45120
  const payload = parsePayload(raw2);
45121
+ const sessionId = getSessionId(payload);
45122
+ drainForSession(sessionId);
44677
45123
  if (name === "user-prompt-submit") {
44678
45124
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
44679
- if (detectCaveatTrigger(prompt)) {
45125
+ const hits = searchCaveatsFromTextSafely(prompt);
45126
+ if (hits.length > 0) {
44680
45127
  process.stdout.write(
44681
- `<system-reminder>${userPromptSubmitReminderText()}</system-reminder>
45128
+ `<system-reminder>${userPromptSubmitReminderText(hits)}</system-reminder>
44682
45129
  `
44683
45130
  );
44684
45131
  }
44685
45132
  process.exit(0);
44686
45133
  }
44687
- if (name === "stop") {
44688
- if (payload.stop_hook_active === true) {
44689
- process.exit(0);
45134
+ if (name === "post-tool-use") {
45135
+ if (!isToolError(payload)) process.exit(0);
45136
+ const errText = extractToolResponseText(payload.tool_response ?? payload);
45137
+ if (errText) {
45138
+ spawnWorker({ sessionId, searchText: errText });
44690
45139
  }
44691
- process.stdout.write(`<system-reminder>${stopReminderText()}</system-reminder>
44692
- `);
45140
+ process.exit(0);
45141
+ }
45142
+ if (name === "stop") {
45143
+ if (payload.stop_hook_active === true) process.exit(0);
45144
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
45145
+ const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
45146
+ if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
45147
+ const related = searchCaveatsFromTextSafely(struggleSearchText(signals));
45148
+ process.stdout.write(
45149
+ `<system-reminder>${stopReminderText(signals, related)}</system-reminder>
45150
+ `
45151
+ );
44693
45152
  process.exit(0);
44694
45153
  }
44695
45154
  process.stderr.write(`[caveat:hook] unknown hook name: ${name}
@@ -44698,10 +45157,10 @@ async function runHook(name) {
44698
45157
  }
44699
45158
 
44700
45159
  // src/commands/pull.ts
44701
- import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
44702
- import { join as join16 } from "node:path";
45160
+ import { existsSync as existsSync15, readdirSync as readdirSync9 } from "node:fs";
45161
+ import { join as join18 } from "node:path";
44703
45162
  async function runPull(ctx) {
44704
- if (!existsSync12(ctx.paths.communityDir)) {
45163
+ if (!existsSync15(ctx.paths.communityDir)) {
44705
45164
  ctx.logger.info(
44706
45165
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
44707
45166
  );
@@ -44721,15 +45180,15 @@ async function runPull(ctx) {
44721
45180
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
44722
45181
  try {
44723
45182
  rebuildAll(db);
44724
- if (existsSync12(ctx.paths.entriesDir)) {
45183
+ if (existsSync15(ctx.paths.entriesDir)) {
44725
45184
  const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
44726
45185
  ctx.logger.info(`own: +${own.added}`);
44727
45186
  }
44728
- for (const entry of readdirSync8(ctx.paths.communityDir, { withFileTypes: true })) {
45187
+ for (const entry of readdirSync9(ctx.paths.communityDir, { withFileTypes: true })) {
44729
45188
  if (!entry.isDirectory()) continue;
44730
45189
  const source = `community/${entry.name}`;
44731
- const root = join16(ctx.paths.communityDir, entry.name, "entries");
44732
- if (!existsSync12(root)) continue;
45190
+ const root = join18(ctx.paths.communityDir, entry.name, "entries");
45191
+ if (!existsSync15(root)) continue;
44733
45192
  const scan = scanSource({ db, source, entriesRoot: root });
44734
45193
  ctx.logger.info(`${source}: +${scan.added}`);
44735
45194
  }
@@ -44873,8 +45332,10 @@ program.command("serve").description("Start the read-only web share portal").opt
44873
45332
  program.command("mcp-server").description("Run the MCP stdio server (registered by `caveat init`)").action(async () => {
44874
45333
  await runMcpServer();
44875
45334
  });
44876
- program.command("hook <name>").description("Run a Claude Code hook. name: user-prompt-submit | stop").action(async (name) => {
44877
- await runHook(name);
45335
+ program.command("hook <name> [arg]").description(
45336
+ "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker"
45337
+ ).action(async (name, arg) => {
45338
+ await runHook(name, arg);
44878
45339
  });
44879
45340
  var community = program.command("community").description("Manage community caveat repos (shallow clones under <knowledgeRepo>/community/)");
44880
45341
  community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url) => {