mindgraph-mcp 0.22.0 → 0.23.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
@@ -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
  `, {
@@ -2561,6 +2565,12 @@ var init_code_tool = __esm({
2561
2565
  });
2562
2566
 
2563
2567
  // src/hook-installer.ts
2568
+ function storedFeatures(homeDir) {
2569
+ const { promptRecall: promptRecall2, capture } = loadHookEnv(
2570
+ import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), ".mindgraph")
2571
+ );
2572
+ return { promptRecall: promptRecall2, capture };
2573
+ }
2564
2574
  function command(harness) {
2565
2575
  return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
2566
2576
  }
@@ -2569,22 +2579,22 @@ function windowsCommand(harness) {
2569
2579
  }
2570
2580
  function installHookRunner(sourceFile, homeDir, version) {
2571
2581
  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 });
2582
+ import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
2573
2583
  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);
2584
+ import_node_fs5.default.copyFileSync(sourceFile, temporary);
2585
+ import_node_fs5.default.renameSync(temporary, target);
2576
2586
  const versionPath = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE);
2577
2587
  if (version) {
2578
- import_node_fs4.default.writeFileSync(versionPath, `${version}
2588
+ import_node_fs5.default.writeFileSync(versionPath, `${version}
2579
2589
  `);
2580
2590
  } else {
2581
- import_node_fs4.default.rmSync(versionPath, { force: true });
2591
+ import_node_fs5.default.rmSync(versionPath, { force: true });
2582
2592
  }
2583
2593
  return target;
2584
2594
  }
2585
2595
  function installedHookRunnerVersion(homeDir) {
2586
2596
  try {
2587
- const raw = import_node_fs4.default.readFileSync(
2597
+ const raw = import_node_fs5.default.readFileSync(
2588
2598
  import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE),
2589
2599
  "utf8"
2590
2600
  ).trim();
@@ -2611,10 +2621,10 @@ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2611
2621
  return { refreshed: false, reason: "disabled" };
2612
2622
  }
2613
2623
  const home = homeDir || import_node_os4.default.homedir();
2614
- if (!import_node_fs4.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
2624
+ if (!import_node_fs5.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
2615
2625
  return { refreshed: false, reason: "no_runner" };
2616
2626
  }
2617
- if (!import_node_fs4.default.existsSync(sourceBundle)) {
2627
+ if (!import_node_fs5.default.existsSync(sourceBundle)) {
2618
2628
  return { refreshed: false, reason: "no_source" };
2619
2629
  }
2620
2630
  const server2 = parseSemver(serverVersion);
@@ -2645,9 +2655,16 @@ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2645
2655
  for (const scope of scopes) {
2646
2656
  try {
2647
2657
  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);
2658
+ if (!import_node_fs5.default.existsSync(file)) continue;
2659
+ if (!import_node_fs5.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2660
+ installHooks(
2661
+ "claude-code",
2662
+ scope,
2663
+ projectDir,
2664
+ void 0,
2665
+ homeDir,
2666
+ storedFeatures(homeDir)
2667
+ );
2651
2668
  refreshed.push(file);
2652
2669
  } catch {
2653
2670
  }
@@ -2666,8 +2683,8 @@ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir =
2666
2683
  return scope === "user" ? import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), ".claude", "settings.json") : import_node_path4.default.join(projectDir, ".claude", "settings.json");
2667
2684
  }
2668
2685
  function readSettings(file) {
2669
- if (!import_node_fs4.default.existsSync(file)) return {};
2670
- return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
2686
+ if (!import_node_fs5.default.existsSync(file)) return {};
2687
+ return JSON.parse(import_node_fs5.default.readFileSync(file, "utf8"));
2671
2688
  }
2672
2689
  function ownedHook(harness, timeout, additionalContextLimit) {
2673
2690
  return {
@@ -2679,7 +2696,7 @@ function ownedHook(harness, timeout, additionalContextLimit) {
2679
2696
  ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
2680
2697
  };
2681
2698
  }
2682
- function desiredEntries(harness) {
2699
+ function desiredEntries(harness, features = {}) {
2683
2700
  const common = {
2684
2701
  SessionStart: [
2685
2702
  {
@@ -2722,11 +2739,17 @@ function desiredEntries(harness) {
2722
2739
  { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
2723
2740
  ]
2724
2741
  };
2725
- return harness === "claude-code" ? {
2742
+ if (harness !== "claude-code") return common;
2743
+ return {
2726
2744
  ...common,
2727
2745
  TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
2728
- TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
2729
- } : common;
2746
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }],
2747
+ // Bounded recall per prompt: the core gives up at 4s and fails open, so
2748
+ // a 5s budget never holds the prompt longer than that.
2749
+ ...features.promptRecall !== false ? { UserPromptSubmit: [{ hooks: [ownedHook(harness, 5)] }] } : {},
2750
+ // Opt-in transcript capture before compaction (cold-tenant ingest).
2751
+ ...features.capture === true ? { PreCompact: [{ hooks: [ownedHook(harness, 30)] }] } : {}
2752
+ };
2730
2753
  }
2731
2754
  function hookCommand(value) {
2732
2755
  if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
@@ -2739,21 +2762,30 @@ function entryHasOwnedHook(value) {
2739
2762
  return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2740
2763
  }
2741
2764
  function writeSettings(file, settings) {
2742
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2765
+ import_node_fs5.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2743
2766
  const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2744
- import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2767
+ import_node_fs5.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2745
2768
  `, {
2746
2769
  mode: 384
2747
2770
  });
2748
- import_node_fs4.default.renameSync(temporary, file);
2771
+ import_node_fs5.default.renameSync(temporary, file);
2749
2772
  }
2750
- function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
2773
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir, features = {}) {
2751
2774
  const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
2752
2775
  const settings = readSettings(file);
2753
2776
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2754
2777
  let added = 0;
2755
2778
  let updated = 0;
2756
- for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
2779
+ const desired = desiredEntries(harness, features);
2780
+ for (const optional of ["UserPromptSubmit", "PreCompact"]) {
2781
+ if (optional in desired || !Array.isArray(hooks[optional])) continue;
2782
+ const kept = hooks[optional].filter(
2783
+ (entry) => !entryHasOwnedHook(entry)
2784
+ );
2785
+ if (kept.length > 0) hooks[optional] = kept;
2786
+ else delete hooks[optional];
2787
+ }
2788
+ for (const [event, wanted] of Object.entries(desired)) {
2757
2789
  const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
2758
2790
  const kept = current.filter((entry) => !entryHasOwnedHook(entry));
2759
2791
  const hadOwned = kept.length !== current.length;
@@ -2770,7 +2802,7 @@ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir,
2770
2802
  }
2771
2803
  function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2772
2804
  const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2773
- if (!import_node_fs4.default.existsSync(file)) return { path: file, removed: 0 };
2805
+ if (!import_node_fs5.default.existsSync(file)) return { path: file, removed: 0 };
2774
2806
  const settings = readSettings(file);
2775
2807
  const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2776
2808
  let removed = 0;
@@ -2804,8 +2836,8 @@ function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir
2804
2836
  writeSettings(file, settings);
2805
2837
  return { path: file, removed };
2806
2838
  }
2807
- function installClaudeHooks(scope, projectDir = process.cwd()) {
2808
- return installHooks("claude-code", scope, projectDir);
2839
+ function installClaudeHooks(scope, projectDir = process.cwd(), features = {}) {
2840
+ return installHooks("claude-code", scope, projectDir, void 0, void 0, features);
2809
2841
  }
2810
2842
  function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
2811
2843
  return uninstallHooks("claude-code", scope, projectDir);
@@ -2816,14 +2848,15 @@ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2816
2848
  function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2817
2849
  return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2818
2850
  }
2819
- var import_node_fs4, import_node_os4, import_node_path4, import_node_crypto5, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE;
2851
+ var import_node_fs5, import_node_os4, import_node_path4, import_node_crypto5, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE;
2820
2852
  var init_hook_installer = __esm({
2821
2853
  "src/hook-installer.ts"() {
2822
2854
  "use strict";
2823
- import_node_fs4 = __toESM(require("fs"));
2855
+ import_node_fs5 = __toESM(require("fs"));
2824
2856
  import_node_os4 = __toESM(require("os"));
2825
2857
  import_node_path4 = __toESM(require("path"));
2826
2858
  import_node_crypto5 = __toESM(require("crypto"));
2859
+ init_hook_env();
2827
2860
  OWNER_MARKER = "--owner mindgraph";
2828
2861
  RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2829
2862
  RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
@@ -2834,14 +2867,14 @@ var init_hook_installer = __esm({
2834
2867
  function obj(value) {
2835
2868
  return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2836
2869
  }
2837
- function str(value) {
2870
+ function str2(value) {
2838
2871
  return typeof value === "string" && value ? value : void 0;
2839
2872
  }
2840
2873
  function num(value) {
2841
2874
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2842
2875
  }
2843
2876
  function edgeTypeName(value) {
2844
- return str(value) ?? str(obj(value)?.Custom);
2877
+ return str2(value) ?? str2(obj(value)?.Custom);
2845
2878
  }
2846
2879
  function renderBudget(requested) {
2847
2880
  if (requested === void 0 || !Number.isFinite(requested)) return RENDER_CHAR_BUDGET;
@@ -2851,7 +2884,7 @@ function renderBudget(requested) {
2851
2884
  );
2852
2885
  }
2853
2886
  function clip(value, max, state) {
2854
- const text2 = str(value);
2887
+ const text2 = str2(value);
2855
2888
  if (!text2) return void 0;
2856
2889
  if (text2.length <= max) return text2;
2857
2890
  state.bounded = true;
@@ -2860,13 +2893,13 @@ function clip(value, max, state) {
2860
2893
  function isNodeShaped(value) {
2861
2894
  const node = obj(value);
2862
2895
  return Boolean(
2863
- node && str(node.uid) && (str(node.label) || str(node.node_type) || obj(node.node_type))
2896
+ node && str2(node.uid) && (str2(node.label) || str2(node.node_type) || obj(node.node_type))
2864
2897
  );
2865
2898
  }
2866
2899
  function nodeDisplayType(node) {
2867
2900
  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";
2901
+ const plain = str2(node.node_type);
2902
+ 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
2903
  }
2871
2904
  function nodeFields(node) {
2872
2905
  const props = obj(node.props);
@@ -2886,10 +2919,10 @@ function statusTag(node) {
2886
2919
  const tags = [];
2887
2920
  if (node.superseded === true) {
2888
2921
  tags.push(
2889
- node.superseded_by ? `SUPERSEDED by ${str(node.superseded_by)}` : "SUPERSEDED"
2922
+ node.superseded_by ? `SUPERSEDED by ${str2(node.superseded_by)}` : "SUPERSEDED"
2890
2923
  );
2891
2924
  }
2892
- const truth = str(node.truth_status) ?? str(obj(node.props)?.truth_status);
2925
+ const truth = str2(node.truth_status) ?? str2(obj(node.props)?.truth_status);
2893
2926
  if (truth && truth !== "accepted") tags.push(`truth: ${truth}`);
2894
2927
  if (node.currently_valid === false) tags.push("OUTSIDE ITS VALIDITY WINDOW");
2895
2928
  if (node.valid_at_time === false) tags.push("NOT VALID AT THE REQUESTED TIME");
@@ -2899,7 +2932,7 @@ function firstQuote(node, state) {
2899
2932
  const chunks = Array.isArray(node.source_chunks) ? node.source_chunks : [];
2900
2933
  for (const raw of chunks) {
2901
2934
  const chunk = obj(raw);
2902
- const quote = str(chunk?.quote) ?? str(obj(chunk?.anchor)?.exact);
2935
+ const quote = str2(chunk?.quote) ?? str2(obj(chunk?.anchor)?.exact);
2903
2936
  if (quote) return clip(quote, QUOTE_MAX, state);
2904
2937
  }
2905
2938
  return void 0;
@@ -2907,7 +2940,7 @@ function firstQuote(node, state) {
2907
2940
  function sourceTag(node) {
2908
2941
  const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
2909
2942
  const first = obj(docs[0]);
2910
- const title = str(first?.title);
2943
+ const title = str2(first?.title);
2911
2944
  if (!title) return void 0;
2912
2945
  const extra = docs.length > 1 ? ` (+${docs.length - 1} more)` : "";
2913
2946
  return `from: ${title}${extra}`;
@@ -2933,7 +2966,7 @@ function nodeLines(node, state) {
2933
2966
  confidence !== void 0 ? `confidence ${confidence}` : void 0,
2934
2967
  score !== void 0 ? `score ${Math.round(score * 100) / 100}` : void 0
2935
2968
  ].filter(Boolean).join(", ");
2936
- const head = `- **${label}** [${str(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
2969
+ const head = `- **${label}** [${str2(node.uid) || "?"}] (${parenthetical})${statusTag(node)}`;
2937
2970
  const summary = nodeSummaryText(node, state);
2938
2971
  const lines = [summary && summary !== label ? `${head}: ${summary}` : head];
2939
2972
  const fields = layer7Fields(node, state);
@@ -2947,7 +2980,7 @@ function nodeLines(node, state) {
2947
2980
  const rendered2 = believers.slice(0, 3).map((raw) => {
2948
2981
  const believer = obj(raw);
2949
2982
  const conf = num(believer?.confidence);
2950
- return `${str(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
2983
+ return `${str2(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
2951
2984
  }).join(", ");
2952
2985
  lines.push(` - believed by: ${rendered2}`);
2953
2986
  }
@@ -2978,8 +3011,8 @@ function relationshipItems(edges, labelByUid, state, options) {
2978
3011
  const edge = obj(raw);
2979
3012
  if (!edge) continue;
2980
3013
  const type = edgeTypeName(edge.edge_type) || "?";
2981
- const from = str(edge.from_uid);
2982
- const to = str(edge.to_uid);
3014
+ const from = str2(edge.from_uid);
3015
+ const to = str2(edge.to_uid);
2983
3016
  if (CURATION_EDGE_TYPES.has(type)) continue;
2984
3017
  if (!from || !to || from === to) continue;
2985
3018
  const fromLabel = labelByUid.get(from) ?? (options?.fallbackToUid ? `[${from}]` : void 0);
@@ -3051,7 +3084,7 @@ function policySections(payload, state) {
3051
3084
  if (policies.length === 0) return [];
3052
3085
  const items = policies.map((raw) => obj(raw)).filter((policy) => Boolean(policy)).map((policy) => {
3053
3086
  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)"}`;
3087
+ return `- ${str2(policy.name) || "(unnamed policy)"}: ${clip(rules, SUMMARY_MAX, state) || "(structured rules apply)"}`;
3055
3088
  });
3056
3089
  return [{ heading: "## Applicable policies", items }];
3057
3090
  }
@@ -3071,7 +3104,7 @@ function renderContext(response, maxChars) {
3071
3104
  const graph = obj(payload.graph);
3072
3105
  const rawNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
3073
3106
  const shaped = rawNodes.filter(isNodeShaped);
3074
- const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str(node.node_type) || ""));
3107
+ const nodes = shaped.filter((node) => !INFRA_NODE_TYPES.has(str2(node.node_type) || ""));
3075
3108
  const infraOnly = shaped.length - nodes.length;
3076
3109
  const articles = Array.isArray(payload.articles) ? payload.articles : [];
3077
3110
  const chunks = Array.isArray(payload.chunks) ? payload.chunks : [];
@@ -3081,7 +3114,7 @@ function renderContext(response, maxChars) {
3081
3114
  sections.push({
3082
3115
  heading: "## Knowledge articles",
3083
3116
  items: articles.map((raw) => obj(raw)).filter((article) => Boolean(article)).map(
3084
- (article) => `### ${str(article.label) || "(untitled)"} [${str(article.uid) || "?"}]
3117
+ (article) => `### ${str2(article.label) || "(untitled)"} [${str2(article.uid) || "?"}]
3085
3118
  ${clip(article.content, ARTICLE_MAX, state) || ""}`
3086
3119
  )
3087
3120
  });
@@ -3089,7 +3122,7 @@ ${clip(article.content, ARTICLE_MAX, state) || ""}`
3089
3122
  if (nodes.length > 0) {
3090
3123
  sections.push(...groupedNodeSections(nodes, state, "## Knowledge graph\n\n"));
3091
3124
  const labelByUid = /* @__PURE__ */ new Map();
3092
- for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
3125
+ for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3093
3126
  const relationships = relationshipItems(
3094
3127
  Array.isArray(graph?.edges) ? graph.edges : [],
3095
3128
  labelByUid,
@@ -3108,7 +3141,7 @@ ${clip(article.content, ARTICLE_MAX, state) || ""}`
3108
3141
  sections.push({
3109
3142
  heading: "## Source excerpts",
3110
3143
  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) || ""}"`
3144
+ (chunk) => `- [${str2(chunk.chunk_uid) || "?"}] from "${str2(chunk.document_title) || "unknown document"}": "${clip(chunk.content, CHUNK_MAX, state) || ""}"`
3112
3145
  )
3113
3146
  });
3114
3147
  }
@@ -3132,15 +3165,15 @@ function resumeEntry(raw, state, fallbackType) {
3132
3165
  if (!wrapper) return void 0;
3133
3166
  const value = obj(wrapper.node) ?? wrapper;
3134
3167
  if (isNodeShaped(value)) return nodeLines(value, state);
3135
- const uid = str(value.uid);
3168
+ const uid = str2(value.uid);
3136
3169
  const props = obj(value.props);
3137
3170
  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);
3171
+ const status = str2(value.status) ?? str2(props?.status);
3139
3172
  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
3173
  const details = [
3141
3174
  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
3175
+ str2(value.path) ? `path ${str2(value.path)}` : void 0,
3176
+ str2(value.symbol) ? `symbol ${str2(value.symbol)}` : void 0
3144
3177
  ].filter(Boolean);
3145
3178
  const head = `- **${label}**${uid ? ` [${uid}]` : ""}${details.length > 0 ? ` (${details.join(", ")})` : ""}`;
3146
3179
  return summary && summary !== label ? `${head}: ${summary}` : head;
@@ -3153,7 +3186,7 @@ function renderResumeContext(response, maxChars) {
3153
3186
  const payload = obj(response);
3154
3187
  if (!payload) return void 0;
3155
3188
  const state = { bounded: false, omitted: 0 };
3156
- const status = str(payload.status);
3189
+ const status = str2(payload.status);
3157
3190
  if (!payload.task && status === "no_eligible_work") {
3158
3191
  const scopedOut = Array.isArray(payload.scoped_out_tasks) ? payload.scoped_out_tasks.length : 0;
3159
3192
  const note = scopedOut > 0 ? ` ${scopedOut} task(s) exist outside the requested scope.` : "";
@@ -3288,13 +3321,13 @@ function renderTraversal(response, maxChars) {
3288
3321
  const state = { bounded: false, omitted: 0 };
3289
3322
  const sections = [];
3290
3323
  const labelByUid = /* @__PURE__ */ new Map();
3291
- for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
3324
+ for (const node of nodes) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3292
3325
  if (paths.length > 0) {
3293
3326
  const items = paths.map((raw) => obj(raw)).filter((path8) => Boolean(path8)).map((path8, index) => {
3294
3327
  const uids = Array.isArray(path8.node_uids) ? path8.node_uids.map(String) : [];
3295
3328
  const labels = Array.isArray(path8.labels) ? path8.labels.map(String) : Array.isArray(path8.nodes) ? path8.nodes.map((hop) => {
3296
3329
  const hopNode = obj(hop);
3297
- return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
3330
+ return str2(hopNode?.label) || labelByUid.get(str2(hopNode?.uid) || "") || str2(hop) || "?";
3298
3331
  }) : [];
3299
3332
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
3300
3333
  const cost = num(path8.cost) ?? num(path8.path_cost);
@@ -3312,12 +3345,12 @@ function renderTraversal(response, maxChars) {
3312
3345
  if (hasSteps) {
3313
3346
  const stepItems = rawSteps.map((raw) => obj(raw)).filter((step) => Boolean(step)).map((step) => {
3314
3347
  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)";
3348
+ const uid = str2(step.node_uid) ?? str2(step.uid) ?? str2(step.edge_to_uid) ?? "?";
3349
+ const parent = str2(step.parent_uid) ?? str2(step.edge_from_uid);
3350
+ const label = str2(step.label) || "(unlabeled)";
3318
3351
  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);
3352
+ const role = str2(step.traversal_role);
3353
+ const type = str2(step.node_type) ?? str2(obj(step.node_type)?.Custom);
3321
3354
  const cost = num(step.path_cost);
3322
3355
  const confidence = num(step.path_confidence);
3323
3356
  const prefix = [
@@ -3336,9 +3369,9 @@ function renderTraversal(response, maxChars) {
3336
3369
  if (stepItems.length > 0) {
3337
3370
  sections.push({ heading: "## Traversal steps", items: stepItems });
3338
3371
  } else {
3339
- const start = str(body.start_uid) || "?";
3340
- const end = str(body.end_uid);
3341
- const mode2 = str(body.mode);
3372
+ const start = str2(body.start_uid) || "?";
3373
+ const end = str2(body.end_uid);
3374
+ const mode2 = str2(body.mode);
3342
3375
  sections.push({
3343
3376
  heading: mode2 === "path" && end ? `No path found from [${start}] to [${end}].` : `No traversal steps returned from [${start}].`,
3344
3377
  items: []
@@ -3356,7 +3389,7 @@ function renderTraversal(response, maxChars) {
3356
3389
  sections.push({ heading: "## Relationships", items: relationships });
3357
3390
  }
3358
3391
  }
3359
- const mode = str(body.mode);
3392
+ const mode = str2(body.mode);
3360
3393
  return assemble(
3361
3394
  `# Graph traversal${mode ? ` (${mode})` : ""}`,
3362
3395
  sections,
@@ -3367,9 +3400,9 @@ function renderTraversal(response, maxChars) {
3367
3400
  function neighborLine(value) {
3368
3401
  const node = obj(value);
3369
3402
  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);
3403
+ const label = str2(node.label) || "(unlabeled)";
3404
+ const uid = str2(node.uid);
3405
+ const type = str2(node.node_type);
3373
3406
  return `- ${label}${uid ? ` [${uid}]` : ""}${type ? ` (${type})` : ""}`;
3374
3407
  }
3375
3408
  function renderOntologyAnswer(response) {
@@ -3389,20 +3422,20 @@ function renderOntologyAnswer(response) {
3389
3422
  sections.push(...groupedNodeSections(objects, state));
3390
3423
  }
3391
3424
  const labelByUid = /* @__PURE__ */ new Map();
3392
- for (const node of objects) labelByUid.set(str(node.uid), str(node.label) || "?");
3425
+ for (const node of objects) labelByUid.set(str2(node.uid), str2(node.label) || "?");
3393
3426
  const graph = obj(payload.graph);
3394
3427
  for (const raw of Array.isArray(graph?.nodes) ? graph.nodes : []) {
3395
3428
  const node = obj(raw);
3396
- const uid = str(node?.uid);
3397
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
3429
+ const uid = str2(node?.uid);
3430
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3398
3431
  }
3399
3432
  if (cognitive) {
3400
3433
  for (const rawNodes of Object.values(cognitive)) {
3401
3434
  if (!Array.isArray(rawNodes)) continue;
3402
3435
  for (const raw of rawNodes) {
3403
3436
  const node = obj(raw);
3404
- const uid = str(node?.uid);
3405
- if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
3437
+ const uid = str2(node?.uid);
3438
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str2(node?.label) || "?");
3406
3439
  }
3407
3440
  }
3408
3441
  }
@@ -3423,9 +3456,9 @@ function renderOntologyAnswer(response) {
3423
3456
  if (provenance.length > 0) {
3424
3457
  const items = provenance.map((raw) => obj(raw)).filter((entry) => Boolean(entry)).map((entry) => {
3425
3458
  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}`;
3459
+ const by = str2(entry.ingested_by_name);
3460
+ const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str2(entry.source_uid) || "?"}]`;
3461
+ return str2(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
3429
3462
  });
3430
3463
  sections.push({ heading: "## Sources", items });
3431
3464
  }
@@ -3471,20 +3504,20 @@ function renderJobs(response) {
3471
3504
  const items = shown.map((job) => {
3472
3505
  const progress = obj(job.progress);
3473
3506
  const failure = obj(job.error_details);
3474
- const message = str(failure?.message) ?? str(job.error);
3507
+ const message = str2(failure?.message) ?? str2(job.error);
3475
3508
  const done = num(progress?.processed_chunks);
3476
3509
  const totalChunks = num(progress?.total_chunks);
3477
3510
  const bits = [
3478
- str(job.status) || "?",
3511
+ str2(job.status) || "?",
3479
3512
  done !== void 0 && totalChunks !== void 0 ? `${done}/${totalChunks} chunks` : void 0,
3480
3513
  num(progress?.nodes_created) !== void 0 ? `${num(progress?.nodes_created)} nodes` : void 0,
3481
3514
  num(job.queue_position) !== void 0 ? `queue #${num(job.queue_position)}` : void 0,
3482
3515
  message ? `error: ${clip(message, 160, state)}` : void 0,
3483
- str(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
3516
+ str2(failure?.code) ? `code: ${clip(failure?.code, 80, state)}` : void 0,
3484
3517
  num(failure?.status) !== void 0 ? `status: ${num(failure?.status)}` : void 0,
3485
3518
  typeof failure?.retriable === "boolean" ? `retriable: ${failure.retriable}` : void 0
3486
3519
  ].filter(Boolean);
3487
- return `- [${str(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
3520
+ return `- [${str2(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
3488
3521
  });
3489
3522
  return assemble(
3490
3523
  `# Ingestion jobs (${shown.length}${shown.length < sorted.length ? ` of ${sorted.length}` : ""})`,
@@ -3500,13 +3533,13 @@ function renderSignals(response) {
3500
3533
  for (const [key, value] of Object.entries(payload)) {
3501
3534
  if (!Array.isArray(value) || value.length === 0) continue;
3502
3535
  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);
3536
+ const fromLabel = str2(item.from_label);
3537
+ const toLabel = str2(item.to_label);
3505
3538
  const anyLabelKey = Object.keys(item).find(
3506
- (k) => k.endsWith("_label") && str(item[k])
3539
+ (k) => k.endsWith("_label") && str2(item[k])
3507
3540
  );
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);
3541
+ 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)";
3542
+ const uid = str2(item.uid) ?? str2(item.target_uid);
3510
3543
  const numbers = Object.entries(item).filter(([, v]) => typeof v === "number").slice(0, 4).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
3511
3544
  const lists = Object.entries(item).filter(([, v]) => Array.isArray(v) && v.length > 0).slice(0, 2).map(([k, v]) => `${k}: ${v.length}`);
3512
3545
  const meta = [...numbers, ...lists].join(", ");
@@ -4025,19 +4058,19 @@ async function handleHostedTool(name, args, client2, identity) {
4025
4058
  try {
4026
4059
  switch (name) {
4027
4060
  case "search": {
4028
- const query = str2(args.query).trim();
4061
+ const query = str3(args.query).trim();
4029
4062
  if (!query) return err3("query is required");
4030
4063
  const nodes = await contextNodes(client2, query, SEARCH_LIMIT);
4031
4064
  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)
4065
+ id: str3(node.uid),
4066
+ title: str3(node.label),
4067
+ url: `${EXPLORER_URL}${encodeURIComponent(str3(node.uid))}`,
4068
+ snippet: str3(node.summary)
4036
4069
  }));
4037
4070
  return twin({ results });
4038
4071
  }
4039
4072
  case "fetch": {
4040
- const id = str2(args.id).trim();
4073
+ const id = str3(args.id).trim();
4041
4074
  if (!id) return err3("id is required");
4042
4075
  let node;
4043
4076
  try {
@@ -4088,7 +4121,7 @@ async function handleHostedTool(name, args, client2, identity) {
4088
4121
  return toolError(e);
4089
4122
  }
4090
4123
  }
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;
4124
+ 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
4125
  var init_hosted_tools = __esm({
4093
4126
  "src/hosted-tools.ts"() {
4094
4127
  "use strict";
@@ -4179,7 +4212,7 @@ var init_hosted_tools = __esm({
4179
4212
  }
4180
4213
  ];
4181
4214
  HOSTED_TOOL_NAMES = new Set(HOSTED_TOOLS.map((tool) => tool.name));
4182
- str2 = (value) => typeof value === "string" ? value : "";
4215
+ str3 = (value) => typeof value === "string" ? value : "";
4183
4216
  }
4184
4217
  });
4185
4218
 
@@ -4414,7 +4447,7 @@ function memoryStatus(config = {}) {
4414
4447
  ]
4415
4448
  };
4416
4449
  }
4417
- function nodeType(node) {
4450
+ function nodeType2(node) {
4418
4451
  const raw = node.node_type;
4419
4452
  if (typeof raw === "string") return raw;
4420
4453
  return text(obj2(raw)?.Custom) ?? "Node";
@@ -4427,7 +4460,7 @@ function compactItem(raw, fallbackType) {
4427
4460
  if (!uid) return void 0;
4428
4461
  return defined({
4429
4462
  uid,
4430
- type: fallbackType ?? nodeType(value),
4463
+ type: fallbackType ?? nodeType2(value),
4431
4464
  label: text(value.label) ?? text(value.document_title),
4432
4465
  truth_status: text(value.truth_status) ?? text(obj2(value.props)?.truth_status),
4433
4466
  superseded: value.superseded === true ? true : void 0,
@@ -4988,7 +5021,7 @@ function safePath(root, candidate) {
4988
5021
  if (relative2.startsWith("..") || import_node_path5.default.isAbsolute(relative2)) {
4989
5022
  throw new Error(`path escapes repository root: ${candidate}`);
4990
5023
  }
4991
- if (import_node_fs5.default.existsSync(absolute)) {
5024
+ if (import_node_fs6.default.existsSync(absolute)) {
4992
5025
  if (!pathContains(realPathOf(root), realPathOf(absolute))) {
4993
5026
  throw new Error(`path escapes repository root via symlink: ${candidate}`);
4994
5027
  }
@@ -4997,23 +5030,23 @@ function safePath(root, candidate) {
4997
5030
  }
4998
5031
  function realPathOf(target) {
4999
5032
  try {
5000
- return import_node_fs5.default.realpathSync(target);
5033
+ return import_node_fs6.default.realpathSync(target);
5001
5034
  } catch {
5002
5035
  return import_node_path5.default.resolve(target);
5003
5036
  }
5004
5037
  }
5005
5038
  function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
5006
- if (!import_node_fs5.default.existsSync(target)) return [];
5039
+ if (!import_node_fs6.default.existsSync(target)) return [];
5007
5040
  const real = realPathOf(target);
5008
5041
  if (!pathContains(realRoot, real)) return [];
5009
- const stat = import_node_fs5.default.statSync(target);
5042
+ const stat = import_node_fs6.default.statSync(target);
5010
5043
  if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
5011
5044
  if (!stat.isDirectory() || visited.has(real)) return [];
5012
5045
  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));
5046
+ return import_node_fs6.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path5.default.join(target, entry.name), realRoot, visited));
5014
5047
  }
5015
5048
  function fileDescriptor(root, absolute) {
5016
- const content = import_node_fs5.default.readFileSync(absolute, "utf8");
5049
+ const content = import_node_fs6.default.readFileSync(absolute, "utf8");
5017
5050
  return {
5018
5051
  logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
5019
5052
  content_hash: import_node_crypto7.default.createHash("sha256").update(content).digest("hex"),
@@ -5085,7 +5118,7 @@ async function handleSyncTool(client2, args) {
5085
5118
  }
5086
5119
  if (action === "status" && request.logical_path && request.file_present !== false) {
5087
5120
  const absolute = safePath(root, String(request.logical_path));
5088
- request.file_present = import_node_fs5.default.existsSync(absolute);
5121
+ request.file_present = import_node_fs6.default.existsSync(absolute);
5089
5122
  if (request.file_present && !request.content_hash) {
5090
5123
  request.content_hash = fileDescriptor(root, absolute).content_hash;
5091
5124
  }
@@ -5129,12 +5162,12 @@ async function scanRepository(remote, repository, requested, provider, agentId)
5129
5162
  files: results
5130
5163
  };
5131
5164
  }
5132
- var import_node_crypto7, import_node_fs5, import_node_path5, ok3, err4, SYNC_TOOL;
5165
+ var import_node_crypto7, import_node_fs6, import_node_path5, ok3, err4, SYNC_TOOL;
5133
5166
  var init_sync_tool = __esm({
5134
5167
  "src/sync-tool.ts"() {
5135
5168
  "use strict";
5136
5169
  import_node_crypto7 = __toESM(require("crypto"));
5137
- import_node_fs5 = __toESM(require("fs"));
5170
+ import_node_fs6 = __toESM(require("fs"));
5138
5171
  import_node_path5 = __toESM(require("path"));
5139
5172
  init_codegraph();
5140
5173
  init_error_detail();
@@ -7804,7 +7837,7 @@ async function main() {
7804
7837
  const transport = new import_stdio.StdioServerTransport();
7805
7838
  await server.connect(transport);
7806
7839
  console.error(
7807
- `MindGraph MCP server v${"0.22.0"} running on stdio (${BASE_URL})`
7840
+ `MindGraph MCP server v${"0.23.0"} running on stdio (${BASE_URL})`
7808
7841
  );
7809
7842
  }
7810
7843
  var import_stdio, import_node_path6, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, GOVERNANCE_CONFIG, MEMORY_RUNTIME_CONFIG, server;
@@ -7916,7 +7949,7 @@ Capture knowledge when the user shares something worth remembering:
7916
7949
  }
7917
7950
  RUNNER_REFRESH = (() => {
7918
7951
  try {
7919
- return refreshHookRunner("0.22.0", import_node_path6.default.join(__dirname, "cli.js"));
7952
+ return refreshHookRunner("0.23.0", import_node_path6.default.join(__dirname, "cli.js"));
7920
7953
  } catch {
7921
7954
  return void 0;
7922
7955
  }
@@ -7930,12 +7963,12 @@ Capture knowledge when the user shares something worth remembering:
7930
7963
  }
7931
7964
  })();
7932
7965
  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(", ")}` : "")
7966
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.23.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
7934
7967
  );
7935
7968
  }
7936
7969
  HOOK_SKEW_NOTE = (() => {
7937
7970
  try {
7938
- return versionSkewNote("0.22.0");
7971
+ return versionSkewNote("0.23.0");
7939
7972
  } catch {
7940
7973
  return void 0;
7941
7974
  }
@@ -7956,7 +7989,7 @@ ${HOOK_SKEW_NOTE}`;
7956
7989
  }
7957
7990
  };
7958
7991
  MEMORY_RUNTIME_CONFIG = {
7959
- serverVersion: "0.22.0",
7992
+ serverVersion: "0.23.0",
7960
7993
  baseUrl: BASE_URL,
7961
7994
  profile: PROFILE,
7962
7995
  harness: HARNESS,
@@ -7965,7 +7998,7 @@ ${HOOK_SKEW_NOTE}`;
7965
7998
  };
7966
7999
  server = buildMindGraphServer({
7967
8000
  client,
7968
- serverVersion: "0.22.0",
8001
+ serverVersion: "0.23.0",
7969
8002
  instructions: INSTRUCTIONS,
7970
8003
  profile: PROFILE,
7971
8004
  agentId: AGENT_ID,
@@ -7980,7 +8013,7 @@ ${HOOK_SKEW_NOTE}`;
7980
8013
  });
7981
8014
 
7982
8015
  // src/cli.ts
7983
- var fs6 = __toESM(require("fs"));
8016
+ var fs7 = __toESM(require("fs"));
7984
8017
  var path7 = __toESM(require("path"));
7985
8018
  var os4 = __toESM(require("os"));
7986
8019
  var crypto5 = __toESM(require("crypto"));
@@ -8001,6 +8034,9 @@ function parseArgs(argv) {
8001
8034
  let agentId = process.env.MINDGRAPH_AGENT_ID;
8002
8035
  const positionals = [];
8003
8036
  let dir;
8037
+ let promptRecall2;
8038
+ let capture;
8039
+ let sessionId;
8004
8040
  for (let i = 0; i < args.length; i++) {
8005
8041
  const arg = args[i];
8006
8042
  switch (arg) {
@@ -8033,6 +8069,21 @@ function parseArgs(argv) {
8033
8069
  case "--hooks":
8034
8070
  hooks = true;
8035
8071
  break;
8072
+ case "--prompt-recall":
8073
+ promptRecall2 = true;
8074
+ break;
8075
+ case "--no-prompt-recall":
8076
+ promptRecall2 = false;
8077
+ break;
8078
+ case "--capture":
8079
+ capture = true;
8080
+ break;
8081
+ case "--no-capture":
8082
+ capture = false;
8083
+ break;
8084
+ case "--session-id":
8085
+ sessionId = args[++i];
8086
+ break;
8036
8087
  case "--owner":
8037
8088
  ++i;
8038
8089
  break;
@@ -8061,7 +8112,10 @@ function parseArgs(argv) {
8061
8112
  hooks,
8062
8113
  agentId,
8063
8114
  positionals,
8064
- dir
8115
+ dir,
8116
+ promptRecall: promptRecall2,
8117
+ capture,
8118
+ sessionId
8065
8119
  };
8066
8120
  }
8067
8121
  function mcpAddFailureOutput(e) {
@@ -8445,7 +8499,7 @@ async function skillsPush(api, skillPath) {
8445
8499
 
8446
8500
  // src/hook-core.ts
8447
8501
  var import_node_crypto4 = __toESM(require("crypto"));
8448
- var import_node_fs3 = __toESM(require("fs"));
8502
+ var import_node_fs4 = __toESM(require("fs"));
8449
8503
  var import_node_os3 = __toESM(require("os"));
8450
8504
  var import_node_path3 = __toESM(require("path"));
8451
8505
  var import_node_child_process2 = require("child_process");
@@ -8453,6 +8507,175 @@ init_codegraph();
8453
8507
  init_code_tool();
8454
8508
  init_error_detail();
8455
8509
  init_hook_env();
8510
+
8511
+ // src/hook-memory.ts
8512
+ var import_node_fs3 = __toESM(require("fs"));
8513
+ var PROMPT_RECALL_LIMIT = 5;
8514
+ var PROMPT_RECALL_MAX_CHARS = 4e3;
8515
+ var MIN_PROMPT_RECALL_CHARS = 12;
8516
+ var CAPTURE_MAX_CHARS = 2e5;
8517
+ var STOPWORDS = new Set(
8518
+ "a an and are as at be but by can could did do does for from had has have how i if in into is it its me my of on or our please should so that the their them then there these they this to us was we were what when where which who why will with would you your about just also any some tell know want need let lets like get got make".split(" ")
8519
+ );
8520
+ var WORD = /[A-Za-z0-9][A-Za-z0-9_.\-/#+]*/g;
8521
+ var SECRET_PATTERNS = [
8522
+ [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, "[REDACTED]"],
8523
+ [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[REDACTED]"],
8524
+ [/\b(?:mg|sk|pk|rk)[_-](?:live[_-]|test[_-]|ant[_-]|proj[_-])?[A-Za-z0-9_-]{16,}/g, "[REDACTED]"],
8525
+ [/\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,})/g, "[REDACTED]"],
8526
+ [/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED]"],
8527
+ [/\bxox[abprs]-[A-Za-z0-9-]{10,}/g, "[REDACTED]"],
8528
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/gi, "Bearer [REDACTED]"],
8529
+ [
8530
+ /\b((?:api[_-]?key|secret|token|password|passwd|access[_-]?key|client[_-]?secret)\b["']?\s*[:=]\s*)["']?[^\s"',;]{8,}["']?/gi,
8531
+ "$1[REDACTED]"
8532
+ ]
8533
+ ];
8534
+ function redactSecrets(text2) {
8535
+ let out = text2;
8536
+ for (const [pattern, replacement] of SECRET_PATTERNS) {
8537
+ out = out.replace(pattern, replacement);
8538
+ }
8539
+ return out;
8540
+ }
8541
+ function promptRecallQuery(prompt, maxTerms = 8) {
8542
+ if (typeof prompt !== "string") return "";
8543
+ const trimmed = prompt.trim();
8544
+ if (trimmed.length < MIN_PROMPT_RECALL_CHARS || trimmed.startsWith("/")) return "";
8545
+ const seen = /* @__PURE__ */ new Set();
8546
+ for (const match of redactSecrets(trimmed.slice(0, 600)).matchAll(WORD)) {
8547
+ const key = match[0].toLowerCase().replace(/^[.\-/]+|[.\-/]+$/g, "");
8548
+ if (key.length < 2 || STOPWORDS.has(key) || key === "redacted" || seen.has(key)) continue;
8549
+ seen.add(key);
8550
+ if (seen.size >= maxTerms) break;
8551
+ }
8552
+ return [...seen].join(" ");
8553
+ }
8554
+ function promptRecallRequest(query) {
8555
+ return {
8556
+ query,
8557
+ node_limit: PROMPT_RECALL_LIMIT,
8558
+ chunk_limit: 0,
8559
+ graph_expansion_limit: 3,
8560
+ graph_max_depth: 2
8561
+ };
8562
+ }
8563
+ var rec = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
8564
+ var str = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
8565
+ var oneLine = (value, max) => {
8566
+ const flat = value.replace(/\s+/g, " ").trim();
8567
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
8568
+ };
8569
+ var INFRA_TYPES = /* @__PURE__ */ new Set(["Chunk", "Document", "Article", "Session"]);
8570
+ function nodeType(node) {
8571
+ const plain = str(node.node_type);
8572
+ return (plain && plain !== "Custom" ? plain : void 0) ?? str(rec(node.node_type)?.Custom) ?? plain ?? "Node";
8573
+ }
8574
+ function renderPromptRecall(response) {
8575
+ const payload = rec(response);
8576
+ if (!payload) return void 0;
8577
+ const lines = [];
8578
+ const nodes = Array.isArray(rec(payload.graph)?.nodes) ? rec(payload.graph).nodes : [];
8579
+ for (const raw of nodes) {
8580
+ const node = rec(raw);
8581
+ if (!node || lines.length >= PROMPT_RECALL_LIMIT) continue;
8582
+ const type = nodeType(node);
8583
+ if (INFRA_TYPES.has(type)) continue;
8584
+ const props = rec(node.props);
8585
+ const label = str(node.label) ?? str(props?.label) ?? str(props?.name);
8586
+ if (!label) continue;
8587
+ const summary = str(node.summary) ?? str(props?.summary) ?? str(props?.description) ?? str(props?.content);
8588
+ const docs = Array.isArray(node.source_documents) ? node.source_documents : [];
8589
+ const source = str(rec(docs[0])?.title);
8590
+ let line = `- [${type}] ${oneLine(label, 140)}`;
8591
+ if (node.superseded === true) line += " (superseded)";
8592
+ if (summary && summary !== label) line += ` \u2014 ${oneLine(summary, 240)}`;
8593
+ if (source) line += ` (source: ${oneLine(source, 100)})`;
8594
+ lines.push(line);
8595
+ }
8596
+ const articles = Array.isArray(payload.articles) ? payload.articles : [];
8597
+ for (const raw of articles) {
8598
+ const article = rec(raw);
8599
+ if (!article || lines.length >= PROMPT_RECALL_LIMIT) continue;
8600
+ const label = str(article.label);
8601
+ if (!label) continue;
8602
+ const content = str(article.content);
8603
+ lines.push(`- [Article] ${oneLine(label, 140)}${content ? ` \u2014 ${oneLine(content, 240)}` : ""}`);
8604
+ }
8605
+ if (lines.length === 0) return void 0;
8606
+ let block = "MindGraph recall (bounded):";
8607
+ for (const line of lines) {
8608
+ if (block.length + 1 + line.length > PROMPT_RECALL_MAX_CHARS) break;
8609
+ block += `
8610
+ ${line}`;
8611
+ }
8612
+ return block;
8613
+ }
8614
+ function messageText(content) {
8615
+ if (typeof content === "string") return content;
8616
+ if (!Array.isArray(content)) return void 0;
8617
+ const parts = content.map((block) => rec(block)).filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text);
8618
+ return parts.length > 0 ? parts.join("\n") : void 0;
8619
+ }
8620
+ function stripHarnessNoise(text2) {
8621
+ 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();
8622
+ }
8623
+ function extractTranscriptTurns(jsonl) {
8624
+ const turns = [];
8625
+ for (const line of jsonl.split("\n")) {
8626
+ if (!line.trim()) continue;
8627
+ let entry;
8628
+ try {
8629
+ entry = rec(JSON.parse(line));
8630
+ } catch {
8631
+ continue;
8632
+ }
8633
+ if (!entry || entry.type !== "user" && entry.type !== "assistant") continue;
8634
+ if (entry.isMeta === true || entry.isSidechain === true || entry.isCompactSummary === true) continue;
8635
+ const message = rec(entry.message);
8636
+ const raw = messageText(message?.content);
8637
+ if (!raw) continue;
8638
+ const text2 = stripHarnessNoise(raw);
8639
+ if (!text2) continue;
8640
+ turns.push({ role: entry.type, text: text2 });
8641
+ }
8642
+ return turns;
8643
+ }
8644
+ function renderTranscript(turns) {
8645
+ const rendered2 = turns.map((turn) => `${turn.role === "user" ? "User" : "Assistant"}: ${redactSecrets(turn.text)}`).join("\n\n");
8646
+ if (rendered2.length <= CAPTURE_MAX_CHARS) return rendered2;
8647
+ const tail = rendered2.slice(rendered2.length - CAPTURE_MAX_CHARS);
8648
+ const start = tail.search(/\n\n(User|Assistant): /);
8649
+ return start >= 0 ? tail.slice(start + 2) : tail;
8650
+ }
8651
+ function readTranscriptSince(file, offset = 0) {
8652
+ const buffer = import_node_fs3.default.readFileSync(file);
8653
+ const start = offset > buffer.length ? 0 : offset;
8654
+ const lastNewline = buffer.lastIndexOf(10);
8655
+ if (lastNewline < start) return { text: "", nextOffset: start };
8656
+ return {
8657
+ text: buffer.subarray(start, lastNewline + 1).toString("utf8"),
8658
+ nextOffset: lastNewline + 1
8659
+ };
8660
+ }
8661
+ function buildCaptureRequest(transcriptPath, sessionId, offset, ids) {
8662
+ const { text: text2, nextOffset } = readTranscriptSince(transcriptPath, offset);
8663
+ const turns = extractTranscriptTurns(text2);
8664
+ const content = renderTranscript(turns);
8665
+ if (!content) return { nextOffset, turns: 0 };
8666
+ return {
8667
+ body: {
8668
+ content,
8669
+ title: `Claude Code session ${sessionId}`,
8670
+ ...ids.sessionUid ? { session_uid: ids.sessionUid } : {},
8671
+ ...ids.agentId ? { agent_id: ids.agentId } : {}
8672
+ },
8673
+ nextOffset,
8674
+ turns: turns.length
8675
+ };
8676
+ }
8677
+
8678
+ // src/hook-core.ts
8456
8679
  var NOOP = { kind: "noop" };
8457
8680
  function defaultLedger(sessionId, options) {
8458
8681
  return {
@@ -8482,21 +8705,21 @@ function waitBriefly(ms) {
8482
8705
  }
8483
8706
  function withLedger(sessionId, options, update) {
8484
8707
  const file = ledgerPath(sessionId, options);
8485
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true, mode: 448 });
8708
+ import_node_fs4.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true, mode: 448 });
8486
8709
  try {
8487
- import_node_fs3.default.chmodSync(import_node_path3.default.dirname(file), 448);
8710
+ import_node_fs4.default.chmodSync(import_node_path3.default.dirname(file), 448);
8488
8711
  } catch {
8489
8712
  }
8490
8713
  const lock = `${file}.lock`;
8491
8714
  let lockFd;
8492
8715
  for (let attempt = 0; attempt < 400; attempt++) {
8493
8716
  try {
8494
- lockFd = import_node_fs3.default.openSync(lock, "wx", 384);
8717
+ lockFd = import_node_fs4.default.openSync(lock, "wx", 384);
8495
8718
  break;
8496
8719
  } catch (error2) {
8497
8720
  if (error2.code !== "EEXIST") throw error2;
8498
8721
  try {
8499
- if (Date.now() - import_node_fs3.default.statSync(lock).mtimeMs > 3e3) import_node_fs3.default.unlinkSync(lock);
8722
+ if (Date.now() - import_node_fs4.default.statSync(lock).mtimeMs > 3e3) import_node_fs4.default.unlinkSync(lock);
8500
8723
  } catch {
8501
8724
  }
8502
8725
  waitBriefly(10);
@@ -8510,25 +8733,25 @@ function withLedger(sessionId, options, update) {
8510
8733
  try {
8511
8734
  ledger = {
8512
8735
  ...ledger,
8513
- ...JSON.parse(import_node_fs3.default.readFileSync(file, "utf8"))
8736
+ ...JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"))
8514
8737
  };
8515
8738
  } catch {
8516
8739
  }
8517
8740
  const result = update(ledger);
8518
8741
  ledger.updatedAt = Date.now();
8519
8742
  const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
8520
- import_node_fs3.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
8743
+ import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
8521
8744
  `, { mode: 384 });
8522
- import_node_fs3.default.renameSync(temporary, file);
8745
+ import_node_fs4.default.renameSync(temporary, file);
8523
8746
  try {
8524
- import_node_fs3.default.chmodSync(file, 384);
8747
+ import_node_fs4.default.chmodSync(file, 384);
8525
8748
  } catch {
8526
8749
  }
8527
8750
  return result;
8528
8751
  } finally {
8529
- import_node_fs3.default.closeSync(lockFd);
8752
+ import_node_fs4.default.closeSync(lockFd);
8530
8753
  try {
8531
- import_node_fs3.default.unlinkSync(lock);
8754
+ import_node_fs4.default.unlinkSync(lock);
8532
8755
  } catch {
8533
8756
  }
8534
8757
  }
@@ -8807,8 +9030,8 @@ function claudeMcpRegistrationNote(input, options) {
8807
9030
  if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
8808
9031
  try {
8809
9032
  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"));
9033
+ if (!import_node_fs4.default.existsSync(configPath)) return void 0;
9034
+ const config = JSON.parse(import_node_fs4.default.readFileSync(configPath, "utf8"));
8812
9035
  if (hasMindgraphServer(config.mcpServers)) return void 0;
8813
9036
  const cwd = import_node_path3.default.resolve(input.cwd || ".");
8814
9037
  for (const [projectDir, project] of Object.entries(config.projects || {})) {
@@ -8817,8 +9040,8 @@ function claudeMcpRegistrationNote(input, options) {
8817
9040
  }
8818
9041
  for (let dir = cwd; ; dir = import_node_path3.default.dirname(dir)) {
8819
9042
  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"));
9043
+ if (import_node_fs4.default.existsSync(candidate)) {
9044
+ const parsed = JSON.parse(import_node_fs4.default.readFileSync(candidate, "utf8"));
8822
9045
  if (hasMindgraphServer(parsed.mcpServers)) return void 0;
8823
9046
  }
8824
9047
  if (dir === import_node_path3.default.dirname(dir)) break;
@@ -9170,6 +9393,9 @@ function stop(input, options) {
9170
9393
  }
9171
9394
  async function sessionEnd(input, client2, options) {
9172
9395
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
9396
+ if (options.harness === "claude-code") {
9397
+ await captureTranscript(input, client2, options);
9398
+ }
9173
9399
  const ledger = readLedger(input.session_id, options);
9174
9400
  if (ledger.executionUid && ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0) {
9175
9401
  try {
@@ -9203,6 +9429,77 @@ async function sessionEnd(input, client2, options) {
9203
9429
  }
9204
9430
  return NOOP;
9205
9431
  }
9432
+ function withDeadline(promise, ms) {
9433
+ let timer;
9434
+ const deadline = new Promise((_, reject) => {
9435
+ timer = setTimeout(() => reject(new Error("deadline")), ms);
9436
+ timer.unref?.();
9437
+ });
9438
+ return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
9439
+ }
9440
+ async function promptRecall(input, client2, options) {
9441
+ if (options.promptRecall === false || process.env.MINDGRAPH_HOOK_PROMPT_RECALL === "off" || !client2.retrieveContext) {
9442
+ return NOOP;
9443
+ }
9444
+ const query = promptRecallQuery(input.prompt);
9445
+ if (!query) return NOOP;
9446
+ try {
9447
+ const response = await withDeadline(
9448
+ client2.retrieveContext(promptRecallRequest(query)),
9449
+ options.promptRecallTimeoutMs ?? 4e3
9450
+ );
9451
+ const block = renderPromptRecall(response);
9452
+ if (!block) return NOOP;
9453
+ const hash = import_node_crypto4.default.createHash("sha256").update(block).digest("hex");
9454
+ const fresh = withLedger(input.session_id, options, (ledger) => {
9455
+ if (ledger.lastRecallHash === hash) return false;
9456
+ ledger.lastRecallHash = hash;
9457
+ return true;
9458
+ });
9459
+ return fresh ? { kind: "context", context: block, event: "UserPromptSubmit" } : NOOP;
9460
+ } catch {
9461
+ return NOOP;
9462
+ }
9463
+ }
9464
+ async function captureTranscript(input, client2, options) {
9465
+ const enabled = process.env.MINDGRAPH_HOOK_CAPTURE === "on" || options.capture === true && process.env.MINDGRAPH_HOOK_CAPTURE !== "off";
9466
+ if (!enabled || !client2.ingestSession || !input.transcript_path) return;
9467
+ try {
9468
+ const agentId = resolveAgentId(options);
9469
+ const ledger = readLedger(input.session_id, options);
9470
+ let sessionUid = ledger.sessionUid;
9471
+ const prepared = buildCaptureRequest(
9472
+ input.transcript_path,
9473
+ input.session_id,
9474
+ ledger.captureOffset ?? 0,
9475
+ { sessionUid, agentId }
9476
+ );
9477
+ if (prepared.body && !sessionUid) {
9478
+ const opened = object(
9479
+ await client2.session({
9480
+ action: "open",
9481
+ label: `Claude Code ${input.session_id}`,
9482
+ harness: options.harness,
9483
+ harness_session_id: input.session_id,
9484
+ model: input.model,
9485
+ agent_id: agentId
9486
+ })
9487
+ );
9488
+ sessionUid = string(opened?.uid);
9489
+ if (sessionUid) prepared.body.session_uid = sessionUid;
9490
+ }
9491
+ if (prepared.body) await client2.ingestSession(prepared.body);
9492
+ withLedger(input.session_id, options, (current) => {
9493
+ if (sessionUid) current.sessionUid ??= sessionUid;
9494
+ current.captureOffset = Math.max(current.captureOffset ?? 0, prepared.nextOffset);
9495
+ return void 0;
9496
+ });
9497
+ } catch (error2) {
9498
+ console.error(
9499
+ `mindgraph-hook: transcript capture failed (${error2?.message || "unknown error"})`
9500
+ );
9501
+ }
9502
+ }
9206
9503
  async function runHookCore(input, client2, options) {
9207
9504
  switch (input.hook_event_name) {
9208
9505
  case "SessionStart":
@@ -9228,6 +9525,11 @@ async function runHookCore(input, client2, options) {
9228
9525
  return stop(input, options);
9229
9526
  case "SessionEnd":
9230
9527
  return sessionEnd(input, client2, options);
9528
+ case "UserPromptSubmit":
9529
+ return promptRecall(input, client2, options);
9530
+ case "PreCompact":
9531
+ await captureTranscript(input, client2, options);
9532
+ return NOOP;
9231
9533
  default:
9232
9534
  return NOOP;
9233
9535
  }
@@ -9239,6 +9541,26 @@ async function readHookInput(stream = process.stdin) {
9239
9541
  }
9240
9542
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
9241
9543
  }
9544
+ function previewCapture(transcriptPath, sessionId, options = {}) {
9545
+ let ledger = {};
9546
+ try {
9547
+ ledger = JSON.parse(
9548
+ import_node_fs4.default.readFileSync(
9549
+ ledgerPath(sessionId, { ...options, harness: "claude-code" }),
9550
+ "utf8"
9551
+ )
9552
+ );
9553
+ } catch {
9554
+ }
9555
+ const offset = ledger.captureOffset ?? 0;
9556
+ return {
9557
+ ...buildCaptureRequest(transcriptPath, sessionId, offset, {
9558
+ sessionUid: ledger.sessionUid,
9559
+ agentId: options.agentId || process.env.MINDGRAPH_AGENT_ID || stableAgentId()
9560
+ }),
9561
+ offset
9562
+ };
9563
+ }
9242
9564
 
9243
9565
  // src/claude-hooks.ts
9244
9566
  async function runClaudeHook(input, client2, options = {}) {
@@ -9253,7 +9575,7 @@ async function runClaudeHook(input, client2, options = {}) {
9253
9575
  case "context":
9254
9576
  return {
9255
9577
  hookSpecificOutput: {
9256
- hookEventName: "SessionStart",
9578
+ hookEventName: output.event ?? "SessionStart",
9257
9579
  additionalContext: output.context
9258
9580
  }
9259
9581
  };
@@ -9283,7 +9605,7 @@ async function runCodexHook(input, client2, options = {}) {
9283
9605
  case "context":
9284
9606
  return {
9285
9607
  hookSpecificOutput: {
9286
- hookEventName: "SessionStart",
9608
+ hookEventName: output.event ?? "SessionStart",
9287
9609
  additionalContext: output.context
9288
9610
  }
9289
9611
  };
@@ -9307,14 +9629,14 @@ init_hook_installer();
9307
9629
  function recordHookHealth(harness, error2) {
9308
9630
  try {
9309
9631
  const dir = process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime");
9310
- fs6.mkdirSync(dir, { recursive: true, mode: 448 });
9632
+ fs7.mkdirSync(dir, { recursive: true, mode: 448 });
9311
9633
  const file = path7.join(dir, "hook-health.json");
9312
9634
  let failures = 0;
9313
9635
  try {
9314
- failures = Number(JSON.parse(fs6.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9636
+ failures = Number(JSON.parse(fs7.readFileSync(file, "utf8")).consecutiveFailures) || 0;
9315
9637
  } catch {
9316
9638
  }
9317
- fs6.writeFileSync(
9639
+ fs7.writeFileSync(
9318
9640
  file,
9319
9641
  `${JSON.stringify({
9320
9642
  harness,
@@ -9334,10 +9656,10 @@ function clearHookHealth(harness) {
9334
9656
  process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
9335
9657
  "hook-health.json"
9336
9658
  );
9337
- if (!fs6.existsSync(file)) return;
9338
- const current = JSON.parse(fs6.readFileSync(file, "utf8"));
9659
+ if (!fs7.existsSync(file)) return;
9660
+ const current = JSON.parse(fs7.readFileSync(file, "utf8"));
9339
9661
  if (!Number(current.consecutiveFailures)) return;
9340
- fs6.writeFileSync(
9662
+ fs7.writeFileSync(
9341
9663
  file,
9342
9664
  `${JSON.stringify({
9343
9665
  harness,
@@ -9378,16 +9700,16 @@ function getClaudeDesktopConfigPath() {
9378
9700
  }
9379
9701
  }
9380
9702
  function readJsonFile(filePath) {
9381
- if (!fs6.existsSync(filePath)) return {};
9382
- const content = fs6.readFileSync(filePath, "utf-8");
9703
+ if (!fs7.existsSync(filePath)) return {};
9704
+ const content = fs7.readFileSync(filePath, "utf-8");
9383
9705
  return JSON.parse(content);
9384
9706
  }
9385
9707
  function writeJsonFile(filePath, data) {
9386
9708
  const dir = path7.dirname(filePath);
9387
- if (!fs6.existsSync(dir)) {
9388
- fs6.mkdirSync(dir, { recursive: true });
9709
+ if (!fs7.existsSync(dir)) {
9710
+ fs7.mkdirSync(dir, { recursive: true });
9389
9711
  }
9390
- fs6.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9712
+ fs7.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
9391
9713
  }
9392
9714
  function getMcpServerEntry(apiKey, baseUrl) {
9393
9715
  const env = { MINDGRAPH_API_KEY: apiKey };
@@ -9474,7 +9796,7 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
9474
9796
  }
9475
9797
  function uninstallClaudeDesktop() {
9476
9798
  const configPath = getClaudeDesktopConfigPath();
9477
- if (!fs6.existsSync(configPath)) {
9799
+ if (!fs7.existsSync(configPath)) {
9478
9800
  console.log("Claude Desktop config not found. Nothing to remove.");
9479
9801
  return;
9480
9802
  }
@@ -9501,7 +9823,7 @@ function uninstallClaudeCode() {
9501
9823
  }
9502
9824
  function printStatus() {
9503
9825
  const desktopPath = getClaudeDesktopConfigPath();
9504
- if (fs6.existsSync(desktopPath)) {
9826
+ if (fs7.existsSync(desktopPath)) {
9505
9827
  const config = readJsonFile(desktopPath);
9506
9828
  const servers = config.mcpServers;
9507
9829
  if (servers && "mindgraph" in servers) {
@@ -9523,8 +9845,8 @@ function printStatus() {
9523
9845
  process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
9524
9846
  "hook-health.json"
9525
9847
  );
9526
- if (fs6.existsSync(healthFile)) {
9527
- const health = JSON.parse(fs6.readFileSync(healthFile, "utf8"));
9848
+ if (fs7.existsSync(healthFile)) {
9849
+ const health = JSON.parse(fs7.readFileSync(healthFile, "utf8"));
9528
9850
  const failures = Number(health.consecutiveFailures) || 0;
9529
9851
  console.log(
9530
9852
  failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
@@ -9539,17 +9861,46 @@ function printStatus() {
9539
9861
  function withHooksFlag() {
9540
9862
  return process.argv.includes("--hooks");
9541
9863
  }
9864
+ function claudeHookFeatures() {
9865
+ const flags = parseArgs(process.argv);
9866
+ const stored = loadHookEnv();
9867
+ return {
9868
+ promptRecall: flags.promptRecall ?? stored.promptRecall ?? true,
9869
+ capture: flags.capture ?? stored.capture ?? false
9870
+ };
9871
+ }
9872
+ function printClaudeHookFeatures(features) {
9873
+ console.log(
9874
+ `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)"}`
9875
+ );
9876
+ }
9877
+ function latestTranscript(cwd) {
9878
+ const dir = path7.join(
9879
+ os4.homedir(),
9880
+ ".claude",
9881
+ "projects",
9882
+ cwd.replace(/[^A-Za-z0-9]/g, "-")
9883
+ );
9884
+ try {
9885
+ return fs7.readdirSync(dir).filter((name) => name.endsWith(".jsonl")).map((name) => path7.join(dir, name)).sort((a, b) => fs7.statSync(b).mtimeMs - fs7.statSync(a).mtimeMs)[0];
9886
+ } catch {
9887
+ return void 0;
9888
+ }
9889
+ }
9542
9890
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
9543
9891
  if (withHooks) {
9544
- const runner = installHookRunner(__filename, void 0, "0.22.0");
9892
+ const runner = installHookRunner(__filename, void 0, "0.23.0");
9545
9893
  console.log(`Installed pinned hook runner at ${runner}`);
9546
- const result = installClaudeHooks("user");
9894
+ const features = claudeHookFeatures();
9895
+ const result = installClaudeHooks("user", void 0, features);
9547
9896
  console.log(
9548
9897
  `Installed ${result.added} and refreshed ${result.updated} MindGraph Claude Code hook entries in ${result.path}`
9549
9898
  );
9899
+ printClaudeHookFeatures(features);
9550
9900
  const envPath = saveHookEnv({
9551
9901
  apiKey,
9552
9902
  baseUrl,
9903
+ ...features,
9553
9904
  // Default the stable per-user identity only when nothing is stored —
9554
9905
  // an explicit or previously persisted agentId must survive reinstalls.
9555
9906
  agentId: agentId || process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId(),
@@ -9588,6 +9939,9 @@ USAGE:
9588
9939
  mindgraph-mcp uninstall-code Remove from Claude Code
9589
9940
  mindgraph-mcp install-hooks --harness claude-code|codex [--scope user|project]
9590
9941
  mindgraph-mcp uninstall-hooks --harness claude-code|codex [--scope user|project]
9942
+ mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]
9943
+ Print exactly what transcript capture would
9944
+ send (POST /ingest/session); sends nothing.
9591
9945
  mindgraph-mcp status Show installation status
9592
9946
  mindgraph-mcp skills pull [--dir .claude/skills]
9593
9947
  Write your org's published+granted skills as
@@ -9610,6 +9964,10 @@ OPTIONS:
9610
9964
  --scope <scope> Hook settings scope: project (default) or user
9611
9965
  --project-dir <dir> Project root for project-scoped hooks
9612
9966
  --harness <name> Hook harness (claude-code or codex)
9967
+ --no-prompt-recall Claude hooks: omit per-prompt recall (UserPromptSubmit)
9968
+ --capture Claude hooks: send user/assistant text to MindGraph
9969
+ before compaction and at session end (off by default;
9970
+ --no-capture turns it back off)
9613
9971
  --help, -h Show this help message
9614
9972
 
9615
9973
  EXAMPLES:
@@ -9856,16 +10214,19 @@ ${total} skill(s) synced.`);
9856
10214
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
9857
10215
  throw new Error("--harness must be claude-code or codex");
9858
10216
  }
9859
- const runner = installHookRunner(__filename, void 0, "0.22.0");
10217
+ const runner = installHookRunner(__filename, void 0, "0.23.0");
9860
10218
  console.log(`Installed pinned hook runner at ${runner}`);
9861
- const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir);
10219
+ const features = hookHarness === "claude-code" ? claudeHookFeatures() : void 0;
10220
+ const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir, features);
9862
10221
  console.log(
9863
10222
  `Installed ${result.added} and refreshed ${result.updated} MindGraph ${hookHarness === "codex" ? "Codex" : "Claude Code"} hook entries in ${result.path}`
9864
10223
  );
10224
+ if (features) printClaudeHookFeatures(features);
9865
10225
  printCodegraphStatus();
9866
10226
  const envPath = saveHookEnv({
9867
10227
  apiKey,
9868
10228
  baseUrl,
10229
+ ...features || {},
9869
10230
  // Default the stable per-user identity only when nothing is stored —
9870
10231
  // an explicit or previously persisted agentId must survive
9871
10232
  // reinstalls.
@@ -9917,9 +10278,14 @@ ${total} skill(s) synced.`);
9917
10278
  // DEFAULT org while the MCP tools write to the pinned one.
9918
10279
  orgId: process.env.MINDGRAPH_ORG_ID || stored.orgId,
9919
10280
  maxRetries: 0,
9920
- telemetrySurface: "mcp"
10281
+ telemetrySurface: "mcp",
10282
+ // Prompt recall must never hold the prompt: abort the request at
10283
+ // the same 4s deadline the core races it against.
10284
+ ...input.hook_event_name === "UserPromptSubmit" ? { timeoutMs: 4e3 } : {}
9921
10285
  });
9922
10286
  const hookOptions = {
10287
+ promptRecall: stored.promptRecall,
10288
+ capture: stored.capture,
9923
10289
  // No harness-name fallback here: hook-core derives a stable
9924
10290
  // per-user default shared by both harnesses, which is what lets a
9925
10291
  // task claimed under Codex rebind under Claude Code.
@@ -9934,9 +10300,11 @@ ${total} skill(s) synced.`);
9934
10300
  client2,
9935
10301
  hookOptions
9936
10302
  );
9937
- process.stdout.write(`${JSON.stringify(output)}
9938
- `);
9939
10303
  clearHookHealth(hookHarness);
10304
+ process.stdout.write(`${JSON.stringify(output)}
10305
+ `, () => {
10306
+ if (input.hook_event_name === "UserPromptSubmit") process.exit(0);
10307
+ });
9940
10308
  } catch (error2) {
9941
10309
  recordHookHealth(
9942
10310
  hookHarness,
@@ -9949,6 +10317,35 @@ ${total} skill(s) synced.`);
9949
10317
  }
9950
10318
  break;
9951
10319
  }
10320
+ case "hooks": {
10321
+ if (positionals[0] !== "preview") {
10322
+ console.error("Usage: mindgraph-mcp hooks preview [transcript_path] [--session-id <id>]");
10323
+ process.exit(1);
10324
+ }
10325
+ const transcript = positionals[1] || latestTranscript(process.cwd());
10326
+ if (!transcript || !fs7.existsSync(transcript)) {
10327
+ console.error(
10328
+ "No transcript found. Pass a Claude Code transcript path (~/.claude/projects/<dir>/<session>.jsonl)."
10329
+ );
10330
+ process.exit(1);
10331
+ }
10332
+ const sessionId = parseArgs(process.argv).sessionId || path7.basename(transcript, ".jsonl");
10333
+ const preview = previewCapture(transcript, sessionId, {
10334
+ agentId: process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId
10335
+ });
10336
+ const enabled = loadHookEnv().capture === true;
10337
+ console.error(
10338
+ `Transcript: ${transcript}
10339
+ Session: ${sessionId}
10340
+ Capture is ${enabled ? "ON" : "OFF (nothing is sent; enable with install-hooks --capture)"}
10341
+ Would send ${preview.turns} turn(s) from byte ${preview.offset} as POST /ingest/session (nothing sent by this preview):`
10342
+ );
10343
+ process.stdout.write(
10344
+ `${JSON.stringify(preview.body ?? null, null, 2)}
10345
+ `
10346
+ );
10347
+ break;
10348
+ }
9952
10349
  case "status":
9953
10350
  printStatus();
9954
10351
  break;