caveat-cli 0.6.2 → 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
@@ -9761,49 +9761,49 @@ var require_fast_uri = __commonJS({
9761
9761
  schemelessOptions.skipEscape = true;
9762
9762
  return serialize(resolved, schemelessOptions);
9763
9763
  }
9764
- function resolveComponent(base2, relative3, options2, skipNormalization) {
9764
+ function resolveComponent(base2, relative2, options2, skipNormalization) {
9765
9765
  const target = {};
9766
9766
  if (!skipNormalization) {
9767
9767
  base2 = parse4(serialize(base2, options2), options2);
9768
- relative3 = parse4(serialize(relative3, options2), options2);
9768
+ relative2 = parse4(serialize(relative2, options2), options2);
9769
9769
  }
9770
9770
  options2 = options2 || {};
9771
- if (!options2.tolerant && relative3.scheme) {
9772
- target.scheme = relative3.scheme;
9773
- target.userinfo = relative3.userinfo;
9774
- target.host = relative3.host;
9775
- target.port = relative3.port;
9776
- target.path = removeDotSegments(relative3.path || "");
9777
- target.query = relative3.query;
9771
+ if (!options2.tolerant && relative2.scheme) {
9772
+ target.scheme = relative2.scheme;
9773
+ target.userinfo = relative2.userinfo;
9774
+ target.host = relative2.host;
9775
+ target.port = relative2.port;
9776
+ target.path = removeDotSegments(relative2.path || "");
9777
+ target.query = relative2.query;
9778
9778
  } else {
9779
- if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
9780
- target.userinfo = relative3.userinfo;
9781
- target.host = relative3.host;
9782
- target.port = relative3.port;
9783
- target.path = removeDotSegments(relative3.path || "");
9784
- target.query = relative3.query;
9779
+ if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
9780
+ target.userinfo = relative2.userinfo;
9781
+ target.host = relative2.host;
9782
+ target.port = relative2.port;
9783
+ target.path = removeDotSegments(relative2.path || "");
9784
+ target.query = relative2.query;
9785
9785
  } else {
9786
- if (!relative3.path) {
9786
+ if (!relative2.path) {
9787
9787
  target.path = base2.path;
9788
- if (relative3.query !== void 0) {
9789
- target.query = relative3.query;
9788
+ if (relative2.query !== void 0) {
9789
+ target.query = relative2.query;
9790
9790
  } else {
9791
9791
  target.query = base2.query;
9792
9792
  }
9793
9793
  } else {
9794
- if (relative3.path[0] === "/") {
9795
- target.path = removeDotSegments(relative3.path);
9794
+ if (relative2.path[0] === "/") {
9795
+ target.path = removeDotSegments(relative2.path);
9796
9796
  } else {
9797
9797
  if ((base2.userinfo !== void 0 || base2.host !== void 0 || base2.port !== void 0) && !base2.path) {
9798
- target.path = "/" + relative3.path;
9798
+ target.path = "/" + relative2.path;
9799
9799
  } else if (!base2.path) {
9800
- target.path = relative3.path;
9800
+ target.path = relative2.path;
9801
9801
  } else {
9802
- target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative3.path;
9802
+ target.path = base2.path.slice(0, base2.path.lastIndexOf("/") + 1) + relative2.path;
9803
9803
  }
9804
9804
  target.path = removeDotSegments(target.path);
9805
9805
  }
9806
- target.query = relative3.query;
9806
+ target.query = relative2.query;
9807
9807
  }
9808
9808
  target.userinfo = base2.userinfo;
9809
9809
  target.host = base2.host;
@@ -9811,7 +9811,7 @@ var require_fast_uri = __commonJS({
9811
9811
  }
9812
9812
  target.scheme = base2.scheme;
9813
9813
  }
9814
- target.fragment = relative3.fragment;
9814
+ target.fragment = relative2.fragment;
9815
9815
  return target;
9816
9816
  }
9817
9817
  function equal(uriA, uriB, options2) {
@@ -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 join9 } 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";
@@ -15819,7 +15819,14 @@ function search(db, opts = {}) {
15819
15819
  conditions.push(`e.confidence IN (${placeholders})`);
15820
15820
  params.push(...filters.confidence);
15821
15821
  }
15822
+ if (filters.visibility === "public" || filters.visibility === "private") {
15823
+ conditions.push(`e.visibility = ?`);
15824
+ params.push(filters.visibility);
15825
+ }
15822
15826
  if (conditions.length) sql += " AND " + conditions.join(" AND ");
15827
+ if (!ftsQuery) {
15828
+ sql += ` ORDER BY json_extract(e.frontmatter_json, '$.updated_at') DESC`;
15829
+ }
15823
15830
  sql += " LIMIT ?";
15824
15831
  params.push(limit);
15825
15832
  const rows = db.prepare(sql).all(...params);
@@ -15864,6 +15871,7 @@ function toSearchResult(row) {
15864
15871
  title: row.title,
15865
15872
  symptomExcerpt: symptom.slice(0, SYMPTOM_EXCERPT_LENGTH),
15866
15873
  confidence: row.confidence,
15874
+ visibility: row.visibility ?? "public",
15867
15875
  environment: fm.environment ?? {}
15868
15876
  };
15869
15877
  }
@@ -16105,12 +16113,10 @@ function resolvePaths(caveatHome, knowledgeRepo, userHome) {
16105
16113
 
16106
16114
  // ../../packages/core/dist/config.js
16107
16115
  import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
16108
- var SHARED_REPO_URL = "https://github.com/kitepon-rgb/Caveat";
16109
16116
  var DEFAULT_CONFIG = {
16110
16117
  knowledgeRepo: "own",
16111
16118
  semverKeys: ["driver", "cuda", "node"],
16112
- communitySources: [],
16113
- sharedRepo: SHARED_REPO_URL
16119
+ communitySources: []
16114
16120
  };
16115
16121
  function loadConfig(userConfigPath) {
16116
16122
  const userCfg = existsSync5(userConfigPath) ? JSON.parse(readFileSync4(userConfigPath, "utf-8")) : {};
@@ -16134,7 +16140,7 @@ function deepMerge(base2, overlay) {
16134
16140
  }
16135
16141
 
16136
16142
  // ../../packages/core/dist/community.js
16137
- import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
16143
+ import { existsSync as existsSync6, readdirSync as readdirSync3, rmSync, statSync as statSync4 } from "node:fs";
16138
16144
  import { join as join6 } from "node:path";
16139
16145
 
16140
16146
  // ../../node_modules/.pnpm/simple-git@3.36.0/node_modules/simple-git/dist/esm/index.js
@@ -21164,304 +21170,367 @@ function communityList(opts) {
21164
21170
  }
21165
21171
  return out;
21166
21172
  }
21167
-
21168
- // ../../packages/core/dist/claudeHooks.js
21169
- var CAVEAT_TRIGGERS = [
21170
- /\b(gpu|cuda|nvidia|amd|rtx|nvenc|nvdec|blackwell|ada|ampere)\b/i,
21171
- /\bdriver\b/i,
21172
- /ドライバ/,
21173
- /\b(vscode|vs\s?code|jetbrains|intellij|pycharm)\b/i,
21174
- /\bclaude\s?code\b/i,
21175
- /\b(flaky|intermittent|reproducib|reproduce|repro)\b/i,
21176
- /再現(しない|性|できない)/,
21177
- /挙動が(違|おかしい|変)/,
21178
- /\b(node|python|cuda|cudnn)\s*\d+(\.\d+)?/i,
21179
- /バージョン(依存|違い|差)/,
21180
- /\b(prebuild|native\s+module|msvc|gyp)\b/i
21181
- ];
21182
- function detectCaveatTrigger(prompt) {
21183
- if (typeof prompt !== "string" || prompt.length === 0) return false;
21184
- return CAVEAT_TRIGGERS.some((re) => re.test(prompt));
21185
- }
21186
- function userPromptSubmitReminderText() {
21187
- return [
21188
- "[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",
21189
- "\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",
21190
- "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"
21191
- ].join("\n");
21192
- }
21193
- function stopReminderText() {
21194
- return [
21195
- "[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",
21196
- "\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",
21197
- "\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",
21198
- "\u8A18\u9332\u3057\u305F\u7F60\u304C\u4ED6\u306E\u30E6\u30FC\u30B6\u306B\u3082\u518D\u5229\u7528\u4FA1\u5024\u304C\u3042\u308B\u306A\u3089 mcp__caveat__caveat_push \u3067\u5171\u6709 DB \u306B PR \u3092\u6295\u3052\u308B\u3053\u3068\u3082\u691C\u8A0E\uFF08\u500B\u4EBA\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u56FA\u6709\u306E\u7269\u306F push \u3057\u306A\u3044\uFF09\u3002"
21199
- ].join("\n");
21200
- }
21201
-
21202
- // ../../packages/core/dist/push.js
21203
- import { spawnSync } from "node:child_process";
21204
- import {
21205
- copyFileSync,
21206
- existsSync as existsSync7,
21207
- mkdirSync as mkdirSync3,
21208
- readdirSync as readdirSync4,
21209
- readFileSync as readFileSync5
21210
- } from "node:fs";
21211
- import { dirname as dirname4, join as join7, relative as relative2 } from "node:path";
21212
- async function pushEntry(opts) {
21213
- if (!checkGhAvailable()) {
21214
- return {
21215
- status: "gh-missing",
21216
- detail: "gh (GitHub CLI) is required. Install: https://cli.github.com/"
21217
- };
21218
- }
21219
- if (!checkGhAuthed()) {
21220
- return {
21221
- status: "gh-unauthed",
21222
- detail: "gh is not authenticated. Run `gh auth login`."
21223
- };
21224
- }
21225
- const validation = validateCommunityUrl(opts.sharedRepoUrl);
21226
- if (!validation.valid) {
21227
- return { status: "failed", detail: `sharedRepo URL invalid: ${validation.reason}` };
21228
- }
21229
- const [sharedOwner, sharedName] = parseOwnerRepo(opts.sharedRepoUrl);
21230
- if (!sharedOwner || !sharedName) {
21231
- return { status: "failed", detail: `cannot parse owner/repo from ${opts.sharedRepoUrl}` };
21232
- }
21233
- const owned = findOwnedEntry(opts.entriesDir, opts.id);
21234
- if (!owned) {
21235
- return {
21236
- status: "not-found",
21237
- detail: `entry id=${opts.id} not found under ${opts.entriesDir}`
21238
- };
21239
- }
21240
- if (owned.visibility === "private") {
21241
- return {
21242
- status: "visibility-private",
21243
- detail: `entry id=${opts.id} has visibility: private and cannot be pushed to the public community DB. Update the entry to visibility: public first (the user declared this entry local-only).`
21244
- };
21173
+ function communityRemove(opts) {
21174
+ const handle = (opts.handle ?? "").trim();
21175
+ if (!handle) {
21176
+ throw new Error("handle is required");
21245
21177
  }
21246
- const ghUser = resolveGhUser();
21247
- if (!ghUser) {
21248
- return { status: "failed", detail: "gh api user failed" };
21178
+ if (handle === "." || handle === ".." || /[\\/]/.test(handle)) {
21179
+ throw new Error(`invalid handle (no path separators or relative segments): ${opts.handle}`);
21249
21180
  }
21181
+ const target = join6(opts.communityDir, handle);
21182
+ const dirExisted = existsSync6(target) && statSync4(target).isDirectory();
21183
+ const source = `community/${handle}`;
21184
+ const row = opts.db.prepare("SELECT COUNT(*) AS n FROM entries WHERE source = ?").get(source);
21185
+ const rowCount = row?.n ?? 0;
21250
21186
  if (opts.dryRun) {
21251
- return {
21252
- status: "dry-run",
21253
- plannedSteps: [
21254
- `fork ${sharedOwner}/${sharedName} as ${ghUser}/${sharedName}`,
21255
- `clone fork \u2192 ${forkStagingDir(opts.caveatHome)}`,
21256
- `copy entry \u2192 entries/${owned.relPath}`,
21257
- "commit, push branch, open PR against main"
21258
- ]
21259
- };
21187
+ return { handle, path: target, dirExisted, rowCount, removed: false, dryRun: true };
21260
21188
  }
21261
- try {
21262
- ensureFork(sharedOwner, sharedName);
21263
- const stagingDir = forkStagingDir(opts.caveatHome);
21264
- ensureStagingClone(stagingDir, ghUser, sharedName, sharedOwner);
21265
- const branch = `caveat-push-${owned.id}-${Date.now().toString(36)}`;
21266
- runOrThrow("git", ["checkout", "-b", branch], { cwd: stagingDir });
21267
- const destPath = join7(stagingDir, "entries", owned.relPath);
21268
- mkdirSync3(dirname4(destPath), { recursive: true });
21269
- copyFileSync(owned.absPath, destPath);
21270
- runOrThrow("git", ["add", join7("entries", owned.relPath)], { cwd: stagingDir });
21271
- const isUpdate = upstreamAlreadyHas(stagingDir, "entries/" + owned.relPath);
21272
- const verb = isUpdate ? "update" : "add";
21273
- const commitMsg = `${verb}: ${owned.title}`;
21274
- runOrThrow("git", ["commit", "-m", commitMsg], { cwd: stagingDir });
21275
- runOrThrow("git", ["push", "--set-upstream", "origin", branch], { cwd: stagingDir });
21276
- const prTitle = `${verb}: ${owned.title}`;
21277
- const prBody = [
21278
- `Contribution of caveat \`${owned.id}\` via \`caveat push\`.`,
21279
- "",
21280
- `- entry path: \`entries/${owned.relPath}\``,
21281
- `- action: ${verb}`,
21282
- "",
21283
- "Merged PRs will appear in subscribers' repos on their next `caveat pull`."
21284
- ].join("\n");
21285
- const pr = runCapture(
21286
- "gh",
21287
- [
21288
- "pr",
21289
- "create",
21290
- "--repo",
21291
- `${sharedOwner}/${sharedName}`,
21292
- "--base",
21293
- "main",
21294
- "--head",
21295
- `${ghUser}:${branch}`,
21296
- "--title",
21297
- prTitle,
21298
- "--body",
21299
- prBody
21300
- ],
21301
- { cwd: stagingDir }
21302
- );
21303
- if (pr.status !== 0) {
21304
- return {
21305
- status: "failed",
21306
- detail: (pr.stderr || "gh pr create failed").trim()
21307
- };
21308
- }
21309
- return { status: "ok", prUrl: pr.stdout.trim() };
21310
- } catch (err) {
21311
- return {
21312
- status: "failed",
21313
- detail: err instanceof Error ? err.message : String(err)
21314
- };
21189
+ if (dirExisted) {
21190
+ rmSync(target, { recursive: true, force: true });
21315
21191
  }
21192
+ if (rowCount > 0) {
21193
+ opts.db.prepare("DELETE FROM entries WHERE source = ?").run(source);
21194
+ }
21195
+ return {
21196
+ handle,
21197
+ path: target,
21198
+ dirExisted,
21199
+ rowCount,
21200
+ removed: dirExisted || rowCount > 0,
21201
+ dryRun: false
21202
+ };
21316
21203
  }
21317
- function shellQuote(s) {
21318
- if (!/[\s&|<>^()"`$!*?\[\]{}\\]/.test(s)) return s;
21319
- return `"${s.replace(/"/g, '""')}"`;
21320
- }
21321
- function runCapture(command, args, opts = {}) {
21322
- const line = [command, ...args].map(shellQuote).join(" ");
21323
- const r2 = spawnSync(line, {
21324
- encoding: "utf-8",
21325
- cwd: opts.cwd,
21326
- shell: true
21327
- });
21204
+
21205
+ // ../../packages/core/dist/claudeHooks.js
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;
21328
21246
  return {
21329
- status: r2.status,
21330
- stdout: typeof r2.stdout === "string" ? r2.stdout : "",
21331
- stderr: typeof r2.stderr === "string" ? r2.stderr : ""
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 ?? {}
21332
21254
  };
21333
21255
  }
21334
- function runOrThrow(command, args, opts = {}) {
21335
- const r2 = runCapture(command, args, opts);
21336
- if (r2.status !== 0) {
21337
- throw new Error(
21338
- `${command} ${args.join(" ")} failed: ${(r2.stderr || r2.stdout || "unknown").trim()}`
21339
- );
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
+ }
21340
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));
21341
21279
  }
21342
- function checkGhAvailable() {
21343
- return runCapture("gh", ["--version"]).status === 0;
21344
- }
21345
- function checkGhAuthed() {
21346
- return runCapture("gh", ["auth", "status"]).status === 0;
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");
21347
21296
  }
21348
- function resolveGhUser() {
21349
- const r2 = runCapture("gh", ["api", "user", "--jq", ".login"]);
21350
- if (r2.status !== 0) return void 0;
21351
- return r2.stdout.trim() || void 0;
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");
21352
21313
  }
21353
- function parseOwnerRepo(url) {
21354
- const m = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(\.git)?\/?$/.exec(url);
21355
- if (!m) return [void 0, void 0];
21356
- return [m[1], m[2]];
21314
+ function shortPath(p2) {
21315
+ const parts = p2.split(/[\\/]/);
21316
+ return parts[parts.length - 1] ?? p2;
21357
21317
  }
21358
- function findOwnedEntry(entriesDir, id) {
21359
- if (!existsSync7(entriesDir)) return void 0;
21360
- for (const categoryDirent of readdirSync4(entriesDir, { withFileTypes: true })) {
21361
- if (!categoryDirent.isDirectory()) continue;
21362
- const categoryDir = join7(entriesDir, categoryDirent.name);
21363
- for (const fileDirent of readdirSync4(categoryDir, { withFileTypes: true })) {
21364
- if (!fileDirent.isFile() || !fileDirent.name.endsWith(".md")) continue;
21365
- const absPath = join7(categoryDir, fileDirent.name);
21366
- const raw2 = readFileSync5(absPath, "utf-8");
21367
- try {
21368
- const parsed = parseMarkdown(raw2);
21369
- if (parsed.frontmatter.id === id) {
21370
- const relPath = relative2(entriesDir, absPath).replace(/\\/g, "/");
21371
- return {
21372
- id,
21373
- title: parsed.frontmatter.title,
21374
- absPath,
21375
- relPath,
21376
- visibility: parsed.frontmatter.visibility
21377
- };
21378
- }
21379
- } catch {
21380
- }
21381
- }
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`);
21382
21323
  }
21383
- return void 0;
21384
- }
21385
- function forkStagingDir(caveatHome) {
21386
- return join7(caveatHome, "push-fork");
21387
- }
21388
- function ensureFork(owner, repo) {
21389
- runCapture("gh", ["repo", "fork", `${owner}/${repo}`, "--clone=false", "--remote=false"]);
21390
- }
21391
- function ensureStagingClone(stagingDir, ghUser, sharedName, upstreamOwner) {
21392
- if (!existsSync7(stagingDir)) {
21393
- mkdirSync3(dirname4(stagingDir), { recursive: true });
21394
- const forkUrl = `https://github.com/${ghUser}/${sharedName}.git`;
21395
- runOrThrow("git", ["clone", "--depth", "30", forkUrl, stagingDir]);
21396
- runOrThrow(
21397
- "git",
21398
- ["remote", "add", "upstream", `https://github.com/${upstreamOwner}/${sharedName}.git`],
21399
- { cwd: stagingDir }
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"
21400
21353
  );
21401
21354
  } else {
21402
- runOrThrow("git", ["checkout", "main"], { cwd: stagingDir });
21403
- runOrThrow("git", ["fetch", "upstream", "main"], { cwd: stagingDir });
21404
- runOrThrow("git", ["reset", "--hard", "upstream/main"], { cwd: stagingDir });
21405
- runOrThrow("git", ["push", "origin", "main", "--force-with-lease"], { cwd: stagingDir });
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
+ );
21406
21358
  }
21407
- }
21408
- function upstreamAlreadyHas(stagingDir, relFromRoot) {
21409
- return existsSync7(join7(stagingDir, relFromRoot));
21359
+ return lines.join("\n");
21410
21360
  }
21411
21361
 
21412
- // ../../packages/core/dist/pullShared.js
21413
- import { existsSync as existsSync8, readdirSync as readdirSync5 } from "node:fs";
21414
- import { join as join8 } from "node:path";
21415
- async function pullShared(opts) {
21416
- const result = { pulled: [], indexed: [] };
21417
- if (!existsSync8(opts.communityDir)) {
21418
- return result;
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(" ");
21419
21380
  }
21420
- const pulls = await communityPull({
21421
- communityDir: opts.communityDir,
21422
- logger: opts.logger
21423
- });
21424
- for (const p2 of pulls) {
21425
- result.pulled.push({
21426
- handle: p2.handle,
21427
- status: p2.status,
21428
- message: p2.message
21429
- });
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;
21430
21395
  }
21431
- rebuildAll(opts.db);
21432
- if (existsSync8(opts.entriesDir)) {
21433
- const own = scanSource({ db: opts.db, source: "own", entriesRoot: opts.entriesDir });
21434
- result.indexed.push({ source: "own", ...own });
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
+ };
21464
+ }
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 [];
21435
21499
  }
21436
- for (const entry of readdirSync5(opts.communityDir, { withFileTypes: true })) {
21437
- if (!entry.isDirectory()) continue;
21438
- const source = `community/${entry.name}`;
21439
- const root = join8(opts.communityDir, entry.name, "entries");
21440
- if (!existsSync8(root)) continue;
21441
- const scan = scanSource({ db: opts.db, source, entriesRoot: root });
21442
- result.indexed.push({ source, ...scan });
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
+ }
21443
21512
  }
21444
- return result;
21513
+ return out;
21445
21514
  }
21446
21515
 
21447
21516
  // src/context.ts
21448
21517
  function buildContext(logger, overrides = {}) {
21449
21518
  const userHome = overrides.userHome ?? homedir();
21450
21519
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
21451
- const userConfigPath = join9(userHome, ".caveatrc.json");
21520
+ const userConfigPath = join8(userHome, ".caveatrc.json");
21452
21521
  const config3 = loadConfig(userConfigPath);
21453
21522
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
21454
21523
  return { caveatHome, userHome, userConfigPath, config: config3, paths, logger };
21455
21524
  }
21456
21525
 
21457
21526
  // src/version.ts
21458
- import { readFileSync as readFileSync6 } from "node:fs";
21459
- import { dirname as dirname5, join as join10 } from "node:path";
21527
+ import { readFileSync as readFileSync7 } from "node:fs";
21528
+ import { dirname as dirname4, join as join9 } from "node:path";
21460
21529
  import { fileURLToPath as fileURLToPath3 } from "node:url";
21461
21530
  function resolveVersion() {
21462
21531
  try {
21463
- const here2 = dirname5(fileURLToPath3(import.meta.url));
21464
- const pkg = JSON.parse(readFileSync6(join10(here2, "..", "package.json"), "utf-8"));
21532
+ const here2 = dirname4(fileURLToPath3(import.meta.url));
21533
+ const pkg = JSON.parse(readFileSync7(join9(here2, "..", "package.json"), "utf-8"));
21465
21534
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
21466
21535
  } catch {
21467
21536
  return "0.0.0";
@@ -21480,14 +21549,15 @@ var stdoutLogger = {
21480
21549
  };
21481
21550
 
21482
21551
  // src/commands/init.ts
21483
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync6, renameSync, rmdirSync, writeFileSync as writeFileSync5 } from "node:fs";
21484
- import { join as join12 } 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";
21485
21554
 
21486
21555
  // src/claudeInstall.ts
21487
- import { spawnSync as spawnSync2 } from "node:child_process";
21488
- import { copyFileSync as copyFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "node:fs";
21489
- import { dirname as dirname6, join as join11 } from "node:path";
21556
+ import { spawnSync } from "node:child_process";
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";
21490
21559
  var EVENT_USER_PROMPT_SUBMIT = "UserPromptSubmit";
21560
+ var EVENT_POST_TOOL_USE = "PostToolUse";
21491
21561
  var EVENT_STOP = "Stop";
21492
21562
  function quote(p2) {
21493
21563
  return p2.includes(" ") ? `"${p2}"` : p2;
@@ -21521,26 +21591,26 @@ function removeHook(settings, event, command) {
21521
21591
  }
21522
21592
  function readSettings(path) {
21523
21593
  if (!existsSync9(path)) return {};
21524
- return JSON.parse(readFileSync7(path, "utf-8"));
21594
+ return JSON.parse(readFileSync8(path, "utf-8"));
21525
21595
  }
21526
21596
  function writeSettings(path, settings) {
21527
- const dir = dirname6(path);
21597
+ const dir = dirname5(path);
21528
21598
  if (!existsSync9(dir)) mkdirSync4(dir, { recursive: true });
21529
21599
  let backupPath = "";
21530
21600
  if (existsSync9(path)) {
21531
21601
  backupPath = `${path}.caveat-backup-${Date.now()}`;
21532
- copyFileSync2(path, backupPath);
21602
+ copyFileSync(path, backupPath);
21533
21603
  }
21534
- writeFileSync4(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21604
+ writeFileSync5(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
21535
21605
  return backupPath;
21536
21606
  }
21537
21607
  var CLAUDE_BIN = "claude";
21538
- function shellQuote2(s) {
21608
+ function shellQuote(s) {
21539
21609
  return /[\s&|<>^()]/.test(s) ? `"${s}"` : s;
21540
21610
  }
21541
21611
  function runClaude(args) {
21542
- const line = [CLAUDE_BIN, ...args].map(shellQuote2).join(" ");
21543
- return spawnSync2(line, { shell: true, encoding: "utf-8" });
21612
+ const line = [CLAUDE_BIN, ...args].map(shellQuote).join(" ");
21613
+ return spawnSync(line, { shell: true, encoding: "utf-8" });
21544
21614
  }
21545
21615
  function registerMcp(nodePath, cliScriptPath, dryRun, logger) {
21546
21616
  const args = mcpArgs(cliScriptPath);
@@ -21590,33 +21660,38 @@ function unregisterMcp(dryRun, logger) {
21590
21660
  return { action: "skipped", detail: "not registered or removal failed" };
21591
21661
  }
21592
21662
  function installClaudeIntegration(opts) {
21593
- const settingsPath = join11(opts.claudeDir, "settings.json");
21663
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21594
21664
  const settings = readSettings(settingsPath);
21595
21665
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21666
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21596
21667
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21597
21668
  const userPromptSubmit = upsertHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21669
+ const postToolUse = upsertHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21598
21670
  const stop = upsertHook(settings, EVENT_STOP, stopCmd);
21599
21671
  let backupPath;
21600
- if (!opts.dryRun && (userPromptSubmit === "added" || stop === "added")) {
21672
+ const anyAdded = userPromptSubmit === "added" || postToolUse === "added" || stop === "added";
21673
+ if (!opts.dryRun && anyAdded) {
21601
21674
  const backup = writeSettings(settingsPath, settings);
21602
21675
  if (backup) backupPath = backup;
21603
21676
  } else if (opts.dryRun) {
21604
21677
  opts.logger.info(
21605
- `[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}`
21606
21679
  );
21607
21680
  }
21608
21681
  const mcp = opts.skipMcpRegistration ? { action: "skipped", detail: "skipped by caller" } : registerMcp(opts.nodePath, opts.cliScriptPath, opts.dryRun, opts.logger);
21609
- return { mcp, hooks: { userPromptSubmit, stop }, backupPath };
21682
+ return { mcp, hooks: { userPromptSubmit, postToolUse, stop }, backupPath };
21610
21683
  }
21611
21684
  function uninstallClaudeIntegration(opts) {
21612
- const settingsPath = join11(opts.claudeDir, "settings.json");
21685
+ const settingsPath = join10(opts.claudeDir, "settings.json");
21613
21686
  const settings = readSettings(settingsPath);
21614
21687
  const usCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "user-prompt-submit");
21688
+ const ptCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "post-tool-use");
21615
21689
  const stopCmd = hookCommand(opts.nodePath, opts.cliScriptPath, "stop");
21616
21690
  const removedUs = removeHook(settings, EVENT_USER_PROMPT_SUBMIT, usCmd);
21691
+ const removedPt = removeHook(settings, EVENT_POST_TOOL_USE, ptCmd);
21617
21692
  const removedStop = removeHook(settings, EVENT_STOP, stopCmd);
21618
21693
  let backupPath;
21619
- if (!opts.dryRun && (removedUs || removedStop)) {
21694
+ if (!opts.dryRun && (removedUs || removedPt || removedStop)) {
21620
21695
  const backup = writeSettings(settingsPath, settings);
21621
21696
  if (backup) backupPath = backup;
21622
21697
  }
@@ -21625,6 +21700,7 @@ function uninstallClaudeIntegration(opts) {
21625
21700
  mcp,
21626
21701
  hooks: {
21627
21702
  userPromptSubmit: removedUs ? "added" : "unchanged",
21703
+ postToolUse: removedPt ? "added" : "unchanged",
21628
21704
  stop: removedStop ? "added" : "unchanged"
21629
21705
  },
21630
21706
  backupPath
@@ -21641,7 +21717,7 @@ var KNOWLEDGE_GITIGNORE = [
21641
21717
  ".obsidian/",
21642
21718
  ""
21643
21719
  ].join("\n");
21644
- async function runInit(ctx, opts = { skipClaude: false, skipShared: false, dryRun: false }) {
21720
+ async function runInit(ctx, opts = { skipClaude: false, dryRun: false }) {
21645
21721
  ensureUserConfig(ctx.userConfigPath);
21646
21722
  ctx.logger.info(`user config: ${ctx.userConfigPath}`);
21647
21723
  if (!existsSync10(ctx.paths.knowledgeRepo)) {
@@ -21652,24 +21728,23 @@ async function runInit(ctx, opts = { skipClaude: false, skipShared: false, dryRu
21652
21728
  ctx.logger.info(`knowledge repo: ${ctx.paths.knowledgeRepo}`);
21653
21729
  }
21654
21730
  migrateLegacyCommunityDir(ctx);
21655
- const gitignorePath = join12(ctx.paths.knowledgeRepo, ".gitignore");
21731
+ const gitignorePath = join11(ctx.paths.knowledgeRepo, ".gitignore");
21656
21732
  if (!existsSync10(gitignorePath)) {
21657
- writeFileSync5(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21733
+ writeFileSync6(gitignorePath, KNOWLEDGE_GITIGNORE, "utf-8");
21658
21734
  ctx.logger.info(`.gitignore created: ${gitignorePath}`);
21659
21735
  }
21660
- const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21661
- try {
21662
- if (!opts.skipShared && !opts.dryRun) {
21663
- await subscribeSharedRepo(ctx, db);
21664
- } else if (opts.skipShared) {
21665
- ctx.logger.info("shared community DB subscription skipped (--skip-shared)");
21666
- } else if (opts.dryRun) {
21667
- ctx.logger.info(`[dry-run] would subscribe to shared community DB: ${ctx.config.sharedRepo}`);
21668
- }
21669
- } finally {
21736
+ if (!opts.dryRun) {
21737
+ const dbDir = dirname6(ctx.paths.dbPath);
21738
+ if (!existsSync10(dbDir)) mkdirSync5(dbDir, { recursive: true });
21739
+ const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
21670
21740
  db.close();
21741
+ ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
21742
+ } else {
21743
+ ctx.logger.info(`[dry-run] db path: ${ctx.paths.dbPath}`);
21671
21744
  }
21672
- ctx.logger.info(`db initialized: ${ctx.paths.dbPath}`);
21745
+ ctx.logger.info(
21746
+ "tip: subscribe to a group repo with `caveat community add <github-url>`, then `caveat pull`."
21747
+ );
21673
21748
  if (opts.skipClaude) {
21674
21749
  ctx.logger.info("Claude Code integration skipped (--skip-claude)");
21675
21750
  return;
@@ -21680,7 +21755,7 @@ async function runInit(ctx, opts = { skipClaude: false, skipShared: false, dryRu
21680
21755
  return;
21681
21756
  }
21682
21757
  const result = installClaudeIntegration({
21683
- claudeDir: join12(ctx.userHome, ".claude"),
21758
+ claudeDir: join11(ctx.userHome, ".claude"),
21684
21759
  cliScriptPath,
21685
21760
  nodePath: process.execPath,
21686
21761
  dryRun: opts.dryRun,
@@ -21689,7 +21764,7 @@ async function runInit(ctx, opts = { skipClaude: false, skipShared: false, dryRu
21689
21764
  reportInstallResult(ctx, result, opts.dryRun);
21690
21765
  }
21691
21766
  function migrateLegacyCommunityDir(ctx) {
21692
- const legacy = join12(ctx.paths.knowledgeRepo, "community");
21767
+ const legacy = join11(ctx.paths.knowledgeRepo, "community");
21693
21768
  const current = ctx.paths.communityDir;
21694
21769
  if (legacy === current) return;
21695
21770
  if (!existsSync10(legacy)) return;
@@ -21700,9 +21775,9 @@ function migrateLegacyCommunityDir(ctx) {
21700
21775
  return;
21701
21776
  }
21702
21777
  mkdirSync5(current, { recursive: true });
21703
- for (const entry of readdirSync6(legacy, { withFileTypes: true })) {
21778
+ for (const entry of readdirSync5(legacy, { withFileTypes: true })) {
21704
21779
  if (!entry.isDirectory()) continue;
21705
- renameSync(join12(legacy, entry.name), join12(current, entry.name));
21780
+ renameSync(join11(legacy, entry.name), join11(current, entry.name));
21706
21781
  }
21707
21782
  try {
21708
21783
  rmdirSync(legacy);
@@ -21710,52 +21785,6 @@ function migrateLegacyCommunityDir(ctx) {
21710
21785
  }
21711
21786
  ctx.logger.info(`migrated legacy community/ \u2192 ${current}`);
21712
21787
  }
21713
- async function subscribeSharedRepo(ctx, db) {
21714
- const url = ctx.config.sharedRepo;
21715
- const validation = validateCommunityUrl(url);
21716
- if (!validation.valid) {
21717
- ctx.logger.warn(
21718
- `sharedRepo is not a valid GitHub URL \u2014 skipping: ${validation.reason}`
21719
- );
21720
- return;
21721
- }
21722
- const handle = validation.handle;
21723
- const target = join12(ctx.paths.communityDir, handle);
21724
- if (!existsSync10(target)) {
21725
- if (!existsSync10(ctx.paths.communityDir)) {
21726
- mkdirSync5(ctx.paths.communityDir, { recursive: true });
21727
- }
21728
- try {
21729
- await communityAdd({
21730
- url,
21731
- communityDir: ctx.paths.communityDir,
21732
- logger: ctx.logger
21733
- });
21734
- ctx.logger.info(`shared community DB subscribed: ${url} \u2192 community/${handle}/`);
21735
- } catch (err) {
21736
- ctx.logger.warn(
21737
- `shared community DB subscription failed: ${err instanceof Error ? err.message : String(err)}`
21738
- );
21739
- return;
21740
- }
21741
- } else {
21742
- ctx.logger.info(`shared community DB already subscribed: community/${handle}/`);
21743
- }
21744
- if (existsSync10(ctx.paths.communityDir)) {
21745
- rebuildAll(db);
21746
- if (existsSync10(ctx.paths.entriesDir)) {
21747
- scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
21748
- }
21749
- for (const entry of readdirSync6(ctx.paths.communityDir, { withFileTypes: true })) {
21750
- if (!entry.isDirectory()) continue;
21751
- const source = `community/${entry.name}`;
21752
- const root = join12(ctx.paths.communityDir, entry.name, "entries");
21753
- if (!existsSync10(root)) continue;
21754
- const result = scanSource({ db, source, entriesRoot: root });
21755
- ctx.logger.info(`indexed ${source}: +${result.added}`);
21756
- }
21757
- }
21758
- }
21759
21788
  function runUninstall(ctx, opts) {
21760
21789
  const cliScriptPath = process.argv[1];
21761
21790
  if (!cliScriptPath) {
@@ -21763,7 +21792,7 @@ function runUninstall(ctx, opts) {
21763
21792
  process.exit(1);
21764
21793
  }
21765
21794
  const result = uninstallClaudeIntegration({
21766
- claudeDir: join12(ctx.userHome, ".claude"),
21795
+ claudeDir: join11(ctx.userHome, ".claude"),
21767
21796
  cliScriptPath,
21768
21797
  nodePath: process.execPath,
21769
21798
  dryRun: opts.dryRun,
@@ -21800,8 +21829,8 @@ function reportInstallResult(ctx, result, dryRun) {
21800
21829
  }
21801
21830
 
21802
21831
  // src/commands/indexCmd.ts
21803
- import { existsSync as existsSync11, readdirSync as readdirSync7, mkdirSync as mkdirSync6 } from "node:fs";
21804
- import { dirname as dirname7, join as join13 } 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";
21805
21834
  function runIndex(ctx, opts) {
21806
21835
  const dbDir = dirname7(ctx.paths.dbPath);
21807
21836
  if (!existsSync11(dbDir)) mkdirSync6(dbDir, { recursive: true });
@@ -21818,10 +21847,10 @@ function runIndex(ctx, opts) {
21818
21847
  ctx.logger.warn(`entries dir not found: ${ctx.paths.entriesDir}`);
21819
21848
  }
21820
21849
  if (existsSync11(ctx.paths.communityDir)) {
21821
- for (const entry of readdirSync7(ctx.paths.communityDir, { withFileTypes: true })) {
21850
+ for (const entry of readdirSync6(ctx.paths.communityDir, { withFileTypes: true })) {
21822
21851
  if (!entry.isDirectory()) continue;
21823
21852
  const source = `community/${entry.name}`;
21824
- const root = join13(ctx.paths.communityDir, entry.name, "entries");
21853
+ const root = join12(ctx.paths.communityDir, entry.name, "entries");
21825
21854
  if (!existsSync11(root)) continue;
21826
21855
  const result = scanSource({ db, source, entriesRoot: root });
21827
21856
  ctx.logger.info(`${source}: +${result.added} ~${result.updated} -${result.deleted}`);
@@ -22596,11 +22625,11 @@ var serve = (options2, listeningListener) => {
22596
22625
 
22597
22626
  // ../web/dist/context.js
22598
22627
  import { homedir as homedir2 } from "node:os";
22599
- import { join as join14 } from "node:path";
22628
+ import { join as join13 } from "node:path";
22600
22629
  function buildWebContext(overrides = {}) {
22601
22630
  const userHome = overrides.userHome ?? homedir2();
22602
22631
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
22603
- const userConfigPath = join14(userHome, ".caveatrc.json");
22632
+ const userConfigPath = join13(userHome, ".caveatrc.json");
22604
22633
  const logger = overrides.logger ?? stderrLogger;
22605
22634
  const config3 = loadConfig(userConfigPath);
22606
22635
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -24705,7 +24734,7 @@ ul.entries li { padding: 0.5rem 0; border-bottom: 1px solid #eee; }
24705
24734
  ul.entries a.title { font-weight: 600; color: #0a5fff; text-decoration: none; }
24706
24735
  ul.entries a.title:hover { text-decoration: underline; }
24707
24736
  ul.entries .meta { font-size: 0.85rem; color: #666; margin-top: 0.2rem; }
24708
- ul.entries .meta .badge {
24737
+ .badge {
24709
24738
  display: inline-block;
24710
24739
  padding: 0 0.4rem;
24711
24740
  margin-right: 0.3rem;
@@ -24713,10 +24742,12 @@ ul.entries .meta .badge {
24713
24742
  background: #f0f0f0;
24714
24743
  font-size: 0.8rem;
24715
24744
  }
24716
- ul.entries .meta .badge.confirmed { background: #d7f4dc; color: #16703a; }
24717
- ul.entries .meta .badge.reproduced { background: #e6eeff; color: #234ea3; }
24718
- ul.entries .meta .badge.tentative { background: #fff4d1; color: #8a5a00; }
24719
- ul.entries .meta .badge.impossible { background: #ffd9d9; color: #8a1a1a; }
24745
+ .badge.confirmed { background: #d7f4dc; color: #16703a; }
24746
+ .badge.reproduced { background: #e6eeff; color: #234ea3; }
24747
+ .badge.tentative { background: #fff4d1; color: #8a5a00; }
24748
+ .badge.impossible { background: #ffd9d9; color: #8a1a1a; }
24749
+ .badge.public { background: #eef2f5; color: #5a6470; }
24750
+ .badge.private { background: #fde7e7; color: #a02525; font-weight: 600; }
24720
24751
  ul.entries .excerpt { margin-top: 0.3rem; color: #444; font-size: 0.92rem; }
24721
24752
  article h2 { border-bottom: 1px solid #eee; padding-bottom: 0.2rem; margin-top: 1.6rem; }
24722
24753
  article pre {
@@ -24791,6 +24822,7 @@ function createIndexRoute(ctx) {
24791
24822
  const q2 = c3.req.query("q")?.trim() ?? "";
24792
24823
  const sourceParam = c3.req.query("source");
24793
24824
  const tagParam = c3.req.query("tag");
24825
+ const visibilityParam = c3.req.query("visibility");
24794
24826
  const filters = {};
24795
24827
  if (sourceParam === "own" || sourceParam === "community" || sourceParam === "all") {
24796
24828
  filters.source = sourceParam;
@@ -24798,17 +24830,22 @@ function createIndexRoute(ctx) {
24798
24830
  if (tagParam) {
24799
24831
  filters.tags = [tagParam];
24800
24832
  }
24801
- const results = q2 ? search(ctx.db, { query: q2, filters, limit: 50 }) : listRecent(ctx.db, 50);
24833
+ if (visibilityParam === "public" || visibilityParam === "private") {
24834
+ filters.visibility = visibilityParam;
24835
+ }
24836
+ const results = search(ctx.db, { query: q2, filters, limit: 50 });
24802
24837
  const rows = results.map((r2) => {
24803
24838
  const envEntries = Object.entries(r2.environment ?? {});
24804
24839
  const envSummary = envEntries.slice(0, 3).map(([k2, v]) => `${escapeHtml(k2)}:${escapeHtml(String(v))}`).join(" ");
24805
24840
  const hrefSource = r2.source === "own" ? "" : `?source=${encodeURIComponent(r2.source)}`;
24841
+ const visibilityBadge = r2.visibility === "private" ? `<span class="badge private" title="local-only \u2014 never pushed to community DB">\u{1F512} private</span>` : `<span class="badge public">public</span>`;
24806
24842
  return `
24807
24843
  <li>
24808
24844
  <a class="title" href="/g/${encodeURIComponent(r2.id)}${hrefSource}">${escapeHtml(r2.title)}</a>
24809
24845
  <div class="meta">
24810
24846
  <span class="badge ${escapeHtml(r2.confidence ?? "")}">${escapeHtml(r2.confidence ?? "")}</span>
24811
24847
  <span class="badge">${escapeHtml(r2.source)}</span>
24848
+ ${visibilityBadge}
24812
24849
  ${envSummary ? `<span>${envSummary}</span>` : ""}
24813
24850
  </div>
24814
24851
  ${r2.symptomExcerpt ? `<div class="excerpt">${escapeHtml(r2.symptomExcerpt)}</div>` : ""}
@@ -24822,9 +24859,18 @@ function createIndexRoute(ctx) {
24822
24859
  <option value="own" ${sourceParam === "own" ? "selected" : ""}>own</option>
24823
24860
  <option value="community" ${sourceParam === "community" ? "selected" : ""}>community</option>
24824
24861
  </select>
24862
+ <select name="visibility" title="visibility filter \u2014 useful when sharing screen">
24863
+ <option value="" ${!visibilityParam ? "selected" : ""}>any visibility</option>
24864
+ <option value="public" ${visibilityParam === "public" ? "selected" : ""}>public only</option>
24865
+ <option value="private" ${visibilityParam === "private" ? "selected" : ""}>private only</option>
24866
+ </select>
24825
24867
  <button type="submit">search</button>
24826
24868
  </form>
24827
- ${results.length > 0 ? `<ul class="entries">${rows}</ul>` : `<div class="empty">${q2 ? `no results for "${escapeHtml(q2)}"` : "no entries yet \u2014 run <code>caveat index</code> after adding md files"}</div>`}`;
24869
+ ${results.length > 0 ? `<ul class="entries">${rows}</ul>` : (() => {
24870
+ if (q2) return `<div class="empty">no results for "${escapeHtml(q2)}"</div>`;
24871
+ const hasFilter = !!(sourceParam || tagParam || visibilityParam);
24872
+ return hasFilter ? `<div class="empty">no entries match the active filters</div>` : `<div class="empty">no entries yet \u2014 run <code>caveat index</code> after adding md files</div>`;
24873
+ })()}`;
24828
24874
  const title = q2 ? `search: ${q2} \xB7 Caveat` : "Caveat";
24829
24875
  return c3.html(layout(title, body));
24830
24876
  });
@@ -30290,10 +30336,11 @@ function createDetailRoute(ctx) {
30290
30336
  }
30291
30337
  const fm = entry.frontmatter;
30292
30338
  const envRows = Object.entries(fm.environment ?? {}).map(([k2, v]) => `<dt>${escapeHtml(k2)}</dt><dd>${escapeHtml(String(v))}</dd>`).join("");
30339
+ const visibilityBadge = fm.visibility === "private" ? `<span class="badge private" title="local-only \u2014 never pushed to community DB">\u{1F512} private</span>` : `<span class="badge public">public</span>`;
30293
30340
  const metaRows = [
30294
30341
  `<dt>id</dt><dd><code>${escapeHtml(fm.id)}</code></dd>`,
30295
30342
  `<dt>source</dt><dd>${escapeHtml(entry.source)}</dd>`,
30296
- `<dt>visibility</dt><dd>${escapeHtml(fm.visibility)}</dd>`,
30343
+ `<dt>visibility</dt><dd>${visibilityBadge}</dd>`,
30297
30344
  `<dt>confidence</dt><dd><span class="badge ${escapeHtml(fm.confidence)}">${escapeHtml(fm.confidence)}</span></dd>`
30298
30345
  ];
30299
30346
  if (fm.outcome) {
@@ -30327,14 +30374,14 @@ function createDetailRoute(ctx) {
30327
30374
  }
30328
30375
 
30329
30376
  // ../web/dist/routes/community.js
30330
- import { existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "node:fs";
30331
- import { join as join15 } 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";
30332
30379
  function listCommunity(communityDir, db) {
30333
30380
  if (!existsSync12(communityDir)) return [];
30334
30381
  const handles = [];
30335
- for (const entry of readdirSync8(communityDir, { withFileTypes: true })) {
30382
+ for (const entry of readdirSync7(communityDir, { withFileTypes: true })) {
30336
30383
  if (!entry.isDirectory()) continue;
30337
- const handlePath = join15(communityDir, entry.name);
30384
+ const handlePath = join14(communityDir, entry.name);
30338
30385
  const countRow = db.prepare("SELECT COUNT(*) AS n FROM entries WHERE source = ?").get(`community/${entry.name}`);
30339
30386
  handles.push({
30340
30387
  handle: entry.name,
@@ -30351,7 +30398,7 @@ function createCommunityRoute(ctx) {
30351
30398
  const list2 = handles.map((h2) => {
30352
30399
  let mtime = "";
30353
30400
  try {
30354
- mtime = statSync4(h2.path).mtime.toISOString().slice(0, 10);
30401
+ mtime = statSync5(h2.path).mtime.toISOString().slice(0, 10);
30355
30402
  } catch {
30356
30403
  mtime = "";
30357
30404
  }
@@ -44632,11 +44679,11 @@ var StdioServerTransport = class {
44632
44679
 
44633
44680
  // ../mcp/dist/context.js
44634
44681
  import { homedir as homedir3 } from "node:os";
44635
- import { join as join16 } from "node:path";
44682
+ import { join as join15 } from "node:path";
44636
44683
  function buildMcpContext(overrides = {}) {
44637
44684
  const userHome = overrides.userHome ?? homedir3();
44638
44685
  const caveatHome = overrides.caveatHome ?? findCaveatHome(userHome);
44639
- const userConfigPath = join16(userHome, ".caveatrc.json");
44686
+ const userConfigPath = join15(userHome, ".caveatrc.json");
44640
44687
  const logger = overrides.logger ?? stderrLogger;
44641
44688
  const config3 = loadConfig(userConfigPath);
44642
44689
  const paths = resolvePaths(caveatHome, config3.knowledgeRepo, userHome);
@@ -44745,45 +44792,37 @@ function handleListRecent(ctx, args) {
44745
44792
  }
44746
44793
 
44747
44794
  // ../mcp/dist/tools/pull.js
44795
+ import { existsSync as existsSync13, readdirSync as readdirSync8 } from "node:fs";
44796
+ import { join as join16 } from "node:path";
44748
44797
  var pullInputShape = {};
44749
44798
  async function handlePull(ctx, _args = {}) {
44750
- const result = await pullShared({
44751
- communityDir: ctx.paths.communityDir,
44752
- entriesDir: ctx.paths.entriesDir,
44753
- db: ctx.db,
44754
- logger: ctx.logger
44755
- });
44756
- return {
44757
- pulled: result.pulled,
44758
- indexed: result.indexed.map((i2) => ({
44759
- source: i2.source,
44760
- added: i2.added,
44761
- updated: i2.updated,
44762
- deleted: i2.deleted
44763
- }))
44764
- };
44765
- }
44766
-
44767
- // ../mcp/dist/tools/push.js
44768
- var pushInputShape = {
44769
- id: external_exports.string().min(1).describe("Entry id (frontmatter.id) to push to the shared community DB"),
44770
- dry_run: external_exports.boolean().optional().describe("If true, compute the plan but do not modify GitHub or local git state")
44771
- };
44772
- async function handlePush(ctx, args) {
44773
- const result = await pushEntry({
44774
- entriesDir: ctx.paths.entriesDir,
44775
- caveatHome: ctx.caveatHome,
44776
- sharedRepoUrl: ctx.config.sharedRepo,
44777
- id: args.id,
44778
- dryRun: args.dry_run ?? false,
44779
- logger: ctx.logger
44780
- });
44781
- return {
44782
- status: result.status,
44783
- detail: result.detail,
44784
- pr_url: result.prUrl,
44785
- planned_steps: result.plannedSteps
44786
- };
44799
+ const pulled = [];
44800
+ const indexed = [];
44801
+ if (existsSync13(ctx.paths.communityDir)) {
44802
+ const results = await communityPull({
44803
+ communityDir: ctx.paths.communityDir,
44804
+ logger: ctx.logger
44805
+ });
44806
+ for (const r2 of results) {
44807
+ pulled.push({ handle: r2.handle, status: r2.status, message: r2.message });
44808
+ }
44809
+ }
44810
+ rebuildAll(ctx.db);
44811
+ if (existsSync13(ctx.paths.entriesDir)) {
44812
+ const own = scanSource({ db: ctx.db, source: "own", entriesRoot: ctx.paths.entriesDir });
44813
+ indexed.push({ source: "own", ...own });
44814
+ }
44815
+ if (existsSync13(ctx.paths.communityDir)) {
44816
+ for (const entry of readdirSync8(ctx.paths.communityDir, { withFileTypes: true })) {
44817
+ if (!entry.isDirectory()) continue;
44818
+ const source = `community/${entry.name}`;
44819
+ const root = join16(ctx.paths.communityDir, entry.name, "entries");
44820
+ if (!existsSync13(root)) continue;
44821
+ const scan = scanSource({ db: ctx.db, source, entriesRoot: root });
44822
+ indexed.push({ source, ...scan });
44823
+ }
44824
+ }
44825
+ return { pulled, indexed };
44787
44826
  }
44788
44827
 
44789
44828
  // ../mcp/dist/registerTools.js
@@ -44847,20 +44886,11 @@ function registerAllTools(server, ctx) {
44847
44886
  "caveat_pull",
44848
44887
  {
44849
44888
  title: "caveat_pull",
44850
- description: "git-pull the subscribed community caveat repos (the shared knowledge DB + any user-added remotes) and re-index. Call when: (a) the user explicitly asks about others' knowledge on a topic, or (b) caveat_search returned empty for a query that feels like it should have hits and the DB might be stale. Do NOT call reflexively at session start \u2014 it is cheap but not free, and stale-by-minutes is acceptable. Safe and idempotent.",
44889
+ description: "git-pull every subscribed community caveat repo (added via `caveat community add`) and re-index. Call when: (a) the user explicitly asks about others' knowledge on a topic, or (b) caveat_search returned empty for a query that feels like it should have hits and a subscribed repo might be stale. Do NOT call reflexively at session start \u2014 it is cheap but not free, and stale-by-minutes is acceptable. Safe and idempotent.",
44851
44890
  inputSchema: pullInputShape
44852
44891
  },
44853
44892
  async (args) => jsonResult(await handlePull(ctx, args))
44854
44893
  );
44855
- server.registerTool(
44856
- "caveat_push",
44857
- {
44858
- title: "caveat_push",
44859
- description: "Contribute a user-owned caveat to the public shared DB via fork + PR on GitHub. This is a PUBLIC, externally-visible action (the PR appears on GitHub and is hard to fully retract). Confirm with the user before calling without dry_run \u2014 or call with dry_run=true first to show the plan. Only push entries that document a trap genuinely reusable by others (not project-specific ties, not duplicated by existing community entries). Entries with `visibility: private` are rejected (`status=visibility-private`) \u2014 the user has declared them local-only. Requires `gh` CLI authenticated; returns `status=gh-missing` / `gh-unauthed` on failure.",
44860
- inputSchema: pushInputShape
44861
- },
44862
- async (args) => jsonResult(await handlePush(ctx, args))
44863
- );
44864
44894
  }
44865
44895
 
44866
44896
  // ../mcp/dist/server.js
@@ -44900,6 +44930,19 @@ async function runMcpServer() {
44900
44930
  }
44901
44931
 
44902
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
+ };
44903
44946
  async function readStdin() {
44904
44947
  const chunks = [];
44905
44948
  for await (const chunk of process.stdin) {
@@ -44918,7 +44961,153 @@ function parsePayload(raw2) {
44918
44961
  return {};
44919
44962
  }
44920
44963
  }
44921
- 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
+ }
44922
45111
  let raw2 = "";
44923
45112
  try {
44924
45113
  raw2 = await readStdin();
@@ -44929,22 +45118,37 @@ async function runHook(name) {
44929
45118
  process.exit(0);
44930
45119
  }
44931
45120
  const payload = parsePayload(raw2);
45121
+ const sessionId = getSessionId(payload);
45122
+ drainForSession(sessionId);
44932
45123
  if (name === "user-prompt-submit") {
44933
45124
  const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
44934
- if (detectCaveatTrigger(prompt)) {
45125
+ const hits = searchCaveatsFromTextSafely(prompt);
45126
+ if (hits.length > 0) {
44935
45127
  process.stdout.write(
44936
- `<system-reminder>${userPromptSubmitReminderText()}</system-reminder>
45128
+ `<system-reminder>${userPromptSubmitReminderText(hits)}</system-reminder>
44937
45129
  `
44938
45130
  );
44939
45131
  }
44940
45132
  process.exit(0);
44941
45133
  }
44942
- if (name === "stop") {
44943
- if (payload.stop_hook_active === true) {
44944
- 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 });
44945
45139
  }
44946
- process.stdout.write(`<system-reminder>${stopReminderText()}</system-reminder>
44947
- `);
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
+ );
44948
45152
  process.exit(0);
44949
45153
  }
44950
45154
  process.stderr.write(`[caveat:hook] unknown hook name: ${name}
@@ -44953,77 +45157,46 @@ async function runHook(name) {
44953
45157
  }
44954
45158
 
44955
45159
  // src/commands/pull.ts
44956
- import { existsSync as existsSync13 } from "node:fs";
45160
+ import { existsSync as existsSync15, readdirSync as readdirSync9 } from "node:fs";
45161
+ import { join as join18 } from "node:path";
44957
45162
  async function runPull(ctx) {
44958
- if (!existsSync13(ctx.paths.communityDir)) {
45163
+ if (!existsSync15(ctx.paths.communityDir)) {
44959
45164
  ctx.logger.info(
44960
- "no community repos yet \u2014 did you run `caveat init`? You can also `caveat community add <url>` manually."
45165
+ "no community repos yet \u2014 add one with `caveat community add <github-url>`."
44961
45166
  );
44962
45167
  return;
44963
45168
  }
45169
+ const pulls = await communityPull({
45170
+ communityDir: ctx.paths.communityDir,
45171
+ logger: ctx.logger
45172
+ });
45173
+ for (const p2 of pulls) {
45174
+ if (p2.status === "ok") {
45175
+ ctx.logger.info(`${p2.handle}: pulled`);
45176
+ } else {
45177
+ ctx.logger.warn(`${p2.handle}: FAILED \u2014 ${p2.message ?? "unknown"}`);
45178
+ }
45179
+ }
44964
45180
  const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
44965
45181
  try {
44966
- const result = await pullShared({
44967
- communityDir: ctx.paths.communityDir,
44968
- entriesDir: ctx.paths.entriesDir,
44969
- db,
44970
- logger: ctx.logger
44971
- });
44972
- for (const p2 of result.pulled) {
44973
- if (p2.status === "ok") {
44974
- ctx.logger.info(`${p2.handle}: pulled`);
44975
- } else {
44976
- ctx.logger.warn(`${p2.handle}: FAILED \u2014 ${p2.message ?? "unknown"}`);
44977
- }
45182
+ rebuildAll(db);
45183
+ if (existsSync15(ctx.paths.entriesDir)) {
45184
+ const own = scanSource({ db, source: "own", entriesRoot: ctx.paths.entriesDir });
45185
+ ctx.logger.info(`own: +${own.added}`);
44978
45186
  }
44979
- for (const i2 of result.indexed) {
44980
- ctx.logger.info(`${i2.source}: +${i2.added}`);
45187
+ for (const entry of readdirSync9(ctx.paths.communityDir, { withFileTypes: true })) {
45188
+ if (!entry.isDirectory()) continue;
45189
+ const source = `community/${entry.name}`;
45190
+ const root = join18(ctx.paths.communityDir, entry.name, "entries");
45191
+ if (!existsSync15(root)) continue;
45192
+ const scan = scanSource({ db, source, entriesRoot: root });
45193
+ ctx.logger.info(`${source}: +${scan.added}`);
44981
45194
  }
44982
45195
  } finally {
44983
45196
  db.close();
44984
45197
  }
44985
45198
  }
44986
45199
 
44987
- // src/commands/push.ts
44988
- async function runPush(ctx, opts) {
44989
- const result = await pushEntry({
44990
- entriesDir: ctx.paths.entriesDir,
44991
- caveatHome: ctx.caveatHome,
44992
- sharedRepoUrl: ctx.config.sharedRepo,
44993
- id: opts.id,
44994
- dryRun: opts.dryRun,
44995
- logger: ctx.logger
44996
- });
44997
- switch (result.status) {
44998
- case "ok":
44999
- ctx.logger.info(`PR opened: ${result.prUrl ?? ""}`);
45000
- ctx.logger.info(
45001
- "Once merged, other subscribers will see your entry after their next `caveat pull`."
45002
- );
45003
- break;
45004
- case "dry-run":
45005
- for (const [i2, step] of (result.plannedSteps ?? []).entries()) {
45006
- ctx.logger.info(`[dry-run] ${i2 + 1}. ${step}`);
45007
- }
45008
- break;
45009
- case "not-found":
45010
- ctx.logger.error(
45011
- `${result.detail}. List entries with \`caveat list\` or write + index first.`
45012
- );
45013
- process.exitCode = 1;
45014
- break;
45015
- case "gh-missing":
45016
- case "gh-unauthed":
45017
- ctx.logger.error(result.detail ?? "gh CLI issue");
45018
- process.exitCode = 1;
45019
- break;
45020
- case "failed":
45021
- ctx.logger.error(result.detail ?? "unknown failure");
45022
- process.exitCode = 1;
45023
- break;
45024
- }
45025
- }
45026
-
45027
45200
  // src/commands/community.ts
45028
45201
  async function runCommunityAdd(ctx, url) {
45029
45202
  try {
@@ -45073,17 +45246,46 @@ function runCommunityList(ctx) {
45073
45246
  db.close();
45074
45247
  }
45075
45248
  }
45249
+ function runCommunityRemove(ctx, handle, opts) {
45250
+ const db = openDb({ path: ctx.paths.dbPath, logger: ctx.logger });
45251
+ try {
45252
+ const result = communityRemove({
45253
+ communityDir: ctx.paths.communityDir,
45254
+ handle,
45255
+ db,
45256
+ dryRun: opts.dryRun,
45257
+ logger: ctx.logger
45258
+ });
45259
+ if (!result.dirExisted && result.rowCount === 0) {
45260
+ ctx.logger.warn(`no community subscription named "${handle}" \u2014 nothing to remove`);
45261
+ return;
45262
+ }
45263
+ const prefix = result.dryRun ? "[dry-run] would " : "";
45264
+ if (result.dirExisted) {
45265
+ ctx.logger.info(`${prefix}remove dir: ${result.path}`);
45266
+ }
45267
+ if (result.rowCount > 0) {
45268
+ ctx.logger.info(
45269
+ `${prefix}delete ${result.rowCount} db row(s) for source=community/${handle}`
45270
+ );
45271
+ }
45272
+ } catch (err) {
45273
+ ctx.logger.error(err instanceof Error ? err.message : String(err));
45274
+ process.exitCode = 1;
45275
+ } finally {
45276
+ db.close();
45277
+ }
45278
+ }
45076
45279
 
45077
45280
  // src/index.ts
45078
45281
  var program = new Command();
45079
45282
  program.name("caveat").description("External spec gotcha knowledge base CLI").version(CAVEAT_VERSION);
45080
45283
  program.command("init").description(
45081
- "Initialize ~/.caveatrc.json, ~/.caveat/, subscribe to the shared community DB, and register Claude Code integration"
45082
- ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--skip-shared", "skip subscribing to the shared community DB", false).option("--dry-run", "show planned changes without writing", false).action(async (opts) => {
45284
+ "Initialize ~/.caveatrc.json, ~/.caveat/, and register Claude Code integration. Add knowledge sources later with `caveat community add <github-url>`."
45285
+ ).option("--skip-claude", "skip Claude Code MCP + hook registration", false).option("--dry-run", "show planned changes without writing", false).action(async (opts) => {
45083
45286
  const ctx = buildContext(stdoutLogger);
45084
45287
  await runInit(ctx, {
45085
45288
  skipClaude: opts.skipClaude,
45086
- skipShared: opts.skipShared,
45087
45289
  dryRun: opts.dryRun
45088
45290
  });
45089
45291
  });
@@ -45119,23 +45321,21 @@ program.command("stats").description("Show aggregate stats").action(() => {
45119
45321
  runStats(ctx);
45120
45322
  });
45121
45323
  program.command("pull").description(
45122
- "Refresh all subscribed community repos (incl. shared DB) and re-index. Use this to receive contributions others have merged."
45324
+ "git-pull every subscribed community repo and re-index. Use this to receive updates from group/teammate repos."
45123
45325
  ).action(async () => {
45124
45326
  const ctx = buildContext(stdoutLogger);
45125
45327
  await runPull(ctx);
45126
45328
  });
45127
- program.command("push").description("Contribute a caveat to the shared community DB via fork + PR (requires gh CLI).").argument("<id>", "entry id (from `caveat list`)").option("--dry-run", "show planned steps without making GitHub changes", false).action(async (id, opts) => {
45128
- const ctx = buildContext(stdoutLogger);
45129
- await runPush(ctx, { id, dryRun: opts.dryRun });
45130
- });
45131
45329
  program.command("serve").description("Start the read-only web share portal").option("--port <n>", "port number", (v) => Number(v), 4242).action((opts) => {
45132
45330
  runServe({ port: opts.port });
45133
45331
  });
45134
45332
  program.command("mcp-server").description("Run the MCP stdio server (registered by `caveat init`)").action(async () => {
45135
45333
  await runMcpServer();
45136
45334
  });
45137
- program.command("hook <name>").description("Run a Claude Code hook. name: user-prompt-submit | stop").action(async (name) => {
45138
- 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);
45139
45339
  });
45140
45340
  var community = program.command("community").description("Manage community caveat repos (shallow clones under <knowledgeRepo>/community/)");
45141
45341
  community.command("add <url>").description("Shallow-clone a GitHub caveat repo into community/<handle>/").action(async (url) => {
@@ -45150,6 +45350,10 @@ community.command("list").description("List imported community repos with entry
45150
45350
  const ctx = buildContext(stdoutLogger);
45151
45351
  runCommunityList(ctx);
45152
45352
  });
45353
+ community.command("remove <handle>").description("Unsubscribe from a community repo: delete community/<handle>/ and purge its DB rows").option("--dry-run", "show what would be removed without touching disk or db", false).action((handle, opts) => {
45354
+ const ctx = buildContext(stdoutLogger);
45355
+ runCommunityRemove(ctx, handle, { dryRun: opts.dryRun });
45356
+ });
45153
45357
  program.parseAsync(process.argv).catch((err) => {
45154
45358
  const msg = err instanceof Error ? err.message : String(err);
45155
45359
  process.stderr.write(`[caveat:error] ${msg}