mindgraph-mcp 0.17.0 → 0.19.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 +58 -2
  2. package/dist/cli.js +1440 -452
  3. package/dist/index.js +563 -52
  4. package/package.json +2 -2
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
@@ -1989,7 +2110,7 @@ function renderSignals(response) {
1989
2110
  var TOOLS = [
1990
2111
  {
1991
2112
  name: "mindgraph_capture",
1992
- description: "Capture knowledge into the graph. Use 'entity' for people/orgs/places/events/nations/concepts (auto-deduplicates \u2014 safe to call even if already exists), 'observation' for factual statements, 'source' for documents/URLs, 'snippet' for quotes from a source, and 'concept' for abstract ideas. Use 'journal' for quick personal notes and 'lesson' for a durable learning attributable to a session or source nodes. Prefer 'entity'/'observation' for objective facts, 'journal' for informal notes, and 'lesson' only for reusable knowledge.",
2113
+ description: "Capture knowledge into the graph. Use 'entity' for people/orgs/places/events/nations/concepts (auto-deduplicates \u2014 safe to call even if already exists), 'observation' for factual statements, 'source' for documents/URLs, 'snippet' for quotes from a source, and 'concept' for abstract ideas. Use 'journal' for quick personal notes, 'lesson' for a durable learning, and 'skill' for a reusable procedure authored from completed work. Skill capture always creates a candidate requiring admin review; it never publishes. Skill names must be 1-64 lowercase a-z, digits, or hyphens, with no leading/trailing/consecutive hyphens and no 'anthropic' or 'claude'. Provide at least one provenance field and write instructions, not a transcript dump.",
1993
2114
  inputSchema: {
1994
2115
  type: "object",
1995
2116
  properties: {
@@ -2002,7 +2123,8 @@ var TOOLS = [
2002
2123
  "snippet",
2003
2124
  "concept",
2004
2125
  "journal",
2005
- "lesson"
2126
+ "lesson",
2127
+ "skill"
2006
2128
  ],
2007
2129
  description: "Type of knowledge to capture"
2008
2130
  },
@@ -2016,11 +2138,30 @@ var TOOLS = [
2016
2138
  },
2017
2139
  content: {
2018
2140
  type: "string",
2019
- description: "Journal content/body text (for action=journal)"
2141
+ maxLength: 65536,
2142
+ description: "Journal body (action=journal) or complete SKILL.md instructions, at most 64 KiB UTF-8 (action=skill)"
2143
+ },
2144
+ name: {
2145
+ type: "string",
2146
+ minLength: 1,
2147
+ maxLength: 64,
2148
+ pattern: "^(?!.*(?:anthropic|claude))(?!-)(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$",
2149
+ description: "Stable skill identifier using lowercase a-z, digits, and hyphens (for action=skill)"
2150
+ },
2151
+ description: {
2152
+ type: "string",
2153
+ minLength: 1,
2154
+ maxLength: 1024,
2155
+ description: "When the skill should be used; XML tags are rejected (for action=skill)"
2156
+ },
2157
+ license: {
2158
+ type: "string",
2159
+ description: "Optional skill license (for action=skill)"
2020
2160
  },
2021
2161
  session_uid: {
2022
2162
  type: "string",
2023
- description: "Session UID that produced a lesson (for action=lesson)"
2163
+ minLength: 1,
2164
+ description: "Session UID that produced a lesson or skill"
2024
2165
  },
2025
2166
  work_uid: {
2026
2167
  type: "string",
@@ -2041,7 +2182,8 @@ var TOOLS = [
2041
2182
  summarizes_uids: {
2042
2183
  type: "array",
2043
2184
  items: { type: "string" },
2044
- description: "Source node UIDs the lesson was learned from (for action=lesson)"
2185
+ minItems: 1,
2186
+ description: "Source node UIDs the lesson or skill was learned from"
2045
2187
  },
2046
2188
  entity_type: {
2047
2189
  type: "string",
@@ -2108,7 +2250,24 @@ var TOOLS = [
2108
2250
  description: "Agent identity"
2109
2251
  }
2110
2252
  },
2111
- required: ["action", "label"]
2253
+ required: ["action", "label"],
2254
+ allOf: [
2255
+ {
2256
+ if: {
2257
+ properties: { action: { const: "skill" } },
2258
+ required: ["action"]
2259
+ },
2260
+ then: {
2261
+ required: ["name", "description", "content"],
2262
+ anyOf: [
2263
+ { required: ["session_uid"] },
2264
+ { required: ["work_uid"] },
2265
+ { required: ["execution_uid"] },
2266
+ { required: ["summarizes_uids"] }
2267
+ ]
2268
+ }
2269
+ }
2270
+ ]
2112
2271
  }
2113
2272
  },
2114
2273
  {
@@ -2694,6 +2853,42 @@ var TOOLS = [
2694
2853
  required: ["action"]
2695
2854
  }
2696
2855
  },
2856
+ {
2857
+ name: "mindgraph_series_query",
2858
+ description: "Read dense numeric time series without placing raw points in general retrieval context. Use list_for_entity to discover series, latest for cached statistics, window for [from,to) keyset pages, and aggregate for bounded calendar buckets.",
2859
+ inputSchema: {
2860
+ type: "object",
2861
+ additionalProperties: false,
2862
+ properties: {
2863
+ action: {
2864
+ type: "string",
2865
+ enum: ["list_for_entity", "latest", "window", "aggregate"]
2866
+ },
2867
+ entity_uid: { type: "string" },
2868
+ series_uid: { type: "string" },
2869
+ from: { type: "number" },
2870
+ to: { type: "number" },
2871
+ cursor: { type: "number" },
2872
+ limit: { type: "number", minimum: 1, maximum: 1e4 },
2873
+ bucket: {
2874
+ type: "string",
2875
+ enum: ["year", "quarter", "month", "week", "day", "hour"]
2876
+ },
2877
+ agg: {
2878
+ type: "string",
2879
+ enum: ["sum", "mean", "min", "max", "count", "std_dev", "last"]
2880
+ },
2881
+ fill: { type: "string", enum: ["none", "null", "prev"] }
2882
+ },
2883
+ required: ["action"]
2884
+ },
2885
+ annotations: {
2886
+ readOnlyHint: true,
2887
+ destructiveHint: false,
2888
+ idempotentHint: true,
2889
+ openWorldHint: false
2890
+ }
2891
+ },
2697
2892
  {
2698
2893
  name: "mindgraph_ingest",
2699
2894
  description: "Ingest long-form content (articles, transcripts, documents, meeting notes) into the knowledge graph. The pipeline automatically chunks the text, extracts entities/claims/relationships via LLM, and deduplicates against existing nodes. Use 'chunk' for a single passage (synchronous), 'document' for full documents (async \u2014 returns a job ID), 'session' for conversation transcripts. Use 'job_status' to check progress. For short individual facts, use the specific cognitive tools instead.",
@@ -3026,6 +3221,9 @@ async function handleTool(client2, name, args) {
3026
3221
  case "mindgraph_retrieve":
3027
3222
  result = await handleRetrieve(client2, args);
3028
3223
  break;
3224
+ case "mindgraph_series_query":
3225
+ result = await handleSeriesQuery(client2, args);
3226
+ break;
3029
3227
  case "mindgraph_ingest":
3030
3228
  result = await handleIngest(client2, args);
3031
3229
  break;
@@ -3070,12 +3268,36 @@ async function handleTool(client2, name, args) {
3070
3268
  return err3(errorDetail(e));
3071
3269
  }
3072
3270
  }
3271
+ async function handleSeriesQuery(client2, args) {
3272
+ const action = typeof args.action === "string" ? args.action : "";
3273
+ if (!["list_for_entity", "latest", "window", "aggregate"].includes(action)) {
3274
+ return err3(`Unknown series query action: ${action}`);
3275
+ }
3276
+ if (action === "list_for_entity" && typeof args.entity_uid !== "string") {
3277
+ return err3("entity_uid is required for action='list_for_entity'");
3278
+ }
3279
+ if (action !== "list_for_entity" && typeof args.series_uid !== "string") {
3280
+ return err3(`series_uid is required for action='${action}'`);
3281
+ }
3282
+ if ((action === "window" || action === "aggregate") && (typeof args.from !== "number" || typeof args.to !== "number")) {
3283
+ return err3(`from and to are required for action='${action}'`);
3284
+ }
3285
+ if (action === "aggregate" && (typeof args.bucket !== "string" || typeof args.agg !== "string")) {
3286
+ return err3("bucket and agg are required for action='aggregate'");
3287
+ }
3288
+ return ok3(
3289
+ await client2.series(args)
3290
+ );
3291
+ }
3073
3292
  async function handleCapture(client2, args) {
3074
3293
  const {
3075
3294
  action,
3076
3295
  label,
3077
3296
  summary,
3078
3297
  content,
3298
+ name,
3299
+ description,
3300
+ license,
3079
3301
  session_uid,
3080
3302
  work_uid,
3081
3303
  execution_uid,
@@ -3110,6 +3332,41 @@ async function handleCapture(client2, args) {
3110
3332
  agent_id
3111
3333
  })
3112
3334
  );
3335
+ case "skill": {
3336
+ if (!name) return err3("name is required for action=skill");
3337
+ if (!description) return err3("description is required for action=skill");
3338
+ if (content === void 0) return err3("content is required for action=skill");
3339
+ const hasProvenance = [session_uid, work_uid, execution_uid].some(
3340
+ (uid) => typeof uid === "string" && uid.length > 0
3341
+ ) || Array.isArray(summarizes_uids) && summarizes_uids.some((uid) => typeof uid === "string" && uid.length > 0);
3342
+ if (!hasProvenance) {
3343
+ return err3(
3344
+ "at least one of session_uid, work_uid, execution_uid, or summarizes_uids is required for action=skill"
3345
+ );
3346
+ }
3347
+ return ok3(
3348
+ await client2.distill({
3349
+ label,
3350
+ output_type: "skill",
3351
+ summary,
3352
+ confidence,
3353
+ salience,
3354
+ session_uid,
3355
+ work_uid,
3356
+ execution_uid,
3357
+ idempotency_key,
3358
+ supersedes_uid,
3359
+ summarizes_uids,
3360
+ props: {
3361
+ name,
3362
+ description,
3363
+ content,
3364
+ ...license ? { license } : {}
3365
+ },
3366
+ agent_id
3367
+ })
3368
+ );
3369
+ }
3113
3370
  case "journal": {
3114
3371
  if (!content) return err3("content is required for action=journal");
3115
3372
  const journalProps = { content };
@@ -4313,6 +4570,7 @@ var ONTOLOGY_READ_ACTIONS = /* @__PURE__ */ new Set([
4313
4570
  ]);
4314
4571
  function toolMutability(toolName, action) {
4315
4572
  if (toolName === "mindgraph_retrieve") return "read";
4573
+ if (toolName === "mindgraph_series_query") return "read";
4316
4574
  if (toolName === "mindgraph_plan") {
4317
4575
  return action && PLAN_READ_ACTIONS.has(action) ? "read" : "write";
4318
4576
  }
@@ -4339,6 +4597,8 @@ function toolMutability(toolName, action) {
4339
4597
  var UID_FIELDS = [
4340
4598
  "uid",
4341
4599
  "source_uid",
4600
+ "entity_uid",
4601
+ "series_uid",
4342
4602
  "session_uid",
4343
4603
  "work_uid",
4344
4604
  "goal_uid",
@@ -4440,7 +4700,7 @@ async function checkMcpGovernance(toolName, args, config) {
4440
4700
  if (!response) {
4441
4701
  return {
4442
4702
  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.`
4703
+ 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
4704
  };
4445
4705
  }
4446
4706
  if (!response.ok) {
@@ -4606,6 +4866,225 @@ async function readGovernedResource(client2, uri, governanceConfig) {
4606
4866
  return readResource(client2, uri);
4607
4867
  }
4608
4868
 
4869
+ // src/index.ts
4870
+ var import_node_path5 = __toESM(require("path"));
4871
+
4872
+ // src/hook-installer.ts
4873
+ var import_node_fs4 = __toESM(require("fs"));
4874
+ var import_node_os3 = __toESM(require("os"));
4875
+ var import_node_path4 = __toESM(require("path"));
4876
+ var import_node_crypto4 = __toESM(require("crypto"));
4877
+ var OWNER_MARKER = "--owner mindgraph";
4878
+ var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
4879
+ function command(harness) {
4880
+ return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
4881
+ }
4882
+ function windowsCommand(harness) {
4883
+ return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
4884
+ }
4885
+ var RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
4886
+ function installHookRunner(sourceFile, homeDir, version) {
4887
+ const target = import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_RELATIVE);
4888
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
4889
+ const temporary = `${target}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
4890
+ import_node_fs4.default.copyFileSync(sourceFile, temporary);
4891
+ import_node_fs4.default.renameSync(temporary, target);
4892
+ const versionPath = import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_VERSION_RELATIVE);
4893
+ if (version) {
4894
+ import_node_fs4.default.writeFileSync(versionPath, `${version}
4895
+ `);
4896
+ } else {
4897
+ import_node_fs4.default.rmSync(versionPath, { force: true });
4898
+ }
4899
+ return target;
4900
+ }
4901
+ function installedHookRunnerVersion(homeDir) {
4902
+ try {
4903
+ const raw = import_node_fs4.default.readFileSync(
4904
+ import_node_path4.default.join(homeDir || import_node_os3.default.homedir(), RUNNER_VERSION_RELATIVE),
4905
+ "utf8"
4906
+ ).trim();
4907
+ return raw || void 0;
4908
+ } catch {
4909
+ return void 0;
4910
+ }
4911
+ }
4912
+ function parseSemver(value) {
4913
+ if (!value) return void 0;
4914
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
4915
+ if (!match) return void 0;
4916
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
4917
+ }
4918
+ function semverNewer(a, b) {
4919
+ for (let index = 0; index < 3; index += 1) {
4920
+ if (a[index] !== b[index]) return a[index] > b[index];
4921
+ }
4922
+ return false;
4923
+ }
4924
+ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
4925
+ try {
4926
+ if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
4927
+ return { refreshed: false, reason: "disabled" };
4928
+ }
4929
+ const home = homeDir || import_node_os3.default.homedir();
4930
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
4931
+ return { refreshed: false, reason: "no_runner" };
4932
+ }
4933
+ if (!import_node_fs4.default.existsSync(sourceBundle)) {
4934
+ return { refreshed: false, reason: "no_source" };
4935
+ }
4936
+ const server2 = parseSemver(serverVersion);
4937
+ if (!server2) return { refreshed: false, reason: "unparseable" };
4938
+ const runnerVersion = installedHookRunnerVersion(home);
4939
+ if (runnerVersion !== void 0) {
4940
+ const pinned = parseSemver(runnerVersion);
4941
+ if (!pinned) {
4942
+ return { refreshed: false, reason: "unparseable", runnerVersion };
4943
+ }
4944
+ if (!semverNewer(server2, pinned)) {
4945
+ return {
4946
+ refreshed: false,
4947
+ reason: runnerVersion === serverVersion ? "current" : "downgrade",
4948
+ runnerVersion
4949
+ };
4950
+ }
4951
+ }
4952
+ installHookRunner(sourceBundle, home, serverVersion);
4953
+ return { refreshed: true, reason: "updated", runnerVersion };
4954
+ } catch {
4955
+ return { refreshed: false, reason: "error" };
4956
+ }
4957
+ }
4958
+ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
4959
+ const refreshed = [];
4960
+ const scopes = ["user", "project"];
4961
+ for (const scope of scopes) {
4962
+ try {
4963
+ const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
4964
+ if (!import_node_fs4.default.existsSync(file)) continue;
4965
+ if (!import_node_fs4.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
4966
+ installHooks("claude-code", scope, projectDir, void 0, homeDir);
4967
+ refreshed.push(file);
4968
+ } catch {
4969
+ }
4970
+ }
4971
+ return refreshed;
4972
+ }
4973
+ function versionSkewNote(serverVersion, homeDir) {
4974
+ const runnerVersion = installedHookRunnerVersion(homeDir);
4975
+ if (!runnerVersion || runnerVersion === serverVersion) return void 0;
4976
+ 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).`;
4977
+ }
4978
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os3.default.homedir(), ".codex"), homeDir) {
4979
+ if (harness === "codex") {
4980
+ return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
4981
+ }
4982
+ 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");
4983
+ }
4984
+ function readSettings(file) {
4985
+ if (!import_node_fs4.default.existsSync(file)) return {};
4986
+ return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
4987
+ }
4988
+ function ownedHook(harness, timeout, additionalContextLimit) {
4989
+ return {
4990
+ type: "command",
4991
+ command: command(harness),
4992
+ ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
4993
+ timeout,
4994
+ statusMessage: "Syncing MindGraph session context",
4995
+ ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
4996
+ };
4997
+ }
4998
+ function desiredEntries(harness) {
4999
+ const common = {
5000
+ SessionStart: [
5001
+ {
5002
+ matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
5003
+ // B7 live acceptance caught cold SessionStart calls crossing the
5004
+ // inherited 20s edge in both harnesses: the hooks correctly failed
5005
+ // open, but no brief reached the model. The same installed runner
5006
+ // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
5007
+ // The declared Codex context limit must not undercut the brief's own
5008
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
5009
+ // truncation the renderer knew nothing about.
5010
+ hooks: [
5011
+ ownedHook(
5012
+ harness,
5013
+ 30,
5014
+ harness === "codex" ? 1e4 : void 0
5015
+ )
5016
+ ]
5017
+ }
5018
+ ],
5019
+ PreToolUse: [
5020
+ {
5021
+ matcher: "mcp__mindgraph__.*",
5022
+ // Repository resolution over a large multi-repo workspace plus three
5023
+ // git probes can exceed 3s on the first (uncached) call; a killed
5024
+ // PreToolUse means the call goes through untagged.
5025
+ hooks: [ownedHook(harness, 5)]
5026
+ }
5027
+ ],
5028
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
5029
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
5030
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
5031
+ Stop: [{ hooks: [ownedHook(harness, 5)] }],
5032
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
5033
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
5034
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
5035
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
5036
+ // map), so the codex adapter instead makes at most ONE call there.
5037
+ SessionEnd: [
5038
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
5039
+ ]
5040
+ };
5041
+ return harness === "claude-code" ? {
5042
+ ...common,
5043
+ TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
5044
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
5045
+ } : common;
5046
+ }
5047
+ function hookCommand(value) {
5048
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
5049
+ const command2 = value.command;
5050
+ return typeof command2 === "string" ? command2 : void 0;
5051
+ }
5052
+ function entryHasOwnedHook(value) {
5053
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5054
+ const hooks = value.hooks;
5055
+ return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
5056
+ }
5057
+ function writeSettings(file, settings) {
5058
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
5059
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
5060
+ import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
5061
+ `, {
5062
+ mode: 384
5063
+ });
5064
+ import_node_fs4.default.renameSync(temporary, file);
5065
+ }
5066
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
5067
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
5068
+ const settings = readSettings(file);
5069
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
5070
+ let added = 0;
5071
+ let updated = 0;
5072
+ for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
5073
+ const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
5074
+ const kept = current.filter((entry) => !entryHasOwnedHook(entry));
5075
+ const hadOwned = kept.length !== current.length;
5076
+ for (const entry of wanted) {
5077
+ kept.push(entry);
5078
+ if (hadOwned) updated += 1;
5079
+ else added += 1;
5080
+ }
5081
+ hooks[event] = kept;
5082
+ }
5083
+ settings.hooks = hooks;
5084
+ writeSettings(file, settings);
5085
+ return { path: file, added, updated };
5086
+ }
5087
+
4609
5088
  // src/index.ts
4610
5089
  var API_KEY = process.env.MINDGRAPH_API_KEY;
4611
5090
  var BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
@@ -4728,10 +5207,42 @@ if (PROFILE === "coding") {
4728
5207
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
4729
5208
  ).join("\n");
4730
5209
  }
5210
+ var RUNNER_REFRESH = (() => {
5211
+ try {
5212
+ return refreshHookRunner("0.19.0", import_node_path5.default.join(__dirname, "cli.js"));
5213
+ } catch {
5214
+ return void 0;
5215
+ }
5216
+ })();
5217
+ if (RUNNER_REFRESH?.refreshed) {
5218
+ const refreshedSettings = (() => {
5219
+ try {
5220
+ return refreshOwnedClaudeHooks();
5221
+ } catch {
5222
+ return [];
5223
+ }
5224
+ })();
5225
+ console.error(
5226
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.19.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
5227
+ );
5228
+ }
5229
+ var HOOK_SKEW_NOTE = (() => {
5230
+ try {
5231
+ return versionSkewNote("0.19.0");
5232
+ } catch {
5233
+ return void 0;
5234
+ }
5235
+ })();
5236
+ if (HOOK_SKEW_NOTE) {
5237
+ INSTRUCTIONS += `
5238
+
5239
+ ${HOOK_SKEW_NOTE}`;
5240
+ console.error(`[mindgraph-mcp] ${HOOK_SKEW_NOTE}`);
5241
+ }
4731
5242
  var server = new import_server.Server(
4732
5243
  {
4733
5244
  name: "mindgraph",
4734
- version: "0.17.0"
5245
+ version: "0.19.0"
4735
5246
  },
4736
5247
  {
4737
5248
  capabilities: {
@@ -4945,7 +5456,7 @@ async function main() {
4945
5456
  const transport = new import_stdio.StdioServerTransport();
4946
5457
  await server.connect(transport);
4947
5458
  console.error(
4948
- `MindGraph MCP server v${"0.17.0"} running on stdio (${BASE_URL})`
5459
+ `MindGraph MCP server v${"0.19.0"} running on stdio (${BASE_URL})`
4949
5460
  );
4950
5461
  }
4951
5462
  main().catch((error) => {