mindgraph-mcp 0.22.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() {
@@ -1184,7 +1184,9 @@ function loadHookEnv(dir) {
1184
1184
  baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : void 0,
1185
1185
  apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
1186
1186
  agentId: typeof parsed.agentId === "string" ? parsed.agentId : void 0,
1187
- orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0
1187
+ orgId: typeof parsed.orgId === "string" ? parsed.orgId : void 0,
1188
+ promptRecall: typeof parsed.promptRecall === "boolean" ? parsed.promptRecall : void 0,
1189
+ capture: typeof parsed.capture === "boolean" ? parsed.capture : void 0
1188
1190
  };
1189
1191
  } catch {
1190
1192
  return {};
@@ -1198,7 +1200,9 @@ function saveHookEnv(env, dir) {
1198
1200
  baseUrl: env.baseUrl ?? existing.baseUrl,
1199
1201
  apiKey: env.apiKey ?? existing.apiKey,
1200
1202
  agentId: env.agentId ?? existing.agentId,
1201
- orgId: env.orgId === null ? void 0 : env.orgId ?? existing.orgId
1203
+ orgId: env.orgId === null ? void 0 : env.orgId ?? existing.orgId,
1204
+ promptRecall: env.promptRecall ?? existing.promptRecall,
1205
+ capture: env.capture ?? existing.capture
1202
1206
  };
1203
1207
  import_node_fs.default.writeFileSync(file, `${JSON.stringify(merged, null, 2)}
1204
1208
  `, {
@@ -1236,14 +1240,14 @@ function unavailabilityCaveats(reason, message) {
1236
1240
  }
1237
1241
  return caveats;
1238
1242
  }
1239
- function normalizePath(path8) {
1240
- return path8.replaceAll("\\", "/").replace(/^\.\//, "");
1243
+ function normalizePath(path9) {
1244
+ return path9.replaceAll("\\", "/").replace(/^\.\//, "");
1241
1245
  }
1242
1246
  function normalizeSignature(signature) {
1243
1247
  return signature.replace(/\s+/g, " ").trim();
1244
1248
  }
1245
- function languageFromPath(path8) {
1246
- const extension = (0, import_node_path2.extname)(path8).toLowerCase();
1249
+ function languageFromPath(path9) {
1250
+ const extension = (0, import_node_path2.extname)(path9).toLowerCase();
1247
1251
  return {
1248
1252
  ".c": "c",
1249
1253
  ".cc": "cpp",
@@ -2560,7 +2564,114 @@ var init_code_tool = __esm({
2560
2564
  }
2561
2565
  });
2562
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
+
2563
2668
  // src/hook-installer.ts
2669
+ function storedFeatures(homeDir) {
2670
+ const { promptRecall: promptRecall2, capture } = loadHookEnv(
2671
+ import_node_path5.default.join(homeDir || import_node_os5.default.homedir(), ".mindgraph")
2672
+ );
2673
+ return { promptRecall: promptRecall2, capture };
2674
+ }
2564
2675
  function command(harness) {
2565
2676
  return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
2566
2677
  }
@@ -2568,24 +2679,24 @@ function windowsCommand(harness) {
2568
2679
  return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
2569
2680
  }
2570
2681
  function installHookRunner(sourceFile, homeDir, version) {
2571
- const target = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_RELATIVE);
2572
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
2573
- const temporary = `${target}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2574
- import_node_fs4.default.copyFileSync(sourceFile, temporary);
2575
- import_node_fs4.default.renameSync(temporary, target);
2576
- 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);
2577
2688
  if (version) {
2578
- import_node_fs4.default.writeFileSync(versionPath, `${version}
2689
+ import_node_fs6.default.writeFileSync(versionPath, `${version}
2579
2690
  `);
2580
2691
  } else {
2581
- import_node_fs4.default.rmSync(versionPath, { force: true });
2692
+ import_node_fs6.default.rmSync(versionPath, { force: true });
2582
2693
  }
2583
2694
  return target;
2584
2695
  }
2585
2696
  function installedHookRunnerVersion(homeDir) {
2586
2697
  try {
2587
- const raw = import_node_fs4.default.readFileSync(
2588
- 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),
2589
2700
  "utf8"
2590
2701
  ).trim();
2591
2702
  return raw || void 0;
@@ -2610,11 +2721,11 @@ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2610
2721
  if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
2611
2722
  return { refreshed: false, reason: "disabled" };
2612
2723
  }
2613
- const home = homeDir || import_node_os4.default.homedir();
2614
- if (!import_node_fs4.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))) {
2615
2726
  return { refreshed: false, reason: "no_runner" };
2616
2727
  }
2617
- if (!import_node_fs4.default.existsSync(sourceBundle)) {
2728
+ if (!import_node_fs6.default.existsSync(sourceBundle)) {
2618
2729
  return { refreshed: false, reason: "no_source" };
2619
2730
  }
2620
2731
  const server2 = parseSemver(serverVersion);
@@ -2645,9 +2756,16 @@ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2645
2756
  for (const scope of scopes) {
2646
2757
  try {
2647
2758
  const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
2648
- if (!import_node_fs4.default.existsSync(file)) continue;
2649
- if (!import_node_fs4.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2650
- installHooks("claude-code", scope, projectDir, void 0, homeDir);
2759
+ if (!import_node_fs6.default.existsSync(file)) continue;
2760
+ if (!import_node_fs6.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2761
+ installHooks(
2762
+ "claude-code",
2763
+ scope,
2764
+ projectDir,
2765
+ void 0,
2766
+ homeDir,
2767
+ storedFeatures(homeDir)
2768
+ );
2651
2769
  refreshed.push(file);
2652
2770
  } catch {
2653
2771
  }
@@ -2659,15 +2777,15 @@ function versionSkewNote(serverVersion, homeDir) {
2659
2777
  if (!runnerVersion || runnerVersion === serverVersion) return void 0;
2660
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).`;
2661
2779
  }
2662
- 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) {
2663
2781
  if (harness === "codex") {
2664
- 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");
2665
2783
  }
2666
- 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");
2667
2785
  }
2668
2786
  function readSettings(file) {
2669
- if (!import_node_fs4.default.existsSync(file)) return {};
2670
- return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
2787
+ if (!import_node_fs6.default.existsSync(file)) return {};
2788
+ return JSON.parse(import_node_fs6.default.readFileSync(file, "utf8"));
2671
2789
  }
2672
2790
  function ownedHook(harness, timeout, additionalContextLimit) {
2673
2791
  return {
@@ -2679,7 +2797,7 @@ function ownedHook(harness, timeout, additionalContextLimit) {
2679
2797
  ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
2680
2798
  };
2681
2799
  }
2682
- function desiredEntries(harness) {
2800
+ function desiredEntries(harness, features = {}) {
2683
2801
  const common = {
2684
2802
  SessionStart: [
2685
2803
  {
@@ -2711,7 +2829,17 @@ function desiredEntries(harness) {
2711
2829
  ],
2712
2830
  // PostToolUse awaits the lease renewal round-trip, which fires exactly
2713
2831
  // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
2714
- 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
+ ],
2715
2843
  Stop: [{ hooks: [ownedHook(harness, 5)] }],
2716
2844
  // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
2717
2845
  // session close) — the SAME cold-tenant latency SessionStart needs 30s
@@ -2722,11 +2850,17 @@ function desiredEntries(harness) {
2722
2850
  { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
2723
2851
  ]
2724
2852
  };
2725
- return harness === "claude-code" ? {
2853
+ if (harness !== "claude-code") return common;
2854
+ return {
2726
2855
  ...common,
2727
2856
  TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
2728
- TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
2729
- } : common;
2857
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }],
2858
+ // Bounded recall per prompt: the core gives up at 4s and fails open, so
2859
+ // a 5s budget never holds the prompt longer than that.
2860
+ ...features.promptRecall !== false ? { UserPromptSubmit: [{ hooks: [ownedHook(harness, 5)] }] } : {},
2861
+ // Opt-in transcript capture before compaction (cold-tenant ingest).
2862
+ ...features.capture === true ? { PreCompact: [{ hooks: [ownedHook(harness, 30)] }] } : {}
2863
+ };
2730
2864
  }
2731
2865
  function hookCommand(value) {
2732
2866
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -2739,21 +2873,30 @@ function entryHasOwnedHook(value) {
2739
2873
  return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2740
2874
  }
2741
2875
  function writeSettings(file, settings) {
2742
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2743
- const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2744
- import_node_fs4.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)}
2745
2879
  `, {
2746
2880
  mode: 384
2747
2881
  });
2748
- import_node_fs4.default.renameSync(temporary, file);
2882
+ import_node_fs6.default.renameSync(temporary, file);
2749
2883
  }
2750
- function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
2884
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir, features = {}) {
2751
2885
  const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
2752
2886
  const settings = readSettings(file);
2753
2887
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2754
2888
  let added = 0;
2755
2889
  let updated = 0;
2756
- for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
2890
+ const desired = desiredEntries(harness, features);
2891
+ for (const optional of ["UserPromptSubmit", "PreCompact"]) {
2892
+ if (optional in desired || !Array.isArray(hooks[optional])) continue;
2893
+ const kept = hooks[optional].filter(
2894
+ (entry) => !entryHasOwnedHook(entry)
2895
+ );
2896
+ if (kept.length > 0) hooks[optional] = kept;
2897
+ else delete hooks[optional];
2898
+ }
2899
+ for (const [event, wanted] of Object.entries(desired)) {
2757
2900
  const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
2758
2901
  const kept = current.filter((entry) => !entryHasOwnedHook(entry));
2759
2902
  const hadOwned = kept.length !== current.length;
@@ -2770,7 +2913,7 @@ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir,
2770
2913
  }
2771
2914
  function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2772
2915
  const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2773
- if (!import_node_fs4.default.existsSync(file)) return { path: file, removed: 0 };
2916
+ if (!import_node_fs6.default.existsSync(file)) return { path: file, removed: 0 };
2774
2917
  const settings = readSettings(file);
2775
2918
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2776
2919
  let removed = 0;
@@ -2804,8 +2947,8 @@ function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir
2804
2947
  writeSettings(file, settings);
2805
2948
  return { path: file, removed };
2806
2949
  }
2807
- function installClaudeHooks(scope, projectDir = process.cwd()) {
2808
- return installHooks("claude-code", scope, projectDir);
2950
+ function installClaudeHooks(scope, projectDir = process.cwd(), features = {}) {
2951
+ return installHooks("claude-code", scope, projectDir, void 0, void 0, features);
2809
2952
  }
2810
2953
  function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
2811
2954
  return uninstallHooks("claude-code", scope, projectDir);
@@ -2816,17 +2959,19 @@ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2816
2959
  function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2817
2960
  return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2818
2961
  }
2819
- var import_node_fs4, 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;
2820
2963
  var init_hook_installer = __esm({
2821
2964
  "src/hook-installer.ts"() {
2822
2965
  "use strict";
2823
- import_node_fs4 = __toESM(require("fs"));
2824
- import_node_os4 = __toESM(require("os"));
2825
- import_node_path4 = __toESM(require("path"));
2826
- 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"));
2970
+ init_hook_env();
2827
2971
  OWNER_MARKER = "--owner mindgraph";
2828
2972
  RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2829
2973
  RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
2974
+ CLAUDE_POST_TOOL_MATCHER = "mcp__mindgraph__.*|Edit|MultiEdit|Write|NotebookEdit|Bash|Task|Agent";
2830
2975
  }
2831
2976
  });
2832
2977
 
@@ -2834,14 +2979,14 @@ var init_hook_installer = __esm({
2834
2979
  function obj(value) {
2835
2980
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2836
2981
  }
2837
- function str(value) {
2982
+ function str2(value) {
2838
2983
  return typeof value === "string" && value ? value : void 0;
2839
2984
  }
2840
2985
  function num(value) {
2841
2986
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2842
2987
  }
2843
2988
  function edgeTypeName(value) {
2844
- return str(value) ?? str(obj(value)?.Custom);
2989
+ return str2(value) ?? str2(obj(value)?.Custom);
2845
2990
  }
2846
2991
  function renderBudget(requested) {
2847
2992
  if (requested === void 0 || !Number.isFinite(requested)) return RENDER_CHAR_BUDGET;
@@ -2851,7 +2996,7 @@ function renderBudget(requested) {
2851
2996
  );
2852
2997
  }
2853
2998
  function clip(value, max, state) {
2854
- const text2 = str(value);
2999
+ const text2 = str2(value);
2855
3000
  if (!text2) return void 0;
2856
3001
  if (text2.length <= max) return text2;
2857
3002
  state.bounded = true;
@@ -2860,13 +3005,13 @@ function clip(value, max, state) {
2860
3005
  function isNodeShaped(value) {
2861
3006
  const node = obj(value);
2862
3007
  return Boolean(
2863
- node && str(node.uid) && (str(node.label) || str(node.node_type) || obj(node.node_type))
3008
+ node && str2(node.uid) && (str2(node.label) || str2(node.node_type) || obj(node.node_type))
2864
3009
  );
2865
3010
  }
2866
3011
  function nodeDisplayType(node) {
2867
3012
  const props = obj(node.props);
2868
- const plain = str(node.node_type);
2869
- return (plain && plain !== "Custom" ? plain : void 0) ?? str(obj(node.node_type)?.Custom) ?? str(props?.object_type) ?? str(props?.type_name) ?? str(obj(props?.data)?.domain_type) ?? plain ?? "Node";
3013
+ const plain = str2(node.node_type);
3014
+ return (plain && plain !== "Custom" ? plain : void 0) ?? str2(obj(node.node_type)?.Custom) ?? str2(props?.object_type) ?? str2(props?.type_name) ?? str2(obj(props?.data)?.domain_type) ?? plain ?? "Node";
2870
3015
  }
2871
3016
  function nodeFields(node) {
2872
3017
  const props = obj(node.props);
@@ -2886,10 +3031,10 @@ function statusTag(node) {
2886
3031
  const tags = [];
2887
3032
  if (node.superseded === true) {
2888
3033
  tags.push(
2889
- node.superseded_by ? `SUPERSEDED by ${str(node.superseded_by)}` : "SUPERSEDED"
3034
+ node.superseded_by ? `SUPERSEDED by ${str2(node.superseded_by)}` : "SUPERSEDED"
2890
3035
  );
2891
3036
  }
2892
- const truth = str(node.truth_status) ?? str(obj(node.props)?.truth_status);
3037
+ const truth = str2(node.truth_status) ?? str2(obj(node.props)?.truth_status);
2893
3038
  if (truth && truth !== "accepted") tags.push(`truth: ${truth}`);
2894
3039
  if (node.currently_valid === false) tags.push("OUTSIDE ITS VALIDITY WINDOW");
2895
3040
  if (node.valid_at_time === false) tags.push("NOT VALID AT THE REQUESTED TIME");
@@ -2899,7 +3044,7 @@ function firstQuote(node, state) {
2899
3044
  const chunks = Array.isArray(node.source_chunks) ? node.source_chunks : [];
2900
3045
  for (const raw of chunks) {
2901
3046
  const chunk = obj(raw);
2902
- const quote = str(chunk?.quote) ?? str(obj(chunk?.anchor)?.exact);
3047
+ const quote = str2(chunk?.quote) ?? str2(obj(chunk?.anchor)?.exact);
2903
3048
  if (quote) return clip(quote, QUOTE_MAX, state);
2904
3049
  }
2905
3050
  return void 0;
@@ -2907,7 +3052,7 @@ function firstQuote(node, state) {
2907
3052
  function sourceTag(node) {
2908
3053
  const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
2909
3054
  const first = obj(docs[0]);
2910
- const title = str(first?.title);
3055
+ const title = str2(first?.title);
2911
3056
  if (!title) return void 0;
2912
3057
  const extra = docs.length > 1 ? ` (+${docs.length - 1} more)` : "";
2913
3058
  return `from: ${title}${extra}`;
@@ -2933,7 +3078,7 @@ function nodeLines(node, state) {
2933
3078
  confidence !== void 0 ? `confidence ${confidence}` : void 0,
2934
3079
  score !== void 0 ? `score ${Math.round(score * 100) / 100}` : void 0
2935
3080
  ].filter(Boolean).join(", ");
2936
- const head = `- **${label}** [${str(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
3081
+ const head = `- **${label}** [${str2(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
2937
3082
  const summary = nodeSummaryText(node, state);
2938
3083
  const lines = [summary && summary !== label ? `${head}: ${summary}` : head];
2939
3084
  const fields = layer7Fields(node, state);
@@ -2947,7 +3092,7 @@ function nodeLines(node, state) {
2947
3092
  const rendered2 = believers.slice(0, 3).map((raw) => {
2948
3093
  const believer = obj(raw);
2949
3094
  const conf = num(believer?.confidence);
2950
- return `${str(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
3095
+ return `${str2(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
2951
3096
  }).join(", ");
2952
3097
  lines.push(` - believed by: ${rendered2}`);
2953
3098
  }
@@ -2978,8 +3123,8 @@ function relationshipItems(edges, labelByUid, state, options) {
2978
3123
  const edge = obj(raw);
2979
3124
  if (!edge) continue;
2980
3125
  const type = edgeTypeName(edge.edge_type) || "?";
2981
- const from = str(edge.from_uid);
2982
- const to = str(edge.to_uid);
3126
+ const from = str2(edge.from_uid);
3127
+ const to = str2(edge.to_uid);
2983
3128
  if (CURATION_EDGE_TYPES.has(type)) continue;
2984
3129
  if (!from || !to || from === to) continue;
2985
3130
  const fromLabel = labelByUid.get(from) ?? (options?.fallbackToUid ? `[${from}]` : void 0);
@@ -3051,7 +3196,7 @@ function policySections(payload, state) {
3051
3196
  if (policies.length === 0) return [];
3052
3197
  const items = policies.map((raw) => obj(raw)).filter((policy) => Boolean(policy)).map((policy) => {
3053
3198
  const rules = Array.isArray(policy.prose_rules) ? policy.prose_rules.map(String).join("; ") : policy.prose_rules;
3054
- return `- ${str(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
3199
+ return `- ${str2(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
3055
3200
  });
3056
3201
  return [{ heading: "## Applicable policies", items }];
3057
3202
  }
@@ -3068,10 +3213,18 @@ function hiddenSections(payload) {
3068
3213
  function renderContext(response, maxChars) {
3069
3214
  const payload = obj(response);
3070
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
+ }
3071
3224
  const graph = obj(payload.graph);
3072
3225
  const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
3073
3226
  const shaped = rawNodes.filter(isNodeShaped);
3074
- const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str(node.node_type) || ""));
3227
+ const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str2(node.node_type) || ""));
3075
3228
  const infraOnly = shaped.length - nodes.length;
3076
3229
  const articles = Array.isArray(payload.articles) ? payload.articles : [];
3077
3230
  const chunks = Array.isArray(payload.chunks) ? payload.chunks : [];
@@ -3081,7 +3234,7 @@ function renderContext(response, maxChars) {
3081
3234
  sections.push({
3082
3235
  heading: "## Knowledge articles",
3083
3236
  items: articles.map((raw) => obj(raw)).filter((article) => Boolean(article)).map(
3084
- (article) => `### ${str(article.label) || "(untitled)"} [${str(article.uid) || "?"}]
3237
+ (article) => `### ${str2(article.label) || "(untitled)"} [${str2(article.uid) || "?"}]
3085
3238
  ${clip(article.content, ARTICLE_MAX, state) || ""}`
3086
3239
  )
3087
3240
  });
@@ -3089,7 +3242,7 @@ ${clip(article.content, ARTICLE_MAX, state) || ""}`
3089
3242
  if (nodes.length > 0) {
3090
3243
  sections.push(...groupedNodeSections(nodes, state, "## Knowledge graph\n\n"));
3091
3244
  const labelByUid = /* @__PURE__ */ new Map();
3092
- for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
3245
+ for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3093
3246
  const relationships = relationshipItems(
3094
3247
  Array.isArray(graph?.edges) ? graph.edges : [],
3095
3248
  labelByUid,
@@ -3108,7 +3261,7 @@ ${clip(article.content, ARTICLE_MAX, state) || ""}`
3108
3261
  sections.push({
3109
3262
  heading: "## Source excerpts",
3110
3263
  items: chunks.map((raw) => obj(raw)).filter((chunk) => Boolean(chunk)).map(
3111
- (chunk) => `- [${str(chunk.chunk_uid) || "?"}] from "${str(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
3264
+ (chunk) => `- [${str2(chunk.chunk_uid) || "?"}] from "${str2(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
3112
3265
  )
3113
3266
  });
3114
3267
  }
@@ -3132,15 +3285,15 @@ function resumeEntry(raw, state, fallbackType) {
3132
3285
  if (!wrapper) return void 0;
3133
3286
  const value = obj(wrapper.node) ?? wrapper;
3134
3287
  if (isNodeShaped(value)) return nodeLines(value, state);
3135
- const uid = str(value.uid);
3288
+ const uid = str2(value.uid);
3136
3289
  const props = obj(value.props);
3137
3290
  const label = clip(value.label, LABEL_MAX, state) ?? clip(value.name, LABEL_MAX, state) ?? clip(value.title, LABEL_MAX, state) ?? clip(value.action, LABEL_MAX, state) ?? fallbackType;
3138
- const status = str(value.status) ?? str(props?.status);
3291
+ const status = str2(value.status) ?? str2(props?.status);
3139
3292
  const summary = clip(value.summary, SUMMARY_MAX, state) ?? clip(value.description, SUMMARY_MAX, state) ?? clip(value.content, SUMMARY_MAX, state) ?? clip(value.outcome_summary, SUMMARY_MAX, state) ?? clip(props?.summary, SUMMARY_MAX, state);
3140
3293
  const details = [
3141
3294
  status ? `status ${status}` : void 0,
3142
- str(value.path) ? `path ${str(value.path)}` : void 0,
3143
- str(value.symbol) ? `symbol ${str(value.symbol)}` : void 0
3295
+ str2(value.path) ? `path ${str2(value.path)}` : void 0,
3296
+ str2(value.symbol) ? `symbol ${str2(value.symbol)}` : void 0
3144
3297
  ].filter(Boolean);
3145
3298
  const head = `- **${label}**${uid ? ` [${uid}]` : ""}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
3146
3299
  return summary && summary !== label ? `${head}: ${summary}` : head;
@@ -3153,7 +3306,7 @@ function renderResumeContext(response, maxChars) {
3153
3306
  const payload = obj(response);
3154
3307
  if (!payload) return void 0;
3155
3308
  const state = { bounded: false, omitted: 0 };
3156
- const status = str(payload.status);
3309
+ const status = str2(payload.status);
3157
3310
  if (!payload.task && status === "no_eligible_work") {
3158
3311
  const scopedOut = Array.isArray(payload.scoped_out_tasks) ? payload.scoped_out_tasks.length : 0;
3159
3312
  const note = scopedOut > 0 ? ` ${scopedOut} task(s) exist outside the requested scope.` : "";
@@ -3288,17 +3441,17 @@ function renderTraversal(response, maxChars) {
3288
3441
  const state = { bounded: false, omitted: 0 };
3289
3442
  const sections = [];
3290
3443
  const labelByUid = /* @__PURE__ */ new Map();
3291
- for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
3444
+ for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3292
3445
  if (paths.length > 0) {
3293
- const items = paths.map((raw) => obj(raw)).filter((path8) => Boolean(path8)).map((path8, index) => {
3294
- const uids = Array.isArray(path8.node_uids) ? path8.node_uids.map(String) : [];
3295
- 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) => {
3296
3449
  const hopNode = obj(hop);
3297
- return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
3450
+ return str2(hopNode?.label) || labelByUid.get(str2(hopNode?.uid) || "") || str2(hop) || "?";
3298
3451
  }) : [];
3299
3452
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
3300
- const cost = num(path8.cost) ?? num(path8.path_cost);
3301
- const confidence = num(path8.path_confidence);
3453
+ const cost = num(path9.cost) ?? num(path9.path_cost);
3454
+ const confidence = num(path9.path_confidence);
3302
3455
  const meta = [
3303
3456
  cost !== void 0 ? `cost ${cost}` : void 0,
3304
3457
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -3312,12 +3465,12 @@ function renderTraversal(response, maxChars) {
3312
3465
  if (hasSteps) {
3313
3466
  const stepItems = rawSteps.map((raw) => obj(raw)).filter((step) => Boolean(step)).map((step) => {
3314
3467
  const depth = num(step.depth);
3315
- const uid = str(step.node_uid) ?? str(step.uid) ?? str(step.edge_to_uid) ?? "?";
3316
- const parent = str(step.parent_uid) ?? str(step.edge_from_uid);
3317
- const label = str(step.label) || "(unlabeled)";
3468
+ const uid = str2(step.node_uid) ?? str2(step.uid) ?? str2(step.edge_to_uid) ?? "?";
3469
+ const parent = str2(step.parent_uid) ?? str2(step.edge_from_uid);
3470
+ const label = str2(step.label) || "(unlabeled)";
3318
3471
  const relation = edgeTypeName(step.edge_type);
3319
- const role = str(step.traversal_role);
3320
- const type = str(step.node_type) ?? str(obj(step.node_type)?.Custom);
3472
+ const role = str2(step.traversal_role);
3473
+ const type = str2(step.node_type) ?? str2(obj(step.node_type)?.Custom);
3321
3474
  const cost = num(step.path_cost);
3322
3475
  const confidence = num(step.path_confidence);
3323
3476
  const prefix = [
@@ -3336,9 +3489,9 @@ function renderTraversal(response, maxChars) {
3336
3489
  if (stepItems.length > 0) {
3337
3490
  sections.push({ heading: "## Traversal steps", items: stepItems });
3338
3491
  } else {
3339
- const start = str(body.start_uid) || "?";
3340
- const end = str(body.end_uid);
3341
- const mode2 = str(body.mode);
3492
+ const start = str2(body.start_uid) || "?";
3493
+ const end = str2(body.end_uid);
3494
+ const mode2 = str2(body.mode);
3342
3495
  sections.push({
3343
3496
  heading: mode2 === "path" && end ? `No path found from [${start}] to [${end}].` : `No traversal steps returned from [${start}].`,
3344
3497
  items: []
@@ -3356,7 +3509,7 @@ function renderTraversal(response, maxChars) {
3356
3509
  sections.push({ heading: "## Relationships", items: relationships });
3357
3510
  }
3358
3511
  }
3359
- const mode = str(body.mode);
3512
+ const mode = str2(body.mode);
3360
3513
  return assemble(
3361
3514
  `# Graph traversal${mode ? ` (${mode})` : ""}`,
3362
3515
  sections,
@@ -3367,9 +3520,9 @@ function renderTraversal(response, maxChars) {
3367
3520
  function neighborLine(value) {
3368
3521
  const node = obj(value);
3369
3522
  if (!node) return void 0;
3370
- const label = str(node.label) || "(unlabeled)";
3371
- const uid = str(node.uid);
3372
- const type = str(node.node_type);
3523
+ const label = str2(node.label) || "(unlabeled)";
3524
+ const uid = str2(node.uid);
3525
+ const type = str2(node.node_type);
3373
3526
  return `- ${label}${uid ? ` [${uid}]` : ""}${type ? ` (${type})` : ""}`;
3374
3527
  }
3375
3528
  function renderOntologyAnswer(response) {
@@ -3389,20 +3542,20 @@ function renderOntologyAnswer(response) {
3389
3542
  sections.push(...groupedNodeSections(objects, state));
3390
3543
  }
3391
3544
  const labelByUid = /* @__PURE__ */ new Map();
3392
- for (const node of objects) labelByUid.set(str(node.uid), str(node.label) || "?");
3545
+ for (const node of objects) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3393
3546
  const graph = obj(payload.graph);
3394
3547
  for (const raw of Array.isArray(graph?.nodes) ? graph.nodes : []) {
3395
3548
  const node = obj(raw);
3396
- const uid = str(node?.uid);
3397
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
3549
+ const uid = str2(node?.uid);
3550
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3398
3551
  }
3399
3552
  if (cognitive) {
3400
3553
  for (const rawNodes of Object.values(cognitive)) {
3401
3554
  if (!Array.isArray(rawNodes)) continue;
3402
3555
  for (const raw of rawNodes) {
3403
3556
  const node = obj(raw);
3404
- const uid = str(node?.uid);
3405
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
3557
+ const uid = str2(node?.uid);
3558
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3406
3559
  }
3407
3560
  }
3408
3561
  }
@@ -3423,9 +3576,9 @@ function renderOntologyAnswer(response) {
3423
3576
  if (provenance.length > 0) {
3424
3577
  const items = provenance.map((raw) => obj(raw)).filter((entry) => Boolean(entry)).map((entry) => {
3425
3578
  const span = clip(entry.text_span, QUOTE_MAX, state) || "";
3426
- const by = str(entry.ingested_by_name);
3427
- const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str(entry.source_uid) || "?"}]`;
3428
- return str(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
3579
+ const by = str2(entry.ingested_by_name);
3580
+ const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str2(entry.source_uid) || "?"}]`;
3581
+ return str2(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
3429
3582
  });
3430
3583
  sections.push({ heading: "## Sources", items });
3431
3584
  }
@@ -3471,20 +3624,20 @@ function renderJobs(response) {
3471
3624
  const items = shown.map((job) => {
3472
3625
  const progress = obj(job.progress);
3473
3626
  const failure = obj(job.error_details);
3474
- const message = str(failure?.message) ?? str(job.error);
3627
+ const message = str2(failure?.message) ?? str2(job.error);
3475
3628
  const done = num(progress?.processed_chunks);
3476
3629
  const totalChunks = num(progress?.total_chunks);
3477
3630
  const bits = [
3478
- str(job.status) || "?",
3631
+ str2(job.status) || "?",
3479
3632
  done !== void 0 && totalChunks !== void 0 ? `${done}/${totalChunks} chunks` : void 0,
3480
3633
  num(progress?.nodes_created) !== void 0 ? `${num(progress?.nodes_created)} nodes` : void 0,
3481
3634
  num(job.queue_position) !== void 0 ? `queue #${num(job.queue_position)}` : void 0,
3482
3635
  message ? `error: ${clip(message, 160, state)}` : void 0,
3483
- str(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
3636
+ str2(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
3484
3637
  num(failure?.status) !== void 0 ? `status: ${num(failure?.status)}` : void 0,
3485
3638
  typeof failure?.retriable === "boolean" ? `retriable: ${failure.retriable}` : void 0
3486
3639
  ].filter(Boolean);
3487
- return `- [${str(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
3640
+ return `- [${str2(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
3488
3641
  });
3489
3642
  return assemble(
3490
3643
  `# Ingestion jobs (${shown.length}${shown.length < sorted.length ? ` of ${sorted.length}` : ""})`,
@@ -3500,13 +3653,13 @@ function renderSignals(response) {
3500
3653
  for (const [key, value] of Object.entries(payload)) {
3501
3654
  if (!Array.isArray(value) || value.length === 0) continue;
3502
3655
  const items = value.map((raw) => obj(raw)).filter((item) => Boolean(item)).map((item) => {
3503
- const fromLabel = str(item.from_label);
3504
- const toLabel = str(item.to_label);
3656
+ const fromLabel = str2(item.from_label);
3657
+ const toLabel = str2(item.to_label);
3505
3658
  const anyLabelKey = Object.keys(item).find(
3506
- (k) => k.endsWith("_label") && str(item[k])
3659
+ (k) => k.endsWith("_label") && str2(item[k])
3507
3660
  );
3508
- const label = str(item.label) ?? str(item.title) ?? str(item.name) ?? (fromLabel && toLabel ? `${fromLabel} \u2194 ${toLabel}` : void 0) ?? (anyLabelKey ? str(item[anyLabelKey]) : void 0) ?? str(item.uid) ?? str(item.target_uid) ?? "(item)";
3509
- const uid = str(item.uid) ?? str(item.target_uid);
3661
+ const label = str2(item.label) ?? str2(item.title) ?? str2(item.name) ?? (fromLabel && toLabel ? `${fromLabel} \u2194 ${toLabel}` : void 0) ?? (anyLabelKey ? str2(item[anyLabelKey]) : void 0) ?? str2(item.uid) ?? str2(item.target_uid) ?? "(item)";
3662
+ const uid = str2(item.uid) ?? str2(item.target_uid);
3510
3663
  const numbers = Object.entries(item).filter(([, v]) => typeof v === "number").slice(0, 4).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
3511
3664
  const lists = Object.entries(item).filter(([, v]) => Array.isArray(v) && v.length > 0).slice(0, 2).map(([k, v]) => `${k}: ${v.length}`);
3512
3665
  const meta = [...numbers, ...lists].join(", ");
@@ -4025,19 +4178,19 @@ async function handleHostedTool(name, args, client2, identity) {
4025
4178
  try {
4026
4179
  switch (name) {
4027
4180
  case "search": {
4028
- const query = str2(args.query).trim();
4181
+ const query = str3(args.query).trim();
4029
4182
  if (!query) return err3("query is required");
4030
4183
  const nodes = await contextNodes(client2, query, SEARCH_LIMIT);
4031
4184
  const results = nodes.slice(0, SEARCH_LIMIT).map((node) => ({
4032
- id: str2(node.uid),
4033
- title: str2(node.label),
4034
- url: `${EXPLORER_URL}${encodeURIComponent(str2(node.uid))}`,
4035
- snippet: str2(node.summary)
4185
+ id: str3(node.uid),
4186
+ title: str3(node.label),
4187
+ url: `${EXPLORER_URL}${encodeURIComponent(str3(node.uid))}`,
4188
+ snippet: str3(node.summary)
4036
4189
  }));
4037
4190
  return twin({ results });
4038
4191
  }
4039
4192
  case "fetch": {
4040
- const id = str2(args.id).trim();
4193
+ const id = str3(args.id).trim();
4041
4194
  if (!id) return err3("id is required");
4042
4195
  let node;
4043
4196
  try {
@@ -4088,7 +4241,7 @@ async function handleHostedTool(name, args, client2, identity) {
4088
4241
  return toolError(e);
4089
4242
  }
4090
4243
  }
4091
- var EXPLORER_URL, SEARCH_LIMIT, FETCH_TEXT_LIMIT, READ_TOOLS, EVOLVE_TOOLS, SEARCH_OUTPUT_SCHEMA, FETCH_OUTPUT_SCHEMA, HOSTED_TOOLS, HOSTED_TOOL_NAMES, str2;
4244
+ var EXPLORER_URL, SEARCH_LIMIT, FETCH_TEXT_LIMIT, READ_TOOLS, EVOLVE_TOOLS, SEARCH_OUTPUT_SCHEMA, FETCH_OUTPUT_SCHEMA, HOSTED_TOOLS, HOSTED_TOOL_NAMES, str3;
4092
4245
  var init_hosted_tools = __esm({
4093
4246
  "src/hosted-tools.ts"() {
4094
4247
  "use strict";
@@ -4179,7 +4332,7 @@ var init_hosted_tools = __esm({
4179
4332
  }
4180
4333
  ];
4181
4334
  HOSTED_TOOL_NAMES = new Set(HOSTED_TOOLS.map((tool) => tool.name));
4182
- str2 = (value) => typeof value === "string" ? value : "";
4335
+ str3 = (value) => typeof value === "string" ? value : "";
4183
4336
  }
4184
4337
  });
4185
4338
 
@@ -4350,15 +4503,15 @@ function obj2(value) {
4350
4503
  function text(value) {
4351
4504
  return typeof value === "string" && value.length > 0 ? value : void 0;
4352
4505
  }
4353
- function stableJson(value) {
4354
- if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
4506
+ function stableJson2(value) {
4507
+ if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
4355
4508
  if (value && typeof value === "object") {
4356
- 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(",")}}`;
4357
4510
  }
4358
4511
  return JSON.stringify(value);
4359
4512
  }
4360
4513
  function sha2562(value) {
4361
- return (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
4514
+ return (0, import_node_crypto7.createHash)("sha256").update(value).digest("hex");
4362
4515
  }
4363
4516
  function outputBudget(value) {
4364
4517
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OUTPUT_CHARS;
@@ -4414,7 +4567,7 @@ function memoryStatus(config = {}) {
4414
4567
  ]
4415
4568
  };
4416
4569
  }
4417
- function nodeType(node) {
4570
+ function nodeType2(node) {
4418
4571
  const raw = node.node_type;
4419
4572
  if (typeof raw === "string") return raw;
4420
4573
  return text(obj2(raw)?.Custom) ?? "Node";
@@ -4427,7 +4580,7 @@ function compactItem(raw, fallbackType) {
4427
4580
  if (!uid) return void 0;
4428
4581
  return defined({
4429
4582
  uid,
4430
- type: fallbackType ?? nodeType(value),
4583
+ type: fallbackType ?? nodeType2(value),
4431
4584
  label: text(value.label) ?? text(value.document_title),
4432
4585
  truth_status: text(value.truth_status) ?? text(obj2(value.props)?.truth_status),
4433
4586
  superseded: value.superseded === true ? true : void 0,
@@ -4664,7 +4817,7 @@ async function handleMemoryTool(client2, args, config = {}) {
4664
4817
  scope_uids: Array.isArray(args.scope_uids) ? args.scope_uids.map(String) : void 0
4665
4818
  });
4666
4819
  const contextId = `ctx_${sha2562(
4667
- stableJson({ mode, scope, strategy, query: text(args.query), rendered_hash: renderedHash })
4820
+ stableJson2({ mode, scope, strategy, query: text(args.query), rendered_hash: renderedHash })
4668
4821
  ).slice(0, 24)}`;
4669
4822
  const bounded = candidates.length > items.length || rendered2.includes("item(s) omitted");
4670
4823
  const result = {
@@ -4709,11 +4862,11 @@ async function handleMemoryTool(client2, args, config = {}) {
4709
4862
  structuredContent: result
4710
4863
  };
4711
4864
  }
4712
- 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;
4713
4866
  var init_memory_tool = __esm({
4714
4867
  "src/memory-tool.ts"() {
4715
4868
  "use strict";
4716
- import_node_crypto6 = require("crypto");
4869
+ import_node_crypto7 = require("crypto");
4717
4870
  init_render();
4718
4871
  RESULT_SCHEMA_VERSION = "mindgraph.memory.result.v1";
4719
4872
  DEFAULT_OUTPUT_CHARS = 8e3;
@@ -4983,12 +5136,12 @@ var init_resources = __esm({
4983
5136
 
4984
5137
  // src/sync-tool.ts
4985
5138
  function safePath(root, candidate) {
4986
- const absolute = import_node_path5.default.resolve(root, candidate);
4987
- const relative2 = import_node_path5.default.relative(root, absolute);
4988
- 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)) {
4989
5142
  throw new Error(`path escapes repository root: ${candidate}`);
4990
5143
  }
4991
- if (import_node_fs5.default.existsSync(absolute)) {
5144
+ if (import_node_fs7.default.existsSync(absolute)) {
4992
5145
  if (!pathContains(realPathOf(root), realPathOf(absolute))) {
4993
5146
  throw new Error(`path escapes repository root via symlink: ${candidate}`);
4994
5147
  }
@@ -4997,26 +5150,26 @@ function safePath(root, candidate) {
4997
5150
  }
4998
5151
  function realPathOf(target) {
4999
5152
  try {
5000
- return import_node_fs5.default.realpathSync(target);
5153
+ return import_node_fs7.default.realpathSync(target);
5001
5154
  } catch {
5002
- return import_node_path5.default.resolve(target);
5155
+ return import_node_path6.default.resolve(target);
5003
5156
  }
5004
5157
  }
5005
5158
  function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
5006
- if (!import_node_fs5.default.existsSync(target)) return [];
5159
+ if (!import_node_fs7.default.existsSync(target)) return [];
5007
5160
  const real = realPathOf(target);
5008
5161
  if (!pathContains(realRoot, real)) return [];
5009
- const stat = import_node_fs5.default.statSync(target);
5162
+ const stat = import_node_fs7.default.statSync(target);
5010
5163
  if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
5011
5164
  if (!stat.isDirectory() || visited.has(real)) return [];
5012
5165
  visited.add(real);
5013
- return import_node_fs5.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));
5014
5167
  }
5015
5168
  function fileDescriptor(root, absolute) {
5016
- const content = import_node_fs5.default.readFileSync(absolute, "utf8");
5169
+ const content = import_node_fs7.default.readFileSync(absolute, "utf8");
5017
5170
  return {
5018
- logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
5019
- 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"),
5020
5173
  bytes: Buffer.byteLength(content),
5021
5174
  content
5022
5175
  };
@@ -5085,7 +5238,7 @@ async function handleSyncTool(client2, args) {
5085
5238
  }
5086
5239
  if (action === "status" && request.logical_path && request.file_present !== false) {
5087
5240
  const absolute = safePath(root, String(request.logical_path));
5088
- request.file_present = import_node_fs5.default.existsSync(absolute);
5241
+ request.file_present = import_node_fs7.default.existsSync(absolute);
5089
5242
  if (request.file_present && !request.content_hash) {
5090
5243
  request.content_hash = fileDescriptor(root, absolute).content_hash;
5091
5244
  }
@@ -5129,13 +5282,13 @@ async function scanRepository(remote, repository, requested, provider, agentId)
5129
5282
  files: results
5130
5283
  };
5131
5284
  }
5132
- var import_node_crypto7, import_node_fs5, import_node_path5, ok3, err4, SYNC_TOOL;
5285
+ var import_node_crypto8, import_node_fs7, import_node_path6, ok3, err4, SYNC_TOOL;
5133
5286
  var init_sync_tool = __esm({
5134
5287
  "src/sync-tool.ts"() {
5135
5288
  "use strict";
5136
- import_node_crypto7 = __toESM(require("crypto"));
5137
- import_node_fs5 = __toESM(require("fs"));
5138
- 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"));
5139
5292
  init_codegraph();
5140
5293
  init_error_detail();
5141
5294
  ok3 = (data) => ({
@@ -5217,10 +5370,10 @@ var init_sync_tool = __esm({
5217
5370
  });
5218
5371
 
5219
5372
  // src/tools.ts
5220
- function stableJson2(value) {
5221
- if (Array.isArray(value)) return `[${value.map(stableJson2).join(",")}]`;
5373
+ function stableJson3(value) {
5374
+ if (Array.isArray(value)) return `[${value.map(stableJson3).join(",")}]`;
5222
5375
  if (value && typeof value === "object") {
5223
- 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(",")}}`;
5224
5377
  }
5225
5378
  return JSON.stringify(value);
5226
5379
  }
@@ -5234,8 +5387,8 @@ function defaultWriteIdempotencyKey(tool, args) {
5234
5387
  ([key]) => key !== "invocation_context" && key !== "idempotency_key"
5235
5388
  )
5236
5389
  );
5237
- return (0, import_node_crypto8.createHash)("sha256").update(
5238
- stableJson2({
5390
+ return (0, import_node_crypto9.createHash)("sha256").update(
5391
+ stableJson3({
5239
5392
  harness_session_id: sessionId,
5240
5393
  harness_turn_id: context?.harnessTurnId ?? null,
5241
5394
  tool,
@@ -6040,6 +6193,8 @@ async function handleRetrieve(client2, args, profile) {
6040
6193
  salience_min,
6041
6194
  article_limit,
6042
6195
  valid_at,
6196
+ as_of,
6197
+ retrieval_mode,
6043
6198
  created_after,
6044
6199
  created_before,
6045
6200
  explain,
@@ -6068,9 +6223,15 @@ async function handleRetrieve(client2, args, profile) {
6068
6223
  // the advertised knob must not get a silent no-op).
6069
6224
  ...article_limit !== void 0 ? { article_limit } : include_documents === false || profile === "coding" && !include_documents ? { article_limit: 0 } : {},
6070
6225
  valid_at,
6226
+ as_of,
6227
+ retrieval_mode,
6071
6228
  include_graph,
6072
6229
  graph_expansion_limit: graph_expansion_limit ?? 3,
6073
- 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)) }
6074
6235
  }),
6075
6236
  (raw) => renderContext(raw, max_output_chars)
6076
6237
  );
@@ -6520,11 +6681,11 @@ async function handleOntology(client2, args) {
6520
6681
  return err5(`Unknown ontology action: ${action}`);
6521
6682
  }
6522
6683
  }
6523
- var import_node_crypto8, RESOLUTION_PROPERTIES, TOOLS, INVOCATION_CONTEXT_SCHEMA;
6684
+ var import_node_crypto9, RESOLUTION_PROPERTIES, TOOLS, INVOCATION_CONTEXT_SCHEMA;
6524
6685
  var init_tools = __esm({
6525
6686
  "src/tools.ts"() {
6526
6687
  "use strict";
6527
- import_node_crypto8 = require("crypto");
6688
+ import_node_crypto9 = require("crypto");
6528
6689
  init_error_detail();
6529
6690
  init_code_tool();
6530
6691
  init_codegraph();
@@ -7249,6 +7410,15 @@ var init_tools = __esm({
7249
7410
  type: "string",
7250
7411
  description: "ISO-8601 date (e.g. '2021-06') for context: nodes with validity windows are annotated as of this date instead of today"
7251
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
+ },
7252
7422
  created_after: {
7253
7423
  type: "number",
7254
7424
  description: "Unix seconds \u2014 'recent' action: only nodes ingested after this time"
@@ -7719,7 +7889,12 @@ function buildMindGraphServer(wiring) {
7719
7889
  });
7720
7890
  server2.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
7721
7891
  const { name, arguments: args } = request.params;
7722
- 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
+ };
7723
7898
  const toolArgs = { ...rawArgs };
7724
7899
  let governanceConfig = wiring.governance;
7725
7900
  if (hosted) {
@@ -7804,18 +7979,19 @@ async function main() {
7804
7979
  const transport = new import_stdio.StdioServerTransport();
7805
7980
  await server.connect(transport);
7806
7981
  console.error(
7807
- `MindGraph MCP server v${"0.22.0"} running on stdio (${BASE_URL})`
7982
+ `MindGraph MCP server v${"0.24.0"} running on stdio (${BASE_URL})`
7808
7983
  );
7809
7984
  }
7810
- 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;
7811
7986
  var init_src = __esm({
7812
7987
  "src/index.ts"() {
7813
7988
  "use strict";
7989
+ init_call_ledger();
7814
7990
  import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
7815
7991
  init_dist();
7816
7992
  init_hook_env();
7817
7993
  init_server();
7818
- import_node_path6 = __toESM(require("path"));
7994
+ import_node_path7 = __toESM(require("path"));
7819
7995
  init_hook_installer();
7820
7996
  API_KEY = process.env.MINDGRAPH_API_KEY;
7821
7997
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -7908,7 +8084,7 @@ Capture knowledge when the user shares something worth remembering:
7908
8084
  - Capture corrections, decisions with rationale, fixes that survived meaningful failed attempts, durable invariants/preferences, and blockers with evidence.
7909
8085
  - If a write returns \`near_duplicate\`, choose \`reuse_uid\` or \`supersedes_uid\`; use \`confirm_new\` only for genuinely distinct facts.
7910
8086
  - Do not capture routine mechanics, transient command output, facts cheaply derivable from code, unsupported speculation as high-confidence lessons, or completion without evidence.
7911
- - 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.`;
7912
8088
  } else {
7913
8089
  INSTRUCTIONS = INSTRUCTIONS.split("\n").filter(
7914
8090
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
@@ -7916,7 +8092,7 @@ Capture knowledge when the user shares something worth remembering:
7916
8092
  }
7917
8093
  RUNNER_REFRESH = (() => {
7918
8094
  try {
7919
- return refreshHookRunner("0.22.0", import_node_path6.default.join(__dirname, "cli.js"));
8095
+ return refreshHookRunner("0.24.0", import_node_path7.default.join(__dirname, "cli.js"));
7920
8096
  } catch {
7921
8097
  return void 0;
7922
8098
  }
@@ -7930,12 +8106,12 @@ Capture knowledge when the user shares something worth remembering:
7930
8106
  }
7931
8107
  })();
7932
8108
  console.error(
7933
- `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.22.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(", ")}` : "")
7934
8110
  );
7935
8111
  }
7936
8112
  HOOK_SKEW_NOTE = (() => {
7937
8113
  try {
7938
- return versionSkewNote("0.22.0");
8114
+ return versionSkewNote("0.24.0");
7939
8115
  } catch {
7940
8116
  return void 0;
7941
8117
  }
@@ -7956,7 +8132,7 @@ ${HOOK_SKEW_NOTE}`;
7956
8132
  }
7957
8133
  };
7958
8134
  MEMORY_RUNTIME_CONFIG = {
7959
- serverVersion: "0.22.0",
8135
+ serverVersion: "0.24.0",
7960
8136
  baseUrl: BASE_URL,
7961
8137
  profile: PROFILE,
7962
8138
  harness: HARNESS,
@@ -7965,12 +8141,13 @@ ${HOOK_SKEW_NOTE}`;
7965
8141
  };
7966
8142
  server = buildMindGraphServer({
7967
8143
  client,
7968
- serverVersion: "0.22.0",
8144
+ serverVersion: "0.24.0",
7969
8145
  instructions: INSTRUCTIONS,
7970
8146
  profile: PROFILE,
7971
8147
  agentId: AGENT_ID,
7972
8148
  governance: GOVERNANCE_CONFIG,
7973
- memory: MEMORY_RUNTIME_CONFIG
8149
+ memory: MEMORY_RUNTIME_CONFIG,
8150
+ resolveHookFields: (name, args) => resolvePendingCall(name, args)
7974
8151
  });
7975
8152
  main().catch((error2) => {
7976
8153
  console.error("Fatal error:", error2);
@@ -7980,10 +8157,10 @@ ${HOOK_SKEW_NOTE}`;
7980
8157
  });
7981
8158
 
7982
8159
  // src/cli.ts
7983
- var fs6 = __toESM(require("fs"));
7984
- var path7 = __toESM(require("path"));
7985
- var os4 = __toESM(require("os"));
7986
- 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"));
7987
8164
  var import_child_process = require("child_process");
7988
8165
  init_dist();
7989
8166
  init_hook_env();
@@ -8001,6 +8178,9 @@ function parseArgs(argv) {
8001
8178
  let agentId = process.env.MINDGRAPH_AGENT_ID;
8002
8179
  const positionals = [];
8003
8180
  let dir;
8181
+ let promptRecall2;
8182
+ let capture;
8183
+ let sessionId;
8004
8184
  for (let i = 0; i < args.length; i++) {
8005
8185
  const arg = args[i];
8006
8186
  switch (arg) {
@@ -8033,6 +8213,21 @@ function parseArgs(argv) {
8033
8213
  case "--hooks":
8034
8214
  hooks = true;
8035
8215
  break;
8216
+ case "--prompt-recall":
8217
+ promptRecall2 = true;
8218
+ break;
8219
+ case "--no-prompt-recall":
8220
+ promptRecall2 = false;
8221
+ break;
8222
+ case "--capture":
8223
+ capture = true;
8224
+ break;
8225
+ case "--no-capture":
8226
+ capture = false;
8227
+ break;
8228
+ case "--session-id":
8229
+ sessionId = args[++i];
8230
+ break;
8036
8231
  case "--owner":
8037
8232
  ++i;
8038
8233
  break;
@@ -8061,7 +8256,10 @@ function parseArgs(argv) {
8061
8256
  hooks,
8062
8257
  agentId,
8063
8258
  positionals,
8064
- dir
8259
+ dir,
8260
+ promptRecall: promptRecall2,
8261
+ capture,
8262
+ sessionId
8065
8263
  };
8066
8264
  }
8067
8265
  function mcpAddFailureOutput(e) {
@@ -8444,15 +8642,210 @@ async function skillsPush(api, skillPath) {
8444
8642
  }
8445
8643
 
8446
8644
  // src/hook-core.ts
8447
- var import_node_crypto4 = __toESM(require("crypto"));
8448
- var import_node_fs3 = __toESM(require("fs"));
8449
- var import_node_os3 = __toESM(require("os"));
8450
- 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"));
8451
8649
  var import_node_child_process2 = require("child_process");
8452
8650
  init_codegraph();
8453
8651
  init_code_tool();
8652
+ init_call_ledger();
8454
8653
  init_error_detail();
8455
8654
  init_hook_env();
8655
+
8656
+ // src/hook-memory.ts
8657
+ var import_node_fs4 = __toESM(require("fs"));
8658
+ var PROMPT_RECALL_LIMIT = 5;
8659
+ var PROMPT_RECALL_MAX_CHARS = 4e3;
8660
+ var MIN_PROMPT_RECALL_CHARS = 12;
8661
+ var CAPTURE_MAX_CHARS = 2e5;
8662
+ var STOPWORDS = new Set(
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(" ")
8664
+ );
8665
+ var WORD = /[A-Za-z0-9][A-Za-z0-9_.\-/#+]*/g;
8666
+ var SECRET_PATTERNS = [
8667
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"],
8668
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[REDACTED]"],
8669
+ [/\b(?:mg|sk|pk|rk)[_-](?:live[_-]|test[_-]|ant[_-]|proj[_-])?[A-Za-z0-9_-]{16,}/g, "[REDACTED]"],
8670
+ [/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,})/g, "[REDACTED]"],
8671
+ [/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED]"],
8672
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "[REDACTED]"],
8673
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]"],
8674
+ [
8675
+ /\b((?:api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret)\b["']?\s*[:=]\s*)["']?[^\s"',;]{8,}["']?/gi,
8676
+ "$1[REDACTED]"
8677
+ ]
8678
+ ];
8679
+ function redactSecrets(text2) {
8680
+ let out = text2;
8681
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
8682
+ out = out.replace(pattern, replacement);
8683
+ }
8684
+ return out;
8685
+ }
8686
+ function promptRecallQuery(prompt, maxTerms = 8) {
8687
+ if (typeof prompt !== "string") return "";
8688
+ const trimmed = prompt.trim();
8689
+ if (trimmed.length < MIN_PROMPT_RECALL_CHARS || trimmed.startsWith("/")) return "";
8690
+ const seen = /* @__PURE__ */ new Set();
8691
+ for (const match of redactSecrets(trimmed.slice(0, 600)).matchAll(WORD)) {
8692
+ const key = match[0].toLowerCase().replace(/^[.\-/]+|[.\-/]+$/g, "");
8693
+ if (key.length < 2 || STOPWORDS.has(key) || key === "redacted" || seen.has(key)) continue;
8694
+ seen.add(key);
8695
+ if (seen.size >= maxTerms) break;
8696
+ }
8697
+ return [...seen].join(" ");
8698
+ }
8699
+ function promptRecallRequest(query) {
8700
+ return {
8701
+ query,
8702
+ node_limit: PROMPT_RECALL_LIMIT,
8703
+ chunk_limit: 0,
8704
+ graph_expansion_limit: 3,
8705
+ graph_max_depth: 2
8706
+ };
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
+ }
8729
+ var rec = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8730
+ var str = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8731
+ var oneLine = (value, max) => {
8732
+ const flat = value.replace(/\s+/g, " ").trim();
8733
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
8734
+ };
8735
+ var INFRA_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article", "Session"]);
8736
+ function nodeType(node) {
8737
+ const plain = str(node.node_type);
8738
+ return (plain && plain !== "Custom" ? plain : void 0) ?? str(rec(node.node_type)?.Custom) ?? plain ?? "Node";
8739
+ }
8740
+ function renderPromptRecall(response, query) {
8741
+ const payload = rec(response);
8742
+ if (!payload) return void 0;
8743
+ const lines = [];
8744
+ const nodes = Array.isArray(rec(payload.graph)?.nodes) ? rec(payload.graph).nodes : [];
8745
+ for (const raw of nodes) {
8746
+ const node = rec(raw);
8747
+ if (!node || lines.length >= PROMPT_RECALL_LIMIT) continue;
8748
+ const type = nodeType(node);
8749
+ if (INFRA_TYPES.has(type)) continue;
8750
+ const props = rec(node.props);
8751
+ const label = str(node.label) ?? str(props?.label) ?? str(props?.name);
8752
+ if (!label) continue;
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;
8757
+ const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
8758
+ const source = str(rec(docs[0])?.title);
8759
+ let line = `- [${type}] ${oneLine(label, 140)}`;
8760
+ if (node.superseded === true) line += " (superseded)";
8761
+ if (summary && summary !== label) line += ` \u2014 ${oneLine(summary, 240)}`;
8762
+ if (source) line += ` (source: ${oneLine(source, 100)})`;
8763
+ lines.push(line);
8764
+ }
8765
+ const articles = Array.isArray(payload.articles) ? payload.articles : [];
8766
+ for (const raw of articles) {
8767
+ const article = rec(raw);
8768
+ if (!article || lines.length >= PROMPT_RECALL_LIMIT) continue;
8769
+ const label = str(article.label);
8770
+ if (!label) continue;
8771
+ const content = str(article.content);
8772
+ if (!relevant(query, `${label} ${content ?? ""}`)) continue;
8773
+ lines.push(`- [Article] ${oneLine(label, 140)}${content ? ` \u2014 ${oneLine(content, 240)}` : ""}`);
8774
+ }
8775
+ if (lines.length === 0) return void 0;
8776
+ let block = "MindGraph recall (bounded):";
8777
+ for (const line of lines) {
8778
+ if (block.length + 1 + line.length > PROMPT_RECALL_MAX_CHARS) break;
8779
+ block += `
8780
+ ${line}`;
8781
+ }
8782
+ return block;
8783
+ }
8784
+ function messageText(content) {
8785
+ if (typeof content === "string") return content;
8786
+ if (!Array.isArray(content)) return void 0;
8787
+ const parts = content.map((block) => rec(block)).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
8788
+ return parts.length > 0 ? parts.join("\n") : void 0;
8789
+ }
8790
+ function stripHarnessNoise(text2) {
8791
+ return text2.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").replace(/<local-command-(stdout|stderr)>[\s\S]*?<\/local-command-\1>/g, "").replace(/<command-(name|message|args)>[\s\S]*?<\/command-\1>/g, "").replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, "").trim();
8792
+ }
8793
+ function extractTranscriptTurns(jsonl) {
8794
+ const turns = [];
8795
+ for (const line of jsonl.split("\n")) {
8796
+ if (!line.trim()) continue;
8797
+ let entry;
8798
+ try {
8799
+ entry = rec(JSON.parse(line));
8800
+ } catch {
8801
+ continue;
8802
+ }
8803
+ if (!entry || entry.type !== "user" && entry.type !== "assistant") continue;
8804
+ if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true) continue;
8805
+ const message = rec(entry.message);
8806
+ const raw = messageText(message?.content);
8807
+ if (!raw) continue;
8808
+ const text2 = stripHarnessNoise(raw);
8809
+ if (!text2) continue;
8810
+ turns.push({ role: entry.type, text: text2 });
8811
+ }
8812
+ return turns;
8813
+ }
8814
+ function renderTranscript(turns) {
8815
+ const rendered2 = turns.map((turn) => `${turn.role === "user" ? "User" : "Assistant"}: ${redactSecrets(turn.text)}`).join("\n\n");
8816
+ if (rendered2.length <= CAPTURE_MAX_CHARS) return rendered2;
8817
+ const tail = rendered2.slice(rendered2.length - CAPTURE_MAX_CHARS);
8818
+ const start = tail.search(/\n\n(User|Assistant): /);
8819
+ return start >= 0 ? tail.slice(start + 2) : tail;
8820
+ }
8821
+ function readTranscriptSince(file, offset = 0) {
8822
+ const buffer = import_node_fs4.default.readFileSync(file);
8823
+ const start = offset > buffer.length ? 0 : offset;
8824
+ const lastNewline = buffer.lastIndexOf(10);
8825
+ if (lastNewline < start) return { text: "", nextOffset: start };
8826
+ return {
8827
+ text: buffer.subarray(start, lastNewline + 1).toString("utf8"),
8828
+ nextOffset: lastNewline + 1
8829
+ };
8830
+ }
8831
+ function buildCaptureRequest(transcriptPath, sessionId, offset, ids) {
8832
+ const { text: text2, nextOffset } = readTranscriptSince(transcriptPath, offset);
8833
+ const turns = extractTranscriptTurns(text2);
8834
+ const content = renderTranscript(turns);
8835
+ if (!content) return { nextOffset, turns: 0 };
8836
+ return {
8837
+ body: {
8838
+ content,
8839
+ title: `Claude Code session ${sessionId}`,
8840
+ ...ids.sessionUid ? { session_uid: ids.sessionUid } : {},
8841
+ ...ids.agentId ? { agent_id: ids.agentId } : {}
8842
+ },
8843
+ nextOffset,
8844
+ turns: turns.length
8845
+ };
8846
+ }
8847
+
8848
+ // src/hook-core.ts
8456
8849
  var NOOP = { kind: "noop" };
8457
8850
  function defaultLedger(sessionId, options) {
8458
8851
  return {
@@ -8470,33 +8863,33 @@ function defaultLedger(sessionId, options) {
8470
8863
  };
8471
8864
  }
8472
8865
  function runtimeDir(options) {
8473
- 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");
8474
8867
  }
8475
8868
  function ledgerPath(sessionId, options) {
8476
8869
  const harness = "harness" in options ? options.harness : "claude-code";
8477
- const key = import_node_crypto4.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
8478
- 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`);
8479
8872
  }
8480
8873
  function waitBriefly(ms) {
8481
8874
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
8482
8875
  }
8483
8876
  function withLedger(sessionId, options, update) {
8484
8877
  const file = ledgerPath(sessionId, options);
8485
- import_node_fs3.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 });
8486
8879
  try {
8487
- import_node_fs3.default.chmodSync(import_node_path3.default.dirname(file), 448);
8880
+ import_node_fs5.default.chmodSync(import_node_path4.default.dirname(file), 448);
8488
8881
  } catch {
8489
8882
  }
8490
8883
  const lock = `${file}.lock`;
8491
8884
  let lockFd;
8492
8885
  for (let attempt = 0; attempt < 400; attempt++) {
8493
8886
  try {
8494
- lockFd = import_node_fs3.default.openSync(lock, "wx", 384);
8887
+ lockFd = import_node_fs5.default.openSync(lock, "wx", 384);
8495
8888
  break;
8496
8889
  } catch (error2) {
8497
8890
  if (error2.code !== "EEXIST") throw error2;
8498
8891
  try {
8499
- if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 3e3) import_node_fs3.default.unlinkSync(lock);
8892
+ if (Date.now() - import_node_fs5.default.statSync(lock).mtimeMs > 3e3) import_node_fs5.default.unlinkSync(lock);
8500
8893
  } catch {
8501
8894
  }
8502
8895
  waitBriefly(10);
@@ -8510,25 +8903,25 @@ function withLedger(sessionId, options, update) {
8510
8903
  try {
8511
8904
  ledger = {
8512
8905
  ...ledger,
8513
- ...JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"))
8906
+ ...JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"))
8514
8907
  };
8515
8908
  } catch {
8516
8909
  }
8517
8910
  const result = update(ledger);
8518
8911
  ledger.updatedAt = Date.now();
8519
- const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
8520
- import_node_fs3.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)}
8521
8914
  `, { mode: 384 });
8522
- import_node_fs3.default.renameSync(temporary, file);
8915
+ import_node_fs5.default.renameSync(temporary, file);
8523
8916
  try {
8524
- import_node_fs3.default.chmodSync(file, 384);
8917
+ import_node_fs5.default.chmodSync(file, 384);
8525
8918
  } catch {
8526
8919
  }
8527
8920
  return result;
8528
8921
  } finally {
8529
- import_node_fs3.default.closeSync(lockFd);
8922
+ import_node_fs5.default.closeSync(lockFd);
8530
8923
  try {
8531
- import_node_fs3.default.unlinkSync(lock);
8924
+ import_node_fs5.default.unlinkSync(lock);
8532
8925
  } catch {
8533
8926
  }
8534
8927
  }
@@ -8798,30 +9191,30 @@ function hasMindgraphServer(servers) {
8798
9191
  return false;
8799
9192
  }
8800
9193
  function pathIsWithin(ancestor, descendant) {
8801
- const base = import_node_path3.default.resolve(ancestor);
8802
- const target = import_node_path3.default.resolve(descendant);
8803
- 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);
8804
9197
  }
8805
9198
  function claudeMcpRegistrationNote(input, options) {
8806
9199
  if (options.harness !== "claude-code") return void 0;
8807
9200
  if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
8808
9201
  try {
8809
- const configPath = options.claudeConfigPath || import_node_path3.default.join(import_node_os3.default.homedir(), ".claude.json");
8810
- if (!import_node_fs3.default.existsSync(configPath)) return void 0;
8811
- const config = JSON.parse(import_node_fs3.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"));
8812
9205
  if (hasMindgraphServer(config.mcpServers)) return void 0;
8813
- const cwd = import_node_path3.default.resolve(input.cwd || ".");
9206
+ const cwd = import_node_path4.default.resolve(input.cwd || ".");
8814
9207
  for (const [projectDir, project] of Object.entries(config.projects || {})) {
8815
9208
  if (!pathIsWithin(projectDir, cwd)) continue;
8816
9209
  if (hasMindgraphServer(project?.mcpServers)) return void 0;
8817
9210
  }
8818
- for (let dir = cwd; ; dir = import_node_path3.default.dirname(dir)) {
8819
- const candidate = import_node_path3.default.join(dir, ".mcp.json");
8820
- if (import_node_fs3.default.existsSync(candidate)) {
8821
- const parsed = JSON.parse(import_node_fs3.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"));
8822
9215
  if (hasMindgraphServer(parsed.mcpServers)) return void 0;
8823
9216
  }
8824
- if (dir === import_node_path3.default.dirname(dir)) break;
9217
+ if (dir === import_node_path4.default.dirname(dir)) break;
8825
9218
  }
8826
9219
  } catch {
8827
9220
  return void 0;
@@ -8924,7 +9317,7 @@ async function sessionStart(input, client2, options) {
8924
9317
  const contextText = registrationNote ? `${registrationNote}
8925
9318
 
8926
9319
  ${briefText}` : briefText;
8927
- 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");
8928
9321
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
8929
9322
  const changed = ledger.lastBriefHash !== briefHash;
8930
9323
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
@@ -8999,6 +9392,20 @@ async function preTool(input, options) {
8999
9392
  if (targetsLedgerTask && fencedAction && ledger.taskVersion !== void 0 && updatedInput.expected_version === void 0) {
9000
9393
  updatedInput.expected_version = ledger.taskVersion;
9001
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
+ }
9002
9409
  return { kind: "rewrite", updatedInput };
9003
9410
  }
9004
9411
  var FENCED_PLAN_ACTIONS = /* @__PURE__ */ new Set([
@@ -9170,6 +9577,9 @@ function stop(input, options) {
9170
9577
  }
9171
9578
  async function sessionEnd(input, client2, options) {
9172
9579
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
9580
+ if (options.harness === "claude-code") {
9581
+ await captureTranscript(input, client2, options);
9582
+ }
9173
9583
  const ledger = readLedger(input.session_id, options);
9174
9584
  if (ledger.executionUid && ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0) {
9175
9585
  try {
@@ -9203,7 +9613,104 @@ async function sessionEnd(input, client2, options) {
9203
9613
  }
9204
9614
  return NOOP;
9205
9615
  }
9616
+ function withDeadline(promise, ms) {
9617
+ let timer;
9618
+ const deadline = new Promise((_, reject) => {
9619
+ timer = setTimeout(() => reject(new Error("deadline")), ms);
9620
+ timer.unref?.();
9621
+ });
9622
+ return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
9623
+ }
9624
+ async function promptRecall(input, client2, options) {
9625
+ if (options.promptRecall === false || process.env.MINDGRAPH_HOOK_PROMPT_RECALL === "off" || !client2.retrieveContext) {
9626
+ return NOOP;
9627
+ }
9628
+ const query = promptRecallQuery(input.prompt);
9629
+ if (!query) return NOOP;
9630
+ try {
9631
+ const response = await withDeadline(
9632
+ client2.retrieveContext(promptRecallRequest(query)),
9633
+ options.promptRecallTimeoutMs ?? 4e3
9634
+ );
9635
+ const block = renderPromptRecall(response, query);
9636
+ if (!block) return NOOP;
9637
+ const hash = import_node_crypto5.default.createHash("sha256").update(block).digest("hex");
9638
+ const fresh = withLedger(input.session_id, options, (ledger) => {
9639
+ if (ledger.lastRecallHash === hash) return false;
9640
+ ledger.lastRecallHash = hash;
9641
+ return true;
9642
+ });
9643
+ return fresh ? { kind: "context", context: block, event: "UserPromptSubmit" } : NOOP;
9644
+ } catch {
9645
+ return NOOP;
9646
+ }
9647
+ }
9648
+ async function captureTranscript(input, client2, options) {
9649
+ const enabled = process.env.MINDGRAPH_HOOK_CAPTURE === "on" || options.capture === true && process.env.MINDGRAPH_HOOK_CAPTURE !== "off";
9650
+ if (!enabled || !client2.ingestSession || !input.transcript_path) return;
9651
+ try {
9652
+ const agentId = resolveAgentId(options);
9653
+ const ledger = readLedger(input.session_id, options);
9654
+ let sessionUid = ledger.sessionUid;
9655
+ const prepared = buildCaptureRequest(
9656
+ input.transcript_path,
9657
+ input.session_id,
9658
+ ledger.captureOffset ?? 0,
9659
+ { sessionUid, agentId }
9660
+ );
9661
+ if (prepared.body && !sessionUid) {
9662
+ const opened = object(
9663
+ await client2.session({
9664
+ action: "open",
9665
+ label: `Claude Code ${input.session_id}`,
9666
+ harness: options.harness,
9667
+ harness_session_id: input.session_id,
9668
+ model: input.model,
9669
+ agent_id: agentId
9670
+ })
9671
+ );
9672
+ sessionUid = string(opened?.uid);
9673
+ if (sessionUid) prepared.body.session_uid = sessionUid;
9674
+ }
9675
+ if (prepared.body) await client2.ingestSession(prepared.body);
9676
+ withLedger(input.session_id, options, (current) => {
9677
+ if (sessionUid) current.sessionUid ??= sessionUid;
9678
+ current.captureOffset = Math.max(current.captureOffset ?? 0, prepared.nextOffset);
9679
+ return void 0;
9680
+ });
9681
+ } catch (error2) {
9682
+ console.error(
9683
+ `mindgraph-hook: transcript capture failed (${error2?.message || "unknown error"})`
9684
+ );
9685
+ }
9686
+ }
9206
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) {
9207
9714
  switch (input.hook_event_name) {
9208
9715
  case "SessionStart":
9209
9716
  return sessionStart(input, client2, options);
@@ -9228,6 +9735,11 @@ async function runHookCore(input, client2, options) {
9228
9735
  return stop(input, options);
9229
9736
  case "SessionEnd":
9230
9737
  return sessionEnd(input, client2, options);
9738
+ case "UserPromptSubmit":
9739
+ return promptRecall(input, client2, options);
9740
+ case "PreCompact":
9741
+ await captureTranscript(input, client2, options);
9742
+ return NOOP;
9231
9743
  default:
9232
9744
  return NOOP;
9233
9745
  }
@@ -9239,6 +9751,54 @@ async function readHookInput(stream = process.stdin) {
9239
9751
  }
9240
9752
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
9241
9753
  }
9754
+ function previewCapture(transcriptPath, sessionId, options = {}) {
9755
+ let ledger = {};
9756
+ try {
9757
+ ledger = JSON.parse(
9758
+ import_node_fs5.default.readFileSync(
9759
+ ledgerPath(sessionId, { ...options, harness: "claude-code" }),
9760
+ "utf8"
9761
+ )
9762
+ );
9763
+ } catch {
9764
+ }
9765
+ const offset = ledger.captureOffset ?? 0;
9766
+ return {
9767
+ ...buildCaptureRequest(transcriptPath, sessionId, offset, {
9768
+ sessionUid: ledger.sessionUid,
9769
+ agentId: options.agentId || process.env.MINDGRAPH_AGENT_ID || stableAgentId()
9770
+ }),
9771
+ offset
9772
+ };
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
+ }
9242
9802
 
9243
9803
  // src/claude-hooks.ts
9244
9804
  async function runClaudeHook(input, client2, options = {}) {
@@ -9253,17 +9813,12 @@ async function runClaudeHook(input, client2, options = {}) {
9253
9813
  case "context":
9254
9814
  return {
9255
9815
  hookSpecificOutput: {
9256
- hookEventName: "SessionStart",
9816
+ hookEventName: output.event ?? "SessionStart",
9257
9817
  additionalContext: output.context
9258
9818
  }
9259
9819
  };
9260
9820
  case "rewrite":
9261
- return {
9262
- hookSpecificOutput: {
9263
- hookEventName: "PreToolUse",
9264
- updatedInput: output.updatedInput
9265
- }
9266
- };
9821
+ return {};
9267
9822
  case "block":
9268
9823
  return { decision: "block", reason: output.reason };
9269
9824
  case "noop":
@@ -9283,7 +9838,7 @@ async function runCodexHook(input, client2, options = {}) {
9283
9838
  case "context":
9284
9839
  return {
9285
9840
  hookSpecificOutput: {
9286
- hookEventName: "SessionStart",
9841
+ hookEventName: output.event ?? "SessionStart",
9287
9842
  additionalContext: output.context
9288
9843
  }
9289
9844
  };
@@ -9306,15 +9861,15 @@ async function runCodexHook(input, client2, options = {}) {
9306
9861
  init_hook_installer();
9307
9862
  function recordHookHealth(harness, error2) {
9308
9863
  try {
9309
- const dir = process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime");
9310
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
9311
- 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");
9312
9867
  let failures = 0;
9313
9868
  try {
9314
- failures = Number(JSON.parse(fs6.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9869
+ failures = Number(JSON.parse(fs8.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9315
9870
  } catch {
9316
9871
  }
9317
- fs6.writeFileSync(
9872
+ fs8.writeFileSync(
9318
9873
  file,
9319
9874
  `${JSON.stringify({
9320
9875
  harness,
@@ -9330,14 +9885,14 @@ function recordHookHealth(harness, error2) {
9330
9885
  }
9331
9886
  function clearHookHealth(harness) {
9332
9887
  try {
9333
- const file = path7.join(
9334
- 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"),
9335
9890
  "hook-health.json"
9336
9891
  );
9337
- if (!fs6.existsSync(file)) return;
9338
- const current = JSON.parse(fs6.readFileSync(file, "utf8"));
9892
+ if (!fs8.existsSync(file)) return;
9893
+ const current = JSON.parse(fs8.readFileSync(file, "utf8"));
9339
9894
  if (!Number(current.consecutiveFailures)) return;
9340
- fs6.writeFileSync(
9895
+ fs8.writeFileSync(
9341
9896
  file,
9342
9897
  `${JSON.stringify({
9343
9898
  harness,
@@ -9353,22 +9908,22 @@ function clearHookHealth(harness) {
9353
9908
  function getClaudeDesktopConfigPath() {
9354
9909
  switch (process.platform) {
9355
9910
  case "darwin":
9356
- return path7.join(
9357
- os4.homedir(),
9911
+ return path8.join(
9912
+ os5.homedir(),
9358
9913
  "Library",
9359
9914
  "Application Support",
9360
9915
  "Claude",
9361
9916
  "claude_desktop_config.json"
9362
9917
  );
9363
9918
  case "win32":
9364
- return path7.join(
9365
- process.env.APPDATA || path7.join(os4.homedir(), "AppData", "Roaming"),
9919
+ return path8.join(
9920
+ process.env.APPDATA || path8.join(os5.homedir(), "AppData", "Roaming"),
9366
9921
  "Claude",
9367
9922
  "claude_desktop_config.json"
9368
9923
  );
9369
9924
  case "linux":
9370
- return path7.join(
9371
- os4.homedir(),
9925
+ return path8.join(
9926
+ os5.homedir(),
9372
9927
  ".config",
9373
9928
  "Claude",
9374
9929
  "claude_desktop_config.json"
@@ -9378,16 +9933,16 @@ function getClaudeDesktopConfigPath() {
9378
9933
  }
9379
9934
  }
9380
9935
  function readJsonFile(filePath) {
9381
- if (!fs6.existsSync(filePath)) return {};
9382
- const content = fs6.readFileSync(filePath, "utf-8");
9936
+ if (!fs8.existsSync(filePath)) return {};
9937
+ const content = fs8.readFileSync(filePath, "utf-8");
9383
9938
  return JSON.parse(content);
9384
9939
  }
9385
9940
  function writeJsonFile(filePath, data) {
9386
- const dir = path7.dirname(filePath);
9387
- if (!fs6.existsSync(dir)) {
9388
- fs6.mkdirSync(dir, { recursive: true });
9941
+ const dir = path8.dirname(filePath);
9942
+ if (!fs8.existsSync(dir)) {
9943
+ fs8.mkdirSync(dir, { recursive: true });
9389
9944
  }
9390
- fs6.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9945
+ fs8.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9391
9946
  }
9392
9947
  function getMcpServerEntry(apiKey, baseUrl) {
9393
9948
  const env = { MINDGRAPH_API_KEY: apiKey };
@@ -9474,7 +10029,7 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
9474
10029
  }
9475
10030
  function uninstallClaudeDesktop() {
9476
10031
  const configPath = getClaudeDesktopConfigPath();
9477
- if (!fs6.existsSync(configPath)) {
10032
+ if (!fs8.existsSync(configPath)) {
9478
10033
  console.log("Claude Desktop config not found. Nothing to remove.");
9479
10034
  return;
9480
10035
  }
@@ -9501,7 +10056,7 @@ function uninstallClaudeCode() {
9501
10056
  }
9502
10057
  function printStatus() {
9503
10058
  const desktopPath = getClaudeDesktopConfigPath();
9504
- if (fs6.existsSync(desktopPath)) {
10059
+ if (fs8.existsSync(desktopPath)) {
9505
10060
  const config = readJsonFile(desktopPath);
9506
10061
  const servers = config.mcpServers;
9507
10062
  if (servers && "mindgraph" in servers) {
@@ -9519,12 +10074,12 @@ function printStatus() {
9519
10074
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
9520
10075
  );
9521
10076
  try {
9522
- const healthFile = path7.join(
9523
- 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"),
9524
10079
  "hook-health.json"
9525
10080
  );
9526
- if (fs6.existsSync(healthFile)) {
9527
- const health = JSON.parse(fs6.readFileSync(healthFile, "utf8"));
10081
+ if (fs8.existsSync(healthFile)) {
10082
+ const health = JSON.parse(fs8.readFileSync(healthFile, "utf8"));
9528
10083
  const failures = Number(health.consecutiveFailures) || 0;
9529
10084
  console.log(
9530
10085
  failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
@@ -9539,17 +10094,46 @@ function printStatus() {
9539
10094
  function withHooksFlag() {
9540
10095
  return process.argv.includes("--hooks");
9541
10096
  }
10097
+ function claudeHookFeatures() {
10098
+ const flags = parseArgs(process.argv);
10099
+ const stored = loadHookEnv();
10100
+ return {
10101
+ promptRecall: flags.promptRecall ?? stored.promptRecall ?? true,
10102
+ capture: flags.capture ?? stored.capture ?? false
10103
+ };
10104
+ }
10105
+ function printClaudeHookFeatures(features) {
10106
+ console.log(
10107
+ `Prompt recall (UserPromptSubmit): ${features.promptRecall ? "on" : "off"}; transcript capture (PreCompact/SessionEnd): ${features.capture ? "ON \u2014 user and assistant text (never tool calls or tool output), secrets redacted, is sent to MindGraph /ingest/session" : "off (enable with --capture)"}`
10108
+ );
10109
+ }
10110
+ function latestTranscript(cwd) {
10111
+ const dir = path8.join(
10112
+ os5.homedir(),
10113
+ ".claude",
10114
+ "projects",
10115
+ cwd.replace(/[^A-Za-z0-9]/g, "-")
10116
+ );
10117
+ try {
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];
10119
+ } catch {
10120
+ return void 0;
10121
+ }
10122
+ }
9542
10123
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
9543
10124
  if (withHooks) {
9544
- const runner = installHookRunner(__filename, void 0, "0.22.0");
10125
+ const runner = installHookRunner(__filename, void 0, "0.24.0");
9545
10126
  console.log(`Installed pinned hook runner at ${runner}`);
9546
- const result = installClaudeHooks("user");
10127
+ const features = claudeHookFeatures();
10128
+ const result = installClaudeHooks("user", void 0, features);
9547
10129
  console.log(
9548
10130
  `Installed ${result.added} and refreshed ${result.updated} MindGraph Claude Code hook entries in ${result.path}`
9549
10131
  );
10132
+ printClaudeHookFeatures(features);
9550
10133
  const envPath = saveHookEnv({
9551
10134
  apiKey,
9552
10135
  baseUrl,
10136
+ ...features,
9553
10137
  // Default the stable per-user identity only when nothing is stored —
9554
10138
  // an explicit or previously persisted agentId must survive reinstalls.
9555
10139
  agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
@@ -9588,6 +10172,12 @@ USAGE:
9588
10172
  mindgraph-mcp uninstall-code Remove from Claude Code
9589
10173
  mindgraph-mcp install-hooks --harness claude-code|codex [--scope user|project]
9590
10174
  mindgraph-mcp uninstall-hooks --harness claude-code|codex [--scope user|project]
10175
+ mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]
10176
+ Print exactly what transcript capture would
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).
9591
10181
  mindgraph-mcp status Show installation status
9592
10182
  mindgraph-mcp skills pull [--dir .claude/skills]
9593
10183
  Write your org's published+granted skills as
@@ -9610,6 +10200,10 @@ OPTIONS:
9610
10200
  --scope <scope> Hook settings scope: project (default) or user
9611
10201
  --project-dir <dir> Project root for project-scoped hooks
9612
10202
  --harness <name> Hook harness (claude-code or codex)
10203
+ --no-prompt-recall Claude hooks: omit per-prompt recall (UserPromptSubmit)
10204
+ --capture Claude hooks: send user/assistant text to MindGraph
10205
+ before compaction and at session end (off by default;
10206
+ --no-capture turns it back off)
9613
10207
  --help, -h Show this help message
9614
10208
 
9615
10209
  EXAMPLES:
@@ -9629,7 +10223,7 @@ var DASHBOARD_URL = process.env.MINDGRAPH_DASHBOARD_URL || "https://mindgraph.cl
9629
10223
  var POLL_INTERVAL = 2e3;
9630
10224
  var POLL_TIMEOUT = 10 * 60 * 1e3;
9631
10225
  function generateCode() {
9632
- return crypto5.randomBytes(32).toString("hex");
10226
+ return crypto6.randomBytes(32).toString("hex");
9633
10227
  }
9634
10228
  function openBrowser(url) {
9635
10229
  try {
@@ -9856,16 +10450,19 @@ ${total} skill(s) synced.`);
9856
10450
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
9857
10451
  throw new Error("--harness must be claude-code or codex");
9858
10452
  }
9859
- const runner = installHookRunner(__filename, void 0, "0.22.0");
10453
+ const runner = installHookRunner(__filename, void 0, "0.24.0");
9860
10454
  console.log(`Installed pinned hook runner at ${runner}`);
9861
- const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir);
10455
+ const features = hookHarness === "claude-code" ? claudeHookFeatures() : void 0;
10456
+ const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir, features);
9862
10457
  console.log(
9863
10458
  `Installed ${result.added} and refreshed ${result.updated} MindGraph ${hookHarness === "codex" ? "Codex" : "Claude Code"} hook entries in ${result.path}`
9864
10459
  );
10460
+ if (features) printClaudeHookFeatures(features);
9865
10461
  printCodegraphStatus();
9866
10462
  const envPath = saveHookEnv({
9867
10463
  apiKey,
9868
10464
  baseUrl,
10465
+ ...features || {},
9869
10466
  // Default the stable per-user identity only when nothing is stored —
9870
10467
  // an explicit or previously persisted agentId must survive
9871
10468
  // reinstalls.
@@ -9917,9 +10514,14 @@ ${total} skill(s) synced.`);
9917
10514
  // DEFAULT org while the MCP tools write to the pinned one.
9918
10515
  orgId: process.env.MINDGRAPH_ORG_ID || stored.orgId,
9919
10516
  maxRetries: 0,
9920
- telemetrySurface: "mcp"
10517
+ telemetrySurface: "mcp",
10518
+ // Prompt recall must never hold the prompt: abort the request at
10519
+ // the same 4s deadline the core races it against.
10520
+ ...input.hook_event_name === "UserPromptSubmit" ? { timeoutMs: 4e3 } : {}
9921
10521
  });
9922
10522
  const hookOptions = {
10523
+ promptRecall: stored.promptRecall,
10524
+ capture: stored.capture,
9923
10525
  // No harness-name fallback here: hook-core derives a stable
9924
10526
  // per-user default shared by both harnesses, which is what lets a
9925
10527
  // task claimed under Codex rebind under Claude Code.
@@ -9934,9 +10536,11 @@ ${total} skill(s) synced.`);
9934
10536
  client2,
9935
10537
  hookOptions
9936
10538
  );
9937
- process.stdout.write(`${JSON.stringify(output)}
9938
- `);
9939
10539
  clearHookHealth(hookHarness);
10540
+ process.stdout.write(`${JSON.stringify(output)}
10541
+ `, () => {
10542
+ if (input.hook_event_name === "UserPromptSubmit") process.exit(0);
10543
+ });
9940
10544
  } catch (error2) {
9941
10545
  recordHookHealth(
9942
10546
  hookHarness,
@@ -9949,6 +10553,59 @@ ${total} skill(s) synced.`);
9949
10553
  }
9950
10554
  break;
9951
10555
  }
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
+ }
10579
+ if (positionals[0] !== "preview") {
10580
+ console.error(
10581
+ "Usage: mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]\n mindgraph-mcp hooks stats [--session-id <id>]"
10582
+ );
10583
+ process.exit(1);
10584
+ }
10585
+ const transcript = positionals[1] || latestTranscript(process.cwd());
10586
+ if (!transcript || !fs8.existsSync(transcript)) {
10587
+ console.error(
10588
+ "No transcript found. Pass a Claude Code transcript path (~/.claude/projects/<dir>/<session>.jsonl)."
10589
+ );
10590
+ process.exit(1);
10591
+ }
10592
+ const sessionId = parseArgs(process.argv).sessionId || path8.basename(transcript, ".jsonl");
10593
+ const preview = previewCapture(transcript, sessionId, {
10594
+ agentId: process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId
10595
+ });
10596
+ const enabled = loadHookEnv().capture === true;
10597
+ console.error(
10598
+ `Transcript: ${transcript}
10599
+ Session: ${sessionId}
10600
+ Capture is ${enabled ? "ON" : "OFF (nothing is sent; enable with install-hooks --capture)"}
10601
+ Would send ${preview.turns} turn(s) from byte ${preview.offset} as POST /ingest/session (nothing sent by this preview):`
10602
+ );
10603
+ process.stdout.write(
10604
+ `${JSON.stringify(preview.body ?? null, null, 2)}
10605
+ `
10606
+ );
10607
+ break;
10608
+ }
9952
10609
  case "status":
9953
10610
  printStatus();
9954
10611
  break;