mindgraph-mcp 0.17.0 → 0.18.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.
Files changed (4) hide show
  1. package/README.md +53 -0
  2. package/dist/cli.js +573 -257
  3. package/dist/index.js +418 -46
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -94,6 +94,24 @@ var import_promises = require("fs/promises");
94
94
  var import_node_os2 = require("os");
95
95
  var import_node_path2 = require("path");
96
96
  var import_node_crypto2 = require("crypto");
97
+ var UnknownRepositoryError = class extends Error {
98
+ constructor(repo) {
99
+ super(
100
+ `unknown repository "${repo}": not declared in .mindgraph/workspace.json, not the current repository, and no such path exists`
101
+ );
102
+ this.repo = repo;
103
+ this.name = "UnknownRepositoryError";
104
+ }
105
+ repo;
106
+ };
107
+ function invocationCwd(args) {
108
+ const context = args.invocation_context;
109
+ if (context && typeof context === "object" && !Array.isArray(context)) {
110
+ const cwd = context.cwd;
111
+ if (typeof cwd === "string" && cwd) return cwd;
112
+ }
113
+ return process.cwd();
114
+ }
97
115
  function unavailabilityCaveats(reason, message) {
98
116
  const caveats = [message || "codegraph status failed"];
99
117
  if (reason === "absent") {
@@ -106,14 +124,14 @@ function unavailabilityCaveats(reason, message) {
106
124
  var DEFAULT_TIMEOUT_MS = 5e3;
107
125
  var DEFAULT_STATUS_TTL_MS = 6e4;
108
126
  var MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
109
- function normalizePath(path3) {
110
- return path3.replaceAll("\\", "/").replace(/^\.\//, "");
127
+ function normalizePath(path5) {
128
+ return path5.replaceAll("\\", "/").replace(/^\.\//, "");
111
129
  }
112
130
  function normalizeSignature(signature) {
113
131
  return signature.replace(/\s+/g, " ").trim();
114
132
  }
115
- function languageFromPath(path3) {
116
- const extension = (0, import_node_path2.extname)(path3).toLowerCase();
133
+ function languageFromPath(path5) {
134
+ const extension = (0, import_node_path2.extname)(path5).toLowerCase();
117
135
  return {
118
136
  ".c": "c",
119
137
  ".cc": "cpp",
@@ -281,7 +299,26 @@ var CodegraphAdapter = class {
281
299
  selected.spaceUid
282
300
  ) : selected;
283
301
  }
284
- const requestedRoot = repo || this.env.MINDGRAPH_REPO_ROOT || this.cwd;
302
+ if (repo) {
303
+ const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, repo)).catch(
304
+ () => (0, import_node_path2.resolve)(this.cwd, repo)
305
+ );
306
+ if (!(0, import_node_fs2.existsSync)(requestedPath)) {
307
+ const current = await this.currentRepository(fallbackSpaceUid);
308
+ if (current.repoId === repo) return current;
309
+ throw new UnknownRepositoryError(repo);
310
+ }
311
+ const repoRoot = await this.gitRoot(requestedPath) ?? requestedPath;
312
+ return this.resolveRepositoryIdentity(
313
+ repoRoot,
314
+ this.env.MINDGRAPH_REPO_ID,
315
+ this.env.MINDGRAPH_CODE_SPACE_UID || fallbackSpaceUid
316
+ );
317
+ }
318
+ return this.currentRepository(fallbackSpaceUid);
319
+ }
320
+ async currentRepository(fallbackSpaceUid) {
321
+ const requestedRoot = this.env.MINDGRAPH_REPO_ROOT || this.cwd;
285
322
  const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, requestedRoot)).catch(
286
323
  () => (0, import_node_path2.resolve)(this.cwd, requestedRoot)
287
324
  );
@@ -371,6 +408,14 @@ var CodegraphAdapter = class {
371
408
  ).catch(() => "");
372
409
  return raw.trim() ? (0, import_node_path2.resolve)(start, raw.trim()) : void 0;
373
410
  }
411
+ /** Why the workspace map yielded no repositories — for callers that were
412
+ * explicitly asked to fan out and would otherwise return silently empty. */
413
+ workspaceDiagnostic() {
414
+ if (!this.workspaceFile || !(0, import_node_fs2.existsSync)(this.workspaceFile)) {
415
+ return "no .mindgraph/workspace.json was found between the working directory and the filesystem root";
416
+ }
417
+ return `${this.workspaceFile} is malformed or declares no repositories`;
418
+ }
374
419
  async availability(repository) {
375
420
  const cached = this.statusCache.get(repository.repoRoot);
376
421
  if (cached && cached.expiresAt > this.now()) return cached.availability;
@@ -790,7 +835,7 @@ var err = (code, message, details) => ({
790
835
  });
791
836
  var CODE_TOOL = {
792
837
  name: "mindgraph_code",
793
- description: "Resolve typed code references through the local codegraph index and join them to persistent MindGraph memory/work. anchor creates exact repository/code anchors; recall reads attached graph context; expand reads callers/callees; affected reads impact for a code ref or Task/Execution. Ambiguous refs return candidates without writing.",
838
+ description: "Resolve typed code references through the local codegraph index and join them to persistent MindGraph memory/work. anchor creates exact repository/code anchors; recall reads attached graph context; expand reads callers/callees; affected reads impact for a code ref or Task/Execution. Ambiguous refs return candidates without writing. Refs with a path route to the repository defining that file (nearest declared root in .mindgraph/workspace.json); bare symbols resolve against the current repository only \u2014 pass path or repo to target a sibling.",
794
839
  inputSchema: {
795
840
  type: "object",
796
841
  properties: {
@@ -832,7 +877,7 @@ var CODE_TOOL = {
832
877
  },
833
878
  repo_space_uid: {
834
879
  type: "string",
835
- description: "Writable shared Project/repository Space for new anchors"
880
+ description: "Writable shared Project/repository Space for new anchors (anchor requires this, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID)"
836
881
  },
837
882
  task_uid: { type: "string" },
838
883
  execution_uid: { type: "string" },
@@ -848,13 +893,13 @@ var CODE_TOOL = {
848
893
  openWorldHint: true
849
894
  }
850
895
  };
851
- async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
896
+ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cwd: invocationCwd(args) })) {
852
897
  const codeClient = client2;
853
898
  const action = args.action;
854
899
  const refs = Array.isArray(args.code_refs) ? args.code_refs : [];
855
900
  const anchorUids = Array.isArray(args.anchor_uids) ? args.anchor_uids : [];
856
901
  const agentId = typeof args.agent_id === "string" ? args.agent_id : "mcp";
857
- const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : `space:agent:${agentId}`;
902
+ const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : void 0;
858
903
  const options = {
859
904
  repo: typeof args.repo === "string" ? args.repo : void 0,
860
905
  fallbackSpaceUid
@@ -978,6 +1023,7 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
978
1023
  const targetUid = typeof args.task_uid === "string" ? args.task_uid : typeof args.execution_uid === "string" ? args.execution_uid : void 0;
979
1024
  let graphContext;
980
1025
  const graphRefs = [];
1026
+ let unavailableRepositories = [];
981
1027
  if (targetUid) {
982
1028
  graphContext = await codeClient.traverse({
983
1029
  action: "neighborhood",
@@ -985,9 +1031,13 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
985
1031
  depth: 2,
986
1032
  max_nodes: 50
987
1033
  });
988
- graphRefs.push(
989
- ...await codeRefsFromGraphContext(codeClient, adapter, graphContext)
1034
+ const fromGraph = await codeRefsFromGraphContext(
1035
+ codeClient,
1036
+ adapter,
1037
+ graphContext
990
1038
  );
1039
+ graphRefs.push(...fromGraph.refs);
1040
+ unavailableRepositories = fromGraph.unavailable;
991
1041
  }
992
1042
  if (refs.length > 0 || anchorUids.length > 0) {
993
1043
  const target = await resolveSingleTarget(
@@ -1006,7 +1056,8 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1006
1056
  target_uid: targetUid,
1007
1057
  graph_context: graphContext,
1008
1058
  affected: [],
1009
- degradation: "no materialized code anchors were attached"
1059
+ degradation: unavailableRepositories.length > 0 ? `attached code anchors reference repositories unavailable here: ${unavailableRepositories.join(", ")}` : "no materialized code anchors were attached",
1060
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1010
1061
  });
1011
1062
  }
1012
1063
  const impacts = await Promise.all(
@@ -1023,13 +1074,17 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1023
1074
  target_uid: targetUid,
1024
1075
  graph_context: graphContext,
1025
1076
  impacts,
1026
- materialized: joined
1077
+ materialized: joined,
1078
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1027
1079
  });
1028
1080
  }
1029
1081
  default:
1030
1082
  return err("unknown_action", `unknown mindgraph_code action: ${action}`);
1031
1083
  }
1032
1084
  } catch (cause) {
1085
+ if (cause instanceof UnknownRepositoryError) {
1086
+ return err("unknown_repository", cause.message);
1087
+ }
1033
1088
  return err("code_tool_failed", errorDetail(cause));
1034
1089
  }
1035
1090
  }
@@ -1055,7 +1110,10 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1055
1110
  code_refs: args.code_refs,
1056
1111
  repo: args.repo,
1057
1112
  repo_space_uid: args.repo_space_uid,
1058
- agent_id: args.agent_id
1113
+ agent_id: args.agent_id,
1114
+ // Keeps the default adapter anchored to the live session directory
1115
+ // rather than wherever the MCP server process was launched.
1116
+ invocation_context: args.invocation_context
1059
1117
  },
1060
1118
  adapter
1061
1119
  );
@@ -1065,17 +1123,33 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1065
1123
  const anchorUids = (anchors.anchors ?? []).flatMap(
1066
1124
  (anchor) => anchor.uid ? [anchor.uid] : []
1067
1125
  );
1126
+ const relateFailures = [];
1068
1127
  for (const targetUid of targetUids) {
1069
1128
  for (const anchorUid of anchorUids) {
1070
- await codeClient.entity({
1071
- action: "relate",
1072
- source_uid: targetUid,
1073
- target_uid: anchorUid,
1074
- edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
1075
- agent_id: args.agent_id
1076
- });
1129
+ try {
1130
+ await codeClient.entity({
1131
+ action: "relate",
1132
+ source_uid: targetUid,
1133
+ target_uid: anchorUid,
1134
+ edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
1135
+ agent_id: args.agent_id
1136
+ });
1137
+ } catch (cause) {
1138
+ relateFailures.push(
1139
+ `${targetUid} -> ${anchorUid}: ${errorDetail(cause)}`
1140
+ );
1141
+ }
1077
1142
  }
1078
1143
  }
1144
+ if (relateFailures.length > 0) {
1145
+ return ok({
1146
+ result: original,
1147
+ code_anchors: {
1148
+ ...anchors,
1149
+ caveat: `anchors written, but ${relateFailures.length} relate edge(s) failed: ${relateFailures.join("; ")}`
1150
+ }
1151
+ });
1152
+ }
1079
1153
  }
1080
1154
  return ok({ result: original, code_anchors: anchors });
1081
1155
  }
@@ -1148,16 +1222,34 @@ async function codeRefFromAnchor(client2, adapter, uid) {
1148
1222
  if (!attributes || typeof attributes !== "object") return void 0;
1149
1223
  const codeRef = attributes.code_ref;
1150
1224
  if (!isStoredCodeRef(codeRef)) return void 0;
1151
- const repository = await adapter.resolveRepository(codeRef.repoId);
1152
- return { ...codeRef, repoRoot: repository.repoRoot };
1225
+ try {
1226
+ const repository = await adapter.resolveRepository(codeRef.repoId);
1227
+ return { ref: { ...codeRef, repoRoot: repository.repoRoot } };
1228
+ } catch (cause) {
1229
+ if (cause instanceof UnknownRepositoryError) {
1230
+ return { missingRepo: codeRef.repoId };
1231
+ }
1232
+ throw cause;
1233
+ }
1153
1234
  }
1154
1235
  async function resolveSingleTarget(client2, adapter, refs, anchorUids, options) {
1155
1236
  if (refs.length > 1 || anchorUids.length > 1) {
1156
1237
  return { error: err("ambiguous_target", "expand/affected accepts exactly one code target") };
1157
1238
  }
1158
1239
  if (anchorUids.length === 1) {
1159
- const ref = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
1160
- return ref ? { ref } : { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
1240
+ const outcome = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
1241
+ if (!outcome) {
1242
+ return { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
1243
+ }
1244
+ if ("missingRepo" in outcome) {
1245
+ return {
1246
+ error: err(
1247
+ "repository_unavailable",
1248
+ `repository ${outcome.missingRepo} is not checked out here or declared in .mindgraph/workspace.json; expand/affected need its local index`
1249
+ )
1250
+ };
1251
+ }
1252
+ return { ref: outcome.ref };
1161
1253
  }
1162
1254
  if (refs.length !== 1) {
1163
1255
  return { error: err("missing_code_ref", "one code_ref or anchor_uid is required") };
@@ -1196,7 +1288,7 @@ async function joinMaterialized(client2, refs, agentId, limit) {
1196
1288
  async function codeRefsFromGraphContext(client2, adapter, context) {
1197
1289
  const uids = /* @__PURE__ */ new Set();
1198
1290
  collectUids(context, uids);
1199
- if (uids.size === 0) return [];
1291
+ if (uids.size === 0) return { refs: [], unavailable: [] };
1200
1292
  const nodes = client2.getNodesBatch ? await client2.getNodesBatch([...uids]) : await Promise.all([...uids].map((uid) => client2.getNode(uid).catch(() => void 0)));
1201
1293
  const stored = nodes.flatMap((node) => {
1202
1294
  if (!node) return [];
@@ -1205,12 +1297,21 @@ async function codeRefsFromGraphContext(client2, adapter, context) {
1205
1297
  const codeRef = attributes.code_ref;
1206
1298
  return isStoredCodeRef(codeRef) ? [codeRef] : [];
1207
1299
  });
1208
- return Promise.all(
1209
- stored.map(async (ref) => {
1300
+ const refs = [];
1301
+ const unavailable = /* @__PURE__ */ new Set();
1302
+ for (const ref of stored) {
1303
+ try {
1210
1304
  const repository = await adapter.resolveRepository(ref.repoId);
1211
- return { ...ref, repoRoot: repository.repoRoot };
1212
- })
1213
- );
1305
+ refs.push({ ...ref, repoRoot: repository.repoRoot });
1306
+ } catch (cause) {
1307
+ if (cause instanceof UnknownRepositoryError) {
1308
+ unavailable.add(ref.repoId);
1309
+ } else {
1310
+ throw cause;
1311
+ }
1312
+ }
1313
+ }
1314
+ return { refs, unavailable: [...unavailable].sort() };
1214
1315
  }
1215
1316
  function collectUids(value, uids) {
1216
1317
  if (Array.isArray(value)) {
@@ -1352,14 +1453,29 @@ function safePath(root, candidate) {
1352
1453
  if (relative2.startsWith("..") || import_node_path3.default.isAbsolute(relative2)) {
1353
1454
  throw new Error(`path escapes repository root: ${candidate}`);
1354
1455
  }
1456
+ if (import_node_fs3.default.existsSync(absolute)) {
1457
+ if (!pathContains(realPathOf(root), realPathOf(absolute))) {
1458
+ throw new Error(`path escapes repository root via symlink: ${candidate}`);
1459
+ }
1460
+ }
1355
1461
  return absolute;
1356
1462
  }
1357
- function markdownFiles(target) {
1463
+ function realPathOf(target) {
1464
+ try {
1465
+ return import_node_fs3.default.realpathSync(target);
1466
+ } catch {
1467
+ return import_node_path3.default.resolve(target);
1468
+ }
1469
+ }
1470
+ function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
1358
1471
  if (!import_node_fs3.default.existsSync(target)) return [];
1472
+ const real = realPathOf(target);
1473
+ if (!pathContains(realRoot, real)) return [];
1359
1474
  const stat = import_node_fs3.default.statSync(target);
1360
1475
  if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
1361
- if (!stat.isDirectory()) return [];
1362
- return import_node_fs3.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path3.default.join(target, entry.name)));
1476
+ if (!stat.isDirectory() || visited.has(real)) return [];
1477
+ visited.add(real);
1478
+ return import_node_fs3.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path3.default.join(target, entry.name), realRoot, visited));
1363
1479
  }
1364
1480
  function fileDescriptor(root, absolute) {
1365
1481
  const content = import_node_fs3.default.readFileSync(absolute, "utf8");
@@ -1374,9 +1490,7 @@ async function handleSyncTool(client2, args) {
1374
1490
  try {
1375
1491
  const action = String(args.action || "");
1376
1492
  const context = args.invocation_context || {};
1377
- const adapter = new CodegraphAdapter({
1378
- cwd: context.cwd || process.cwd()
1379
- });
1493
+ const adapter = new CodegraphAdapter({ cwd: invocationCwd(args) });
1380
1494
  const requestedRepo = typeof args.repo === "string" ? args.repo : typeof args.repo_id === "string" ? args.repo_id : context.repoId;
1381
1495
  const repository = await adapter.resolveRepository(requestedRepo);
1382
1496
  const root = repository.repoRoot;
@@ -1385,6 +1499,12 @@ async function handleSyncTool(client2, args) {
1385
1499
  const remote = client2;
1386
1500
  if (action === "scan") {
1387
1501
  const repositories = args.workspace === true ? await adapter.resolveRepositories() : [repository];
1502
+ if (args.workspace === true && repositories.length === 0) {
1503
+ return err2(
1504
+ "workspace_not_configured",
1505
+ `workspace scan found no declared repositories: ${adapter.workspaceDiagnostic()}`
1506
+ );
1507
+ }
1388
1508
  const requested = Array.isArray(args.paths) && args.paths.length > 0 ? args.paths.map(String) : [".claude/agent-memory", "memory"];
1389
1509
  const scans = [];
1390
1510
  for (const selected of repositories) {
@@ -1441,10 +1561,11 @@ async function handleSyncTool(client2, args) {
1441
1561
  }
1442
1562
  }
1443
1563
  async function scanRepository(remote, repository, requested, provider, agentId) {
1564
+ const realRoot = realPathOf(repository.repoRoot);
1444
1565
  const files = [
1445
1566
  ...new Set(
1446
1567
  requested.flatMap(
1447
- (candidate) => markdownFiles(safePath(repository.repoRoot, candidate))
1568
+ (candidate) => markdownFiles(safePath(repository.repoRoot, candidate), realRoot)
1448
1569
  )
1449
1570
  )
1450
1571
  ].sort();
@@ -1801,15 +1922,15 @@ function renderTraversal(response) {
1801
1922
  const labelByUid = /* @__PURE__ */ new Map();
1802
1923
  for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
1803
1924
  if (paths.length > 0) {
1804
- const items = paths.map((raw) => obj(raw)).filter((path3) => Boolean(path3)).map((path3, index) => {
1805
- const uids = Array.isArray(path3.node_uids) ? path3.node_uids.map(String) : [];
1806
- const labels = Array.isArray(path3.labels) ? path3.labels.map(String) : Array.isArray(path3.nodes) ? path3.nodes.map((hop) => {
1925
+ const items = paths.map((raw) => obj(raw)).filter((path5) => Boolean(path5)).map((path5, index) => {
1926
+ const uids = Array.isArray(path5.node_uids) ? path5.node_uids.map(String) : [];
1927
+ const labels = Array.isArray(path5.labels) ? path5.labels.map(String) : Array.isArray(path5.nodes) ? path5.nodes.map((hop) => {
1807
1928
  const hopNode = obj(hop);
1808
1929
  return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
1809
1930
  }) : [];
1810
1931
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
1811
- const cost = num(path3.cost) ?? num(path3.path_cost);
1812
- const confidence = num(path3.path_confidence);
1932
+ const cost = num(path5.cost) ?? num(path5.path_cost);
1933
+ const confidence = num(path5.path_confidence);
1813
1934
  const meta = [
1814
1935
  cost !== void 0 ? `cost ${cost}` : void 0,
1815
1936
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -4440,7 +4561,7 @@ async function checkMcpGovernance(toolName, args, config) {
4440
4561
  if (!response) {
4441
4562
  return {
4442
4563
  allowed: false,
4443
- message: `MindGraph governance could not establish permission after ${retryDelays.length + 1} attempts (${lastFailure}). Set MINDGRAPH_GOVERNANCE=off to run this adapter ungoverned.`
4564
+ message: `MindGraph at ${config.baseUrl} is unreachable (${lastFailure}; ${retryDelays.length + 1} attempts). The tool call was NOT evaluated against policy and NOT executed \u2014 this is a connectivity failure, not a governance denial. Check the network and MINDGRAPH_BASE_URL, then retry.`
4444
4565
  };
4445
4566
  }
4446
4567
  if (!response.ok) {
@@ -4606,6 +4727,225 @@ async function readGovernedResource(client2, uri, governanceConfig) {
4606
4727
  return readResource(client2, uri);
4607
4728
  }
4608
4729
 
4730
+ // src/index.ts
4731
+ var import_node_path5 = __toESM(require("path"));
4732
+
4733
+ // src/hook-installer.ts
4734
+ var import_node_fs4 = __toESM(require("fs"));
4735
+ var import_node_os3 = __toESM(require("os"));
4736
+ var import_node_path4 = __toESM(require("path"));
4737
+ var import_node_crypto4 = __toESM(require("crypto"));
4738
+ var OWNER_MARKER = "--owner mindgraph";
4739
+ var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
4740
+ function command(harness) {
4741
+ return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
4742
+ }
4743
+ function windowsCommand(harness) {
4744
+ return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
4745
+ }
4746
+ var RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
4747
+ function installHookRunner(sourceFile, homeDir, version) {
4748
+ const target = import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_RELATIVE);
4749
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
4750
+ const temporary = `${target}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
4751
+ import_node_fs4.default.copyFileSync(sourceFile, temporary);
4752
+ import_node_fs4.default.renameSync(temporary, target);
4753
+ const versionPath = import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_VERSION_RELATIVE);
4754
+ if (version) {
4755
+ import_node_fs4.default.writeFileSync(versionPath, `${version}
4756
+ `);
4757
+ } else {
4758
+ import_node_fs4.default.rmSync(versionPath, { force: true });
4759
+ }
4760
+ return target;
4761
+ }
4762
+ function installedHookRunnerVersion(homeDir) {
4763
+ try {
4764
+ const raw = import_node_fs4.default.readFileSync(
4765
+ import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_VERSION_RELATIVE),
4766
+ "utf8"
4767
+ ).trim();
4768
+ return raw || void 0;
4769
+ } catch {
4770
+ return void 0;
4771
+ }
4772
+ }
4773
+ function parseSemver(value) {
4774
+ if (!value) return void 0;
4775
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
4776
+ if (!match) return void 0;
4777
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
4778
+ }
4779
+ function semverNewer(a, b) {
4780
+ for (let index = 0; index < 3; index += 1) {
4781
+ if (a[index] !== b[index]) return a[index] > b[index];
4782
+ }
4783
+ return false;
4784
+ }
4785
+ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
4786
+ try {
4787
+ if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
4788
+ return { refreshed: false, reason: "disabled" };
4789
+ }
4790
+ const home = homeDir || import_node_os3.default.homedir();
4791
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
4792
+ return { refreshed: false, reason: "no_runner" };
4793
+ }
4794
+ if (!import_node_fs4.default.existsSync(sourceBundle)) {
4795
+ return { refreshed: false, reason: "no_source" };
4796
+ }
4797
+ const server2 = parseSemver(serverVersion);
4798
+ if (!server2) return { refreshed: false, reason: "unparseable" };
4799
+ const runnerVersion = installedHookRunnerVersion(home);
4800
+ if (runnerVersion !== void 0) {
4801
+ const pinned = parseSemver(runnerVersion);
4802
+ if (!pinned) {
4803
+ return { refreshed: false, reason: "unparseable", runnerVersion };
4804
+ }
4805
+ if (!semverNewer(server2, pinned)) {
4806
+ return {
4807
+ refreshed: false,
4808
+ reason: runnerVersion === serverVersion ? "current" : "downgrade",
4809
+ runnerVersion
4810
+ };
4811
+ }
4812
+ }
4813
+ installHookRunner(sourceBundle, home, serverVersion);
4814
+ return { refreshed: true, reason: "updated", runnerVersion };
4815
+ } catch {
4816
+ return { refreshed: false, reason: "error" };
4817
+ }
4818
+ }
4819
+ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
4820
+ const refreshed = [];
4821
+ const scopes = ["user", "project"];
4822
+ for (const scope of scopes) {
4823
+ try {
4824
+ const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
4825
+ if (!import_node_fs4.default.existsSync(file)) continue;
4826
+ if (!import_node_fs4.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
4827
+ installHooks("claude-code", scope, projectDir, void 0, homeDir);
4828
+ refreshed.push(file);
4829
+ } catch {
4830
+ }
4831
+ }
4832
+ return refreshed;
4833
+ }
4834
+ function versionSkewNote(serverVersion, homeDir) {
4835
+ const runnerVersion = installedHookRunnerVersion(homeDir);
4836
+ if (!runnerVersion || runnerVersion === serverVersion) return void 0;
4837
+ 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).`;
4838
+ }
4839
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os3.default.homedir(), ".codex"), homeDir) {
4840
+ if (harness === "codex") {
4841
+ return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
4842
+ }
4843
+ return scope === "user" ? import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), ".claude", "settings.json") : import_node_path4.default.join(projectDir, ".claude", "settings.json");
4844
+ }
4845
+ function readSettings(file) {
4846
+ if (!import_node_fs4.default.existsSync(file)) return {};
4847
+ return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
4848
+ }
4849
+ function ownedHook(harness, timeout, additionalContextLimit) {
4850
+ return {
4851
+ type: "command",
4852
+ command: command(harness),
4853
+ ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
4854
+ timeout,
4855
+ statusMessage: "Syncing MindGraph session context",
4856
+ ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
4857
+ };
4858
+ }
4859
+ function desiredEntries(harness) {
4860
+ const common = {
4861
+ SessionStart: [
4862
+ {
4863
+ matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
4864
+ // B7 live acceptance caught cold SessionStart calls crossing the
4865
+ // inherited 20s edge in both harnesses: the hooks correctly failed
4866
+ // open, but no brief reached the model. The same installed runner
4867
+ // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
4868
+ // The declared Codex context limit must not undercut the brief's own
4869
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
4870
+ // truncation the renderer knew nothing about.
4871
+ hooks: [
4872
+ ownedHook(
4873
+ harness,
4874
+ 30,
4875
+ harness === "codex" ? 1e4 : void 0
4876
+ )
4877
+ ]
4878
+ }
4879
+ ],
4880
+ PreToolUse: [
4881
+ {
4882
+ matcher: "mcp__mindgraph__.*",
4883
+ // Repository resolution over a large multi-repo workspace plus three
4884
+ // git probes can exceed 3s on the first (uncached) call; a killed
4885
+ // PreToolUse means the call goes through untagged.
4886
+ hooks: [ownedHook(harness, 5)]
4887
+ }
4888
+ ],
4889
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
4890
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
4891
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
4892
+ Stop: [{ hooks: [ownedHook(harness, 5)] }],
4893
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
4894
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
4895
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
4896
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
4897
+ // map), so the codex adapter instead makes at most ONE call there.
4898
+ SessionEnd: [
4899
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
4900
+ ]
4901
+ };
4902
+ return harness === "claude-code" ? {
4903
+ ...common,
4904
+ TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
4905
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
4906
+ } : common;
4907
+ }
4908
+ function hookCommand(value) {
4909
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
4910
+ const command2 = value.command;
4911
+ return typeof command2 === "string" ? command2 : void 0;
4912
+ }
4913
+ function entryHasOwnedHook(value) {
4914
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4915
+ const hooks = value.hooks;
4916
+ return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
4917
+ }
4918
+ function writeSettings(file, settings) {
4919
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
4920
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
4921
+ import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
4922
+ `, {
4923
+ mode: 384
4924
+ });
4925
+ import_node_fs4.default.renameSync(temporary, file);
4926
+ }
4927
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
4928
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
4929
+ const settings = readSettings(file);
4930
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
4931
+ let added = 0;
4932
+ let updated = 0;
4933
+ for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
4934
+ const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
4935
+ const kept = current.filter((entry) => !entryHasOwnedHook(entry));
4936
+ const hadOwned = kept.length !== current.length;
4937
+ for (const entry of wanted) {
4938
+ kept.push(entry);
4939
+ if (hadOwned) updated += 1;
4940
+ else added += 1;
4941
+ }
4942
+ hooks[event] = kept;
4943
+ }
4944
+ settings.hooks = hooks;
4945
+ writeSettings(file, settings);
4946
+ return { path: file, added, updated };
4947
+ }
4948
+
4609
4949
  // src/index.ts
4610
4950
  var API_KEY = process.env.MINDGRAPH_API_KEY;
4611
4951
  var BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -4728,10 +5068,42 @@ if (PROFILE === "coding") {
4728
5068
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
4729
5069
  ).join("\n");
4730
5070
  }
5071
+ var RUNNER_REFRESH = (() => {
5072
+ try {
5073
+ return refreshHookRunner("0.18.0", import_node_path5.default.join(__dirname, "cli.js"));
5074
+ } catch {
5075
+ return void 0;
5076
+ }
5077
+ })();
5078
+ if (RUNNER_REFRESH?.refreshed) {
5079
+ const refreshedSettings = (() => {
5080
+ try {
5081
+ return refreshOwnedClaudeHooks();
5082
+ } catch {
5083
+ return [];
5084
+ }
5085
+ })();
5086
+ console.error(
5087
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.18.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
5088
+ );
5089
+ }
5090
+ var HOOK_SKEW_NOTE = (() => {
5091
+ try {
5092
+ return versionSkewNote("0.18.0");
5093
+ } catch {
5094
+ return void 0;
5095
+ }
5096
+ })();
5097
+ if (HOOK_SKEW_NOTE) {
5098
+ INSTRUCTIONS += `
5099
+
5100
+ ${HOOK_SKEW_NOTE}`;
5101
+ console.error(`[mindgraph-mcp] ${HOOK_SKEW_NOTE}`);
5102
+ }
4731
5103
  var server = new import_server.Server(
4732
5104
  {
4733
5105
  name: "mindgraph",
4734
- version: "0.17.0"
5106
+ version: "0.18.0"
4735
5107
  },
4736
5108
  {
4737
5109
  capabilities: {
@@ -4945,7 +5317,7 @@ async function main() {
4945
5317
  const transport = new import_stdio.StdioServerTransport();
4946
5318
  await server.connect(transport);
4947
5319
  console.error(
4948
- `MindGraph MCP server v${"0.17.0"} running on stdio (${BASE_URL})`
5320
+ `MindGraph MCP server v${"0.18.0"} running on stdio (${BASE_URL})`
4949
5321
  );
4950
5322
  }
4951
5323
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindgraph-mcp",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "MCP server for MindGraph — connect Claude Desktop, Claude Code, and any MCP client to your knowledge graph",
5
5
  "repository": {
6
6
  "type": "git",