mindgraph-mcp 0.23.0 → 0.24.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/cli.js CHANGED
@@ -35,14 +35,14 @@ function errorFields(body) {
35
35
  ...typeof fields.retriable === "boolean" ? { retriable: fields.retriable } : {}
36
36
  };
37
37
  }
38
- function canRetryRequest(method, path8, body) {
38
+ function canRetryRequest(method, path9, body) {
39
39
  if (method === "GET" || method === "HEAD") return true;
40
40
  if (method !== "POST") return false;
41
- if (READ_POSTS.has(path8)) return true;
41
+ if (READ_POSTS.has(path9)) return true;
42
42
  const fields = body && typeof body === "object" && !Array.isArray(body) ? body : {};
43
43
  if (typeof fields.action !== "string") return false;
44
- if (Object.prototype.hasOwnProperty.call(READ_ACTIONS, path8) && READ_ACTIONS[path8].includes(fields.action)) return true;
45
- return path8 === "/agent/plan" && IDEMPOTENT_WORK.has(fields.action) && typeof fields.idempotency_key === "string" && fields.idempotency_key.trim().length > 0;
44
+ if (Object.prototype.hasOwnProperty.call(READ_ACTIONS, path9) && READ_ACTIONS[path9].includes(fields.action)) return true;
45
+ return path9 === "/agent/plan" && IDEMPOTENT_WORK.has(fields.action) && typeof fields.idempotency_key === "string" && fields.idempotency_key.trim().length > 0;
46
46
  }
47
47
  function permitsRetry(fields) {
48
48
  return fields.retriable !== false && ![
@@ -179,8 +179,8 @@ var init_dist = __esm({
179
179
  }
180
180
  }
181
181
  // ---- HTTP helpers ----
182
- async request(method, path8, body) {
183
- const url = `${this.baseUrl}${path8}`;
182
+ async request(method, path9, body) {
183
+ const url = `${this.baseUrl}${path9}`;
184
184
  const requestId = createRequestId();
185
185
  const init = {
186
186
  method,
@@ -195,12 +195,12 @@ var init_dist = __esm({
195
195
  }
196
196
  const retrySafe = canRetryRequest(
197
197
  method,
198
- path8,
198
+ path9,
199
199
  typeof init.body === "string" ? JSON.parse(init.body) : void 0
200
200
  );
201
201
  let lastError;
202
202
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
203
- const res = await this.fetchWithDeadline(url, init, method, path8);
203
+ const res = await this.fetchWithDeadline(url, init, method, path9);
204
204
  if (!res.ok) {
205
205
  const text22 = await res.text().catch(() => "");
206
206
  let parsed;
@@ -210,7 +210,7 @@ var init_dist = __esm({
210
210
  parsed = text22;
211
211
  }
212
212
  const err6 = new MindGraphError(
213
- `${method} ${path8} failed: ${res.status}`,
213
+ `${method} ${path9} failed: ${res.status}`,
214
214
  res.status,
215
215
  parsed
216
216
  );
@@ -234,7 +234,7 @@ var init_dist = __esm({
234
234
  * "timeout"`, `status: 0`). Without it the request is sent unchanged — no
235
235
  * AbortSignal is attached, preserving the pre-0.16 behaviour.
236
236
  */
237
- async fetchWithDeadline(url, init, method, path8) {
237
+ async fetchWithDeadline(url, init, method, path9) {
238
238
  if (this.timeoutMs === void 0) {
239
239
  return fetch(url, init);
240
240
  }
@@ -245,7 +245,7 @@ var init_dist = __esm({
245
245
  } catch (error2) {
246
246
  if (controller.signal.aborted) {
247
247
  throw new MindGraphError(
248
- `${method} ${path8} timed out after ${this.timeoutMs}ms`,
248
+ `${method} ${path9} timed out after ${this.timeoutMs}ms`,
249
249
  0,
250
250
  { error: "timeout", code: "timeout", retriable: false }
251
251
  );
@@ -255,17 +255,17 @@ var init_dist = __esm({
255
255
  clearTimeout(timer);
256
256
  }
257
257
  }
258
- get(path8) {
259
- return this.request("GET", path8);
258
+ get(path9) {
259
+ return this.request("GET", path9);
260
260
  }
261
- post(path8, body) {
262
- return this.request("POST", path8, body);
261
+ post(path9, body) {
262
+ return this.request("POST", path9, body);
263
263
  }
264
- patch(path8, body) {
265
- return this.request("PATCH", path8, body);
264
+ patch(path9, body) {
265
+ return this.request("PATCH", path9, body);
266
266
  }
267
- del(path8) {
268
- return this.request("DELETE", path8);
267
+ del(path9) {
268
+ return this.request("DELETE", path9);
269
269
  }
270
270
  // ---- Health ----
271
271
  async health() {
@@ -1240,14 +1240,14 @@ function unavailabilityCaveats(reason, message) {
1240
1240
  }
1241
1241
  return caveats;
1242
1242
  }
1243
- function normalizePath(path8) {
1244
- return path8.replaceAll("\\", "/").replace(/^\.\//, "");
1243
+ function normalizePath(path9) {
1244
+ return path9.replaceAll("\\", "/").replace(/^\.\//, "");
1245
1245
  }
1246
1246
  function normalizeSignature(signature) {
1247
1247
  return signature.replace(/\s+/g, " ").trim();
1248
1248
  }
1249
- function languageFromPath(path8) {
1250
- const extension = (0, import_node_path2.extname)(path8).toLowerCase();
1249
+ function languageFromPath(path9) {
1250
+ const extension = (0, import_node_path2.extname)(path9).toLowerCase();
1251
1251
  return {
1252
1252
  ".c": "c",
1253
1253
  ".cc": "cpp",
@@ -2564,10 +2564,111 @@ var init_code_tool = __esm({
2564
2564
  }
2565
2565
  });
2566
2566
 
2567
+ // src/call-ledger.ts
2568
+ function callsDir(options) {
2569
+ return import_node_path3.default.join(
2570
+ options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path3.default.join(import_node_os3.default.homedir(), ".mindgraph", "runtime"),
2571
+ "calls"
2572
+ );
2573
+ }
2574
+ function stableJson(value) {
2575
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
2576
+ if (value && typeof value === "object") {
2577
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
2578
+ }
2579
+ return JSON.stringify(value) ?? "null";
2580
+ }
2581
+ function bareToolName(name) {
2582
+ const cut = name.lastIndexOf("__");
2583
+ return cut === -1 ? name : name.slice(cut + 2);
2584
+ }
2585
+ function callKey(toolName, toolInput) {
2586
+ return import_node_crypto4.default.createHash("sha256").update(`${bareToolName(toolName)}\0${stableJson(toolInput)}`).digest("hex");
2587
+ }
2588
+ function sessionKey(sessionId) {
2589
+ return import_node_crypto4.default.createHash("sha256").update(sessionId).digest("hex").slice(0, 16);
2590
+ }
2591
+ function prune(dir, now) {
2592
+ let names;
2593
+ try {
2594
+ names = import_node_fs3.default.readdirSync(dir);
2595
+ } catch {
2596
+ return;
2597
+ }
2598
+ for (const name of names) {
2599
+ const file = import_node_path3.default.join(dir, name);
2600
+ try {
2601
+ if (now - import_node_fs3.default.statSync(file).mtimeMs > TTL_MS) import_node_fs3.default.unlinkSync(file);
2602
+ } catch {
2603
+ }
2604
+ }
2605
+ }
2606
+ function recordPendingCall(toolName, toolInput, harnessSessionId, fills, options = {}) {
2607
+ const now = (options.now ?? Date.now)();
2608
+ const dir = callsDir(options);
2609
+ try {
2610
+ import_node_fs3.default.mkdirSync(dir, { recursive: true, mode: 448 });
2611
+ prune(dir, now);
2612
+ const record = {
2613
+ harnessSessionId,
2614
+ projectDir: options.projectDir ?? process.env.CLAUDE_PROJECT_DIR,
2615
+ fills,
2616
+ createdAt: now
2617
+ };
2618
+ const file = import_node_path3.default.join(
2619
+ dir,
2620
+ `${callKey(toolName, toolInput)}.${sessionKey(harnessSessionId)}.json`
2621
+ );
2622
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
2623
+ import_node_fs3.default.writeFileSync(temporary, JSON.stringify(record), { mode: 384 });
2624
+ import_node_fs3.default.renameSync(temporary, file);
2625
+ } catch {
2626
+ }
2627
+ }
2628
+ function resolvePendingCall(toolName, toolInput, options = {}) {
2629
+ const now = (options.now ?? Date.now)();
2630
+ const dir = callsDir(options);
2631
+ const prefix = `${callKey(toolName, toolInput)}.`;
2632
+ let names;
2633
+ try {
2634
+ names = import_node_fs3.default.readdirSync(dir).filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
2635
+ } catch {
2636
+ return void 0;
2637
+ }
2638
+ let matches = [];
2639
+ for (const name of names) {
2640
+ try {
2641
+ const record = JSON.parse(
2642
+ import_node_fs3.default.readFileSync(import_node_path3.default.join(dir, name), "utf8")
2643
+ );
2644
+ if (typeof record.createdAt === "number" && now - record.createdAt <= TTL_MS && record.fills && typeof record.fills === "object") {
2645
+ matches.push(record);
2646
+ }
2647
+ } catch {
2648
+ }
2649
+ }
2650
+ if (matches.length > 1) {
2651
+ const projectDir = options.projectDir ?? process.env.CLAUDE_PROJECT_DIR;
2652
+ matches = projectDir ? matches.filter((record) => record.projectDir === projectDir) : [];
2653
+ }
2654
+ return matches.length === 1 ? matches[0].fills : void 0;
2655
+ }
2656
+ var import_node_crypto4, import_node_fs3, import_node_os3, import_node_path3, TTL_MS;
2657
+ var init_call_ledger = __esm({
2658
+ "src/call-ledger.ts"() {
2659
+ "use strict";
2660
+ import_node_crypto4 = __toESM(require("crypto"));
2661
+ import_node_fs3 = __toESM(require("fs"));
2662
+ import_node_os3 = __toESM(require("os"));
2663
+ import_node_path3 = __toESM(require("path"));
2664
+ TTL_MS = 10 * 6e4;
2665
+ }
2666
+ });
2667
+
2567
2668
  // src/hook-installer.ts
2568
2669
  function storedFeatures(homeDir) {
2569
2670
  const { promptRecall: promptRecall2, capture } = loadHookEnv(
2570
- import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), ".mindgraph")
2671
+ import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), ".mindgraph")
2571
2672
  );
2572
2673
  return { promptRecall: promptRecall2, capture };
2573
2674
  }
@@ -2578,24 +2679,24 @@ function windowsCommand(harness) {
2578
2679
  return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
2579
2680
  }
2580
2681
  function installHookRunner(sourceFile, homeDir, version) {
2581
- const target = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_RELATIVE);
2582
- import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
2583
- const temporary = `${target}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2584
- import_node_fs5.default.copyFileSync(sourceFile, temporary);
2585
- import_node_fs5.default.renameSync(temporary, target);
2586
- const versionPath = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE);
2682
+ const target = import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_RELATIVE);
2683
+ import_node_fs6.default.mkdirSync(import_node_path5.default.dirname(target), { recursive: true });
2684
+ const temporary = `${target}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
2685
+ import_node_fs6.default.copyFileSync(sourceFile, temporary);
2686
+ import_node_fs6.default.renameSync(temporary, target);
2687
+ const versionPath = import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE);
2587
2688
  if (version) {
2588
- import_node_fs5.default.writeFileSync(versionPath, `${version}
2689
+ import_node_fs6.default.writeFileSync(versionPath, `${version}
2589
2690
  `);
2590
2691
  } else {
2591
- import_node_fs5.default.rmSync(versionPath, { force: true });
2692
+ import_node_fs6.default.rmSync(versionPath, { force: true });
2592
2693
  }
2593
2694
  return target;
2594
2695
  }
2595
2696
  function installedHookRunnerVersion(homeDir) {
2596
2697
  try {
2597
- const raw = import_node_fs5.default.readFileSync(
2598
- import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE),
2698
+ const raw = import_node_fs6.default.readFileSync(
2699
+ import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), RUNNER_VERSION_RELATIVE),
2599
2700
  "utf8"
2600
2701
  ).trim();
2601
2702
  return raw || void 0;
@@ -2620,11 +2721,11 @@ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2620
2721
  if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
2621
2722
  return { refreshed: false, reason: "disabled" };
2622
2723
  }
2623
- const home = homeDir || import_node_os4.default.homedir();
2624
- if (!import_node_fs5.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
2724
+ const home = homeDir || import_node_os5.default.homedir();
2725
+ if (!import_node_fs6.default.existsSync(import_node_path5.default.join(home, RUNNER_RELATIVE))) {
2625
2726
  return { refreshed: false, reason: "no_runner" };
2626
2727
  }
2627
- if (!import_node_fs5.default.existsSync(sourceBundle)) {
2728
+ if (!import_node_fs6.default.existsSync(sourceBundle)) {
2628
2729
  return { refreshed: false, reason: "no_source" };
2629
2730
  }
2630
2731
  const server2 = parseSemver(serverVersion);
@@ -2655,8 +2756,8 @@ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2655
2756
  for (const scope of scopes) {
2656
2757
  try {
2657
2758
  const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
2658
- if (!import_node_fs5.default.existsSync(file)) continue;
2659
- if (!import_node_fs5.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2759
+ if (!import_node_fs6.default.existsSync(file)) continue;
2760
+ if (!import_node_fs6.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2660
2761
  installHooks(
2661
2762
  "claude-code",
2662
2763
  scope,
@@ -2676,15 +2777,15 @@ function versionSkewNote(serverVersion, homeDir) {
2676
2777
  if (!runnerVersion || runnerVersion === serverVersion) return void 0;
2677
2778
  return `NOTE: the installed MindGraph hook runner is v${runnerVersion} but this MCP server is v${serverVersion} \u2014 hook behavior and tool contracts may be out of sync. Tell the user to run: npx -y mindgraph-mcp@latest install-hooks (refreshes the pinned runner).`;
2678
2779
  }
2679
- function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os4.default.homedir(), ".codex"), homeDir) {
2780
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path5.default.join(import_node_os5.default.homedir(), ".codex"), homeDir) {
2680
2781
  if (harness === "codex") {
2681
- return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
2782
+ return scope === "user" ? import_node_path5.default.join(codexHomeDir, "hooks.json") : import_node_path5.default.join(projectDir, ".codex", "hooks.json");
2682
2783
  }
2683
- return scope === "user" ? import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), ".claude", "settings.json") : import_node_path4.default.join(projectDir, ".claude", "settings.json");
2784
+ return scope === "user" ? import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), ".claude", "settings.json") : import_node_path5.default.join(projectDir, ".claude", "settings.json");
2684
2785
  }
2685
2786
  function readSettings(file) {
2686
- if (!import_node_fs5.default.existsSync(file)) return {};
2687
- return JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
2787
+ if (!import_node_fs6.default.existsSync(file)) return {};
2788
+ return JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
2688
2789
  }
2689
2790
  function ownedHook(harness, timeout, additionalContextLimit) {
2690
2791
  return {
@@ -2728,7 +2829,17 @@ function desiredEntries(harness, features = {}) {
2728
2829
  ],
2729
2830
  // PostToolUse awaits the lease renewal round-trip, which fires exactly
2730
2831
  // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
2731
- PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
2832
+ // Claude Code: only the tools the hook acts on (MindGraph calls, edits,
2833
+ // and the shell/agent calls that keep a lease alive). Read, Grep and Glob
2834
+ // are most of a session's tool calls and the hook did nothing useful for
2835
+ // them but start a process (roadmap H8). Codex names its tools
2836
+ // differently, so it keeps the catch-all.
2837
+ PostToolUse: [
2838
+ {
2839
+ matcher: harness === "claude-code" ? CLAUDE_POST_TOOL_MATCHER : ".*",
2840
+ hooks: [ownedHook(harness, 10)]
2841
+ }
2842
+ ],
2732
2843
  Stop: [{ hooks: [ownedHook(harness, 5)] }],
2733
2844
  // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
2734
2845
  // session close) — the SAME cold-tenant latency SessionStart needs 30s
@@ -2762,13 +2873,13 @@ function entryHasOwnedHook(value) {
2762
2873
  return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2763
2874
  }
2764
2875
  function writeSettings(file, settings) {
2765
- import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2766
- const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2767
- import_node_fs5.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2876
+ import_node_fs6.default.mkdirSync(import_node_path5.default.dirname(file), { recursive: true });
2877
+ const temporary = `${file}.${process.pid}.${import_node_crypto6.default.randomUUID()}.tmp`;
2878
+ import_node_fs6.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2768
2879
  `, {
2769
2880
  mode: 384
2770
2881
  });
2771
- import_node_fs5.default.renameSync(temporary, file);
2882
+ import_node_fs6.default.renameSync(temporary, file);
2772
2883
  }
2773
2884
  function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir, features = {}) {
2774
2885
  const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
@@ -2802,7 +2913,7 @@ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir,
2802
2913
  }
2803
2914
  function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2804
2915
  const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2805
- if (!import_node_fs5.default.existsSync(file)) return { path: file, removed: 0 };
2916
+ if (!import_node_fs6.default.existsSync(file)) return { path: file, removed: 0 };
2806
2917
  const settings = readSettings(file);
2807
2918
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2808
2919
  let removed = 0;
@@ -2848,18 +2959,19 @@ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2848
2959
  function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2849
2960
  return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2850
2961
  }
2851
- var import_node_fs5, import_node_os4, import_node_path4, import_node_crypto5, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE;
2962
+ var import_node_fs6, import_node_os5, import_node_path5, import_node_crypto6, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE, CLAUDE_POST_TOOL_MATCHER;
2852
2963
  var init_hook_installer = __esm({
2853
2964
  "src/hook-installer.ts"() {
2854
2965
  "use strict";
2855
- import_node_fs5 = __toESM(require("fs"));
2856
- import_node_os4 = __toESM(require("os"));
2857
- import_node_path4 = __toESM(require("path"));
2858
- import_node_crypto5 = __toESM(require("crypto"));
2966
+ import_node_fs6 = __toESM(require("fs"));
2967
+ import_node_os5 = __toESM(require("os"));
2968
+ import_node_path5 = __toESM(require("path"));
2969
+ import_node_crypto6 = __toESM(require("crypto"));
2859
2970
  init_hook_env();
2860
2971
  OWNER_MARKER = "--owner mindgraph";
2861
2972
  RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2862
2973
  RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
2974
+ CLAUDE_POST_TOOL_MATCHER = "mcp__mindgraph__.*|Edit|MultiEdit|Write|NotebookEdit|Bash|Task|Agent";
2863
2975
  }
2864
2976
  });
2865
2977
 
@@ -3101,6 +3213,14 @@ function hiddenSections(payload) {
3101
3213
  function renderContext(response, maxChars) {
3102
3214
  const payload = obj(response);
3103
3215
  if (!payload) return void 0;
3216
+ if (typeof payload.context_text === "string" && payload.context_text.length > 0) {
3217
+ const text2 = payload.context_text;
3218
+ if (maxChars !== void 0 && text2.length > maxChars) {
3219
+ return `${text2.slice(0, maxChars)}
3220
+ [truncated to ${maxChars} characters]`;
3221
+ }
3222
+ return text2;
3223
+ }
3104
3224
  const graph = obj(payload.graph);
3105
3225
  const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
3106
3226
  const shaped = rawNodes.filter(isNodeShaped);
@@ -3323,15 +3443,15 @@ function renderTraversal(response, maxChars) {
3323
3443
  const labelByUid = /* @__PURE__ */ new Map();
3324
3444
  for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3325
3445
  if (paths.length > 0) {
3326
- const items = paths.map((raw) => obj(raw)).filter((path8) => Boolean(path8)).map((path8, index) => {
3327
- const uids = Array.isArray(path8.node_uids) ? path8.node_uids.map(String) : [];
3328
- const labels = Array.isArray(path8.labels) ? path8.labels.map(String) : Array.isArray(path8.nodes) ? path8.nodes.map((hop) => {
3446
+ const items = paths.map((raw) => obj(raw)).filter((path9) => Boolean(path9)).map((path9, index) => {
3447
+ const uids = Array.isArray(path9.node_uids) ? path9.node_uids.map(String) : [];
3448
+ const labels = Array.isArray(path9.labels) ? path9.labels.map(String) : Array.isArray(path9.nodes) ? path9.nodes.map((hop) => {
3329
3449
  const hopNode = obj(hop);
3330
3450
  return str2(hopNode?.label) || labelByUid.get(str2(hopNode?.uid) || "") || str2(hop) || "?";
3331
3451
  }) : [];
3332
3452
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
3333
- const cost = num(path8.cost) ?? num(path8.path_cost);
3334
- const confidence = num(path8.path_confidence);
3453
+ const cost = num(path9.cost) ?? num(path9.path_cost);
3454
+ const confidence = num(path9.path_confidence);
3335
3455
  const meta = [
3336
3456
  cost !== void 0 ? `cost ${cost}` : void 0,
3337
3457
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -4383,15 +4503,15 @@ function obj2(value) {
4383
4503
  function text(value) {
4384
4504
  return typeof value === "string" && value.length > 0 ? value : void 0;
4385
4505
  }
4386
- function stableJson(value) {
4387
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4506
+ function stableJson2(value) {
4507
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
4388
4508
  if (value && typeof value === "object") {
4389
- return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
4509
+ return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson2(item)}`).join(",")}}`;
4390
4510
  }
4391
4511
  return JSON.stringify(value);
4392
4512
  }
4393
4513
  function sha2562(value) {
4394
- return (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
4514
+ return (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
4395
4515
  }
4396
4516
  function outputBudget(value) {
4397
4517
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OUTPUT_CHARS;
@@ -4697,7 +4817,7 @@ async function handleMemoryTool(client2, args, config = {}) {
4697
4817
  scope_uids: Array.isArray(args.scope_uids) ? args.scope_uids.map(String) : void 0
4698
4818
  });
4699
4819
  const contextId = `ctx_${sha2562(
4700
- stableJson({ mode, scope, strategy, query: text(args.query), rendered_hash: renderedHash })
4820
+ stableJson2({ mode, scope, strategy, query: text(args.query), rendered_hash: renderedHash })
4701
4821
  ).slice(0, 24)}`;
4702
4822
  const bounded = candidates.length > items.length || rendered2.includes("item(s) omitted");
4703
4823
  const result = {
@@ -4742,11 +4862,11 @@ async function handleMemoryTool(client2, args, config = {}) {
4742
4862
  structuredContent: result
4743
4863
  };
4744
4864
  }
4745
- var import_node_crypto6, RESULT_SCHEMA_VERSION, DEFAULT_OUTPUT_CHARS, MIN_OUTPUT_CHARS, MAX_OUTPUT_CHARS, HIDDEN_CONTEXT_NODE_TYPES, HIDDEN_CONTEXT_EDGE_TYPES, MEMORY_TOOL;
4865
+ var import_node_crypto7, RESULT_SCHEMA_VERSION, DEFAULT_OUTPUT_CHARS, MIN_OUTPUT_CHARS, MAX_OUTPUT_CHARS, HIDDEN_CONTEXT_NODE_TYPES, HIDDEN_CONTEXT_EDGE_TYPES, MEMORY_TOOL;
4746
4866
  var init_memory_tool = __esm({
4747
4867
  "src/memory-tool.ts"() {
4748
4868
  "use strict";
4749
- import_node_crypto6 = require("crypto");
4869
+ import_node_crypto7 = require("crypto");
4750
4870
  init_render();
4751
4871
  RESULT_SCHEMA_VERSION = "mindgraph.memory.result.v1";
4752
4872
  DEFAULT_OUTPUT_CHARS = 8e3;
@@ -5016,12 +5136,12 @@ var init_resources = __esm({
5016
5136
 
5017
5137
  // src/sync-tool.ts
5018
5138
  function safePath(root, candidate) {
5019
- const absolute = import_node_path5.default.resolve(root, candidate);
5020
- const relative2 = import_node_path5.default.relative(root, absolute);
5021
- if (relative2.startsWith("..") || import_node_path5.default.isAbsolute(relative2)) {
5139
+ const absolute = import_node_path6.default.resolve(root, candidate);
5140
+ const relative2 = import_node_path6.default.relative(root, absolute);
5141
+ if (relative2.startsWith("..") || import_node_path6.default.isAbsolute(relative2)) {
5022
5142
  throw new Error(`path escapes repository root: ${candidate}`);
5023
5143
  }
5024
- if (import_node_fs6.default.existsSync(absolute)) {
5144
+ if (import_node_fs7.default.existsSync(absolute)) {
5025
5145
  if (!pathContains(realPathOf(root), realPathOf(absolute))) {
5026
5146
  throw new Error(`path escapes repository root via symlink: ${candidate}`);
5027
5147
  }
@@ -5030,26 +5150,26 @@ function safePath(root, candidate) {
5030
5150
  }
5031
5151
  function realPathOf(target) {
5032
5152
  try {
5033
- return import_node_fs6.default.realpathSync(target);
5153
+ return import_node_fs7.default.realpathSync(target);
5034
5154
  } catch {
5035
- return import_node_path5.default.resolve(target);
5155
+ return import_node_path6.default.resolve(target);
5036
5156
  }
5037
5157
  }
5038
5158
  function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
5039
- if (!import_node_fs6.default.existsSync(target)) return [];
5159
+ if (!import_node_fs7.default.existsSync(target)) return [];
5040
5160
  const real = realPathOf(target);
5041
5161
  if (!pathContains(realRoot, real)) return [];
5042
- const stat = import_node_fs6.default.statSync(target);
5162
+ const stat = import_node_fs7.default.statSync(target);
5043
5163
  if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
5044
5164
  if (!stat.isDirectory() || visited.has(real)) return [];
5045
5165
  visited.add(real);
5046
- return import_node_fs6.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path5.default.join(target, entry.name), realRoot, visited));
5166
+ return import_node_fs7.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path6.default.join(target, entry.name), realRoot, visited));
5047
5167
  }
5048
5168
  function fileDescriptor(root, absolute) {
5049
- const content = import_node_fs6.default.readFileSync(absolute, "utf8");
5169
+ const content = import_node_fs7.default.readFileSync(absolute, "utf8");
5050
5170
  return {
5051
- logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
5052
- content_hash: import_node_crypto7.default.createHash("sha256").update(content).digest("hex"),
5171
+ logical_path: import_node_path6.default.relative(root, absolute).split(import_node_path6.default.sep).join("/"),
5172
+ content_hash: import_node_crypto8.default.createHash("sha256").update(content).digest("hex"),
5053
5173
  bytes: Buffer.byteLength(content),
5054
5174
  content
5055
5175
  };
@@ -5118,7 +5238,7 @@ async function handleSyncTool(client2, args) {
5118
5238
  }
5119
5239
  if (action === "status" && request.logical_path && request.file_present !== false) {
5120
5240
  const absolute = safePath(root, String(request.logical_path));
5121
- request.file_present = import_node_fs6.default.existsSync(absolute);
5241
+ request.file_present = import_node_fs7.default.existsSync(absolute);
5122
5242
  if (request.file_present && !request.content_hash) {
5123
5243
  request.content_hash = fileDescriptor(root, absolute).content_hash;
5124
5244
  }
@@ -5162,13 +5282,13 @@ async function scanRepository(remote, repository, requested, provider, agentId)
5162
5282
  files: results
5163
5283
  };
5164
5284
  }
5165
- var import_node_crypto7, import_node_fs6, import_node_path5, ok3, err4, SYNC_TOOL;
5285
+ var import_node_crypto8, import_node_fs7, import_node_path6, ok3, err4, SYNC_TOOL;
5166
5286
  var init_sync_tool = __esm({
5167
5287
  "src/sync-tool.ts"() {
5168
5288
  "use strict";
5169
- import_node_crypto7 = __toESM(require("crypto"));
5170
- import_node_fs6 = __toESM(require("fs"));
5171
- import_node_path5 = __toESM(require("path"));
5289
+ import_node_crypto8 = __toESM(require("crypto"));
5290
+ import_node_fs7 = __toESM(require("fs"));
5291
+ import_node_path6 = __toESM(require("path"));
5172
5292
  init_codegraph();
5173
5293
  init_error_detail();
5174
5294
  ok3 = (data) => ({
@@ -5250,10 +5370,10 @@ var init_sync_tool = __esm({
5250
5370
  });
5251
5371
 
5252
5372
  // src/tools.ts
5253
- function stableJson2(value) {
5254
- if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
5373
+ function stableJson3(value) {
5374
+ if (Array.isArray(value)) return `[${value.map(stableJson3).join(",")}]`;
5255
5375
  if (value && typeof value === "object") {
5256
- return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson2(item)}`).join(",")}}`;
5376
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableJson3(item)}`).join(",")}}`;
5257
5377
  }
5258
5378
  return JSON.stringify(value);
5259
5379
  }
@@ -5267,8 +5387,8 @@ function defaultWriteIdempotencyKey(tool, args) {
5267
5387
  ([key]) => key !== "invocation_context" && key !== "idempotency_key"
5268
5388
  )
5269
5389
  );
5270
- return (0, import_node_crypto8.createHash)("sha256").update(
5271
- stableJson2({
5390
+ return (0, import_node_crypto9.createHash)("sha256").update(
5391
+ stableJson3({
5272
5392
  harness_session_id: sessionId,
5273
5393
  harness_turn_id: context?.harnessTurnId ?? null,
5274
5394
  tool,
@@ -6073,6 +6193,8 @@ async function handleRetrieve(client2, args, profile) {
6073
6193
  salience_min,
6074
6194
  article_limit,
6075
6195
  valid_at,
6196
+ as_of,
6197
+ retrieval_mode,
6076
6198
  created_after,
6077
6199
  created_before,
6078
6200
  explain,
@@ -6101,9 +6223,15 @@ async function handleRetrieve(client2, args, profile) {
6101
6223
  // the advertised knob must not get a silent no-op).
6102
6224
  ...article_limit !== void 0 ? { article_limit } : include_documents === false || profile === "coding" && !include_documents ? { article_limit: 0 } : {},
6103
6225
  valid_at,
6226
+ as_of,
6227
+ retrieval_mode,
6104
6228
  include_graph,
6105
6229
  graph_expansion_limit: graph_expansion_limit ?? 3,
6106
- graph_max_depth: graph_max_depth ?? 2
6230
+ graph_max_depth: graph_max_depth ?? 2,
6231
+ // Evidence plan R2: the server's canonical rendering under a token
6232
+ // budget derived from the character budget; the local renderer is
6233
+ // the fallback for servers that predate it.
6234
+ ...format === "json" ? {} : { format: "text", max_tokens: Math.max(256, Math.floor((max_output_chars ?? 24e3) / 4)) }
6107
6235
  }),
6108
6236
  (raw) => renderContext(raw, max_output_chars)
6109
6237
  );
@@ -6553,11 +6681,11 @@ async function handleOntology(client2, args) {
6553
6681
  return err5(`Unknown ontology action: ${action}`);
6554
6682
  }
6555
6683
  }
6556
- var import_node_crypto8, RESOLUTION_PROPERTIES, TOOLS, INVOCATION_CONTEXT_SCHEMA;
6684
+ var import_node_crypto9, RESOLUTION_PROPERTIES, TOOLS, INVOCATION_CONTEXT_SCHEMA;
6557
6685
  var init_tools = __esm({
6558
6686
  "src/tools.ts"() {
6559
6687
  "use strict";
6560
- import_node_crypto8 = require("crypto");
6688
+ import_node_crypto9 = require("crypto");
6561
6689
  init_error_detail();
6562
6690
  init_code_tool();
6563
6691
  init_codegraph();
@@ -7282,6 +7410,15 @@ var init_tools = __esm({
7282
7410
  type: "string",
7283
7411
  description: "ISO-8601 date (e.g. '2021-06') for context: nodes with validity windows are annotated as of this date instead of today"
7284
7412
  },
7413
+ as_of: {
7414
+ type: "string",
7415
+ description: "ISO-8601 date for context: answer with what had been STATED by this date (a node is admitted when one of its sources was stated on or before it; fact dates never exclude it). Use for 'what did we know / what was the value as of <date>' questions."
7416
+ },
7417
+ retrieval_mode: {
7418
+ type: "string",
7419
+ enum: ["normal", "exhaustive"],
7420
+ description: "context: 'exhaustive' widens retrieval for list/count questions ('how many different \u2026 across', 'what two \u2026'); omitted lets the server's cue decide."
7421
+ },
7285
7422
  created_after: {
7286
7423
  type: "number",
7287
7424
  description: "Unix seconds \u2014 'recent' action: only nodes ingested after this time"
@@ -7752,7 +7889,12 @@ function buildMindGraphServer(wiring) {
7752
7889
  });
7753
7890
  server2.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
7754
7891
  const { name, arguments: args } = request.params;
7755
- const rawArgs = { ...args || {} };
7892
+ const rawArgs = {
7893
+ ...args || {},
7894
+ // H1: session, lease and invocation fields the local hook recorded for
7895
+ // this exact call (stdio only; hosted mode has no local hook).
7896
+ ...hosted ? {} : wiring.resolveHookFields?.(name, args || {})
7897
+ };
7756
7898
  const toolArgs = { ...rawArgs };
7757
7899
  let governanceConfig = wiring.governance;
7758
7900
  if (hosted) {
@@ -7837,18 +7979,19 @@ async function main() {
7837
7979
  const transport = new import_stdio.StdioServerTransport();
7838
7980
  await server.connect(transport);
7839
7981
  console.error(
7840
- `MindGraph MCP server v${"0.23.0"} running on stdio (${BASE_URL})`
7982
+ `MindGraph MCP server v${"0.24.0"} running on stdio (${BASE_URL})`
7841
7983
  );
7842
7984
  }
7843
- var import_stdio, import_node_path6, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, server;
7985
+ var import_stdio, import_node_path7, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, server;
7844
7986
  var init_src = __esm({
7845
7987
  "src/index.ts"() {
7846
7988
  "use strict";
7989
+ init_call_ledger();
7847
7990
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7848
7991
  init_dist();
7849
7992
  init_hook_env();
7850
7993
  init_server();
7851
- import_node_path6 = __toESM(require("path"));
7994
+ import_node_path7 = __toESM(require("path"));
7852
7995
  init_hook_installer();
7853
7996
  API_KEY = process.env.MINDGRAPH_API_KEY;
7854
7997
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -7941,7 +8084,7 @@ Capture knowledge when the user shares something worth remembering:
7941
8084
  - Capture corrections, decisions with rationale, fixes that survived meaningful failed attempts, durable invariants/preferences, and blockers with evidence.
7942
8085
  - If a write returns \`near_duplicate\`, choose \`reuse_uid\` or \`supersedes_uid\`; use \`confirm_new\` only for genuinely distinct facts.
7943
8086
  - Do not capture routine mechanics, transient command output, facts cheaply derivable from code, unsupported speculation as high-confidence lessons, or completion without evidence.
7944
- - Adapter context is owned by ${HARNESS}; omit invocation_context because hooks replace it.`;
8087
+ - Adapter context is owned by ${HARNESS}; omit invocation_context because the adapter supplies it.`;
7945
8088
  } else {
7946
8089
  INSTRUCTIONS = INSTRUCTIONS.split("\n").filter(
7947
8090
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
@@ -7949,7 +8092,7 @@ Capture knowledge when the user shares something worth remembering:
7949
8092
  }
7950
8093
  RUNNER_REFRESH = (() => {
7951
8094
  try {
7952
- return refreshHookRunner("0.23.0", import_node_path6.default.join(__dirname, "cli.js"));
8095
+ return refreshHookRunner("0.24.0", import_node_path7.default.join(__dirname, "cli.js"));
7953
8096
  } catch {
7954
8097
  return void 0;
7955
8098
  }
@@ -7963,12 +8106,12 @@ Capture knowledge when the user shares something worth remembering:
7963
8106
  }
7964
8107
  })();
7965
8108
  console.error(
7966
- `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.23.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
8109
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.24.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
7967
8110
  );
7968
8111
  }
7969
8112
  HOOK_SKEW_NOTE = (() => {
7970
8113
  try {
7971
- return versionSkewNote("0.23.0");
8114
+ return versionSkewNote("0.24.0");
7972
8115
  } catch {
7973
8116
  return void 0;
7974
8117
  }
@@ -7989,7 +8132,7 @@ ${HOOK_SKEW_NOTE}`;
7989
8132
  }
7990
8133
  };
7991
8134
  MEMORY_RUNTIME_CONFIG = {
7992
- serverVersion: "0.23.0",
8135
+ serverVersion: "0.24.0",
7993
8136
  baseUrl: BASE_URL,
7994
8137
  profile: PROFILE,
7995
8138
  harness: HARNESS,
@@ -7998,12 +8141,13 @@ ${HOOK_SKEW_NOTE}`;
7998
8141
  };
7999
8142
  server = buildMindGraphServer({
8000
8143
  client,
8001
- serverVersion: "0.23.0",
8144
+ serverVersion: "0.24.0",
8002
8145
  instructions: INSTRUCTIONS,
8003
8146
  profile: PROFILE,
8004
8147
  agentId: AGENT_ID,
8005
8148
  governance: GOVERNANCE_CONFIG,
8006
- memory: MEMORY_RUNTIME_CONFIG
8149
+ memory: MEMORY_RUNTIME_CONFIG,
8150
+ resolveHookFields: (name, args) => resolvePendingCall(name, args)
8007
8151
  });
8008
8152
  main().catch((error2) => {
8009
8153
  console.error("Fatal error:", error2);
@@ -8013,10 +8157,10 @@ ${HOOK_SKEW_NOTE}`;
8013
8157
  });
8014
8158
 
8015
8159
  // src/cli.ts
8016
- var fs7 = __toESM(require("fs"));
8017
- var path7 = __toESM(require("path"));
8018
- var os4 = __toESM(require("os"));
8019
- var crypto5 = __toESM(require("crypto"));
8160
+ var fs8 = __toESM(require("fs"));
8161
+ var path8 = __toESM(require("path"));
8162
+ var os5 = __toESM(require("os"));
8163
+ var crypto6 = __toESM(require("crypto"));
8020
8164
  var import_child_process = require("child_process");
8021
8165
  init_dist();
8022
8166
  init_hook_env();
@@ -8498,24 +8642,25 @@ async function skillsPush(api, skillPath) {
8498
8642
  }
8499
8643
 
8500
8644
  // src/hook-core.ts
8501
- var import_node_crypto4 = __toESM(require("crypto"));
8502
- var import_node_fs4 = __toESM(require("fs"));
8503
- var import_node_os3 = __toESM(require("os"));
8504
- var import_node_path3 = __toESM(require("path"));
8645
+ var import_node_crypto5 = __toESM(require("crypto"));
8646
+ var import_node_fs5 = __toESM(require("fs"));
8647
+ var import_node_os4 = __toESM(require("os"));
8648
+ var import_node_path4 = __toESM(require("path"));
8505
8649
  var import_node_child_process2 = require("child_process");
8506
8650
  init_codegraph();
8507
8651
  init_code_tool();
8652
+ init_call_ledger();
8508
8653
  init_error_detail();
8509
8654
  init_hook_env();
8510
8655
 
8511
8656
  // src/hook-memory.ts
8512
- var import_node_fs3 = __toESM(require("fs"));
8657
+ var import_node_fs4 = __toESM(require("fs"));
8513
8658
  var PROMPT_RECALL_LIMIT = 5;
8514
8659
  var PROMPT_RECALL_MAX_CHARS = 4e3;
8515
8660
  var MIN_PROMPT_RECALL_CHARS = 12;
8516
8661
  var CAPTURE_MAX_CHARS = 2e5;
8517
8662
  var STOPWORDS = new Set(
8518
- "a an and are as at be but by can could did do does for from had has have how i if in into is it its me my of on or our please should so that the their them then there these they this to us was we were what when where which who why will with would you your about just also any some tell know want need let lets like get got make".split(" ")
8663
+ "a an and are as at be but by can could did do does for from had has have how i if in into is it its me my of on or our please should so that the their them then there these they this to us was we were what when where which who why will with would you your about just also any some tell know want need let lets like get got make yes no not ok okay sure may might must up down out off over under again after before while without within including include use using used keep now next here than very more most other each both all one two been being am done going go".split(" ")
8519
8664
  );
8520
8665
  var WORD = /[A-Za-z0-9][A-Za-z0-9_.\-/#+]*/g;
8521
8666
  var SECRET_PATTERNS = [
@@ -8560,6 +8705,27 @@ function promptRecallRequest(query) {
8560
8705
  graph_max_depth: 2
8561
8706
  };
8562
8707
  }
8708
+ function wordParts(text2) {
8709
+ return text2.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
8710
+ }
8711
+ var sameWord = (left, right) => left === right || left.length >= 4 && right.length >= 4 && (left.startsWith(right) || right.startsWith(left));
8712
+ function recallTermOverlap(query, text2) {
8713
+ const hay = wordParts(text2);
8714
+ let matched = 0;
8715
+ let usable = 0;
8716
+ for (const term of new Set(query.split(/\s+/).filter(Boolean))) {
8717
+ const parts = wordParts(term).filter((part) => part.length >= 3 && !/^\d+$/.test(part));
8718
+ if (parts.length === 0) continue;
8719
+ usable += 1;
8720
+ if (parts.some((part) => hay.some((word) => sameWord(word, part)))) matched += 1;
8721
+ }
8722
+ return { matched, usable };
8723
+ }
8724
+ function relevant(query, text2) {
8725
+ if (query === void 0) return true;
8726
+ const { matched, usable } = recallTermOverlap(query, text2);
8727
+ return usable > 0 && matched >= Math.min(2, usable);
8728
+ }
8563
8729
  var rec = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8564
8730
  var str = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8565
8731
  var oneLine = (value, max) => {
@@ -8571,7 +8737,7 @@ function nodeType(node) {
8571
8737
  const plain = str(node.node_type);
8572
8738
  return (plain && plain !== "Custom" ? plain : void 0) ?? str(rec(node.node_type)?.Custom) ?? plain ?? "Node";
8573
8739
  }
8574
- function renderPromptRecall(response) {
8740
+ function renderPromptRecall(response, query) {
8575
8741
  const payload = rec(response);
8576
8742
  if (!payload) return void 0;
8577
8743
  const lines = [];
@@ -8585,6 +8751,9 @@ function renderPromptRecall(response) {
8585
8751
  const label = str(node.label) ?? str(props?.label) ?? str(props?.name);
8586
8752
  if (!label) continue;
8587
8753
  const summary = str(node.summary) ?? str(props?.summary) ?? str(props?.description) ?? str(props?.content);
8754
+ const informative = summary && summary !== label ? summary : void 0;
8755
+ if (!informative && type === "Entity") continue;
8756
+ if (!relevant(query, `${label} ${informative ?? ""}`)) continue;
8588
8757
  const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
8589
8758
  const source = str(rec(docs[0])?.title);
8590
8759
  let line = `- [${type}] ${oneLine(label, 140)}`;
@@ -8600,6 +8769,7 @@ function renderPromptRecall(response) {
8600
8769
  const label = str(article.label);
8601
8770
  if (!label) continue;
8602
8771
  const content = str(article.content);
8772
+ if (!relevant(query, `${label} ${content ?? ""}`)) continue;
8603
8773
  lines.push(`- [Article] ${oneLine(label, 140)}${content ? ` \u2014 ${oneLine(content, 240)}` : ""}`);
8604
8774
  }
8605
8775
  if (lines.length === 0) return void 0;
@@ -8649,7 +8819,7 @@ function renderTranscript(turns) {
8649
8819
  return start >= 0 ? tail.slice(start + 2) : tail;
8650
8820
  }
8651
8821
  function readTranscriptSince(file, offset = 0) {
8652
- const buffer = import_node_fs3.default.readFileSync(file);
8822
+ const buffer = import_node_fs4.default.readFileSync(file);
8653
8823
  const start = offset > buffer.length ? 0 : offset;
8654
8824
  const lastNewline = buffer.lastIndexOf(10);
8655
8825
  if (lastNewline < start) return { text: "", nextOffset: start };
@@ -8693,33 +8863,33 @@ function defaultLedger(sessionId, options) {
8693
8863
  };
8694
8864
  }
8695
8865
  function runtimeDir(options) {
8696
- return options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path3.default.join(import_node_os3.default.homedir(), ".mindgraph", "runtime");
8866
+ return options.runtimeDir || process.env.MINDGRAPH_RUNTIME_DIR || import_node_path4.default.join(import_node_os4.default.homedir(), ".mindgraph", "runtime");
8697
8867
  }
8698
8868
  function ledgerPath(sessionId, options) {
8699
8869
  const harness = "harness" in options ? options.harness : "claude-code";
8700
- const key = import_node_crypto4.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
8701
- return import_node_path3.default.join(runtimeDir(options), `${key}.json`);
8870
+ const key = import_node_crypto5.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
8871
+ return import_node_path4.default.join(runtimeDir(options), `${key}.json`);
8702
8872
  }
8703
8873
  function waitBriefly(ms) {
8704
8874
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
8705
8875
  }
8706
8876
  function withLedger(sessionId, options, update) {
8707
8877
  const file = ledgerPath(sessionId, options);
8708
- import_node_fs4.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true, mode: 448 });
8878
+ import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true, mode: 448 });
8709
8879
  try {
8710
- import_node_fs4.default.chmodSync(import_node_path3.default.dirname(file), 448);
8880
+ import_node_fs5.default.chmodSync(import_node_path4.default.dirname(file), 448);
8711
8881
  } catch {
8712
8882
  }
8713
8883
  const lock = `${file}.lock`;
8714
8884
  let lockFd;
8715
8885
  for (let attempt = 0; attempt < 400; attempt++) {
8716
8886
  try {
8717
- lockFd = import_node_fs4.default.openSync(lock, "wx", 384);
8887
+ lockFd = import_node_fs5.default.openSync(lock, "wx", 384);
8718
8888
  break;
8719
8889
  } catch (error2) {
8720
8890
  if (error2.code !== "EEXIST") throw error2;
8721
8891
  try {
8722
- if (Date.now() - import_node_fs4.default.statSync(lock).mtimeMs > 3e3) import_node_fs4.default.unlinkSync(lock);
8892
+ if (Date.now() - import_node_fs5.default.statSync(lock).mtimeMs > 3e3) import_node_fs5.default.unlinkSync(lock);
8723
8893
  } catch {
8724
8894
  }
8725
8895
  waitBriefly(10);
@@ -8733,25 +8903,25 @@ function withLedger(sessionId, options, update) {
8733
8903
  try {
8734
8904
  ledger = {
8735
8905
  ...ledger,
8736
- ...JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"))
8906
+ ...JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"))
8737
8907
  };
8738
8908
  } catch {
8739
8909
  }
8740
8910
  const result = update(ledger);
8741
8911
  ledger.updatedAt = Date.now();
8742
- const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
8743
- import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
8912
+ const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
8913
+ import_node_fs5.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
8744
8914
  `, { mode: 384 });
8745
- import_node_fs4.default.renameSync(temporary, file);
8915
+ import_node_fs5.default.renameSync(temporary, file);
8746
8916
  try {
8747
- import_node_fs4.default.chmodSync(file, 384);
8917
+ import_node_fs5.default.chmodSync(file, 384);
8748
8918
  } catch {
8749
8919
  }
8750
8920
  return result;
8751
8921
  } finally {
8752
- import_node_fs4.default.closeSync(lockFd);
8922
+ import_node_fs5.default.closeSync(lockFd);
8753
8923
  try {
8754
- import_node_fs4.default.unlinkSync(lock);
8924
+ import_node_fs5.default.unlinkSync(lock);
8755
8925
  } catch {
8756
8926
  }
8757
8927
  }
@@ -9021,30 +9191,30 @@ function hasMindgraphServer(servers) {
9021
9191
  return false;
9022
9192
  }
9023
9193
  function pathIsWithin(ancestor, descendant) {
9024
- const base = import_node_path3.default.resolve(ancestor);
9025
- const target = import_node_path3.default.resolve(descendant);
9026
- return target === base || target.startsWith(base + import_node_path3.default.sep);
9194
+ const base = import_node_path4.default.resolve(ancestor);
9195
+ const target = import_node_path4.default.resolve(descendant);
9196
+ return target === base || target.startsWith(base + import_node_path4.default.sep);
9027
9197
  }
9028
9198
  function claudeMcpRegistrationNote(input, options) {
9029
9199
  if (options.harness !== "claude-code") return void 0;
9030
9200
  if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
9031
9201
  try {
9032
- const configPath = options.claudeConfigPath || import_node_path3.default.join(import_node_os3.default.homedir(), ".claude.json");
9033
- if (!import_node_fs4.default.existsSync(configPath)) return void 0;
9034
- const config = JSON.parse(import_node_fs4.default.readFileSync(configPath, "utf8"));
9202
+ const configPath = options.claudeConfigPath || import_node_path4.default.join(import_node_os4.default.homedir(), ".claude.json");
9203
+ if (!import_node_fs5.default.existsSync(configPath)) return void 0;
9204
+ const config = JSON.parse(import_node_fs5.default.readFileSync(configPath, "utf8"));
9035
9205
  if (hasMindgraphServer(config.mcpServers)) return void 0;
9036
- const cwd = import_node_path3.default.resolve(input.cwd || ".");
9206
+ const cwd = import_node_path4.default.resolve(input.cwd || ".");
9037
9207
  for (const [projectDir, project] of Object.entries(config.projects || {})) {
9038
9208
  if (!pathIsWithin(projectDir, cwd)) continue;
9039
9209
  if (hasMindgraphServer(project?.mcpServers)) return void 0;
9040
9210
  }
9041
- for (let dir = cwd; ; dir = import_node_path3.default.dirname(dir)) {
9042
- const candidate = import_node_path3.default.join(dir, ".mcp.json");
9043
- if (import_node_fs4.default.existsSync(candidate)) {
9044
- const parsed = JSON.parse(import_node_fs4.default.readFileSync(candidate, "utf8"));
9211
+ for (let dir = cwd; ; dir = import_node_path4.default.dirname(dir)) {
9212
+ const candidate = import_node_path4.default.join(dir, ".mcp.json");
9213
+ if (import_node_fs5.default.existsSync(candidate)) {
9214
+ const parsed = JSON.parse(import_node_fs5.default.readFileSync(candidate, "utf8"));
9045
9215
  if (hasMindgraphServer(parsed.mcpServers)) return void 0;
9046
9216
  }
9047
- if (dir === import_node_path3.default.dirname(dir)) break;
9217
+ if (dir === import_node_path4.default.dirname(dir)) break;
9048
9218
  }
9049
9219
  } catch {
9050
9220
  return void 0;
@@ -9147,7 +9317,7 @@ async function sessionStart(input, client2, options) {
9147
9317
  const contextText = registrationNote ? `${registrationNote}
9148
9318
 
9149
9319
  ${briefText}` : briefText;
9150
- const briefHash = import_node_crypto4.default.createHash("sha256").update(contextText).digest("hex");
9320
+ const briefHash = import_node_crypto5.default.createHash("sha256").update(contextText).digest("hex");
9151
9321
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
9152
9322
  const changed = ledger.lastBriefHash !== briefHash;
9153
9323
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
@@ -9222,6 +9392,20 @@ async function preTool(input, options) {
9222
9392
  if (targetsLedgerTask && fencedAction && ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
9223
9393
  updatedInput.expected_version = ledger.taskVersion;
9224
9394
  }
9395
+ if (options.harness === "claude-code") {
9396
+ const fills = Object.fromEntries(
9397
+ Object.entries(updatedInput).filter(
9398
+ ([key, value]) => key === "invocation_context" || original[key] !== value
9399
+ )
9400
+ );
9401
+ recordPendingCall(
9402
+ input.tool_name || "",
9403
+ original,
9404
+ input.session_id,
9405
+ fills,
9406
+ { runtimeDir: options.runtimeDir, now: options.now }
9407
+ );
9408
+ }
9225
9409
  return { kind: "rewrite", updatedInput };
9226
9410
  }
9227
9411
  var FENCED_PLAN_ACTIONS = /* @__PURE__ */ new Set([
@@ -9448,9 +9632,9 @@ async function promptRecall(input, client2, options) {
9448
9632
  client2.retrieveContext(promptRecallRequest(query)),
9449
9633
  options.promptRecallTimeoutMs ?? 4e3
9450
9634
  );
9451
- const block = renderPromptRecall(response);
9635
+ const block = renderPromptRecall(response, query);
9452
9636
  if (!block) return NOOP;
9453
- const hash = import_node_crypto4.default.createHash("sha256").update(block).digest("hex");
9637
+ const hash = import_node_crypto5.default.createHash("sha256").update(block).digest("hex");
9454
9638
  const fresh = withLedger(input.session_id, options, (ledger) => {
9455
9639
  if (ledger.lastRecallHash === hash) return false;
9456
9640
  ledger.lastRecallHash = hash;
@@ -9501,6 +9685,32 @@ async function captureTranscript(input, client2, options) {
9501
9685
  }
9502
9686
  }
9503
9687
  async function runHookCore(input, client2, options) {
9688
+ const started = performance.now();
9689
+ const output = await dispatchHook(input, client2, options);
9690
+ if (input.session_id && !(input.hook_event_name === "PreToolUse" && output.kind === "noop")) {
9691
+ try {
9692
+ const elapsed = Math.round(performance.now() - started);
9693
+ const injected = output.kind === "context" ? output.context.length : 0;
9694
+ withLedger(input.session_id, options, (ledger) => {
9695
+ const stats = ledger.hookStats ??= {};
9696
+ const entry = stats[input.hook_event_name] ??= {
9697
+ count: 0,
9698
+ totalMs: 0,
9699
+ maxMs: 0,
9700
+ injectedChars: 0
9701
+ };
9702
+ entry.count += 1;
9703
+ entry.totalMs += elapsed;
9704
+ entry.maxMs = Math.max(entry.maxMs, elapsed);
9705
+ entry.injectedChars += injected;
9706
+ return void 0;
9707
+ });
9708
+ } catch {
9709
+ }
9710
+ }
9711
+ return output;
9712
+ }
9713
+ async function dispatchHook(input, client2, options) {
9504
9714
  switch (input.hook_event_name) {
9505
9715
  case "SessionStart":
9506
9716
  return sessionStart(input, client2, options);
@@ -9545,7 +9755,7 @@ function previewCapture(transcriptPath, sessionId, options = {}) {
9545
9755
  let ledger = {};
9546
9756
  try {
9547
9757
  ledger = JSON.parse(
9548
- import_node_fs4.default.readFileSync(
9758
+ import_node_fs5.default.readFileSync(
9549
9759
  ledgerPath(sessionId, { ...options, harness: "claude-code" }),
9550
9760
  "utf8"
9551
9761
  )
@@ -9561,6 +9771,34 @@ function previewCapture(transcriptPath, sessionId, options = {}) {
9561
9771
  offset
9562
9772
  };
9563
9773
  }
9774
+ function readHookStats(sessionId, options = {}) {
9775
+ const dir = runtimeDir(options);
9776
+ let best;
9777
+ let names = [];
9778
+ try {
9779
+ names = import_node_fs5.default.readdirSync(dir).filter((name) => /^[0-9a-f]{64}\.json$/.test(name));
9780
+ } catch {
9781
+ return void 0;
9782
+ }
9783
+ for (const name of names) {
9784
+ try {
9785
+ const ledger = JSON.parse(import_node_fs5.default.readFileSync(import_node_path4.default.join(dir, name), "utf8"));
9786
+ if (!ledger.harnessSessionId || !ledger.hookStats) continue;
9787
+ if (sessionId && ledger.harnessSessionId !== sessionId) continue;
9788
+ const updatedAt = Number(ledger.updatedAt) || 0;
9789
+ if (!best || updatedAt > best.updatedAt) {
9790
+ best = {
9791
+ harness: String(ledger.harness ?? "unknown"),
9792
+ sessionId: ledger.harnessSessionId,
9793
+ updatedAt,
9794
+ events: ledger.hookStats
9795
+ };
9796
+ }
9797
+ } catch {
9798
+ }
9799
+ }
9800
+ return best;
9801
+ }
9564
9802
 
9565
9803
  // src/claude-hooks.ts
9566
9804
  async function runClaudeHook(input, client2, options = {}) {
@@ -9580,12 +9818,7 @@ async function runClaudeHook(input, client2, options = {}) {
9580
9818
  }
9581
9819
  };
9582
9820
  case "rewrite":
9583
- return {
9584
- hookSpecificOutput: {
9585
- hookEventName: "PreToolUse",
9586
- updatedInput: output.updatedInput
9587
- }
9588
- };
9821
+ return {};
9589
9822
  case "block":
9590
9823
  return { decision: "block", reason: output.reason };
9591
9824
  case "noop":
@@ -9628,15 +9861,15 @@ async function runCodexHook(input, client2, options = {}) {
9628
9861
  init_hook_installer();
9629
9862
  function recordHookHealth(harness, error2) {
9630
9863
  try {
9631
- const dir = process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime");
9632
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
9633
- const file = path7.join(dir, "hook-health.json");
9864
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime");
9865
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
9866
+ const file = path8.join(dir, "hook-health.json");
9634
9867
  let failures = 0;
9635
9868
  try {
9636
- failures = Number(JSON.parse(fs7.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9869
+ failures = Number(JSON.parse(fs8.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9637
9870
  } catch {
9638
9871
  }
9639
- fs7.writeFileSync(
9872
+ fs8.writeFileSync(
9640
9873
  file,
9641
9874
  `${JSON.stringify({
9642
9875
  harness,
@@ -9652,14 +9885,14 @@ function recordHookHealth(harness, error2) {
9652
9885
  }
9653
9886
  function clearHookHealth(harness) {
9654
9887
  try {
9655
- const file = path7.join(
9656
- process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
9888
+ const file = path8.join(
9889
+ process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime"),
9657
9890
  "hook-health.json"
9658
9891
  );
9659
- if (!fs7.existsSync(file)) return;
9660
- const current = JSON.parse(fs7.readFileSync(file, "utf8"));
9892
+ if (!fs8.existsSync(file)) return;
9893
+ const current = JSON.parse(fs8.readFileSync(file, "utf8"));
9661
9894
  if (!Number(current.consecutiveFailures)) return;
9662
- fs7.writeFileSync(
9895
+ fs8.writeFileSync(
9663
9896
  file,
9664
9897
  `${JSON.stringify({
9665
9898
  harness,
@@ -9675,22 +9908,22 @@ function clearHookHealth(harness) {
9675
9908
  function getClaudeDesktopConfigPath() {
9676
9909
  switch (process.platform) {
9677
9910
  case "darwin":
9678
- return path7.join(
9679
- os4.homedir(),
9911
+ return path8.join(
9912
+ os5.homedir(),
9680
9913
  "Library",
9681
9914
  "Application Support",
9682
9915
  "Claude",
9683
9916
  "claude_desktop_config.json"
9684
9917
  );
9685
9918
  case "win32":
9686
- return path7.join(
9687
- process.env.APPDATA || path7.join(os4.homedir(), "AppData", "Roaming"),
9919
+ return path8.join(
9920
+ process.env.APPDATA || path8.join(os5.homedir(), "AppData", "Roaming"),
9688
9921
  "Claude",
9689
9922
  "claude_desktop_config.json"
9690
9923
  );
9691
9924
  case "linux":
9692
- return path7.join(
9693
- os4.homedir(),
9925
+ return path8.join(
9926
+ os5.homedir(),
9694
9927
  ".config",
9695
9928
  "Claude",
9696
9929
  "claude_desktop_config.json"
@@ -9700,16 +9933,16 @@ function getClaudeDesktopConfigPath() {
9700
9933
  }
9701
9934
  }
9702
9935
  function readJsonFile(filePath) {
9703
- if (!fs7.existsSync(filePath)) return {};
9704
- const content = fs7.readFileSync(filePath, "utf-8");
9936
+ if (!fs8.existsSync(filePath)) return {};
9937
+ const content = fs8.readFileSync(filePath, "utf-8");
9705
9938
  return JSON.parse(content);
9706
9939
  }
9707
9940
  function writeJsonFile(filePath, data) {
9708
- const dir = path7.dirname(filePath);
9709
- if (!fs7.existsSync(dir)) {
9710
- fs7.mkdirSync(dir, { recursive: true });
9941
+ const dir = path8.dirname(filePath);
9942
+ if (!fs8.existsSync(dir)) {
9943
+ fs8.mkdirSync(dir, { recursive: true });
9711
9944
  }
9712
- fs7.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9945
+ fs8.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9713
9946
  }
9714
9947
  function getMcpServerEntry(apiKey, baseUrl) {
9715
9948
  const env = { MINDGRAPH_API_KEY: apiKey };
@@ -9796,7 +10029,7 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
9796
10029
  }
9797
10030
  function uninstallClaudeDesktop() {
9798
10031
  const configPath = getClaudeDesktopConfigPath();
9799
- if (!fs7.existsSync(configPath)) {
10032
+ if (!fs8.existsSync(configPath)) {
9800
10033
  console.log("Claude Desktop config not found. Nothing to remove.");
9801
10034
  return;
9802
10035
  }
@@ -9823,7 +10056,7 @@ function uninstallClaudeCode() {
9823
10056
  }
9824
10057
  function printStatus() {
9825
10058
  const desktopPath = getClaudeDesktopConfigPath();
9826
- if (fs7.existsSync(desktopPath)) {
10059
+ if (fs8.existsSync(desktopPath)) {
9827
10060
  const config = readJsonFile(desktopPath);
9828
10061
  const servers = config.mcpServers;
9829
10062
  if (servers && "mindgraph" in servers) {
@@ -9841,12 +10074,12 @@ function printStatus() {
9841
10074
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
9842
10075
  );
9843
10076
  try {
9844
- const healthFile = path7.join(
9845
- process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
10077
+ const healthFile = path8.join(
10078
+ process.env.MINDGRAPH_RUNTIME_DIR || path8.join(os5.homedir(), ".mindgraph", "runtime"),
9846
10079
  "hook-health.json"
9847
10080
  );
9848
- if (fs7.existsSync(healthFile)) {
9849
- const health = JSON.parse(fs7.readFileSync(healthFile, "utf8"));
10081
+ if (fs8.existsSync(healthFile)) {
10082
+ const health = JSON.parse(fs8.readFileSync(healthFile, "utf8"));
9850
10083
  const failures = Number(health.consecutiveFailures) || 0;
9851
10084
  console.log(
9852
10085
  failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
@@ -9875,21 +10108,21 @@ function printClaudeHookFeatures(features) {
9875
10108
  );
9876
10109
  }
9877
10110
  function latestTranscript(cwd) {
9878
- const dir = path7.join(
9879
- os4.homedir(),
10111
+ const dir = path8.join(
10112
+ os5.homedir(),
9880
10113
  ".claude",
9881
10114
  "projects",
9882
10115
  cwd.replace(/[^A-Za-z0-9]/g, "-")
9883
10116
  );
9884
10117
  try {
9885
- return fs7.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path7.join(dir, name)).sort((a, b) => fs7.statSync(b).mtimeMs - fs7.statSync(a).mtimeMs)[0];
10118
+ return fs8.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path8.join(dir, name)).sort((a, b) => fs8.statSync(b).mtimeMs - fs8.statSync(a).mtimeMs)[0];
9886
10119
  } catch {
9887
10120
  return void 0;
9888
10121
  }
9889
10122
  }
9890
10123
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
9891
10124
  if (withHooks) {
9892
- const runner = installHookRunner(__filename, void 0, "0.23.0");
10125
+ const runner = installHookRunner(__filename, void 0, "0.24.0");
9893
10126
  console.log(`Installed pinned hook runner at ${runner}`);
9894
10127
  const features = claudeHookFeatures();
9895
10128
  const result = installClaudeHooks("user", void 0, features);
@@ -9942,6 +10175,9 @@ USAGE:
9942
10175
  mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]
9943
10176
  Print exactly what transcript capture would
9944
10177
  send (POST /ingest/session); sends nothing.
10178
+ mindgraph-mcp hooks stats [--session-id <id>]
10179
+ Per-event hook calls, time and characters
10180
+ injected for a session (latest by default).
9945
10181
  mindgraph-mcp status Show installation status
9946
10182
  mindgraph-mcp skills pull [--dir .claude/skills]
9947
10183
  Write your org's published+granted skills as
@@ -9987,7 +10223,7 @@ var DASHBOARD_URL = process.env.MINDGRAPH_DASHBOARD_URL || "https://mindgraph.cl
9987
10223
  var POLL_INTERVAL = 2e3;
9988
10224
  var POLL_TIMEOUT = 10 * 60 * 1e3;
9989
10225
  function generateCode() {
9990
- return crypto5.randomBytes(32).toString("hex");
10226
+ return crypto6.randomBytes(32).toString("hex");
9991
10227
  }
9992
10228
  function openBrowser(url) {
9993
10229
  try {
@@ -10214,7 +10450,7 @@ ${total} skill(s) synced.`);
10214
10450
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
10215
10451
  throw new Error("--harness must be claude-code or codex");
10216
10452
  }
10217
- const runner = installHookRunner(__filename, void 0, "0.23.0");
10453
+ const runner = installHookRunner(__filename, void 0, "0.24.0");
10218
10454
  console.log(`Installed pinned hook runner at ${runner}`);
10219
10455
  const features = hookHarness === "claude-code" ? claudeHookFeatures() : void 0;
10220
10456
  const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir, features);
@@ -10318,18 +10554,42 @@ ${total} skill(s) synced.`);
10318
10554
  break;
10319
10555
  }
10320
10556
  case "hooks": {
10557
+ if (positionals[0] === "stats") {
10558
+ const stats = readHookStats(parseArgs(process.argv).sessionId);
10559
+ if (!stats) {
10560
+ console.error(
10561
+ "No hook statistics recorded yet. They are kept per session once the hooks from this version have run."
10562
+ );
10563
+ process.exit(1);
10564
+ }
10565
+ const rows = Object.entries(stats.events).sort(([a], [b]) => a.localeCompare(b));
10566
+ const injected = rows.reduce((sum, [, row]) => sum + row.injectedChars, 0);
10567
+ console.log(`Session ${stats.sessionId} (${stats.harness}), last active ${new Date(stats.updatedAt).toISOString()}`);
10568
+ console.log("event calls avg ms max ms injected chars");
10569
+ for (const [event, row] of rows) {
10570
+ console.log(
10571
+ `${event.padEnd(18)} ${String(row.count).padStart(6)} ${String(Math.round(row.totalMs / Math.max(row.count, 1))).padStart(8)} ${String(row.maxMs).padStart(8)} ${String(row.injectedChars).padStart(16)}`
10572
+ );
10573
+ }
10574
+ console.log(
10575
+ `Injected into context: ${injected} characters (about ${Math.round(injected / 4)} tokens). Times are measured inside the hook and exclude process start.`
10576
+ );
10577
+ break;
10578
+ }
10321
10579
  if (positionals[0] !== "preview") {
10322
- console.error("Usage: mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]");
10580
+ console.error(
10581
+ "Usage: mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]\n mindgraph-mcp hooks stats [--session-id <id>]"
10582
+ );
10323
10583
  process.exit(1);
10324
10584
  }
10325
10585
  const transcript = positionals[1] || latestTranscript(process.cwd());
10326
- if (!transcript || !fs7.existsSync(transcript)) {
10586
+ if (!transcript || !fs8.existsSync(transcript)) {
10327
10587
  console.error(
10328
10588
  "No transcript found. Pass a Claude Code transcript path (~/.claude/projects/<dir>/<session>.jsonl)."
10329
10589
  );
10330
10590
  process.exit(1);
10331
10591
  }
10332
- const sessionId = parseArgs(process.argv).sessionId || path7.basename(transcript, ".jsonl");
10592
+ const sessionId = parseArgs(process.argv).sessionId || path8.basename(transcript, ".jsonl");
10333
10593
  const preview = previewCapture(transcript, sessionId, {
10334
10594
  agentId: process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId
10335
10595
  });