caveat-cli 0.7.0 → 0.11.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,383 @@ 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
+ const externalLookup = signals.webSearchCount + signals.webFetchCount > 0;
21343
+ lines.push(
21344
+ `- \u5206\u985E\u30D2\u30F3\u30C8: ${externalLookup ? "\u5916\u90E8\u4ED5\u69D8\u8ABF\u67FB\u3042\u308A \u2192 public \u5BC4\u308A" : "\u5916\u90E8\u8ABF\u67FB\u306A\u3057 \u2192 private \u5BC4\u308A"}`
21345
+ );
21346
+ lines.push("");
21347
+ if (related.length > 0) {
21348
+ lines.push(
21349
+ `\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:`
21350
+ );
21351
+ related.forEach((h2, i2) => {
21352
+ lines.push(`${i2 + 1}. ${h2.id} (${h2.source}) \u2014 ${h2.title}`);
21353
+ });
21354
+ lines.push("");
21355
+ lines.push(
21356
+ "\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"
21357
+ );
21358
+ } else {
21359
+ lines.push(
21360
+ "\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"
21361
+ );
21362
+ }
21363
+ lines.push(
21364
+ "\u8A18\u9332\u6642\u306F tool \u8AAC\u660E\u306E\u4E8C\u9805\u57FA\u6E96\u3067 visibility \u3092\u9078\u3076\uFF08public = \u7B2C\u4E09\u8005\u518D\u73FE\u53EF\u80FD / private = repo \u56FA\u6709\uFF09\u3002\u8FF7\u3063\u305F\u3089 private\u3002"
21365
+ );
21366
+ return lines.join("\n");
21367
+ }
21368
+
21369
+ // ../../packages/core/dist/transcriptSignals.js
21370
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
21371
+ var MAX_ERROR_SNIPPETS = 10;
21372
+ var MAX_ERROR_SNIPPET_LENGTH = 300;
21373
+ var MAX_SEARCH_QUERIES = 10;
21374
+ var MAX_SEARCH_QUERY_LENGTH = 200;
21375
+ var MAX_FILE_EDIT_ENTRIES = 20;
21376
+ function isRecord(v) {
21377
+ return typeof v === "object" && v !== null && !Array.isArray(v);
21378
+ }
21379
+ function extractResultText(content) {
21380
+ if (typeof content === "string") return content;
21381
+ if (Array.isArray(content)) {
21382
+ const parts = [];
21383
+ for (const c3 of content) {
21384
+ if (isRecord(c3) && typeof c3.text === "string") parts.push(c3.text);
21385
+ }
21386
+ return parts.join(" ");
21387
+ }
21388
+ return "";
21222
21389
  }
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");
21390
+ function parseTimestamp(raw2) {
21391
+ if (typeof raw2 !== "string") return void 0;
21392
+ const ms = Date.parse(raw2);
21393
+ return Number.isNaN(ms) ? void 0 : ms;
21229
21394
  }
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");
21395
+ function readSessionSignals(transcriptPath) {
21396
+ if (!transcriptPath || !existsSync7(transcriptPath)) return null;
21397
+ let raw2;
21398
+ try {
21399
+ raw2 = readFileSync5(transcriptPath, "utf-8");
21400
+ } catch {
21401
+ return null;
21402
+ }
21403
+ const editCounts = /* @__PURE__ */ new Map();
21404
+ const bashCounts = /* @__PURE__ */ new Map();
21405
+ const errorSnippets = [];
21406
+ const searchQueries = [];
21407
+ let toolFailureCount = 0;
21408
+ let webSearchCount = 0;
21409
+ let webFetchCount = 0;
21410
+ let firstTs;
21411
+ let lastTs;
21412
+ for (const line of raw2.split("\n")) {
21413
+ if (line.length === 0) continue;
21414
+ let parsed;
21415
+ try {
21416
+ parsed = JSON.parse(line);
21417
+ } catch {
21418
+ continue;
21419
+ }
21420
+ const ts = parseTimestamp(parsed.timestamp);
21421
+ if (ts !== void 0) {
21422
+ if (firstTs === void 0 || ts < firstTs) firstTs = ts;
21423
+ if (lastTs === void 0 || ts > lastTs) lastTs = ts;
21424
+ }
21425
+ if (parsed.type !== "assistant" && parsed.type !== "user") continue;
21426
+ const content = parsed.message?.content;
21427
+ if (!Array.isArray(content)) continue;
21428
+ for (const item of content) {
21429
+ if (!isRecord(item)) continue;
21430
+ if (item.type === "tool_use") {
21431
+ const name = item.name;
21432
+ const input = isRecord(item.input) ? item.input : {};
21433
+ if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
21434
+ const p2 = typeof input.file_path === "string" ? input.file_path : "";
21435
+ if (p2) editCounts.set(p2, (editCounts.get(p2) ?? 0) + 1);
21436
+ } else if (name === "WebSearch") {
21437
+ webSearchCount += 1;
21438
+ if (typeof input.query === "string" && searchQueries.length < MAX_SEARCH_QUERIES) {
21439
+ searchQueries.push(input.query.slice(0, MAX_SEARCH_QUERY_LENGTH));
21440
+ }
21441
+ } else if (name === "WebFetch") {
21442
+ webFetchCount += 1;
21443
+ } else if (name === "Bash") {
21444
+ const cmd = typeof input.command === "string" ? input.command : "";
21445
+ if (cmd) bashCounts.set(cmd, (bashCounts.get(cmd) ?? 0) + 1);
21446
+ }
21447
+ } else if (item.type === "tool_result") {
21448
+ if (item.is_error === true) {
21449
+ toolFailureCount += 1;
21450
+ const text2 = extractResultText(item.content).replace(/\s+/g, " ").trim();
21451
+ if (text2 && errorSnippets.length < MAX_ERROR_SNIPPETS) {
21452
+ errorSnippets.push(text2.slice(0, MAX_ERROR_SNIPPET_LENGTH));
21453
+ }
21454
+ }
21455
+ }
21456
+ }
21457
+ }
21458
+ 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);
21459
+ const bashRetryCount = [...bashCounts.values()].filter((c3) => c3 > 1).length;
21460
+ const durationMinutes = firstTs !== void 0 && lastTs !== void 0 ? Math.max(0, Math.round((lastTs - firstTs) / 6e4)) : 0;
21461
+ return {
21462
+ toolFailureCount,
21463
+ fileEditCounts,
21464
+ webSearchCount,
21465
+ webFetchCount,
21466
+ bashRetryCount,
21467
+ durationMinutes,
21468
+ errorSnippets,
21469
+ searchQueries
21470
+ };
21471
+ }
21472
+ function hasAnyStruggleSignal(s) {
21473
+ return s.toolFailureCount > 0 || s.fileEditCounts.length > 0 || s.webSearchCount > 0 || s.webFetchCount > 0 || s.bashRetryCount > 0;
21474
+ }
21475
+ function struggleSearchText(s) {
21476
+ return [...s.errorSnippets, ...s.searchQueries].join(" ");
21477
+ }
21478
+
21479
+ // ../../packages/core/dist/pendingReminders.js
21480
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
21481
+ import { join as join7 } from "node:path";
21482
+ import { randomBytes as randomBytes2 } from "node:crypto";
21483
+ function sanitizeSessionId(raw2) {
21484
+ const clean = raw2.replace(/[^A-Za-z0-9_-]/g, "");
21485
+ return clean.length > 0 ? clean : "_unknown";
21486
+ }
21487
+ function pendingDirFor(caveatHome, sessionId) {
21488
+ return join7(caveatHome, "pending", sanitizeSessionId(sessionId));
21489
+ }
21490
+ function appendPendingReminder(caveatHome, sessionId, text2) {
21491
+ const dir = pendingDirFor(caveatHome, sessionId);
21492
+ mkdirSync3(dir, { recursive: true });
21493
+ const name = `${Date.now()}-${randomBytes2(4).toString("hex")}.txt`;
21494
+ const path = join7(dir, name);
21495
+ writeFileSync4(path, text2, "utf-8");
21496
+ return path;
21497
+ }
21498
+ function drainPendingReminders(caveatHome, sessionId) {
21499
+ const dir = pendingDirFor(caveatHome, sessionId);
21500
+ if (!existsSync8(dir)) return [];
21501
+ let entries;
21502
+ try {
21503
+ entries = readdirSync4(dir).filter((f) => f.endsWith(".txt")).sort();
21504
+ } catch {
21505
+ return [];
21506
+ }
21507
+ const out = [];
21508
+ for (const entry of entries) {
21509
+ const path = join7(dir, entry);
21510
+ try {
21511
+ out.push(readFileSync6(path, "utf-8"));
21512
+ } catch {
21513
+ continue;
21514
+ }
21515
+ try {
21516
+ unlinkSync(path);
21517
+ } catch {
21518
+ }
21519
+ }
21520
+ return out;
21521
+ }
21522
+
21523
+ // ../../packages/core/dist/markHit.js
21524
+ function markHit(db, keys, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
21525
+ if (keys.length === 0) return;
21526
+ const ts = now();
21527
+ const stmt = db.prepare(
21528
+ "UPDATE entries SET last_hit_at = ? WHERE source = ? AND id = ?"
21529
+ );
21530
+ for (const k2 of keys) {
21531
+ stmt.run(ts, k2.source, k2.id);
21532
+ }
21533
+ }
21534
+
21535
+ // ../../packages/core/dist/stale.js
21536
+ function listStale(db, opts = {}) {
21537
+ const days = opts.days ?? 90;
21538
+ const limit = opts.limit ?? 50;
21539
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
21540
+ const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3).toISOString();
21541
+ const conditions = ["(last_hit_at IS NULL OR last_hit_at < ?)"];
21542
+ const params = [cutoff];
21543
+ if (opts.visibility === "public" || opts.visibility === "private") {
21544
+ conditions.push("visibility = ?");
21545
+ params.push(opts.visibility);
21546
+ }
21547
+ const sql = `
21548
+ SELECT id, source, title, visibility, last_hit_at
21549
+ FROM entries
21550
+ WHERE ${conditions.join(" AND ")}
21551
+ ORDER BY last_hit_at IS NULL DESC, last_hit_at ASC
21552
+ LIMIT ?
21553
+ `;
21554
+ params.push(limit);
21555
+ const rows = db.prepare(sql).all(...params);
21556
+ return rows.map((r2) => ({
21557
+ id: r2.id,
21558
+ source: r2.source,
21559
+ title: r2.title,
21560
+ visibility: r2.visibility ?? "public",
21561
+ last_hit_at: r2.last_hit_at
21562
+ }));
21236
21563
  }
21237
21564
 
21238
21565
  // src/context.ts
21239
21566
  function buildContext(logger, overrides = {}) {
21240
21567
  const userHome = overrides.userHome ?? homedir();
21241
21568
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
21242
- const userConfigPath = join7(userHome, ".caveatrc.json");
21569
+ const userConfigPath = join8(userHome, ".caveatrc.json");
21243
21570
  const config3 = loadConfig(userConfigPath);
21244
21571
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
21245
21572
  return { caveatHome, userHome, userConfigPath, config: config3, paths, logger };
21246
21573
  }
21247
21574
 
21248
21575
  // src/version.ts
21249
- import { readFileSync as readFileSync5 } from "node:fs";
21250
- import { dirname as dirname4, join as join8 } from "node:path";
21576
+ import { readFileSync as readFileSync7 } from "node:fs";
21577
+ import { dirname as dirname4, join as join9 } from "node:path";
21251
21578
  import { fileURLToPath as fileURLToPath3 } from "node:url";
21252
21579
  function resolveVersion() {
21253
21580
  try {
21254
21581
  const here2 = dirname4(fileURLToPath3(import.meta.url));
21255
- const pkg = JSON.parse(readFileSync5(join8(here2, "..", "package.json"), "utf-8"));
21582
+ const pkg = JSON.parse(readFileSync7(join9(here2, "..", "package.json"), "utf-8"));
21256
21583
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
21257
21584
  } catch {
21258
21585
  return "0.0.0";
@@ -21271,14 +21598,15 @@ var stdoutLogger = {
21271
21598
  };
21272
21599
 
21273
21600
  // 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";
21601
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync5, renameSync, rmdirSync, writeFileSync as writeFileSync6 } from "node:fs";
21602
+ import { dirname as dirname6, join as join11 } from "node:path";
21276
21603
 
21277
21604
  // src/claudeInstall.ts
21278
21605
  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";
21606
+ import { copyFileSync, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
21607
+ import { dirname as dirname5, join as join10 } from "node:path";
21281
21608
  var EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
21609
+ var EVENT_POST_TOOL_USE = "PostToolUse";
21282
21610
  var EVENT_STOP = "Stop";
21283
21611
  function quote(p2) {
21284
21612
  return p2.includes(" ") ? `"${p2}"` : p2;
@@ -21311,18 +21639,18 @@ function removeHook(settings, event, command) {
21311
21639
  return true;
21312
21640
  }
21313
21641
  function readSettings(path) {
21314
- if (!existsSync7(path)) return {};
21315
- return JSON.parse(readFileSync6(path, "utf-8"));
21642
+ if (!existsSync9(path)) return {};
21643
+ return JSON.parse(readFileSync8(path, "utf-8"));
21316
21644
  }
21317
21645
  function writeSettings(path, settings) {
21318
21646
  const dir = dirname5(path);
21319
- if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
21647
+ if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
21320
21648
  let backupPath = "";
21321
- if (existsSync7(path)) {
21649
+ if (existsSync9(path)) {
21322
21650
  backupPath = `${path}.caveat-backup-${Date.now()}`;
21323
21651
  copyFileSync(path, backupPath);
21324
21652
  }
21325
- writeFileSync4(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21653
+ writeFileSync5(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21326
21654
  return backupPath;
21327
21655
  }
21328
21656
  var CLAUDE_BIN = "claude";
@@ -21381,33 +21709,38 @@ function unregisterMcp(dryRun, logger) {
21381
21709
  return { action: "skipped", detail: "not registered or removal failed" };
21382
21710
  }
21383
21711
  function installClaudeIntegration(opts) {
21384
- const settingsPath = join9(opts.claudeDir, "settings.json");
21712
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21385
21713
  const settings = readSettings(settingsPath);
21386
21714
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21715
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21387
21716
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21388
21717
  const userPromptSubmit = upsertHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21718
+ const postToolUse = upsertHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21389
21719
  const stop = upsertHook(settings, EVENT_STOP, stopCmd);
21390
21720
  let backupPath;
21391
- if (!opts.dryRun && (userPromptSubmit === "added" || stop === "added")) {
21721
+ const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
21722
+ if (!opts.dryRun && anyAdded) {
21392
21723
  const backup = writeSettings(settingsPath, settings);
21393
21724
  if (backup) backupPath = backup;
21394
21725
  } else if (opts.dryRun) {
21395
21726
  opts.logger.info(
21396
- `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit hook and ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
21727
+ `[dry-run] would ${userPromptSubmit === "added" ? "add" : "keep"} UserPromptSubmit, ${postToolUse === "added" ? "add" : "keep"} PostToolUse, ${stop === "added" ? "add" : "keep"} Stop hook in ${settingsPath}`
21397
21728
  );
21398
21729
  }
21399
21730
  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 };
21731
+ return { mcp, hooks: { userPromptSubmit, postToolUse, stop }, backupPath };
21401
21732
  }
21402
21733
  function uninstallClaudeIntegration(opts) {
21403
- const settingsPath = join9(opts.claudeDir, "settings.json");
21734
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21404
21735
  const settings = readSettings(settingsPath);
21405
21736
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21737
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21406
21738
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21407
21739
  const removedUs = removeHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21740
+ const removedPt = removeHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21408
21741
  const removedStop = removeHook(settings, EVENT_STOP, stopCmd);
21409
21742
  let backupPath;
21410
- if (!opts.dryRun && (removedUs || removedStop)) {
21743
+ if (!opts.dryRun && (removedUs || removedPt || removedStop)) {
21411
21744
  const backup = writeSettings(settingsPath, settings);
21412
21745
  if (backup) backupPath = backup;
21413
21746
  }
@@ -21416,6 +21749,7 @@ function uninstallClaudeIntegration(opts) {
21416
21749
  mcp,
21417
21750
  hooks: {
21418
21751
  userPromptSubmit: removedUs ? "added" : "unchanged",
21752
+ postToolUse: removedPt ? "added" : "unchanged",
21419
21753
  stop: removedStop ? "added" : "unchanged"
21420
21754
  },
21421
21755
  backupPath
@@ -21435,22 +21769,22 @@ var KNOWLEDGE_GITIGNORE = [
21435
21769
  async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21436
21770
  ensureUserConfig(ctx.userConfigPath);
21437
21771
  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 });
21772
+ if (!existsSync10(ctx.paths.knowledgeRepo)) {
21773
+ mkdirSync5(ctx.paths.knowledgeRepo, { recursive: true });
21774
+ mkdirSync5(ctx.paths.entriesDir, { recursive: true });
21441
21775
  ctx.logger.info(`knowledge repo scaffolded: ${ctx.paths.knowledgeRepo}`);
21442
21776
  } else {
21443
21777
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
21444
21778
  }
21445
21779
  migrateLegacyCommunityDir(ctx);
21446
- const gitignorePath = join10(ctx.paths.knowledgeRepo, ".gitignore");
21447
- if (!existsSync8(gitignorePath)) {
21448
- writeFileSync5(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21780
+ const gitignorePath = join11(ctx.paths.knowledgeRepo, ".gitignore");
21781
+ if (!existsSync10(gitignorePath)) {
21782
+ writeFileSync6(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21449
21783
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
21450
21784
  }
21451
21785
  if (!opts.dryRun) {
21452
21786
  const dbDir = dirname6(ctx.paths.dbPath);
21453
- if (!existsSync8(dbDir)) mkdirSync4(dbDir, { recursive: true });
21787
+ if (!existsSync10(dbDir)) mkdirSync5(dbDir, { recursive: true });
21454
21788
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21455
21789
  db.close();
21456
21790
  ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
@@ -21470,7 +21804,7 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21470
21804
  return;
21471
21805
  }
21472
21806
  const result = installClaudeIntegration({
21473
- claudeDir: join10(ctx.userHome, ".claude"),
21807
+ claudeDir: join11(ctx.userHome, ".claude"),
21474
21808
  cliScriptPath,
21475
21809
  nodePath: process.execPath,
21476
21810
  dryRun: opts.dryRun,
@@ -21479,20 +21813,20 @@ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21479
21813
  reportInstallResult(ctx, result, opts.dryRun);
21480
21814
  }
21481
21815
  function migrateLegacyCommunityDir(ctx) {
21482
- const legacy = join10(ctx.paths.knowledgeRepo, "community");
21816
+ const legacy = join11(ctx.paths.knowledgeRepo, "community");
21483
21817
  const current = ctx.paths.communityDir;
21484
21818
  if (legacy === current) return;
21485
- if (!existsSync8(legacy)) return;
21486
- if (existsSync8(current)) {
21819
+ if (!existsSync10(legacy)) return;
21820
+ if (existsSync10(current)) {
21487
21821
  ctx.logger.warn(
21488
21822
  `legacy community dir still exists at ${legacy} \u2014 remove manually (new location in use)`
21489
21823
  );
21490
21824
  return;
21491
21825
  }
21492
- mkdirSync4(current, { recursive: true });
21493
- for (const entry of readdirSync4(legacy, { withFileTypes: true })) {
21826
+ mkdirSync5(current, { recursive: true });
21827
+ for (const entry of readdirSync5(legacy, { withFileTypes: true })) {
21494
21828
  if (!entry.isDirectory()) continue;
21495
- renameSync(join10(legacy, entry.name), join10(current, entry.name));
21829
+ renameSync(join11(legacy, entry.name), join11(current, entry.name));
21496
21830
  }
21497
21831
  try {
21498
21832
  rmdirSync(legacy);
@@ -21507,7 +21841,7 @@ function runUninstall(ctx, opts) {
21507
21841
  process.exit(1);
21508
21842
  }
21509
21843
  const result = uninstallClaudeIntegration({
21510
- claudeDir: join10(ctx.userHome, ".claude"),
21844
+ claudeDir: join11(ctx.userHome, ".claude"),
21511
21845
  cliScriptPath,
21512
21846
  nodePath: process.execPath,
21513
21847
  dryRun: opts.dryRun,
@@ -21544,29 +21878,29 @@ function reportInstallResult(ctx, result, dryRun) {
21544
21878
  }
21545
21879
 
21546
21880
  // 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";
21881
+ import { existsSync as existsSync11, readdirSync as readdirSync6, mkdirSync as mkdirSync6 } from "node:fs";
21882
+ import { dirname as dirname7, join as join12 } from "node:path";
21549
21883
  function runIndex(ctx, opts) {
21550
21884
  const dbDir = dirname7(ctx.paths.dbPath);
21551
- if (!existsSync9(dbDir)) mkdirSync5(dbDir, { recursive: true });
21885
+ if (!existsSync11(dbDir)) mkdirSync6(dbDir, { recursive: true });
21552
21886
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21553
21887
  try {
21554
21888
  if (opts.full) {
21555
21889
  ctx.logger.info("full rebuild: DELETE FROM entries");
21556
21890
  rebuildAll(db);
21557
21891
  }
21558
- if (existsSync9(ctx.paths.entriesDir)) {
21892
+ if (existsSync11(ctx.paths.entriesDir)) {
21559
21893
  const result = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
21560
21894
  ctx.logger.info(`own: +${result.added} ~${result.updated} -${result.deleted}`);
21561
21895
  } else {
21562
21896
  ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
21563
21897
  }
21564
- if (existsSync9(ctx.paths.communityDir)) {
21565
- for (const entry of readdirSync5(ctx.paths.communityDir, { withFileTypes: true })) {
21898
+ if (existsSync11(ctx.paths.communityDir)) {
21899
+ for (const entry of readdirSync6(ctx.paths.communityDir, { withFileTypes: true })) {
21566
21900
  if (!entry.isDirectory()) continue;
21567
21901
  const source = `community/${entry.name}`;
21568
- const root = join11(ctx.paths.communityDir, entry.name, "entries");
21569
- if (!existsSync9(root)) continue;
21902
+ const root = join12(ctx.paths.communityDir, entry.name, "entries");
21903
+ if (!existsSync11(root)) continue;
21570
21904
  const result = scanSource({ db, source, entriesRoot: root });
21571
21905
  ctx.logger.info(`${source}: +${result.added} ~${result.updated} -${result.deleted}`);
21572
21906
  }
@@ -21621,6 +21955,29 @@ function runList(ctx, opts) {
21621
21955
  }
21622
21956
  }
21623
21957
 
21958
+ // src/commands/stale.ts
21959
+ function runStale(ctx, opts) {
21960
+ const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21961
+ try {
21962
+ const rows = listStale(db, {
21963
+ days: opts.days,
21964
+ visibility: opts.visibility,
21965
+ limit: opts.limit
21966
+ });
21967
+ if (rows.length === 0) {
21968
+ process.stdout.write("(no stale entries)\n");
21969
+ return;
21970
+ }
21971
+ for (const r2 of rows) {
21972
+ const age = r2.last_hit_at ?? "never";
21973
+ process.stdout.write(`${r2.id} [${r2.source}] (${r2.visibility}) ${age} \u2014 ${r2.title}
21974
+ `);
21975
+ }
21976
+ } finally {
21977
+ db.close();
21978
+ }
21979
+ }
21980
+
21624
21981
  // src/commands/show.ts
21625
21982
  function runShow(ctx, opts) {
21626
21983
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
@@ -22340,11 +22697,11 @@ var serve = (options2, listeningListener) => {
22340
22697
 
22341
22698
  // ../web/dist/context.js
22342
22699
  import { homedir as homedir2 } from "node:os";
22343
- import { join as join12 } from "node:path";
22700
+ import { join as join13 } from "node:path";
22344
22701
  function buildWebContext(overrides = {}) {
22345
22702
  const userHome = overrides.userHome ?? homedir2();
22346
22703
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
22347
- const userConfigPath = join12(userHome, ".caveatrc.json");
22704
+ const userConfigPath = join13(userHome, ".caveatrc.json");
22348
22705
  const logger = overrides.logger ?? stderrLogger;
22349
22706
  const config3 = loadConfig(userConfigPath);
22350
22707
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -30089,14 +30446,14 @@ function createDetailRoute(ctx) {
30089
30446
  }
30090
30447
 
30091
30448
  // ../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";
30449
+ import { existsSync as existsSync12, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
30450
+ import { join as join14 } from "node:path";
30094
30451
  function listCommunity(communityDir, db) {
30095
- if (!existsSync10(communityDir)) return [];
30452
+ if (!existsSync12(communityDir)) return [];
30096
30453
  const handles = [];
30097
- for (const entry of readdirSync6(communityDir, { withFileTypes: true })) {
30454
+ for (const entry of readdirSync7(communityDir, { withFileTypes: true })) {
30098
30455
  if (!entry.isDirectory()) continue;
30099
- const handlePath = join13(communityDir, entry.name);
30456
+ const handlePath = join14(communityDir, entry.name);
30100
30457
  const countRow = db.prepare("SELECT COUNT(*) AS n FROM entries WHERE source = ?").get(`community/${entry.name}`);
30101
30458
  handles.push({
30102
30459
  handle: entry.name,
@@ -44394,11 +44751,11 @@ var StdioServerTransport = class {
44394
44751
 
44395
44752
  // ../mcp/dist/context.js
44396
44753
  import { homedir as homedir3 } from "node:os";
44397
- import { join as join14 } from "node:path";
44754
+ import { join as join15 } from "node:path";
44398
44755
  function buildMcpContext(overrides = {}) {
44399
44756
  const userHome = overrides.userHome ?? homedir3();
44400
44757
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
44401
- const userConfigPath = join14(userHome, ".caveatrc.json");
44758
+ const userConfigPath = join15(userHome, ".caveatrc.json");
44402
44759
  const logger = overrides.logger ?? stderrLogger;
44403
44760
  const config3 = loadConfig(userConfigPath);
44404
44761
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -44409,12 +44766,24 @@ function buildMcpContext(overrides = {}) {
44409
44766
  // ../mcp/dist/tools/search.js
44410
44767
  var sourceFilter = external_exports.enum(["own", "community", "all"]);
44411
44768
  var confidenceSchema = external_exports.enum(["confirmed", "reproduced", "tentative"]);
44769
+ var visibilityFilter = external_exports.enum(["public", "private", "all"]);
44412
44770
  var searchInputShape = {
44413
44771
  query: external_exports.string().describe("FTS query (3+ chars for trigram). Empty string lists without text filter."),
44414
44772
  filters: external_exports.object({
44415
44773
  tags: external_exports.array(external_exports.string()).optional(),
44416
44774
  confidence: external_exports.array(confidenceSchema).optional(),
44417
- source: sourceFilter.optional()
44775
+ source: sourceFilter.optional(),
44776
+ visibility: visibilityFilter.optional().describe(
44777
+ [
44778
+ "Narrow by publish tier.",
44779
+ "'public' = external-spec gotchas reproducible by any third party (PyInstaller, Stripe, Claude Code hook behavior, etc).",
44780
+ "'private' = your own cross-project notes (repo-specific, your workflow, intentional non-standard design).",
44781
+ "'all' (or omit) = both tiers.",
44782
+ "Use 'public' when drafting externally-visible output (PR descriptions, public docs, answers to third parties) so private notes do not bleed into external content.",
44783
+ "Use 'private' when specifically recalling your own past decisions.",
44784
+ "Default to omitting this filter \u2014 narrowing too aggressively hides relevant entries."
44785
+ ].join(" ")
44786
+ )
44418
44787
  }).optional(),
44419
44788
  limit: external_exports.number().int().min(1).max(200).optional()
44420
44789
  };
@@ -44424,6 +44793,7 @@ function handleSearch(ctx, args) {
44424
44793
  filters: args.filters,
44425
44794
  limit: args.limit
44426
44795
  });
44796
+ if (results.length > 0) markHit(ctx.db, results);
44427
44797
  return results;
44428
44798
  }
44429
44799
 
@@ -44455,7 +44825,14 @@ var recordInputShape = {
44455
44825
  confidence: confidenceSchema2.optional(),
44456
44826
  outcome: outcomeSchema.optional(),
44457
44827
  visibility: visibilitySchema.describe(
44458
- 'REQUIRED. Ask the user "public (share to community) or private (keep local only)?" before calling \u2014 never auto-classify. The user owns the knowledge; they decide its reach.'
44828
+ [
44829
+ "REQUIRED. Classify using this binary criterion:",
44830
+ "- 'public' if a third party running the same external tool/spec could reproduce this gotcha (external-spec trap, e.g. PyInstaller/Stripe/Podman/Claude Code hook behavior).",
44831
+ "- 'private' if it is specific to your repo, your workflow, an intentional non-standard design, or context that only exists in this project.",
44832
+ "- When unclear, prefer 'private' (leak-safety).",
44833
+ `Exception: if the user explicitly asks to record it as private/public (e.g. "save this as private", "\u3053\u308C\u306F\u81EA\u5206\u7528\u306B\u30E1\u30E2\u3057\u3066"), follow the user's instruction regardless of the criterion \u2014 explicit user intent overrides auto-classification.`,
44834
+ "When recording with visibility: 'private', always include repo-specific identifiers (function names, file paths, class names, custom terminology) in the body so the entry can be retrieved later by co-occurrence FTS when you touch that area again."
44835
+ ].join(" ")
44459
44836
  ),
44460
44837
  tags: external_exports.array(external_exports.string()).optional(),
44461
44838
  environment: external_exports.record(external_exports.string(), external_exports.string()).optional(),
@@ -44476,7 +44853,13 @@ var patchFrontmatterSchema = external_exports.object({
44476
44853
  title: external_exports.string().optional(),
44477
44854
  confidence: confidenceSchema3.optional(),
44478
44855
  outcome: outcomeSchema2.optional(),
44479
- visibility: visibilitySchema2.optional(),
44856
+ visibility: visibilitySchema2.optional().describe(
44857
+ [
44858
+ "Change the publish tier. Use the same binary criterion as caveat_record:",
44859
+ "'public' if third-party reproducible, 'private' if repo-specific/your-workflow-specific.",
44860
+ "When unclear, prefer 'private'. Explicit user instruction overrides auto-classification."
44861
+ ].join(" ")
44862
+ ),
44480
44863
  tags: external_exports.array(external_exports.string()).optional(),
44481
44864
  environment: external_exports.record(external_exports.string(), external_exports.string()).optional(),
44482
44865
  last_verified: external_exports.string().optional()
@@ -44507,13 +44890,13 @@ function handleListRecent(ctx, args) {
44507
44890
  }
44508
44891
 
44509
44892
  // ../mcp/dist/tools/pull.js
44510
- import { existsSync as existsSync11, readdirSync as readdirSync7 } from "node:fs";
44511
- import { join as join15 } from "node:path";
44893
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
44894
+ import { join as join16 } from "node:path";
44512
44895
  var pullInputShape = {};
44513
44896
  async function handlePull(ctx, _args = {}) {
44514
44897
  const pulled = [];
44515
44898
  const indexed = [];
44516
- if (existsSync11(ctx.paths.communityDir)) {
44899
+ if (existsSync13(ctx.paths.communityDir)) {
44517
44900
  const results = await communityPull({
44518
44901
  communityDir: ctx.paths.communityDir,
44519
44902
  logger: ctx.logger
@@ -44523,16 +44906,16 @@ async function handlePull(ctx, _args = {}) {
44523
44906
  }
44524
44907
  }
44525
44908
  rebuildAll(ctx.db);
44526
- if (existsSync11(ctx.paths.entriesDir)) {
44909
+ if (existsSync13(ctx.paths.entriesDir)) {
44527
44910
  const own = scanSource({ db: ctx.db, source: "own", entriesRoot: ctx.paths.entriesDir });
44528
44911
  indexed.push({ source: "own", ...own });
44529
44912
  }
44530
- if (existsSync11(ctx.paths.communityDir)) {
44531
- for (const entry of readdirSync7(ctx.paths.communityDir, { withFileTypes: true })) {
44913
+ if (existsSync13(ctx.paths.communityDir)) {
44914
+ for (const entry of readdirSync8(ctx.paths.communityDir, { withFileTypes: true })) {
44532
44915
  if (!entry.isDirectory()) continue;
44533
44916
  const source = `community/${entry.name}`;
44534
- const root = join15(ctx.paths.communityDir, entry.name, "entries");
44535
- if (!existsSync11(root)) continue;
44917
+ const root = join16(ctx.paths.communityDir, entry.name, "entries");
44918
+ if (!existsSync13(root)) continue;
44536
44919
  const scan = scanSource({ db: ctx.db, source, entriesRoot: root });
44537
44920
  indexed.push({ source, ...scan });
44538
44921
  }
@@ -44645,6 +45028,19 @@ async function runMcpServer() {
44645
45028
  }
44646
45029
 
44647
45030
  // src/commands/hookCmd.ts
45031
+ import { spawn as spawn2 } from "node:child_process";
45032
+ import { existsSync as existsSync14, readFileSync as readFileSync9, unlinkSync as unlinkSync2, writeFileSync as writeFileSync7 } from "node:fs";
45033
+ import { tmpdir } from "node:os";
45034
+ import { join as join17 } from "node:path";
45035
+ import { randomBytes as randomBytes3 } from "node:crypto";
45036
+ var silentLogger = {
45037
+ info: () => {
45038
+ },
45039
+ warn: () => {
45040
+ },
45041
+ error: (m) => process.stderr.write(`[caveat:hook] ${m}
45042
+ `)
45043
+ };
44648
45044
  async function readStdin() {
44649
45045
  const chunks = [];
44650
45046
  for await (const chunk of process.stdin) {
@@ -44663,7 +45059,163 @@ function parsePayload(raw2) {
44663
45059
  return {};
44664
45060
  }
44665
45061
  }
44666
- async function runHook(name) {
45062
+ function getSessionId(payload) {
45063
+ const v = payload.session_id ?? payload.sessionId;
45064
+ return typeof v === "string" && v.length > 0 ? v : "_unknown";
45065
+ }
45066
+ function buildContextSafely() {
45067
+ try {
45068
+ return buildContext(silentLogger);
45069
+ } catch (err) {
45070
+ const msg = err instanceof Error ? err.message : String(err);
45071
+ process.stderr.write(`[caveat:hook] context error: ${msg}
45072
+ `);
45073
+ return null;
45074
+ }
45075
+ }
45076
+ function searchCaveatsFromTextSafely(text2) {
45077
+ if (!text2) return [];
45078
+ let db;
45079
+ try {
45080
+ const ctx = buildContextSafely();
45081
+ if (!ctx || !existsSync14(ctx.paths.dbPath)) return [];
45082
+ db = openDb({ path: ctx.paths.dbPath });
45083
+ const hits = findCaveatsForPrompt(db, text2);
45084
+ if (hits.length > 0) {
45085
+ try {
45086
+ markHit(db, hits);
45087
+ } catch (err) {
45088
+ const msg = err instanceof Error ? err.message : String(err);
45089
+ process.stderr.write(`[caveat:hook] markHit error: ${msg}
45090
+ `);
45091
+ }
45092
+ }
45093
+ return hits;
45094
+ } catch (err) {
45095
+ const msg = err instanceof Error ? err.message : String(err);
45096
+ process.stderr.write(`[caveat:hook] search error: ${msg}
45097
+ `);
45098
+ return [];
45099
+ } finally {
45100
+ db?.close();
45101
+ }
45102
+ }
45103
+ function loadSignalsSafely(path) {
45104
+ try {
45105
+ return readSessionSignals(path);
45106
+ } catch (err) {
45107
+ const msg = err instanceof Error ? err.message : String(err);
45108
+ process.stderr.write(`[caveat:hook] transcript read error: ${msg}
45109
+ `);
45110
+ return null;
45111
+ }
45112
+ }
45113
+ function drainForSession(sessionId) {
45114
+ const ctx = buildContextSafely();
45115
+ if (!ctx) return;
45116
+ const reminders = drainPendingReminders(ctx.caveatHome, sessionId);
45117
+ for (const text2 of reminders) {
45118
+ process.stdout.write(`<system-reminder>${text2}</system-reminder>
45119
+ `);
45120
+ }
45121
+ }
45122
+ function extractToolResponseText(response) {
45123
+ if (typeof response === "string") return response;
45124
+ if (Array.isArray(response)) {
45125
+ const parts = [];
45126
+ for (const item of response) {
45127
+ if (typeof item === "string") parts.push(item);
45128
+ else if (item !== null && typeof item === "object" && typeof item.text === "string") {
45129
+ parts.push(item.text);
45130
+ }
45131
+ }
45132
+ return parts.join(" ");
45133
+ }
45134
+ if (response !== null && typeof response === "object") {
45135
+ const r2 = response;
45136
+ if (typeof r2.content === "string") return r2.content;
45137
+ if (Array.isArray(r2.content)) return extractToolResponseText(r2.content);
45138
+ if (typeof r2.output === "string") return r2.output;
45139
+ if (typeof r2.stdout === "string" || typeof r2.stderr === "string") {
45140
+ return [r2.stdout, r2.stderr].filter((x2) => typeof x2 === "string").join(" ");
45141
+ }
45142
+ }
45143
+ return "";
45144
+ }
45145
+ function isToolError(payload) {
45146
+ const resp = payload.tool_response ?? payload.toolResponse;
45147
+ if (resp !== null && typeof resp === "object" && !Array.isArray(resp)) {
45148
+ if (resp.is_error === true) return true;
45149
+ }
45150
+ if (payload.is_error === true) return true;
45151
+ return false;
45152
+ }
45153
+ function spawnWorker(job) {
45154
+ const workFile = join17(
45155
+ tmpdir(),
45156
+ `caveat-worker-${Date.now()}-${randomBytes3(4).toString("hex")}.json`
45157
+ );
45158
+ try {
45159
+ writeFileSync7(workFile, JSON.stringify(job), "utf-8");
45160
+ } catch (err) {
45161
+ const msg = err instanceof Error ? err.message : String(err);
45162
+ process.stderr.write(`[caveat:hook] worker writefile error: ${msg}
45163
+ `);
45164
+ return;
45165
+ }
45166
+ const cliScript = process.argv[1];
45167
+ if (!cliScript) return;
45168
+ try {
45169
+ const child = spawn2(
45170
+ process.execPath,
45171
+ ["--disable-warning=ExperimentalWarning", cliScript, "hook", "worker", workFile],
45172
+ { detached: true, stdio: "ignore", windowsHide: true }
45173
+ );
45174
+ child.unref();
45175
+ } catch (err) {
45176
+ const msg = err instanceof Error ? err.message : String(err);
45177
+ process.stderr.write(`[caveat:hook] worker spawn error: ${msg}
45178
+ `);
45179
+ try {
45180
+ unlinkSync2(workFile);
45181
+ } catch {
45182
+ }
45183
+ }
45184
+ }
45185
+ async function runWorker(workFile) {
45186
+ let raw2;
45187
+ try {
45188
+ raw2 = readFileSync9(workFile, "utf-8");
45189
+ } catch {
45190
+ process.exit(0);
45191
+ }
45192
+ try {
45193
+ unlinkSync2(workFile);
45194
+ } catch {
45195
+ }
45196
+ let job;
45197
+ try {
45198
+ job = JSON.parse(raw2);
45199
+ } catch {
45200
+ process.exit(0);
45201
+ }
45202
+ if (!job.searchText || !job.sessionId) process.exit(0);
45203
+ const hits = searchCaveatsFromTextSafely(job.searchText);
45204
+ if (hits.length === 0) process.exit(0);
45205
+ const ctx = buildContextSafely();
45206
+ if (!ctx) process.exit(0);
45207
+ try {
45208
+ appendPendingReminder(ctx.caveatHome, job.sessionId, toolErrorReminderText(hits));
45209
+ } catch {
45210
+ }
45211
+ process.exit(0);
45212
+ }
45213
+ async function runHook(name, arg) {
45214
+ if (name === "worker") {
45215
+ if (!arg) process.exit(0);
45216
+ await runWorker(arg);
45217
+ return;
45218
+ }
44667
45219
  let raw2 = "";
44668
45220
  try {
44669
45221
  raw2 = await readStdin();
@@ -44674,22 +45226,37 @@ async function runHook(name) {
44674
45226
  process.exit(0);
44675
45227
  }
44676
45228
  const payload = parsePayload(raw2);
45229
+ const sessionId = getSessionId(payload);
45230
+ drainForSession(sessionId);
44677
45231
  if (name === "user-prompt-submit") {
44678
45232
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
44679
- if (detectCaveatTrigger(prompt)) {
45233
+ const hits = searchCaveatsFromTextSafely(prompt);
45234
+ if (hits.length > 0) {
44680
45235
  process.stdout.write(
44681
- `<system-reminder>${userPromptSubmitReminderText()}</system-reminder>
45236
+ `<system-reminder>${userPromptSubmitReminderText(hits)}</system-reminder>
44682
45237
  `
44683
45238
  );
44684
45239
  }
44685
45240
  process.exit(0);
44686
45241
  }
44687
- if (name === "stop") {
44688
- if (payload.stop_hook_active === true) {
44689
- process.exit(0);
45242
+ if (name === "post-tool-use") {
45243
+ if (!isToolError(payload)) process.exit(0);
45244
+ const errText = extractToolResponseText(payload.tool_response ?? payload);
45245
+ if (errText) {
45246
+ spawnWorker({ sessionId, searchText: errText });
44690
45247
  }
44691
- process.stdout.write(`<system-reminder>${stopReminderText()}</system-reminder>
44692
- `);
45248
+ process.exit(0);
45249
+ }
45250
+ if (name === "stop") {
45251
+ if (payload.stop_hook_active === true) process.exit(0);
45252
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
45253
+ const signals = transcriptPath ? loadSignalsSafely(transcriptPath) : null;
45254
+ if (!signals || !hasAnyStruggleSignal(signals)) process.exit(0);
45255
+ const related = searchCaveatsFromTextSafely(struggleSearchText(signals));
45256
+ process.stdout.write(
45257
+ `<system-reminder>${stopReminderText(signals, related)}</system-reminder>
45258
+ `
45259
+ );
44693
45260
  process.exit(0);
44694
45261
  }
44695
45262
  process.stderr.write(`[caveat:hook] unknown hook name: ${name}
@@ -44698,10 +45265,10 @@ async function runHook(name) {
44698
45265
  }
44699
45266
 
44700
45267
  // src/commands/pull.ts
44701
- import { existsSync as existsSync12, readdirSync as readdirSync8 } from "node:fs";
44702
- import { join as join16 } from "node:path";
45268
+ import { existsSync as existsSync15, readdirSync as readdirSync9 } from "node:fs";
45269
+ import { join as join18 } from "node:path";
44703
45270
  async function runPull(ctx) {
44704
- if (!existsSync12(ctx.paths.communityDir)) {
45271
+ if (!existsSync15(ctx.paths.communityDir)) {
44705
45272
  ctx.logger.info(
44706
45273
  "no community repos yet \u2014 add one with `caveat community add <github-url>`."
44707
45274
  );
@@ -44721,15 +45288,15 @@ async function runPull(ctx) {
44721
45288
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
44722
45289
  try {
44723
45290
  rebuildAll(db);
44724
- if (existsSync12(ctx.paths.entriesDir)) {
45291
+ if (existsSync15(ctx.paths.entriesDir)) {
44725
45292
  const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
44726
45293
  ctx.logger.info(`own: +${own.added}`);
44727
45294
  }
44728
- for (const entry of readdirSync8(ctx.paths.communityDir, { withFileTypes: true })) {
45295
+ for (const entry of readdirSync9(ctx.paths.communityDir, { withFileTypes: true })) {
44729
45296
  if (!entry.isDirectory()) continue;
44730
45297
  const source = `community/${entry.name}`;
44731
- const root = join16(ctx.paths.communityDir, entry.name, "entries");
44732
- if (!existsSync12(root)) continue;
45298
+ const root = join18(ctx.paths.communityDir, entry.name, "entries");
45299
+ if (!existsSync15(root)) continue;
44733
45300
  const scan = scanSource({ db, source, entriesRoot: root });
44734
45301
  ctx.logger.info(`${source}: +${scan.added}`);
44735
45302
  }
@@ -44853,6 +45420,13 @@ program.command("list").description("List caveats by updated_at DESC").option("-
44853
45420
  const ctx = buildContext(stdoutLogger);
44854
45421
  runList(ctx, { limit: opts.recent });
44855
45422
  });
45423
+ program.command("stale").description(
45424
+ "List entries not surfaced by retrieval for N days (default 90). Use this to find private caveats that may be buried \u2014 if a 3-month-old private entry never surfaces, rewrite its body to include repo-specific identifiers, or delete it."
45425
+ ).option("--days <n>", "age threshold in days", (v) => Number(v), 90).option("--visibility <v>", "public | private").option("--limit <n>", "max rows", (v) => Number(v), 50).action((opts) => {
45426
+ const ctx = buildContext(stdoutLogger);
45427
+ const vis = opts.visibility === "public" || opts.visibility === "private" ? opts.visibility : void 0;
45428
+ runStale(ctx, { days: opts.days, visibility: vis, limit: opts.limit });
45429
+ });
44856
45430
  program.command("show").description("Show full caveat by id").argument("<id>", "entry id").option("--source <source>", "own or community/<handle>", "own").action((id, opts) => {
44857
45431
  const ctx = buildContext(stdoutLogger);
44858
45432
  runShow(ctx, { id, source: opts.source });
@@ -44873,8 +45447,10 @@ program.command("serve").description("Start the read-only web share portal").opt
44873
45447
  program.command("mcp-server").description("Run the MCP stdio server (registered by `caveat init`)").action(async () => {
44874
45448
  await runMcpServer();
44875
45449
  });
44876
- program.command("hook <name>").description("Run a Claude Code hook. name: user-prompt-submit | stop").action(async (name) => {
44877
- await runHook(name);
45450
+ program.command("hook <name> [arg]").description(
45451
+ "Run a Claude Code hook. name: user-prompt-submit | post-tool-use | stop | worker"
45452
+ ).action(async (name, arg) => {
45453
+ await runHook(name, arg);
44878
45454
  });
44879
45455
  var community = program.command("community").description("Manage community caveat repos (shallow clones under <knowledgeRepo>/community/)");
44880
45456
  community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url) => {