mindgraph-mcp 0.16.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 +57 -4
  2. package/dist/cli.js +879 -335
  3. package/dist/index.js +718 -119
  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;
@@ -777,20 +822,20 @@ function codeRefIdentityKey(ref) {
777
822
 
778
823
  // src/code-tool.ts
779
824
  var ok = (data) => ({
780
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
825
+ content: [{ type: "text", text: JSON.stringify(data) }]
781
826
  });
782
827
  var err = (code, message, details) => ({
783
828
  content: [
784
829
  {
785
830
  type: "text",
786
- text: JSON.stringify({ error: message, code, details }, null, 2)
831
+ text: JSON.stringify({ error: message, code, details })
787
832
  }
788
833
  ],
789
834
  isError: true
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)) {
@@ -1272,7 +1373,7 @@ var import_node_crypto3 = __toESM(require("crypto"));
1272
1373
  var import_node_fs3 = __toESM(require("fs"));
1273
1374
  var import_node_path3 = __toESM(require("path"));
1274
1375
  var ok2 = (data) => ({
1275
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
1376
+ content: [{ type: "text", text: JSON.stringify(data) }]
1276
1377
  });
1277
1378
  var err2 = (code, message) => ({
1278
1379
  content: [{ type: "text", text: JSON.stringify({ code, error: message }) }],
@@ -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();
@@ -1502,12 +1623,23 @@ function clip(value, max, state) {
1502
1623
  }
1503
1624
  function isNodeShaped(value) {
1504
1625
  const node = obj(value);
1505
- return Boolean(node && str(node.uid) && (str(node.label) || str(node.node_type)));
1626
+ return Boolean(
1627
+ node && str(node.uid) && (str(node.label) || str(node.node_type) || obj(node.node_type))
1628
+ );
1629
+ }
1630
+ function nodeDisplayType(node) {
1631
+ const props = obj(node.props);
1632
+ const plain = str(node.node_type);
1633
+ 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";
1634
+ }
1635
+ function nodeFields(node) {
1636
+ const props = obj(node.props);
1637
+ return obj(props?.fields) ?? obj(obj(props?.data)?.fields);
1506
1638
  }
1507
1639
  function unwrapSearchResult(value) {
1508
1640
  if (isNodeShaped(value)) return value;
1509
1641
  const wrapper = obj(value);
1510
- const inner = obj(wrapper?.node);
1642
+ const inner = obj(wrapper?.node) ?? obj(wrapper?.object);
1511
1643
  if (inner && isNodeShaped(inner)) {
1512
1644
  const score = num(wrapper.score);
1513
1645
  return score !== void 0 ? { ...inner, retrieval_score: score } : inner;
@@ -1545,7 +1677,7 @@ function sourceTag(node) {
1545
1677
  return `from: ${title}${extra}`;
1546
1678
  }
1547
1679
  function layer7Fields(node, state) {
1548
- const fields = obj(obj(node.props)?.fields);
1680
+ const fields = nodeFields(node);
1549
1681
  if (!fields) return void 0;
1550
1682
  const pairs = Object.entries(fields).filter(([, value]) => value !== null && value !== void 0 && value !== "").map(([key, value]) => `${key}: ${Array.isArray(value) ? value.join(", ") : String(value)}`);
1551
1683
  if (pairs.length === 0) return void 0;
@@ -1557,7 +1689,7 @@ function nodeSummaryText(node, state) {
1557
1689
  }
1558
1690
  function nodeLines(node, state) {
1559
1691
  const label = clip(node.label, LABEL_MAX, state) || "(unlabeled)";
1560
- const type = str(node.node_type) || "Node";
1692
+ const type = nodeDisplayType(node);
1561
1693
  const confidence = num(node.confidence);
1562
1694
  const score = num(node.retrieval_score);
1563
1695
  const parenthetical = [
@@ -1576,19 +1708,19 @@ function nodeLines(node, state) {
1576
1708
  if (source) lines.push(` - ${source}`);
1577
1709
  const believers = Array.isArray(node.believed_by) ? node.believed_by : [];
1578
1710
  if (believers.length > 0) {
1579
- const rendered = believers.slice(0, 3).map((raw) => {
1711
+ const rendered2 = believers.slice(0, 3).map((raw) => {
1580
1712
  const believer = obj(raw);
1581
1713
  const conf = num(believer?.confidence);
1582
1714
  return `${str(believer?.agent_label) || "?"}${conf !== void 0 ? ` (${conf})` : ""}`;
1583
1715
  }).join(", ");
1584
- lines.push(` - believed by: ${rendered}`);
1716
+ lines.push(` - believed by: ${rendered2}`);
1585
1717
  }
1586
1718
  return lines.join("\n");
1587
1719
  }
1588
1720
  function groupedNodeSections(nodes, state, headingPrefix = "") {
1589
1721
  const groups = /* @__PURE__ */ new Map();
1590
1722
  for (const node of nodes) {
1591
- const type = str(node.node_type) || "Node";
1723
+ const type = nodeDisplayType(node);
1592
1724
  const group = groups.get(type) ?? [];
1593
1725
  group.push(node);
1594
1726
  groups.set(type, group);
@@ -1604,7 +1736,7 @@ function groupedNodeSections(nodes, state, headingPrefix = "") {
1604
1736
  }
1605
1737
  return sections;
1606
1738
  }
1607
- function relationshipItems(edges, labelByUid, state) {
1739
+ function relationshipItems(edges, labelByUid, state, options) {
1608
1740
  const lines = [];
1609
1741
  for (const raw of edges) {
1610
1742
  const edge = obj(raw);
@@ -1614,8 +1746,8 @@ function relationshipItems(edges, labelByUid, state) {
1614
1746
  const to = str(edge.to_uid);
1615
1747
  if (CURATION_EDGE_TYPES.has(type)) continue;
1616
1748
  if (!from || !to || from === to) continue;
1617
- const fromLabel = labelByUid.get(from);
1618
- const toLabel = labelByUid.get(to);
1749
+ const fromLabel = labelByUid.get(from) ?? (options?.fallbackToUid ? `[${from}]` : void 0);
1750
+ const toLabel = labelByUid.get(to) ?? (options?.fallbackToUid ? `[${to}]` : void 0);
1619
1751
  if (!fromLabel || !toLabel) continue;
1620
1752
  lines.push(`- ${fromLabel} \u2014${type}\u2192 ${toLabel}`);
1621
1753
  }
@@ -1658,8 +1790,8 @@ function assemble(header, sections, state) {
1658
1790
  sectionUsed += marker.length + 1;
1659
1791
  }
1660
1792
  lines.push(
1661
- section.items.length === 0 ? section.heading : `${section.heading}
1662
- ${kept.join("\n")}`
1793
+ section.items.length === 0 ? section.heading : section.heading ? `${section.heading}
1794
+ ${kept.join("\n")}` : kept.join("\n")
1663
1795
  );
1664
1796
  used += headingCost + sectionUsed;
1665
1797
  }
@@ -1790,15 +1922,15 @@ function renderTraversal(response) {
1790
1922
  const labelByUid = /* @__PURE__ */ new Map();
1791
1923
  for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
1792
1924
  if (paths.length > 0) {
1793
- const items = paths.map((raw) => obj(raw)).filter((path3) => Boolean(path3)).map((path3, index) => {
1794
- const uids = Array.isArray(path3.node_uids) ? path3.node_uids.map(String) : [];
1795
- 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) => {
1796
1928
  const hopNode = obj(hop);
1797
1929
  return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
1798
1930
  }) : [];
1799
1931
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
1800
- const cost = num(path3.cost) ?? num(path3.path_cost);
1801
- const confidence = num(path3.path_confidence);
1932
+ const cost = num(path5.cost) ?? num(path5.path_cost);
1933
+ const confidence = num(path5.path_confidence);
1802
1934
  const meta = [
1803
1935
  cost !== void 0 ? `cost ${cost}` : void 0,
1804
1936
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -1822,6 +1954,157 @@ function renderTraversal(response) {
1822
1954
  }
1823
1955
  return assemble("# Graph traversal", sections, state);
1824
1956
  }
1957
+ function neighborLine(value) {
1958
+ const node = obj(value);
1959
+ if (!node) return void 0;
1960
+ const label = str(node.label) || "(unlabeled)";
1961
+ const uid = str(node.uid);
1962
+ const type = str(node.node_type);
1963
+ return `- ${label}${uid ? ` [${uid}]` : ""}${type ? ` (${type})` : ""}`;
1964
+ }
1965
+ function renderOntologyAnswer(response) {
1966
+ const payload = obj(response);
1967
+ if (!payload) return void 0;
1968
+ const objects = Array.isArray(payload.objects) ? payload.objects.filter(isNodeShaped) : isNodeShaped(payload.object) ? [payload.object] : [];
1969
+ const cognitive = obj(payload.cognitive_context);
1970
+ const provenance = Array.isArray(payload.provenance) ? payload.provenance : [];
1971
+ const hasQuerySignature = Array.isArray(payload.objects) || obj(payload.confidence) !== void 0;
1972
+ const cognitiveEmpty = !cognitive || Object.values(cognitive).every((v) => !Array.isArray(v) || v.length === 0);
1973
+ if (objects.length === 0 && cognitiveEmpty && provenance.length === 0) {
1974
+ return hasQuerySignature ? "No matching domain objects." : void 0;
1975
+ }
1976
+ const state = { bounded: false };
1977
+ const sections = [];
1978
+ if (objects.length > 0) {
1979
+ sections.push(...groupedNodeSections(objects, state));
1980
+ }
1981
+ const labelByUid = /* @__PURE__ */ new Map();
1982
+ for (const node of objects) labelByUid.set(str(node.uid), str(node.label) || "?");
1983
+ const graph = obj(payload.graph);
1984
+ for (const raw of Array.isArray(graph?.nodes) ? graph.nodes : []) {
1985
+ const node = obj(raw);
1986
+ const uid = str(node?.uid);
1987
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
1988
+ }
1989
+ if (cognitive) {
1990
+ for (const rawNodes of Object.values(cognitive)) {
1991
+ if (!Array.isArray(rawNodes)) continue;
1992
+ for (const raw of rawNodes) {
1993
+ const node = obj(raw);
1994
+ const uid = str(node?.uid);
1995
+ if (uid && !labelByUid.has(uid)) labelByUid.set(uid, str(node?.label) || "?");
1996
+ }
1997
+ }
1998
+ }
1999
+ const relations = Array.isArray(payload.relations) ? payload.relations : [];
2000
+ const relationLines = relationshipItems(relations, labelByUid, state, { fallbackToUid: true });
2001
+ if (relationLines.length > 0) {
2002
+ sections.push({ heading: "## Relations", items: relationLines });
2003
+ }
2004
+ if (cognitive) {
2005
+ for (const [category, rawNodes] of Object.entries(cognitive)) {
2006
+ if (!Array.isArray(rawNodes) || rawNodes.length === 0) continue;
2007
+ const items = rawNodes.map(neighborLine).filter((line) => Boolean(line));
2008
+ if (items.length > 0) {
2009
+ sections.push({ heading: `### Cognitive context \u2014 ${category}`, items });
2010
+ }
2011
+ }
2012
+ }
2013
+ if (provenance.length > 0) {
2014
+ const items = provenance.map((raw) => obj(raw)).filter((entry) => Boolean(entry)).map((entry) => {
2015
+ const span = clip(entry.text_span, QUOTE_MAX, state) || "";
2016
+ const by = str(entry.ingested_by_name);
2017
+ const suffix = `${by ? ` (ingested by ${by})` : ""} [source ${str(entry.source_uid) || "?"}]`;
2018
+ return str(entry.span_kind) === "anchor" ? `- "${span}"${suffix}` : `- context: ${span}${suffix}`;
2019
+ });
2020
+ sections.push({ heading: "## Sources", items });
2021
+ }
2022
+ const confidence = num(obj(payload.confidence)?.overall);
2023
+ const notes = [];
2024
+ if (confidence !== void 0) notes.push(`overall confidence ${confidence}`);
2025
+ if (payload.has_more === true) notes.push("more results exist (has_more)");
2026
+ if (payload.seed_cap_hit === true) notes.push("seed cap hit \u2014 narrow the query for a complete set");
2027
+ if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
2028
+ return assemble(
2029
+ `# Ontology results (${objects.length} object${objects.length === 1 ? "" : "s"})`,
2030
+ sections,
2031
+ state
2032
+ );
2033
+ }
2034
+ function renderObjectList(response, title) {
2035
+ const payload = obj(response);
2036
+ if (!payload) return void 0;
2037
+ const rawItems = Array.isArray(payload.items) ? payload.items : Array.isArray(payload.rows) && payload.rows.some((row) => unwrapSearchResult(row)) ? payload.rows : void 0;
2038
+ const items = (rawItems ?? []).map(unwrapSearchResult).filter((node) => Boolean(node));
2039
+ if (items.length === 0) {
2040
+ return rawItems ? `# ${title} (0 results)` : void 0;
2041
+ }
2042
+ const state = { bounded: false };
2043
+ const sections = groupedNodeSections(items, state);
2044
+ const total = num(payload.total_count);
2045
+ const truncations = Array.isArray(payload.truncation_reasons) ? payload.truncation_reasons.filter((reason) => typeof reason === "string") : [];
2046
+ const notes = [];
2047
+ if (payload.has_more === true) notes.push("more results exist");
2048
+ if (truncations.length > 0) notes.push(`truncated: ${truncations.join(", ")}`);
2049
+ if (notes.length > 0) sections.push({ heading: `Note: ${notes.join("; ")}.`, items: [] });
2050
+ const count = total !== void 0 && total !== items.length ? `${items.length} of ${total}` : `${items.length}`;
2051
+ return assemble(`# ${title} (${count})`, sections, state);
2052
+ }
2053
+ var JOBS_RENDER_MAX = 20;
2054
+ function renderJobs(response) {
2055
+ const jobs = Array.isArray(response) ? response : obj(response)?.jobs;
2056
+ if (!Array.isArray(jobs)) return void 0;
2057
+ if (jobs.length === 0) return "No ingestion jobs.";
2058
+ const state = { bounded: false };
2059
+ const sorted = jobs.map((raw) => obj(raw)).filter((job) => Boolean(job)).sort((a, b) => (num(b.created_at) ?? 0) - (num(a.created_at) ?? 0));
2060
+ const shown = sorted.slice(0, JOBS_RENDER_MAX);
2061
+ if (shown.length < sorted.length) state.bounded = true;
2062
+ const items = shown.map((job) => {
2063
+ const progress = obj(job.progress);
2064
+ const done = num(progress?.processed_chunks);
2065
+ const totalChunks = num(progress?.total_chunks);
2066
+ const bits = [
2067
+ str(job.status) || "?",
2068
+ done !== void 0 && totalChunks !== void 0 ? `${done}/${totalChunks} chunks` : void 0,
2069
+ num(progress?.nodes_created) !== void 0 ? `${num(progress?.nodes_created)} nodes` : void 0,
2070
+ num(job.queue_position) !== void 0 ? `queue #${num(job.queue_position)}` : void 0,
2071
+ str(job.error) ? `error: ${clip(job.error, 160, state)}` : void 0
2072
+ ].filter(Boolean);
2073
+ return `- [${str(job.id) || "?"}] ${clip(job.title, 120, state) || "(untitled)"} \u2014 ${bits.join(", ")}`;
2074
+ });
2075
+ return assemble(
2076
+ `# Ingestion jobs (${shown.length}${shown.length < sorted.length ? ` of ${sorted.length}` : ""})`,
2077
+ [{ heading: "", items }],
2078
+ state
2079
+ );
2080
+ }
2081
+ function renderSignals(response) {
2082
+ const payload = obj(response);
2083
+ if (!payload) return void 0;
2084
+ const state = { bounded: false };
2085
+ const sections = [];
2086
+ for (const [key, value] of Object.entries(payload)) {
2087
+ if (!Array.isArray(value) || value.length === 0) continue;
2088
+ const items = value.map((raw) => obj(raw)).filter((item) => Boolean(item)).map((item) => {
2089
+ const fromLabel = str(item.from_label);
2090
+ const toLabel = str(item.to_label);
2091
+ const anyLabelKey = Object.keys(item).find(
2092
+ (k) => k.endsWith("_label") && str(item[k])
2093
+ );
2094
+ 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)";
2095
+ const uid = str(item.uid) ?? str(item.target_uid);
2096
+ const numbers = Object.entries(item).filter(([, v]) => typeof v === "number").slice(0, 4).map(([k, v]) => `${k} ${Math.round(v * 100) / 100}`);
2097
+ const lists = Object.entries(item).filter(([, v]) => Array.isArray(v) && v.length > 0).slice(0, 2).map(([k, v]) => `${k}: ${v.length}`);
2098
+ const meta = [...numbers, ...lists].join(", ");
2099
+ return `- ${clip(label, 160, state)}${uid && uid !== label ? ` [${uid}]` : ""}${meta ? ` (${meta})` : ""}`;
2100
+ });
2101
+ if (items.length > 0) {
2102
+ sections.push({ heading: `## ${key.replace(/_/g, " ")}`, items });
2103
+ }
2104
+ }
2105
+ if (sections.length === 0) return void 0;
2106
+ return assemble("# Synthesis signals", sections, state);
2107
+ }
1825
2108
 
1826
2109
  // src/tools.ts
1827
2110
  var TOOLS = [
@@ -2631,6 +2914,11 @@ var TOOLS = [
2631
2914
  type: "string",
2632
2915
  description: "Job ID to check status (for job_status action)"
2633
2916
  },
2917
+ format: {
2918
+ type: "string",
2919
+ enum: ["text", "json"],
2920
+ description: "job_status without job_id: 'text' (default) renders the 20 most recent jobs; 'json' returns the full raw list."
2921
+ },
2634
2922
  agent_id: {
2635
2923
  type: "string",
2636
2924
  description: "Agent identity"
@@ -2666,6 +2954,11 @@ var TOOLS = [
2666
2954
  type: "string",
2667
2955
  description: "Job ID to poll (for action='job_status')"
2668
2956
  },
2957
+ format: {
2958
+ type: "string",
2959
+ enum: ["text", "json"],
2960
+ description: "signals action: 'text' (default) renders signal sections; 'json' returns the raw payload."
2961
+ },
2669
2962
  agent_id: {
2670
2963
  type: "string",
2671
2964
  description: "Agent identity"
@@ -2774,6 +3067,11 @@ var TOOLS = [
2774
3067
  items: { type: "string" },
2775
3068
  description: "Chunk/document UIDs to extract from (for action=extract)"
2776
3069
  },
3070
+ format: {
3071
+ type: "string",
3072
+ enum: ["text", "json"],
3073
+ description: "Output format for the read actions query/search/objects/object/object_context (default 'text'): a compact rendered block. Pass 'json' for the raw server response."
3074
+ },
2777
3075
  agent_id: {
2778
3076
  type: "string",
2779
3077
  description: "Agent identity"
@@ -2816,9 +3114,14 @@ function toolsForProfile(profile = process.env.MINDGRAPH_PROFILE || "general") {
2816
3114
  }
2817
3115
  function ok3(data) {
2818
3116
  return {
2819
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
3117
+ content: [{ type: "text", text: JSON.stringify(data) }]
2820
3118
  };
2821
3119
  }
3120
+ function renderedResult(format, raw, render) {
3121
+ if (format === "json") return ok3(raw);
3122
+ const text = render(raw);
3123
+ return text !== void 0 ? { content: [{ type: "text", text }] } : ok3(raw);
3124
+ }
2822
3125
  function err3(message) {
2823
3126
  return {
2824
3127
  content: [{ type: "text", text: JSON.stringify({ error: message }) }],
@@ -3430,15 +3733,11 @@ async function handleRetrieve(client2, args) {
3430
3733
  max_cost,
3431
3734
  agent_id
3432
3735
  } = args;
3433
- const rendered = (raw, render) => {
3434
- if (format === "json") return ok3(raw);
3435
- const text = render(raw);
3436
- return text !== void 0 ? { content: [{ type: "text", text }] } : ok3(raw);
3437
- };
3736
+ const rendered2 = (raw, render) => renderedResult(format, raw, render);
3438
3737
  switch (action) {
3439
3738
  case "context":
3440
3739
  if (!query) return err3("query is required for context retrieval");
3441
- return rendered(
3740
+ return rendered2(
3442
3741
  await client2.retrieveContext({
3443
3742
  query,
3444
3743
  node_limit: limit,
@@ -3463,7 +3762,7 @@ async function handleRetrieve(client2, args) {
3463
3762
  );
3464
3763
  case "text":
3465
3764
  if (!query) return err3("query is required for text search");
3466
- return rendered(
3765
+ return rendered2(
3467
3766
  await client2.retrieve({
3468
3767
  action: "text",
3469
3768
  query,
@@ -3477,7 +3776,7 @@ async function handleRetrieve(client2, args) {
3477
3776
  );
3478
3777
  case "semantic":
3479
3778
  if (!query) return err3("query is required for semantic search");
3480
- return rendered(
3779
+ return rendered2(
3481
3780
  await client2.retrieve({
3482
3781
  action: "semantic",
3483
3782
  query,
@@ -3491,7 +3790,7 @@ async function handleRetrieve(client2, args) {
3491
3790
  );
3492
3791
  case "hybrid":
3493
3792
  if (!query) return err3("query is required for hybrid search");
3494
- return rendered(
3793
+ return rendered2(
3495
3794
  await client2.retrieve({
3496
3795
  action: "hybrid",
3497
3796
  query,
@@ -3513,7 +3812,7 @@ async function handleRetrieve(client2, args) {
3513
3812
  case "weak_claims":
3514
3813
  case "pending_approvals":
3515
3814
  case "unresolved_contradictions":
3516
- return rendered(
3815
+ return rendered2(
3517
3816
  await client2.retrieve({
3518
3817
  action,
3519
3818
  limit,
@@ -3523,7 +3822,7 @@ async function handleRetrieve(client2, args) {
3523
3822
  (raw) => renderNodeList(raw, action.replace(/_/g, " "), limit)
3524
3823
  );
3525
3824
  case "stale_derivations":
3526
- return rendered(
3825
+ return rendered2(
3527
3826
  await client2.retrieve({
3528
3827
  action: "stale_derivations",
3529
3828
  limit,
@@ -3532,7 +3831,7 @@ async function handleRetrieve(client2, args) {
3532
3831
  (raw) => renderNodeList(raw, "stale derivations", limit)
3533
3832
  );
3534
3833
  case "preferences":
3535
- return rendered(
3834
+ return rendered2(
3536
3835
  await client2.retrieve({
3537
3836
  action: "preferences",
3538
3837
  ...query ? { query, k: limit } : {},
@@ -3543,7 +3842,7 @@ async function handleRetrieve(client2, args) {
3543
3842
  );
3544
3843
  case "layer":
3545
3844
  if (!layer) return err3("layer is required for layer retrieval");
3546
- return rendered(
3845
+ return rendered2(
3547
3846
  await client2.retrieve({
3548
3847
  action: "layer",
3549
3848
  layer,
@@ -3554,7 +3853,7 @@ async function handleRetrieve(client2, args) {
3554
3853
  (raw) => renderNodeList(raw, `Layer: ${layer}`, limit)
3555
3854
  );
3556
3855
  case "recent":
3557
- return rendered(
3856
+ return rendered2(
3558
3857
  await client2.retrieve({
3559
3858
  action: "recent",
3560
3859
  limit,
@@ -3569,7 +3868,7 @@ async function handleRetrieve(client2, args) {
3569
3868
  );
3570
3869
  // Document inventory
3571
3870
  case "document_index":
3572
- return rendered(
3871
+ return rendered2(
3573
3872
  await client2.getNodes({
3574
3873
  node_type: "Document",
3575
3874
  limit: limit ?? 100
@@ -3579,7 +3878,7 @@ async function handleRetrieve(client2, args) {
3579
3878
  // Graph traversal
3580
3879
  case "chain":
3581
3880
  if (!start_uid) return err3("start_uid is required for chain traversal");
3582
- return rendered(
3881
+ return rendered2(
3583
3882
  await client2.traverse({
3584
3883
  action: "chain",
3585
3884
  start_uid,
@@ -3592,7 +3891,7 @@ async function handleRetrieve(client2, args) {
3592
3891
  );
3593
3892
  case "neighborhood":
3594
3893
  if (!start_uid) return err3("start_uid is required for neighborhood traversal");
3595
- return rendered(
3894
+ return rendered2(
3596
3895
  await client2.traverse({
3597
3896
  action: "neighborhood",
3598
3897
  start_uid,
@@ -3608,7 +3907,7 @@ async function handleRetrieve(client2, args) {
3608
3907
  case "path":
3609
3908
  if (!start_uid) return err3("start_uid is required for path traversal");
3610
3909
  if (!end_uid) return err3("end_uid is required for path traversal");
3611
- return rendered(
3910
+ return rendered2(
3612
3911
  await client2.traverse({
3613
3912
  action: "path",
3614
3913
  start_uid,
@@ -3624,7 +3923,7 @@ async function handleRetrieve(client2, args) {
3624
3923
  );
3625
3924
  case "subgraph":
3626
3925
  if (!start_uid) return err3("start_uid is required for subgraph traversal");
3627
- return rendered(
3926
+ return rendered2(
3628
3927
  await client2.traverse({
3629
3928
  action: "subgraph",
3630
3929
  start_uid,
@@ -3640,7 +3939,7 @@ async function handleRetrieve(client2, args) {
3640
3939
  case "top_k_paths":
3641
3940
  if (!start_uid) return err3("start_uid is required for top_k_paths");
3642
3941
  if (!end_uid) return err3("end_uid is required for top_k_paths");
3643
- return rendered(
3942
+ return rendered2(
3644
3943
  await client2.traverse({
3645
3944
  action: "top_k_paths",
3646
3945
  start_uid,
@@ -3677,6 +3976,7 @@ async function handleIngest(client2, args) {
3677
3976
  chunk_size,
3678
3977
  chunk_overlap,
3679
3978
  job_id,
3979
+ format,
3680
3980
  agent_id
3681
3981
  } = args;
3682
3982
  const src = source_uri ?? source;
@@ -3746,19 +4046,21 @@ async function handleIngest(client2, args) {
3746
4046
  if (job_id) {
3747
4047
  return ok3(await client2.getJob(job_id));
3748
4048
  }
3749
- return ok3(await client2.listJobs());
4049
+ return renderedResult(format, await client2.listJobs(), renderJobs);
3750
4050
  default:
3751
4051
  return err3(`Unknown ingest action: ${action}`);
3752
4052
  }
3753
4053
  }
3754
4054
  async function handleSynthesize(client2, args) {
3755
- const { action, project_uid, signals, target_types, job_id } = args;
4055
+ const { action, project_uid, signals, target_types, job_id, format } = args;
3756
4056
  switch (action) {
3757
4057
  case "signals":
3758
4058
  if (!project_uid)
3759
4059
  return err3("project_uid is required for action='signals'");
3760
- return ok3(
3761
- await client2.signals(project_uid, { signals, target_types })
4060
+ return renderedResult(
4061
+ format,
4062
+ await client2.signals(project_uid, { signals, target_types }),
4063
+ renderSignals
3762
4064
  );
3763
4065
  case "run":
3764
4066
  if (!project_uid)
@@ -3792,6 +4094,7 @@ async function handleOntology(client2, args) {
3792
4094
  relation_type,
3793
4095
  fields,
3794
4096
  source_uids,
4097
+ format,
3795
4098
  agent_id
3796
4099
  } = args;
3797
4100
  switch (action) {
@@ -3807,7 +4110,8 @@ async function handleOntology(client2, args) {
3807
4110
  "schema_id is required for action='query' (use action='schemas' to find the active schema)"
3808
4111
  );
3809
4112
  }
3810
- return ok3(
4113
+ return renderedResult(
4114
+ format,
3811
4115
  await client2.queryOntology({
3812
4116
  query,
3813
4117
  schema_id,
@@ -3816,29 +4120,42 @@ async function handleOntology(client2, args) {
3816
4120
  include_sources: include_sources ?? true,
3817
4121
  depth,
3818
4122
  limit
3819
- })
4123
+ }),
4124
+ renderOntologyAnswer
3820
4125
  );
3821
4126
  }
3822
4127
  case "search":
3823
4128
  if (!query) return err3("query is required for action='search'");
3824
- return ok3(
4129
+ return renderedResult(
4130
+ format,
3825
4131
  await client2.searchDomainObjects(query, {
3826
4132
  schema_id,
3827
4133
  object_types,
3828
4134
  limit
3829
- })
4135
+ }),
4136
+ (raw) => renderObjectList(raw, `Domain search: ${query}`)
3830
4137
  );
3831
4138
  case "objects":
3832
4139
  if (!schema_id) return err3("schema_id is required for action='objects'");
3833
- return ok3(
3834
- await client2.listDomainObjects({ schema_id, object_type, limit })
4140
+ return renderedResult(
4141
+ format,
4142
+ await client2.listDomainObjects({ schema_id, object_type, limit }),
4143
+ (raw) => renderObjectList(raw, `Domain objects${object_type ? `: ${object_type}` : ""}`)
3835
4144
  );
3836
4145
  case "object":
3837
4146
  if (!uid) return err3("uid is required for action='object'");
3838
- return ok3(await client2.getDomainObject(uid));
4147
+ return renderedResult(
4148
+ format,
4149
+ await client2.getDomainObject(uid),
4150
+ (raw) => renderNodeList([raw], "Domain object")
4151
+ );
3839
4152
  case "object_context":
3840
4153
  if (!uid) return err3("uid is required for action='object_context'");
3841
- return ok3(await client2.getDomainObjectContext(uid, depth));
4154
+ return renderedResult(
4155
+ format,
4156
+ await client2.getDomainObjectContext(uid, depth),
4157
+ renderOntologyAnswer
4158
+ );
3842
4159
  case "proposals":
3843
4160
  return ok3(
3844
4161
  await client2.listOntologyProposals({
@@ -3892,8 +4209,14 @@ async function handleOntology(client2, args) {
3892
4209
 
3893
4210
  // src/generated-tools.ts
3894
4211
  var ok4 = (data) => ({
3895
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
4212
+ // Compact on purpose — pretty-printing doubled the token cost.
4213
+ content: [{ type: "text", text: JSON.stringify(data) }]
3896
4214
  });
4215
+ var rendered = (format, raw, render) => {
4216
+ if (format === "json") return ok4(raw);
4217
+ const text = render(raw);
4218
+ return text !== void 0 ? { content: [{ type: "text", text }] } : ok4(raw);
4219
+ };
3897
4220
  var err4 = (message) => ({
3898
4221
  content: [{ type: "text", text: JSON.stringify({ error: message }) }],
3899
4222
  isError: true
@@ -3932,10 +4255,21 @@ async function getGeneratedTools(client2) {
3932
4255
  );
3933
4256
  }
3934
4257
  byName.set(d.name, d);
4258
+ const inputSchema = d.maps_to === "structured_query" ? d.input_schema : {
4259
+ ...d.input_schema,
4260
+ properties: {
4261
+ ...d.input_schema.properties,
4262
+ format: {
4263
+ type: "string",
4264
+ enum: ["text", "json"],
4265
+ description: "Output format (default 'text': compact rendered block). Pass 'json' for the raw server response."
4266
+ }
4267
+ }
4268
+ };
3935
4269
  tools.push({
3936
4270
  name: d.name,
3937
4271
  description: d.description,
3938
- inputSchema: d.input_schema
4272
+ inputSchema
3939
4273
  });
3940
4274
  }
3941
4275
  cache = { tools, byName, fetchedAt: Date.now() };
@@ -3964,31 +4298,41 @@ async function handleGeneratedTool(client2, desc, args) {
3964
4298
  if (!query) return err4(`query is required for ${desc.name}`);
3965
4299
  const limit = typeof args.limit === "number" ? args.limit : void 0;
3966
4300
  const filters = Array.isArray(args.filters) ? args.filters : void 0;
3967
- return ok4(
4301
+ return rendered(
4302
+ args.format,
3968
4303
  await ontologyClient.searchDomainObjects(query, {
3969
4304
  schema_id: desc.schema_id,
3970
4305
  object_types: [desc.object_type],
3971
4306
  filters,
3972
4307
  limit
3973
- })
4308
+ }),
4309
+ (raw) => renderObjectList(raw, `${desc.object_type} search: ${query}`)
3974
4310
  );
3975
4311
  }
3976
4312
  case "object": {
3977
4313
  const uid = args.uid;
3978
4314
  if (!uid) return err4(`uid is required for ${desc.name}`);
3979
- return ok4(await ontologyClient.getDomainObject(uid, {
3980
- schema_id: desc.schema_id,
3981
- object_type: desc.object_type
3982
- }));
4315
+ return rendered(
4316
+ args.format,
4317
+ await ontologyClient.getDomainObject(uid, {
4318
+ schema_id: desc.schema_id,
4319
+ object_type: desc.object_type
4320
+ }),
4321
+ (raw) => renderNodeList([raw], desc.object_type)
4322
+ );
3983
4323
  }
3984
4324
  case "object_context": {
3985
4325
  const uid = args.uid;
3986
4326
  if (!uid) return err4(`uid is required for ${desc.name}`);
3987
4327
  const depth = typeof args.depth === "number" ? args.depth : void 0;
3988
- return ok4(await ontologyClient.getDomainObjectContext(uid, depth, {
3989
- schema_id: desc.schema_id,
3990
- object_type: desc.object_type
3991
- }));
4328
+ return rendered(
4329
+ args.format,
4330
+ await ontologyClient.getDomainObjectContext(uid, depth, {
4331
+ schema_id: desc.schema_id,
4332
+ object_type: desc.object_type
4333
+ }),
4334
+ renderOntologyAnswer
4335
+ );
3992
4336
  }
3993
4337
  case "related": {
3994
4338
  const uid = args.uid;
@@ -4001,18 +4345,22 @@ async function handleGeneratedTool(client2, desc, args) {
4001
4345
  "related ontology tools require a graph-aware version of the mindgraph SDK"
4002
4346
  );
4003
4347
  }
4004
- return ok4(await ontologyClient.queryRelatedDomainObjects({
4005
- schema_id: desc.schema_id,
4006
- entry_type: desc.object_type,
4007
- uid,
4008
- relation: desc.relation,
4009
- entry_role: desc.entry_role,
4010
- far_type: desc.far_type,
4011
- where: Array.isArray(args.where) ? args.where : void 0,
4012
- include_provenance: args.include_provenance === true,
4013
- limit: typeof args.limit === "number" ? args.limit : void 0,
4014
- offset: typeof args.offset === "number" ? args.offset : void 0
4015
- }));
4348
+ return rendered(
4349
+ args.format,
4350
+ await ontologyClient.queryRelatedDomainObjects({
4351
+ schema_id: desc.schema_id,
4352
+ entry_type: desc.object_type,
4353
+ uid,
4354
+ relation: desc.relation,
4355
+ entry_role: desc.entry_role,
4356
+ far_type: desc.far_type,
4357
+ where: Array.isArray(args.where) ? args.where : void 0,
4358
+ include_provenance: args.include_provenance === true,
4359
+ limit: typeof args.limit === "number" ? args.limit : void 0,
4360
+ offset: typeof args.offset === "number" ? args.offset : void 0
4361
+ }),
4362
+ (raw) => renderObjectList(raw, `${desc.far_type} related via ${desc.relation}`)
4363
+ );
4016
4364
  }
4017
4365
  case "structured_query": {
4018
4366
  const select = args.select;
@@ -4213,7 +4561,7 @@ async function checkMcpGovernance(toolName, args, config) {
4213
4561
  if (!response) {
4214
4562
  return {
4215
4563
  allowed: false,
4216
- 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.`
4217
4565
  };
4218
4566
  }
4219
4567
  if (!response.ok) {
@@ -4331,7 +4679,7 @@ var RESOURCE_TEMPLATES = [
4331
4679
  var RESOURCE_GOVERNANCE_TOOL = "mindgraph_retrieve";
4332
4680
  function json(uri, data) {
4333
4681
  return {
4334
- contents: [{ uri, mimeType: "application/json", text: JSON.stringify(data, null, 2) }]
4682
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(data) }]
4335
4683
  };
4336
4684
  }
4337
4685
  async function readResource(client2, uri) {
@@ -4379,6 +4727,225 @@ async function readGovernedResource(client2, uri, governanceConfig) {
4379
4727
  return readResource(client2, uri);
4380
4728
  }
4381
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
+
4382
4949
  // src/index.ts
4383
4950
  var API_KEY = process.env.MINDGRAPH_API_KEY;
4384
4951
  var BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -4501,10 +5068,42 @@ if (PROFILE === "coding") {
4501
5068
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
4502
5069
  ).join("\n");
4503
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
+ }
4504
5103
  var server = new import_server.Server(
4505
5104
  {
4506
5105
  name: "mindgraph",
4507
- version: "0.16.0"
5106
+ version: "0.18.0"
4508
5107
  },
4509
5108
  {
4510
5109
  capabilities: {
@@ -4718,7 +5317,7 @@ async function main() {
4718
5317
  const transport = new import_stdio.StdioServerTransport();
4719
5318
  await server.connect(transport);
4720
5319
  console.error(
4721
- `MindGraph MCP server v${"0.16.0"} running on stdio (${BASE_URL})`
5320
+ `MindGraph MCP server v${"0.18.0"} running on stdio (${BASE_URL})`
4722
5321
  );
4723
5322
  }
4724
5323
  main().catch((error) => {