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/cli.js CHANGED
@@ -65,8 +65,8 @@ var init_dist = __esm({
65
65
  this.retryBackoffMs = config.retryBackoffMs ?? 1e3;
66
66
  }
67
67
  // ---- HTTP helpers ----
68
- async request(method, path6, body) {
69
- const url = `${this.baseUrl}${path6}`;
68
+ async request(method, path8, body) {
69
+ const url = `${this.baseUrl}${path8}`;
70
70
  const requestId = createRequestId();
71
71
  const init = {
72
72
  method,
@@ -91,13 +91,14 @@ var init_dist = __esm({
91
91
  parsed = text2;
92
92
  }
93
93
  const err5 = new MindGraphError(
94
- `${method} ${path6} failed: ${res.status}`,
94
+ `${method} ${path8} failed: ${res.status}`,
95
95
  res.status,
96
96
  parsed
97
97
  );
98
98
  if (res.status === 503 && attempt < this.maxRetries) {
99
99
  lastError = err5;
100
- const delay = this.retryBackoffMs * 2 ** attempt;
100
+ const retryAfterSecs = Number(res.headers.get("retry-after"));
101
+ const delay = Number.isFinite(retryAfterSecs) && retryAfterSecs > 0 ? Math.min(retryAfterSecs * 1e3, 1e4) : this.retryBackoffMs * 2 ** attempt;
101
102
  await new Promise((r) => setTimeout(r, delay));
102
103
  continue;
103
104
  }
@@ -109,17 +110,17 @@ var init_dist = __esm({
109
110
  }
110
111
  throw lastError;
111
112
  }
112
- get(path6) {
113
- return this.request("GET", path6);
113
+ get(path8) {
114
+ return this.request("GET", path8);
114
115
  }
115
- post(path6, body) {
116
- return this.request("POST", path6, body);
116
+ post(path8, body) {
117
+ return this.request("POST", path8, body);
117
118
  }
118
- patch(path6, body) {
119
- return this.request("PATCH", path6, body);
119
+ patch(path8, body) {
120
+ return this.request("PATCH", path8, body);
120
121
  }
121
- del(path6) {
122
- return this.request("DELETE", path6);
122
+ del(path8) {
123
+ return this.request("DELETE", path8);
123
124
  }
124
125
  // ---- Health ----
125
126
  async health() {
@@ -148,6 +149,52 @@ var init_dist = __esm({
148
149
  async entity(req) {
149
150
  return this.post("/reality/entity", req);
150
151
  }
152
+ async series(req) {
153
+ return this.post("/reality/series", req);
154
+ }
155
+ async createSeries(req) {
156
+ return this.post("/reality/series", { action: "create", ...req });
157
+ }
158
+ async appendSeries(req) {
159
+ return this.post("/reality/series", { action: "append", ...req });
160
+ }
161
+ async seriesWindow(req) {
162
+ return this.post("/reality/series", { action: "window", ...req });
163
+ }
164
+ async aggregateSeries(req) {
165
+ return this.post("/reality/series", { action: "aggregate", ...req });
166
+ }
167
+ async latestSeries(seriesUid, options) {
168
+ return this.post("/reality/series", {
169
+ action: "latest",
170
+ series_uid: seriesUid,
171
+ ...options
172
+ });
173
+ }
174
+ async listSeriesForEntity(entityUid, options) {
175
+ return this.post("/reality/series", {
176
+ action: "list_for_entity",
177
+ entity_uid: entityUid,
178
+ ...options
179
+ });
180
+ }
181
+ async batchLatestSeries(entityUids, options) {
182
+ return this.post("/reality/series", {
183
+ action: "batch_latest",
184
+ entity_uids: entityUids,
185
+ ...options
186
+ });
187
+ }
188
+ async batchAggregateSeries(req) {
189
+ return this.post("/reality/series", { action: "batch_aggregate", ...req });
190
+ }
191
+ async deleteSeries(seriesUid, options) {
192
+ return this.post("/reality/series", {
193
+ action: "delete_series",
194
+ series_uid: seriesUid,
195
+ ...options
196
+ });
197
+ }
151
198
  /** Convenience: create or find an entity by label. */
152
199
  async findOrCreateEntity(label, props, agentId) {
153
200
  return this.post("/reality/entity", {
@@ -717,6 +764,20 @@ var init_dist = __esm({
717
764
  async getOntologySchema(id) {
718
765
  return this.get(`/v1/ontology/schemas/${seg(id)}`);
719
766
  }
767
+ async createOntologySeriesBinding(schemaId, request) {
768
+ return this.post(`/v1/ontology/schemas/${seg(schemaId)}/series-bindings`, request);
769
+ }
770
+ async syncOntologySeriesBinding(bindingId, mode) {
771
+ return this.post(
772
+ `/v1/ontology/series-bindings/${seg(bindingId)}/sync`,
773
+ mode ? { mode } : {}
774
+ );
775
+ }
776
+ async archiveOntologySeriesBinding(schemaId, bindingId) {
777
+ return this.del(
778
+ `/v1/ontology/schemas/${seg(schemaId)}/series-bindings/${seg(bindingId)}`
779
+ );
780
+ }
720
781
  async createOntologySchema(req) {
721
782
  return this.post("/v1/ontology/schemas", req);
722
783
  }
@@ -971,6 +1032,14 @@ var init_hook_env = __esm({
971
1032
  });
972
1033
 
973
1034
  // src/codegraph.ts
1035
+ function invocationCwd(args) {
1036
+ const context = args.invocation_context;
1037
+ if (context && typeof context === "object" && !Array.isArray(context)) {
1038
+ const cwd = context.cwd;
1039
+ if (typeof cwd === "string" && cwd) return cwd;
1040
+ }
1041
+ return process.cwd();
1042
+ }
974
1043
  function unavailabilityCaveats(reason, message) {
975
1044
  const caveats = [message || "codegraph status failed"];
976
1045
  if (reason === "absent") {
@@ -980,14 +1049,14 @@ function unavailabilityCaveats(reason, message) {
980
1049
  }
981
1050
  return caveats;
982
1051
  }
983
- function normalizePath(path6) {
984
- return path6.replaceAll("\\", "/").replace(/^\.\//, "");
1052
+ function normalizePath(path8) {
1053
+ return path8.replaceAll("\\", "/").replace(/^\.\//, "");
985
1054
  }
986
1055
  function normalizeSignature(signature) {
987
1056
  return signature.replace(/\s+/g, " ").trim();
988
1057
  }
989
- function languageFromPath(path6) {
990
- const extension = (0, import_node_path2.extname)(path6).toLowerCase();
1058
+ function languageFromPath(path8) {
1059
+ const extension = (0, import_node_path2.extname)(path8).toLowerCase();
991
1060
  return {
992
1061
  ".c": "c",
993
1062
  ".cc": "cpp",
@@ -1150,7 +1219,7 @@ function codeRefIdentityKey(ref) {
1150
1219
  }
1151
1220
  };
1152
1221
  }
1153
- var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto2, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1222
+ var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto3, UnknownRepositoryError, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1154
1223
  var init_codegraph = __esm({
1155
1224
  "src/codegraph.ts"() {
1156
1225
  "use strict";
@@ -1159,7 +1228,17 @@ var init_codegraph = __esm({
1159
1228
  import_promises = require("fs/promises");
1160
1229
  import_node_os2 = require("os");
1161
1230
  import_node_path2 = require("path");
1162
- import_node_crypto2 = require("crypto");
1231
+ import_node_crypto3 = require("crypto");
1232
+ UnknownRepositoryError = class extends Error {
1233
+ constructor(repo) {
1234
+ super(
1235
+ `unknown repository "${repo}": not declared in .mindgraph/workspace.json, not the current repository, and no such path exists`
1236
+ );
1237
+ this.repo = repo;
1238
+ this.name = "UnknownRepositoryError";
1239
+ }
1240
+ repo;
1241
+ };
1163
1242
  DEFAULT_TIMEOUT_MS = 5e3;
1164
1243
  DEFAULT_STATUS_TTL_MS = 6e4;
1165
1244
  MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
@@ -1195,7 +1274,26 @@ var init_codegraph = __esm({
1195
1274
  selected.spaceUid
1196
1275
  ) : selected;
1197
1276
  }
1198
- const requestedRoot = repo || this.env.MINDGRAPH_REPO_ROOT || this.cwd;
1277
+ if (repo) {
1278
+ const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, repo)).catch(
1279
+ () => (0, import_node_path2.resolve)(this.cwd, repo)
1280
+ );
1281
+ if (!(0, import_node_fs2.existsSync)(requestedPath)) {
1282
+ const current = await this.currentRepository(fallbackSpaceUid);
1283
+ if (current.repoId === repo) return current;
1284
+ throw new UnknownRepositoryError(repo);
1285
+ }
1286
+ const repoRoot = await this.gitRoot(requestedPath) ?? requestedPath;
1287
+ return this.resolveRepositoryIdentity(
1288
+ repoRoot,
1289
+ this.env.MINDGRAPH_REPO_ID,
1290
+ this.env.MINDGRAPH_CODE_SPACE_UID || fallbackSpaceUid
1291
+ );
1292
+ }
1293
+ return this.currentRepository(fallbackSpaceUid);
1294
+ }
1295
+ async currentRepository(fallbackSpaceUid) {
1296
+ const requestedRoot = this.env.MINDGRAPH_REPO_ROOT || this.cwd;
1199
1297
  const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, requestedRoot)).catch(
1200
1298
  () => (0, import_node_path2.resolve)(this.cwd, requestedRoot)
1201
1299
  );
@@ -1259,7 +1357,7 @@ var init_codegraph = __esm({
1259
1357
  } catch {
1260
1358
  }
1261
1359
  (0, import_node_fs2.mkdirSync)(identityDir, { recursive: true, mode: 448 });
1262
- const generated = (0, import_node_crypto2.randomUUID)();
1360
+ const generated = (0, import_node_crypto3.randomUUID)();
1263
1361
  try {
1264
1362
  (0, import_node_fs2.writeFileSync)(identityFile, `${generated}
1265
1363
  `, {
@@ -1285,6 +1383,14 @@ var init_codegraph = __esm({
1285
1383
  ).catch(() => "");
1286
1384
  return raw.trim() ? (0, import_node_path2.resolve)(start, raw.trim()) : void 0;
1287
1385
  }
1386
+ /** Why the workspace map yielded no repositories — for callers that were
1387
+ * explicitly asked to fan out and would otherwise return silently empty. */
1388
+ workspaceDiagnostic() {
1389
+ if (!this.workspaceFile || !(0, import_node_fs2.existsSync)(this.workspaceFile)) {
1390
+ return "no .mindgraph/workspace.json was found between the working directory and the filesystem root";
1391
+ }
1392
+ return `${this.workspaceFile} is malformed or declares no repositories`;
1393
+ }
1288
1394
  async availability(repository) {
1289
1395
  const cached = this.statusCache.get(repository.repoRoot);
1290
1396
  if (cached && cached.expiresAt > this.now()) return cached.availability;
@@ -1698,13 +1804,13 @@ var init_error_detail = __esm({
1698
1804
  });
1699
1805
 
1700
1806
  // src/code-tool.ts
1701
- async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1807
+ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cwd: invocationCwd(args) })) {
1702
1808
  const codeClient = client2;
1703
1809
  const action = args.action;
1704
1810
  const refs = Array.isArray(args.code_refs) ? args.code_refs : [];
1705
1811
  const anchorUids = Array.isArray(args.anchor_uids) ? args.anchor_uids : [];
1706
1812
  const agentId = typeof args.agent_id === "string" ? args.agent_id : "mcp";
1707
- const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : `space:agent:${agentId}`;
1813
+ const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : void 0;
1708
1814
  const options = {
1709
1815
  repo: typeof args.repo === "string" ? args.repo : void 0,
1710
1816
  fallbackSpaceUid
@@ -1828,6 +1934,7 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1828
1934
  const targetUid = typeof args.task_uid === "string" ? args.task_uid : typeof args.execution_uid === "string" ? args.execution_uid : void 0;
1829
1935
  let graphContext;
1830
1936
  const graphRefs = [];
1937
+ let unavailableRepositories = [];
1831
1938
  if (targetUid) {
1832
1939
  graphContext = await codeClient.traverse({
1833
1940
  action: "neighborhood",
@@ -1835,9 +1942,13 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1835
1942
  depth: 2,
1836
1943
  max_nodes: 50
1837
1944
  });
1838
- graphRefs.push(
1839
- ...await codeRefsFromGraphContext(codeClient, adapter, graphContext)
1945
+ const fromGraph = await codeRefsFromGraphContext(
1946
+ codeClient,
1947
+ adapter,
1948
+ graphContext
1840
1949
  );
1950
+ graphRefs.push(...fromGraph.refs);
1951
+ unavailableRepositories = fromGraph.unavailable;
1841
1952
  }
1842
1953
  if (refs.length > 0 || anchorUids.length > 0) {
1843
1954
  const target = await resolveSingleTarget(
@@ -1856,7 +1967,8 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1856
1967
  target_uid: targetUid,
1857
1968
  graph_context: graphContext,
1858
1969
  affected: [],
1859
- degradation: "no materialized code anchors were attached"
1970
+ degradation: unavailableRepositories.length > 0 ? `attached code anchors reference repositories unavailable here: ${unavailableRepositories.join(", ")}` : "no materialized code anchors were attached",
1971
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1860
1972
  });
1861
1973
  }
1862
1974
  const impacts = await Promise.all(
@@ -1873,13 +1985,17 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1873
1985
  target_uid: targetUid,
1874
1986
  graph_context: graphContext,
1875
1987
  impacts,
1876
- materialized: joined
1988
+ materialized: joined,
1989
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1877
1990
  });
1878
1991
  }
1879
1992
  default:
1880
1993
  return err("unknown_action", `unknown mindgraph_code action: ${action}`);
1881
1994
  }
1882
1995
  } catch (cause) {
1996
+ if (cause instanceof UnknownRepositoryError) {
1997
+ return err("unknown_repository", cause.message);
1998
+ }
1883
1999
  return err("code_tool_failed", errorDetail(cause));
1884
2000
  }
1885
2001
  }
@@ -1905,7 +2021,10 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1905
2021
  code_refs: args.code_refs,
1906
2022
  repo: args.repo,
1907
2023
  repo_space_uid: args.repo_space_uid,
1908
- agent_id: args.agent_id
2024
+ agent_id: args.agent_id,
2025
+ // Keeps the default adapter anchored to the live session directory
2026
+ // rather than wherever the MCP server process was launched.
2027
+ invocation_context: args.invocation_context
1909
2028
  },
1910
2029
  adapter
1911
2030
  );
@@ -1915,17 +2034,33 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1915
2034
  const anchorUids = (anchors.anchors ?? []).flatMap(
1916
2035
  (anchor) => anchor.uid ? [anchor.uid] : []
1917
2036
  );
2037
+ const relateFailures = [];
1918
2038
  for (const targetUid of targetUids) {
1919
2039
  for (const anchorUid of anchorUids) {
1920
- await codeClient.entity({
1921
- action: "relate",
1922
- source_uid: targetUid,
1923
- target_uid: anchorUid,
1924
- edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
1925
- agent_id: args.agent_id
1926
- });
2040
+ try {
2041
+ await codeClient.entity({
2042
+ action: "relate",
2043
+ source_uid: targetUid,
2044
+ target_uid: anchorUid,
2045
+ edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
2046
+ agent_id: args.agent_id
2047
+ });
2048
+ } catch (cause) {
2049
+ relateFailures.push(
2050
+ `${targetUid} -> ${anchorUid}: ${errorDetail(cause)}`
2051
+ );
2052
+ }
1927
2053
  }
1928
2054
  }
2055
+ if (relateFailures.length > 0) {
2056
+ return ok({
2057
+ result: original,
2058
+ code_anchors: {
2059
+ ...anchors,
2060
+ caveat: `anchors written, but ${relateFailures.length} relate edge(s) failed: ${relateFailures.join("; ")}`
2061
+ }
2062
+ });
2063
+ }
1929
2064
  }
1930
2065
  return ok({ result: original, code_anchors: anchors });
1931
2066
  }
@@ -1998,16 +2133,34 @@ async function codeRefFromAnchor(client2, adapter, uid) {
1998
2133
  if (!attributes || typeof attributes !== "object") return void 0;
1999
2134
  const codeRef = attributes.code_ref;
2000
2135
  if (!isStoredCodeRef(codeRef)) return void 0;
2001
- const repository = await adapter.resolveRepository(codeRef.repoId);
2002
- return { ...codeRef, repoRoot: repository.repoRoot };
2136
+ try {
2137
+ const repository = await adapter.resolveRepository(codeRef.repoId);
2138
+ return { ref: { ...codeRef, repoRoot: repository.repoRoot } };
2139
+ } catch (cause) {
2140
+ if (cause instanceof UnknownRepositoryError) {
2141
+ return { missingRepo: codeRef.repoId };
2142
+ }
2143
+ throw cause;
2144
+ }
2003
2145
  }
2004
2146
  async function resolveSingleTarget(client2, adapter, refs, anchorUids, options) {
2005
2147
  if (refs.length > 1 || anchorUids.length > 1) {
2006
2148
  return { error: err("ambiguous_target", "expand/affected accepts exactly one code target") };
2007
2149
  }
2008
2150
  if (anchorUids.length === 1) {
2009
- const ref = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
2010
- return ref ? { ref } : { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
2151
+ const outcome = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
2152
+ if (!outcome) {
2153
+ return { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
2154
+ }
2155
+ if ("missingRepo" in outcome) {
2156
+ return {
2157
+ error: err(
2158
+ "repository_unavailable",
2159
+ `repository ${outcome.missingRepo} is not checked out here or declared in .mindgraph/workspace.json; expand/affected need its local index`
2160
+ )
2161
+ };
2162
+ }
2163
+ return { ref: outcome.ref };
2011
2164
  }
2012
2165
  if (refs.length !== 1) {
2013
2166
  return { error: err("missing_code_ref", "one code_ref or anchor_uid is required") };
@@ -2046,7 +2199,7 @@ async function joinMaterialized(client2, refs, agentId, limit) {
2046
2199
  async function codeRefsFromGraphContext(client2, adapter, context) {
2047
2200
  const uids = /* @__PURE__ */ new Set();
2048
2201
  collectUids(context, uids);
2049
- if (uids.size === 0) return [];
2202
+ if (uids.size === 0) return { refs: [], unavailable: [] };
2050
2203
  const nodes = client2.getNodesBatch ? await client2.getNodesBatch([...uids]) : await Promise.all([...uids].map((uid) => client2.getNode(uid).catch(() => void 0)));
2051
2204
  const stored = nodes.flatMap((node) => {
2052
2205
  if (!node) return [];
@@ -2055,12 +2208,21 @@ async function codeRefsFromGraphContext(client2, adapter, context) {
2055
2208
  const codeRef = attributes.code_ref;
2056
2209
  return isStoredCodeRef(codeRef) ? [codeRef] : [];
2057
2210
  });
2058
- return Promise.all(
2059
- stored.map(async (ref) => {
2211
+ const refs = [];
2212
+ const unavailable = /* @__PURE__ */ new Set();
2213
+ for (const ref of stored) {
2214
+ try {
2060
2215
  const repository = await adapter.resolveRepository(ref.repoId);
2061
- return { ...ref, repoRoot: repository.repoRoot };
2062
- })
2063
- );
2216
+ refs.push({ ...ref, repoRoot: repository.repoRoot });
2217
+ } catch (cause) {
2218
+ if (cause instanceof UnknownRepositoryError) {
2219
+ unavailable.add(ref.repoId);
2220
+ } else {
2221
+ throw cause;
2222
+ }
2223
+ }
2224
+ }
2225
+ return { refs, unavailable: [...unavailable].sort() };
2064
2226
  }
2065
2227
  function collectUids(value, uids) {
2066
2228
  if (Array.isArray(value)) {
@@ -2136,7 +2298,7 @@ var init_code_tool = __esm({
2136
2298
  });
2137
2299
  CODE_TOOL = {
2138
2300
  name: "mindgraph_code",
2139
- 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.",
2301
+ 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.",
2140
2302
  inputSchema: {
2141
2303
  type: "object",
2142
2304
  properties: {
@@ -2178,7 +2340,7 @@ var init_code_tool = __esm({
2178
2340
  },
2179
2341
  repo_space_uid: {
2180
2342
  type: "string",
2181
- description: "Writable shared Project/repository Space for new anchors"
2343
+ description: "Writable shared Project/repository Space for new anchors (anchor requires this, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID)"
2182
2344
  },
2183
2345
  task_uid: { type: "string" },
2184
2346
  execution_uid: { type: "string" },
@@ -2197,179 +2359,469 @@ var init_code_tool = __esm({
2197
2359
  }
2198
2360
  });
2199
2361
 
2200
- // src/sync-tool.ts
2201
- function safePath(root, candidate) {
2202
- const absolute = import_node_path5.default.resolve(root, candidate);
2203
- const relative2 = import_node_path5.default.relative(root, absolute);
2204
- if (relative2.startsWith("..") || import_node_path5.default.isAbsolute(relative2)) {
2205
- throw new Error(`path escapes repository root: ${candidate}`);
2362
+ // src/hook-installer.ts
2363
+ function command(harness) {
2364
+ return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
2365
+ }
2366
+ function windowsCommand(harness) {
2367
+ return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
2368
+ }
2369
+ function installHookRunner(sourceFile, homeDir, version) {
2370
+ const target = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_RELATIVE);
2371
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
2372
+ const temporary = `${target}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2373
+ import_node_fs4.default.copyFileSync(sourceFile, temporary);
2374
+ import_node_fs4.default.renameSync(temporary, target);
2375
+ const versionPath = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE);
2376
+ if (version) {
2377
+ import_node_fs4.default.writeFileSync(versionPath, `${version}
2378
+ `);
2379
+ } else {
2380
+ import_node_fs4.default.rmSync(versionPath, { force: true });
2206
2381
  }
2207
- return absolute;
2382
+ return target;
2208
2383
  }
2209
- function markdownFiles(target) {
2210
- if (!import_node_fs5.default.existsSync(target)) return [];
2211
- const stat = import_node_fs5.default.statSync(target);
2212
- if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
2213
- if (!stat.isDirectory()) return [];
2214
- return import_node_fs5.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path5.default.join(target, entry.name)));
2384
+ function installedHookRunnerVersion(homeDir) {
2385
+ try {
2386
+ const raw = import_node_fs4.default.readFileSync(
2387
+ import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE),
2388
+ "utf8"
2389
+ ).trim();
2390
+ return raw || void 0;
2391
+ } catch {
2392
+ return void 0;
2393
+ }
2215
2394
  }
2216
- function fileDescriptor(root, absolute) {
2217
- const content = import_node_fs5.default.readFileSync(absolute, "utf8");
2218
- return {
2219
- logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
2220
- content_hash: import_node_crypto5.default.createHash("sha256").update(content).digest("hex"),
2221
- bytes: Buffer.byteLength(content),
2222
- content
2223
- };
2395
+ function parseSemver(value) {
2396
+ if (!value) return void 0;
2397
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
2398
+ if (!match) return void 0;
2399
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
2224
2400
  }
2225
- async function handleSyncTool(client2, args) {
2401
+ function semverNewer(a, b) {
2402
+ for (let index = 0; index < 3; index += 1) {
2403
+ if (a[index] !== b[index]) return a[index] > b[index];
2404
+ }
2405
+ return false;
2406
+ }
2407
+ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2226
2408
  try {
2227
- const action = String(args.action || "");
2228
- const context = args.invocation_context || {};
2229
- const adapter = new CodegraphAdapter({
2230
- cwd: context.cwd || process.cwd()
2231
- });
2232
- const requestedRepo = typeof args.repo === "string" ? args.repo : typeof args.repo_id === "string" ? args.repo_id : context.repoId;
2233
- const repository = await adapter.resolveRepository(requestedRepo);
2234
- const root = repository.repoRoot;
2235
- const repoId = repository.repoId;
2236
- const provider = String(args.provider || "claude_project");
2237
- const remote = client2;
2238
- if (action === "scan") {
2239
- const repositories = args.workspace === true ? await adapter.resolveRepositories() : [repository];
2240
- const requested = Array.isArray(args.paths) && args.paths.length > 0 ? args.paths.map(String) : [".claude/agent-memory", "memory"];
2241
- const scans = [];
2242
- for (const selected of repositories) {
2243
- scans.push(
2244
- await scanRepository(
2245
- remote,
2246
- selected,
2247
- requested,
2248
- provider,
2249
- args.agent_id
2250
- )
2251
- );
2252
- }
2253
- if (args.workspace === true) {
2254
- return ok2({
2255
- provider,
2256
- workspace: true,
2257
- repositories: scans,
2258
- one_way: "files_to_graph"
2259
- });
2260
- }
2261
- return ok2(scans[0]);
2409
+ if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
2410
+ return { refreshed: false, reason: "disabled" };
2262
2411
  }
2263
- const request = {
2264
- ...args,
2265
- provider,
2266
- repo_id: repoId
2267
- };
2268
- delete request.invocation_context;
2269
- delete request.paths;
2270
- delete request.repo;
2271
- delete request.workspace;
2272
- if (action === "begin" && !request.content) {
2273
- if (!request.logical_path) {
2274
- return err2("missing_field", "logical_path is required for begin");
2275
- }
2276
- const file = fileDescriptor(
2277
- root,
2278
- safePath(root, String(request.logical_path))
2279
- );
2280
- request.content = file.content;
2281
- request.content_hash ||= file.content_hash;
2412
+ const home = homeDir || import_node_os4.default.homedir();
2413
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
2414
+ return { refreshed: false, reason: "no_runner" };
2282
2415
  }
2283
- if (action === "status" && request.logical_path && request.file_present !== false) {
2284
- const absolute = safePath(root, String(request.logical_path));
2285
- request.file_present = import_node_fs5.default.existsSync(absolute);
2286
- if (request.file_present && !request.content_hash) {
2287
- request.content_hash = fileDescriptor(root, absolute).content_hash;
2416
+ if (!import_node_fs4.default.existsSync(sourceBundle)) {
2417
+ return { refreshed: false, reason: "no_source" };
2418
+ }
2419
+ const server2 = parseSemver(serverVersion);
2420
+ if (!server2) return { refreshed: false, reason: "unparseable" };
2421
+ const runnerVersion = installedHookRunnerVersion(home);
2422
+ if (runnerVersion !== void 0) {
2423
+ const pinned = parseSemver(runnerVersion);
2424
+ if (!pinned) {
2425
+ return { refreshed: false, reason: "unparseable", runnerVersion };
2426
+ }
2427
+ if (!semverNewer(server2, pinned)) {
2428
+ return {
2429
+ refreshed: false,
2430
+ reason: runnerVersion === serverVersion ? "current" : "downgrade",
2431
+ runnerVersion
2432
+ };
2288
2433
  }
2289
2434
  }
2290
- return ok2(await remote.memorySync(request));
2291
- } catch (error) {
2292
- return err2("sync_failed", errorDetail(error));
2435
+ installHookRunner(sourceBundle, home, serverVersion);
2436
+ return { refreshed: true, reason: "updated", runnerVersion };
2437
+ } catch {
2438
+ return { refreshed: false, reason: "error" };
2293
2439
  }
2294
2440
  }
2295
- async function scanRepository(remote, repository, requested, provider, agentId) {
2296
- const files = [
2297
- ...new Set(
2298
- requested.flatMap(
2299
- (candidate) => markdownFiles(safePath(repository.repoRoot, candidate))
2300
- )
2301
- )
2302
- ].sort();
2303
- const results = [];
2304
- for (const absolute of files) {
2305
- const file = fileDescriptor(repository.repoRoot, absolute);
2306
- const status = await remote.memorySync({
2307
- action: "scan",
2308
- provider,
2309
- repo_id: repository.repoId,
2310
- logical_path: file.logical_path,
2311
- content_hash: file.content_hash,
2312
- file_present: true,
2313
- agent_id: agentId
2314
- });
2315
- results.push({
2316
- logical_path: file.logical_path,
2317
- content_hash: file.content_hash,
2318
- bytes: file.bytes,
2319
- status
2320
- });
2441
+ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2442
+ const refreshed = [];
2443
+ const scopes = ["user", "project"];
2444
+ for (const scope of scopes) {
2445
+ try {
2446
+ const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
2447
+ if (!import_node_fs4.default.existsSync(file)) continue;
2448
+ if (!import_node_fs4.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2449
+ installHooks("claude-code", scope, projectDir, void 0, homeDir);
2450
+ refreshed.push(file);
2451
+ } catch {
2452
+ }
2321
2453
  }
2454
+ return refreshed;
2455
+ }
2456
+ function versionSkewNote(serverVersion, homeDir) {
2457
+ const runnerVersion = installedHookRunnerVersion(homeDir);
2458
+ if (!runnerVersion || runnerVersion === serverVersion) return void 0;
2459
+ 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).`;
2460
+ }
2461
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os4.default.homedir(), ".codex"), homeDir) {
2462
+ if (harness === "codex") {
2463
+ return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
2464
+ }
2465
+ return scope === "user" ? import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), ".claude", "settings.json") : import_node_path4.default.join(projectDir, ".claude", "settings.json");
2466
+ }
2467
+ function readSettings(file) {
2468
+ if (!import_node_fs4.default.existsSync(file)) return {};
2469
+ return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
2470
+ }
2471
+ function ownedHook(harness, timeout, additionalContextLimit) {
2322
2472
  return {
2323
- repo_id: repository.repoId,
2324
- root: ".",
2325
- files: results
2473
+ type: "command",
2474
+ command: command(harness),
2475
+ ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
2476
+ timeout,
2477
+ statusMessage: "Syncing MindGraph session context",
2478
+ ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
2326
2479
  };
2327
2480
  }
2328
- var import_node_crypto5, import_node_fs5, import_node_path5, ok2, err2, SYNC_TOOL;
2329
- var init_sync_tool = __esm({
2330
- "src/sync-tool.ts"() {
2331
- "use strict";
2332
- import_node_crypto5 = __toESM(require("crypto"));
2333
- import_node_fs5 = __toESM(require("fs"));
2334
- import_node_path5 = __toESM(require("path"));
2335
- init_codegraph();
2336
- init_error_detail();
2337
- ok2 = (data) => ({
2338
- content: [{ type: "text", text: JSON.stringify(data) }]
2339
- });
2340
- err2 = (code, message) => ({
2341
- content: [{ type: "text", text: JSON.stringify({ code, error: message }) }],
2342
- isError: true
2343
- });
2344
- SYNC_TOOL = {
2345
- name: "mindgraph_sync",
2346
- description: "Transactional one-way project memory-file sync. scan/status are reads; begin/record/finalize/abandon are writes. Discovery and hashing are automatic; semantic assertions are deliberately supplied through record. This never writes graph state back to files.",
2347
- annotations: {
2348
- readOnlyHint: false,
2349
- destructiveHint: false,
2350
- idempotentHint: true,
2351
- openWorldHint: true
2352
- },
2353
- inputSchema: {
2354
- type: "object",
2355
- properties: {
2356
- action: {
2357
- type: "string",
2358
- enum: ["scan", "begin", "record", "finalize", "status", "abandon"]
2359
- },
2360
- paths: { type: "array", items: { type: "string" } },
2361
- workspace: {
2362
- type: "boolean",
2363
- description: "For scan only: fan out across repositories explicitly listed in .mindgraph/workspace.json"
2364
- },
2365
- repo: {
2366
- type: "string",
2367
- description: "Configured repository id, name, or root"
2368
- },
2369
- logical_path: { type: "string" },
2370
- content: { type: "string" },
2371
- content_hash: { type: "string" },
2372
- source_uid: { type: "string" },
2481
+ function desiredEntries(harness) {
2482
+ const common = {
2483
+ SessionStart: [
2484
+ {
2485
+ matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
2486
+ // B7 live acceptance caught cold SessionStart calls crossing the
2487
+ // inherited 20s edge in both harnesses: the hooks correctly failed
2488
+ // open, but no brief reached the model. The same installed runner
2489
+ // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
2490
+ // The declared Codex context limit must not undercut the brief's own
2491
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
2492
+ // truncation the renderer knew nothing about.
2493
+ hooks: [
2494
+ ownedHook(
2495
+ harness,
2496
+ 30,
2497
+ harness === "codex" ? 1e4 : void 0
2498
+ )
2499
+ ]
2500
+ }
2501
+ ],
2502
+ PreToolUse: [
2503
+ {
2504
+ matcher: "mcp__mindgraph__.*",
2505
+ // Repository resolution over a large multi-repo workspace plus three
2506
+ // git probes can exceed 3s on the first (uncached) call; a killed
2507
+ // PreToolUse means the call goes through untagged.
2508
+ hooks: [ownedHook(harness, 5)]
2509
+ }
2510
+ ],
2511
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
2512
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
2513
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
2514
+ Stop: [{ hooks: [ownedHook(harness, 5)] }],
2515
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
2516
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
2517
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
2518
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
2519
+ // map), so the codex adapter instead makes at most ONE call there.
2520
+ SessionEnd: [
2521
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
2522
+ ]
2523
+ };
2524
+ return harness === "claude-code" ? {
2525
+ ...common,
2526
+ TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
2527
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
2528
+ } : common;
2529
+ }
2530
+ function hookCommand(value) {
2531
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2532
+ const command2 = value.command;
2533
+ return typeof command2 === "string" ? command2 : void 0;
2534
+ }
2535
+ function entryHasOwnedHook(value) {
2536
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2537
+ const hooks = value.hooks;
2538
+ return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2539
+ }
2540
+ function writeSettings(file, settings) {
2541
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2542
+ const temporary = `${file}.${process.pid}.${import_node_crypto5.default.randomUUID()}.tmp`;
2543
+ import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2544
+ `, {
2545
+ mode: 384
2546
+ });
2547
+ import_node_fs4.default.renameSync(temporary, file);
2548
+ }
2549
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
2550
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
2551
+ const settings = readSettings(file);
2552
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2553
+ let added = 0;
2554
+ let updated = 0;
2555
+ for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
2556
+ const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
2557
+ const kept = current.filter((entry) => !entryHasOwnedHook(entry));
2558
+ const hadOwned = kept.length !== current.length;
2559
+ for (const entry of wanted) {
2560
+ kept.push(entry);
2561
+ if (hadOwned) updated += 1;
2562
+ else added += 1;
2563
+ }
2564
+ hooks[event] = kept;
2565
+ }
2566
+ settings.hooks = hooks;
2567
+ writeSettings(file, settings);
2568
+ return { path: file, added, updated };
2569
+ }
2570
+ function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2571
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2572
+ if (!import_node_fs4.default.existsSync(file)) return { path: file, removed: 0 };
2573
+ const settings = readSettings(file);
2574
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2575
+ let removed = 0;
2576
+ for (const [event, value] of Object.entries(hooks)) {
2577
+ if (!Array.isArray(value)) continue;
2578
+ const retainedEntries = [];
2579
+ for (const rawEntry of value) {
2580
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
2581
+ retainedEntries.push(rawEntry);
2582
+ continue;
2583
+ }
2584
+ const entry = { ...rawEntry };
2585
+ if (!Array.isArray(entry.hooks)) {
2586
+ retainedEntries.push(entry);
2587
+ continue;
2588
+ }
2589
+ const retainedHooks = entry.hooks.filter((hook) => {
2590
+ const owned = hookCommand(hook)?.includes(OWNER_MARKER) || false;
2591
+ if (owned) removed += 1;
2592
+ return !owned;
2593
+ });
2594
+ if (retainedHooks.length > 0) {
2595
+ entry.hooks = retainedHooks;
2596
+ retainedEntries.push(entry);
2597
+ }
2598
+ }
2599
+ if (retainedEntries.length > 0) hooks[event] = retainedEntries;
2600
+ else delete hooks[event];
2601
+ }
2602
+ settings.hooks = hooks;
2603
+ writeSettings(file, settings);
2604
+ return { path: file, removed };
2605
+ }
2606
+ function installClaudeHooks(scope, projectDir = process.cwd()) {
2607
+ return installHooks("claude-code", scope, projectDir);
2608
+ }
2609
+ function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
2610
+ return uninstallHooks("claude-code", scope, projectDir);
2611
+ }
2612
+ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2613
+ return installHooks("codex", scope, projectDir, codexHomeDir);
2614
+ }
2615
+ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2616
+ return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2617
+ }
2618
+ var import_node_fs4, import_node_os4, import_node_path4, import_node_crypto5, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE;
2619
+ var init_hook_installer = __esm({
2620
+ "src/hook-installer.ts"() {
2621
+ "use strict";
2622
+ import_node_fs4 = __toESM(require("fs"));
2623
+ import_node_os4 = __toESM(require("os"));
2624
+ import_node_path4 = __toESM(require("path"));
2625
+ import_node_crypto5 = __toESM(require("crypto"));
2626
+ OWNER_MARKER = "--owner mindgraph";
2627
+ RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2628
+ RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
2629
+ }
2630
+ });
2631
+
2632
+ // src/sync-tool.ts
2633
+ function safePath(root, candidate) {
2634
+ const absolute = import_node_path5.default.resolve(root, candidate);
2635
+ const relative2 = import_node_path5.default.relative(root, absolute);
2636
+ if (relative2.startsWith("..") || import_node_path5.default.isAbsolute(relative2)) {
2637
+ throw new Error(`path escapes repository root: ${candidate}`);
2638
+ }
2639
+ if (import_node_fs5.default.existsSync(absolute)) {
2640
+ if (!pathContains(realPathOf(root), realPathOf(absolute))) {
2641
+ throw new Error(`path escapes repository root via symlink: ${candidate}`);
2642
+ }
2643
+ }
2644
+ return absolute;
2645
+ }
2646
+ function realPathOf(target) {
2647
+ try {
2648
+ return import_node_fs5.default.realpathSync(target);
2649
+ } catch {
2650
+ return import_node_path5.default.resolve(target);
2651
+ }
2652
+ }
2653
+ function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
2654
+ if (!import_node_fs5.default.existsSync(target)) return [];
2655
+ const real = realPathOf(target);
2656
+ if (!pathContains(realRoot, real)) return [];
2657
+ const stat = import_node_fs5.default.statSync(target);
2658
+ if (stat.isFile()) return target.endsWith(".md") ? [target] : [];
2659
+ if (!stat.isDirectory() || visited.has(real)) return [];
2660
+ visited.add(real);
2661
+ return import_node_fs5.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path5.default.join(target, entry.name), realRoot, visited));
2662
+ }
2663
+ function fileDescriptor(root, absolute) {
2664
+ const content = import_node_fs5.default.readFileSync(absolute, "utf8");
2665
+ return {
2666
+ logical_path: import_node_path5.default.relative(root, absolute).split(import_node_path5.default.sep).join("/"),
2667
+ content_hash: import_node_crypto6.default.createHash("sha256").update(content).digest("hex"),
2668
+ bytes: Buffer.byteLength(content),
2669
+ content
2670
+ };
2671
+ }
2672
+ async function handleSyncTool(client2, args) {
2673
+ try {
2674
+ const action = String(args.action || "");
2675
+ const context = args.invocation_context || {};
2676
+ const adapter = new CodegraphAdapter({ cwd: invocationCwd(args) });
2677
+ const requestedRepo = typeof args.repo === "string" ? args.repo : typeof args.repo_id === "string" ? args.repo_id : context.repoId;
2678
+ const repository = await adapter.resolveRepository(requestedRepo);
2679
+ const root = repository.repoRoot;
2680
+ const repoId = repository.repoId;
2681
+ const provider = String(args.provider || "claude_project");
2682
+ const remote = client2;
2683
+ if (action === "scan") {
2684
+ const repositories = args.workspace === true ? await adapter.resolveRepositories() : [repository];
2685
+ if (args.workspace === true && repositories.length === 0) {
2686
+ return err2(
2687
+ "workspace_not_configured",
2688
+ `workspace scan found no declared repositories: ${adapter.workspaceDiagnostic()}`
2689
+ );
2690
+ }
2691
+ const requested = Array.isArray(args.paths) && args.paths.length > 0 ? args.paths.map(String) : [".claude/agent-memory", "memory"];
2692
+ const scans = [];
2693
+ for (const selected of repositories) {
2694
+ scans.push(
2695
+ await scanRepository(
2696
+ remote,
2697
+ selected,
2698
+ requested,
2699
+ provider,
2700
+ args.agent_id
2701
+ )
2702
+ );
2703
+ }
2704
+ if (args.workspace === true) {
2705
+ return ok2({
2706
+ provider,
2707
+ workspace: true,
2708
+ repositories: scans,
2709
+ one_way: "files_to_graph"
2710
+ });
2711
+ }
2712
+ return ok2(scans[0]);
2713
+ }
2714
+ const request = {
2715
+ ...args,
2716
+ provider,
2717
+ repo_id: repoId
2718
+ };
2719
+ delete request.invocation_context;
2720
+ delete request.paths;
2721
+ delete request.repo;
2722
+ delete request.workspace;
2723
+ if (action === "begin" && !request.content) {
2724
+ if (!request.logical_path) {
2725
+ return err2("missing_field", "logical_path is required for begin");
2726
+ }
2727
+ const file = fileDescriptor(
2728
+ root,
2729
+ safePath(root, String(request.logical_path))
2730
+ );
2731
+ request.content = file.content;
2732
+ request.content_hash ||= file.content_hash;
2733
+ }
2734
+ if (action === "status" && request.logical_path && request.file_present !== false) {
2735
+ const absolute = safePath(root, String(request.logical_path));
2736
+ request.file_present = import_node_fs5.default.existsSync(absolute);
2737
+ if (request.file_present && !request.content_hash) {
2738
+ request.content_hash = fileDescriptor(root, absolute).content_hash;
2739
+ }
2740
+ }
2741
+ return ok2(await remote.memorySync(request));
2742
+ } catch (error) {
2743
+ return err2("sync_failed", errorDetail(error));
2744
+ }
2745
+ }
2746
+ async function scanRepository(remote, repository, requested, provider, agentId) {
2747
+ const realRoot = realPathOf(repository.repoRoot);
2748
+ const files = [
2749
+ ...new Set(
2750
+ requested.flatMap(
2751
+ (candidate) => markdownFiles(safePath(repository.repoRoot, candidate), realRoot)
2752
+ )
2753
+ )
2754
+ ].sort();
2755
+ const results = [];
2756
+ for (const absolute of files) {
2757
+ const file = fileDescriptor(repository.repoRoot, absolute);
2758
+ const status = await remote.memorySync({
2759
+ action: "scan",
2760
+ provider,
2761
+ repo_id: repository.repoId,
2762
+ logical_path: file.logical_path,
2763
+ content_hash: file.content_hash,
2764
+ file_present: true,
2765
+ agent_id: agentId
2766
+ });
2767
+ results.push({
2768
+ logical_path: file.logical_path,
2769
+ content_hash: file.content_hash,
2770
+ bytes: file.bytes,
2771
+ status
2772
+ });
2773
+ }
2774
+ return {
2775
+ repo_id: repository.repoId,
2776
+ root: ".",
2777
+ files: results
2778
+ };
2779
+ }
2780
+ var import_node_crypto6, import_node_fs5, import_node_path5, ok2, err2, SYNC_TOOL;
2781
+ var init_sync_tool = __esm({
2782
+ "src/sync-tool.ts"() {
2783
+ "use strict";
2784
+ import_node_crypto6 = __toESM(require("crypto"));
2785
+ import_node_fs5 = __toESM(require("fs"));
2786
+ import_node_path5 = __toESM(require("path"));
2787
+ init_codegraph();
2788
+ init_error_detail();
2789
+ ok2 = (data) => ({
2790
+ content: [{ type: "text", text: JSON.stringify(data) }]
2791
+ });
2792
+ err2 = (code, message) => ({
2793
+ content: [{ type: "text", text: JSON.stringify({ code, error: message }) }],
2794
+ isError: true
2795
+ });
2796
+ SYNC_TOOL = {
2797
+ name: "mindgraph_sync",
2798
+ description: "Transactional one-way project memory-file sync. scan/status are reads; begin/record/finalize/abandon are writes. Discovery and hashing are automatic; semantic assertions are deliberately supplied through record. This never writes graph state back to files.",
2799
+ annotations: {
2800
+ readOnlyHint: false,
2801
+ destructiveHint: false,
2802
+ idempotentHint: true,
2803
+ openWorldHint: true
2804
+ },
2805
+ inputSchema: {
2806
+ type: "object",
2807
+ properties: {
2808
+ action: {
2809
+ type: "string",
2810
+ enum: ["scan", "begin", "record", "finalize", "status", "abandon"]
2811
+ },
2812
+ paths: { type: "array", items: { type: "string" } },
2813
+ workspace: {
2814
+ type: "boolean",
2815
+ description: "For scan only: fan out across repositories explicitly listed in .mindgraph/workspace.json"
2816
+ },
2817
+ repo: {
2818
+ type: "string",
2819
+ description: "Configured repository id, name, or root"
2820
+ },
2821
+ logical_path: { type: "string" },
2822
+ content: { type: "string" },
2823
+ content_hash: { type: "string" },
2824
+ source_uid: { type: "string" },
2373
2825
  execution_uid: { type: "string" },
2374
2826
  assertion_fingerprint: { type: "string" },
2375
2827
  planned_fingerprints: { type: "array", items: { type: "string" } },
@@ -2730,15 +3182,15 @@ function renderTraversal(response) {
2730
3182
  const labelByUid = /* @__PURE__ */ new Map();
2731
3183
  for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2732
3184
  if (paths.length > 0) {
2733
- const items = paths.map((raw) => obj(raw)).filter((path6) => Boolean(path6)).map((path6, index) => {
2734
- const uids = Array.isArray(path6.node_uids) ? path6.node_uids.map(String) : [];
2735
- const labels = Array.isArray(path6.labels) ? path6.labels.map(String) : Array.isArray(path6.nodes) ? path6.nodes.map((hop) => {
3185
+ const items = paths.map((raw) => obj(raw)).filter((path8) => Boolean(path8)).map((path8, index) => {
3186
+ const uids = Array.isArray(path8.node_uids) ? path8.node_uids.map(String) : [];
3187
+ const labels = Array.isArray(path8.labels) ? path8.labels.map(String) : Array.isArray(path8.nodes) ? path8.nodes.map((hop) => {
2736
3188
  const hopNode = obj(hop);
2737
3189
  return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
2738
3190
  }) : [];
2739
3191
  const route = labels.length > 0 ? labels.join(" \u2192 ") : uids.join(" \u2192 ") || "(path)";
2740
- const cost = num(path6.cost) ?? num(path6.path_cost);
2741
- const confidence = num(path6.path_confidence);
3192
+ const cost = num(path8.cost) ?? num(path8.path_cost);
3193
+ const confidence = num(path8.path_confidence);
2742
3194
  const meta = [
2743
3195
  cost !== void 0 ? `cost ${cost}` : void 0,
2744
3196
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -2971,6 +3423,9 @@ async function handleTool(client2, name, args) {
2971
3423
  case "mindgraph_retrieve":
2972
3424
  result = await handleRetrieve(client2, args);
2973
3425
  break;
3426
+ case "mindgraph_series_query":
3427
+ result = await handleSeriesQuery(client2, args);
3428
+ break;
2974
3429
  case "mindgraph_ingest":
2975
3430
  result = await handleIngest(client2, args);
2976
3431
  break;
@@ -3015,12 +3470,36 @@ async function handleTool(client2, name, args) {
3015
3470
  return err3(errorDetail(e));
3016
3471
  }
3017
3472
  }
3473
+ async function handleSeriesQuery(client2, args) {
3474
+ const action = typeof args.action === "string" ? args.action : "";
3475
+ if (!["list_for_entity", "latest", "window", "aggregate"].includes(action)) {
3476
+ return err3(`Unknown series query action: ${action}`);
3477
+ }
3478
+ if (action === "list_for_entity" && typeof args.entity_uid !== "string") {
3479
+ return err3("entity_uid is required for action='list_for_entity'");
3480
+ }
3481
+ if (action !== "list_for_entity" && typeof args.series_uid !== "string") {
3482
+ return err3(`series_uid is required for action='${action}'`);
3483
+ }
3484
+ if ((action === "window" || action === "aggregate") && (typeof args.from !== "number" || typeof args.to !== "number")) {
3485
+ return err3(`from and to are required for action='${action}'`);
3486
+ }
3487
+ if (action === "aggregate" && (typeof args.bucket !== "string" || typeof args.agg !== "string")) {
3488
+ return err3("bucket and agg are required for action='aggregate'");
3489
+ }
3490
+ return ok3(
3491
+ await client2.series(args)
3492
+ );
3493
+ }
3018
3494
  async function handleCapture(client2, args) {
3019
3495
  const {
3020
3496
  action,
3021
3497
  label,
3022
3498
  summary,
3023
3499
  content,
3500
+ name,
3501
+ description,
3502
+ license,
3024
3503
  session_uid,
3025
3504
  work_uid,
3026
3505
  execution_uid,
@@ -3055,6 +3534,41 @@ async function handleCapture(client2, args) {
3055
3534
  agent_id
3056
3535
  })
3057
3536
  );
3537
+ case "skill": {
3538
+ if (!name) return err3("name is required for action=skill");
3539
+ if (!description) return err3("description is required for action=skill");
3540
+ if (content === void 0) return err3("content is required for action=skill");
3541
+ const hasProvenance = [session_uid, work_uid, execution_uid].some(
3542
+ (uid) => typeof uid === "string" && uid.length > 0
3543
+ ) || Array.isArray(summarizes_uids) && summarizes_uids.some((uid) => typeof uid === "string" && uid.length > 0);
3544
+ if (!hasProvenance) {
3545
+ return err3(
3546
+ "at least one of session_uid, work_uid, execution_uid, or summarizes_uids is required for action=skill"
3547
+ );
3548
+ }
3549
+ return ok3(
3550
+ await client2.distill({
3551
+ label,
3552
+ output_type: "skill",
3553
+ summary,
3554
+ confidence,
3555
+ salience,
3556
+ session_uid,
3557
+ work_uid,
3558
+ execution_uid,
3559
+ idempotency_key,
3560
+ supersedes_uid,
3561
+ summarizes_uids,
3562
+ props: {
3563
+ name,
3564
+ description,
3565
+ content,
3566
+ ...license ? { license } : {}
3567
+ },
3568
+ agent_id
3569
+ })
3570
+ );
3571
+ }
3058
3572
  case "journal": {
3059
3573
  if (!content) return err3("content is required for action=journal");
3060
3574
  const journalProps = { content };
@@ -4042,7 +4556,7 @@ var init_tools = __esm({
4042
4556
  TOOLS = [
4043
4557
  {
4044
4558
  name: "mindgraph_capture",
4045
- 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.",
4559
+ 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.",
4046
4560
  inputSchema: {
4047
4561
  type: "object",
4048
4562
  properties: {
@@ -4055,7 +4569,8 @@ var init_tools = __esm({
4055
4569
  "snippet",
4056
4570
  "concept",
4057
4571
  "journal",
4058
- "lesson"
4572
+ "lesson",
4573
+ "skill"
4059
4574
  ],
4060
4575
  description: "Type of knowledge to capture"
4061
4576
  },
@@ -4069,11 +4584,30 @@ var init_tools = __esm({
4069
4584
  },
4070
4585
  content: {
4071
4586
  type: "string",
4072
- description: "Journal content/body text (for action=journal)"
4587
+ maxLength: 65536,
4588
+ description: "Journal body (action=journal) or complete SKILL.md instructions, at most 64 KiB UTF-8 (action=skill)"
4589
+ },
4590
+ name: {
4591
+ type: "string",
4592
+ minLength: 1,
4593
+ maxLength: 64,
4594
+ pattern: "^(?!.*(?:anthropic|claude))(?!-)(?!.*--)[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$",
4595
+ description: "Stable skill identifier using lowercase a-z, digits, and hyphens (for action=skill)"
4596
+ },
4597
+ description: {
4598
+ type: "string",
4599
+ minLength: 1,
4600
+ maxLength: 1024,
4601
+ description: "When the skill should be used; XML tags are rejected (for action=skill)"
4602
+ },
4603
+ license: {
4604
+ type: "string",
4605
+ description: "Optional skill license (for action=skill)"
4073
4606
  },
4074
4607
  session_uid: {
4075
4608
  type: "string",
4076
- description: "Session UID that produced a lesson (for action=lesson)"
4609
+ minLength: 1,
4610
+ description: "Session UID that produced a lesson or skill"
4077
4611
  },
4078
4612
  work_uid: {
4079
4613
  type: "string",
@@ -4094,7 +4628,8 @@ var init_tools = __esm({
4094
4628
  summarizes_uids: {
4095
4629
  type: "array",
4096
4630
  items: { type: "string" },
4097
- description: "Source node UIDs the lesson was learned from (for action=lesson)"
4631
+ minItems: 1,
4632
+ description: "Source node UIDs the lesson or skill was learned from"
4098
4633
  },
4099
4634
  entity_type: {
4100
4635
  type: "string",
@@ -4161,7 +4696,24 @@ var init_tools = __esm({
4161
4696
  description: "Agent identity"
4162
4697
  }
4163
4698
  },
4164
- required: ["action", "label"]
4699
+ required: ["action", "label"],
4700
+ allOf: [
4701
+ {
4702
+ if: {
4703
+ properties: { action: { const: "skill" } },
4704
+ required: ["action"]
4705
+ },
4706
+ then: {
4707
+ required: ["name", "description", "content"],
4708
+ anyOf: [
4709
+ { required: ["session_uid"] },
4710
+ { required: ["work_uid"] },
4711
+ { required: ["execution_uid"] },
4712
+ { required: ["summarizes_uids"] }
4713
+ ]
4714
+ }
4715
+ }
4716
+ ]
4165
4717
  }
4166
4718
  },
4167
4719
  {
@@ -4747,6 +5299,42 @@ var init_tools = __esm({
4747
5299
  required: ["action"]
4748
5300
  }
4749
5301
  },
5302
+ {
5303
+ name: "mindgraph_series_query",
5304
+ 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.",
5305
+ inputSchema: {
5306
+ type: "object",
5307
+ additionalProperties: false,
5308
+ properties: {
5309
+ action: {
5310
+ type: "string",
5311
+ enum: ["list_for_entity", "latest", "window", "aggregate"]
5312
+ },
5313
+ entity_uid: { type: "string" },
5314
+ series_uid: { type: "string" },
5315
+ from: { type: "number" },
5316
+ to: { type: "number" },
5317
+ cursor: { type: "number" },
5318
+ limit: { type: "number", minimum: 1, maximum: 1e4 },
5319
+ bucket: {
5320
+ type: "string",
5321
+ enum: ["year", "quarter", "month", "week", "day", "hour"]
5322
+ },
5323
+ agg: {
5324
+ type: "string",
5325
+ enum: ["sum", "mean", "min", "max", "count", "std_dev", "last"]
5326
+ },
5327
+ fill: { type: "string", enum: ["none", "null", "prev"] }
5328
+ },
5329
+ required: ["action"]
5330
+ },
5331
+ annotations: {
5332
+ readOnlyHint: true,
5333
+ destructiveHint: false,
5334
+ idempotentHint: true,
5335
+ openWorldHint: false
5336
+ }
5337
+ },
4750
5338
  {
4751
5339
  name: "mindgraph_ingest",
4752
5340
  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.",
@@ -5233,7 +5821,7 @@ function isTransientStatus(status) {
5233
5821
  return status === 408 || status === 425 || status === 429 || status >= 500;
5234
5822
  }
5235
5823
  function sleep(ms) {
5236
- return new Promise((resolve2) => setTimeout(resolve2, ms));
5824
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5237
5825
  }
5238
5826
  function headers(apiKey, orgId) {
5239
5827
  const value = {
@@ -5251,6 +5839,7 @@ function unsupportedResponse(status, body) {
5251
5839
  }
5252
5840
  function toolMutability(toolName, action) {
5253
5841
  if (toolName === "mindgraph_retrieve") return "read";
5842
+ if (toolName === "mindgraph_series_query") return "read";
5254
5843
  if (toolName === "mindgraph_plan") {
5255
5844
  return action && PLAN_READ_ACTIONS.has(action) ? "read" : "write";
5256
5845
  }
@@ -5343,7 +5932,7 @@ async function checkMcpGovernance(toolName, args, config) {
5343
5932
  if (!response) {
5344
5933
  return {
5345
5934
  allowed: false,
5346
- message: `MindGraph governance could not establish permission after ${retryDelays.length + 1} attempts (${lastFailure}). Set MINDGRAPH_GOVERNANCE=off to run this adapter ungoverned.`
5935
+ 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.`
5347
5936
  };
5348
5937
  }
5349
5938
  if (!response.ok) {
@@ -5435,6 +6024,8 @@ var init_governance = __esm({
5435
6024
  UID_FIELDS = [
5436
6025
  "uid",
5437
6026
  "source_uid",
6027
+ "entity_uid",
6028
+ "series_uid",
5438
6029
  "session_uid",
5439
6030
  "work_uid",
5440
6031
  "goal_uid",
@@ -5587,10 +6178,10 @@ async function main() {
5587
6178
  const transport = new import_stdio.StdioServerTransport();
5588
6179
  await server.connect(transport);
5589
6180
  console.error(
5590
- `MindGraph MCP server v${"0.17.0"} running on stdio (${BASE_URL})`
6181
+ `MindGraph MCP server v${"0.19.0"} running on stdio (${BASE_URL})`
5591
6182
  );
5592
6183
  }
5593
- var import_server, import_stdio, import_types, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, server, GOVERNANCE_CONFIG, PROMPTS;
6184
+ var import_server, import_stdio, import_types, import_node_path6, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, RUNNER_REFRESH, HOOK_SKEW_NOTE, server, GOVERNANCE_CONFIG, PROMPTS;
5594
6185
  var init_src = __esm({
5595
6186
  "src/index.ts"() {
5596
6187
  "use strict";
@@ -5603,6 +6194,8 @@ var init_src = __esm({
5603
6194
  init_generated_tools();
5604
6195
  init_governance();
5605
6196
  init_resources();
6197
+ import_node_path6 = __toESM(require("path"));
6198
+ init_hook_installer();
5606
6199
  API_KEY = process.env.MINDGRAPH_API_KEY;
5607
6200
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
5608
6201
  AGENT_ID = process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId();
@@ -5724,10 +6317,42 @@ Capture knowledge when the user shares something worth remembering:
5724
6317
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
5725
6318
  ).join("\n");
5726
6319
  }
6320
+ RUNNER_REFRESH = (() => {
6321
+ try {
6322
+ return refreshHookRunner("0.19.0", import_node_path6.default.join(__dirname, "cli.js"));
6323
+ } catch {
6324
+ return void 0;
6325
+ }
6326
+ })();
6327
+ if (RUNNER_REFRESH?.refreshed) {
6328
+ const refreshedSettings = (() => {
6329
+ try {
6330
+ return refreshOwnedClaudeHooks();
6331
+ } catch {
6332
+ return [];
6333
+ }
6334
+ })();
6335
+ console.error(
6336
+ `[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(", ")}` : "")
6337
+ );
6338
+ }
6339
+ HOOK_SKEW_NOTE = (() => {
6340
+ try {
6341
+ return versionSkewNote("0.19.0");
6342
+ } catch {
6343
+ return void 0;
6344
+ }
6345
+ })();
6346
+ if (HOOK_SKEW_NOTE) {
6347
+ INSTRUCTIONS += `
6348
+
6349
+ ${HOOK_SKEW_NOTE}`;
6350
+ console.error(`[mindgraph-mcp] ${HOOK_SKEW_NOTE}`);
6351
+ }
5727
6352
  server = new import_server.Server(
5728
6353
  {
5729
6354
  name: "mindgraph",
5730
- version: "0.17.0"
6355
+ version: "0.19.0"
5731
6356
  },
5732
6357
  {
5733
6358
  capabilities: {
@@ -5945,8 +6570,8 @@ ${conversation}`
5945
6570
  });
5946
6571
 
5947
6572
  // src/cli.ts
5948
- var fs5 = __toESM(require("fs"));
5949
- var path5 = __toESM(require("path"));
6573
+ var fs6 = __toESM(require("fs"));
6574
+ var path7 = __toESM(require("path"));
5950
6575
  var os4 = __toESM(require("os"));
5951
6576
  var crypto5 = __toESM(require("crypto"));
5952
6577
  var import_child_process = require("child_process");
@@ -5964,6 +6589,8 @@ function parseArgs(argv) {
5964
6589
  let harness;
5965
6590
  let hooks = false;
5966
6591
  let agentId = process.env.MINDGRAPH_AGENT_ID;
6592
+ const positionals = [];
6593
+ let dir;
5967
6594
  for (let i = 0; i < args.length; i++) {
5968
6595
  const arg = args[i];
5969
6596
  switch (arg) {
@@ -5984,6 +6611,9 @@ function parseArgs(argv) {
5984
6611
  case "--project-dir":
5985
6612
  projectDir = args[++i];
5986
6613
  break;
6614
+ case "--dir":
6615
+ dir = args[++i];
6616
+ break;
5987
6617
  case "--harness":
5988
6618
  harness = args[++i];
5989
6619
  break;
@@ -6002,33 +6632,416 @@ function parseArgs(argv) {
6002
6632
  break;
6003
6633
  default:
6004
6634
  if (!arg.startsWith("-")) {
6005
- command2 = arg;
6635
+ if (command2 === "serve") {
6636
+ command2 = arg;
6637
+ } else {
6638
+ positionals.push(arg);
6639
+ }
6640
+ }
6641
+ break;
6642
+ }
6643
+ }
6644
+ return {
6645
+ command: command2,
6646
+ apiKey,
6647
+ baseUrl,
6648
+ scope,
6649
+ projectDir,
6650
+ harness,
6651
+ hooks,
6652
+ agentId,
6653
+ positionals,
6654
+ dir
6655
+ };
6656
+ }
6657
+ function mcpAddFailureOutput(e) {
6658
+ const err5 = e;
6659
+ return `${err5.stdout ?? ""}${err5.stderr ?? ""}`.trim();
6660
+ }
6661
+ function classifyMcpAddFailure(e) {
6662
+ if (e?.code === "ENOENT") return "missing-cli";
6663
+ if (mcpAddFailureOutput(e).toLowerCase().includes("already exists")) {
6664
+ return "already-exists";
6665
+ }
6666
+ return "other";
6667
+ }
6668
+
6669
+ // src/skills-cli.ts
6670
+ var import_node_crypto2 = require("crypto");
6671
+ var fs2 = __toESM(require("fs"));
6672
+ var path2 = __toESM(require("path"));
6673
+ function httpSkillsApi(baseUrl, apiKey, agentId) {
6674
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/action/skill`;
6675
+ const call = async (body) => {
6676
+ const response = await fetch(endpoint, {
6677
+ method: "POST",
6678
+ headers: {
6679
+ "content-type": "application/json",
6680
+ authorization: `Bearer ${apiKey}`
6681
+ },
6682
+ body: JSON.stringify(
6683
+ agentId ? { agent_id: agentId, ...body } : body
6684
+ )
6685
+ });
6686
+ let parsed = {};
6687
+ try {
6688
+ parsed = await response.json();
6689
+ } catch {
6690
+ parsed = { error: `non-JSON response (HTTP ${response.status})` };
6691
+ }
6692
+ return { status: response.status, body: parsed };
6693
+ };
6694
+ return {
6695
+ async list(grantedTo) {
6696
+ const { status, body } = await call({
6697
+ action: "list",
6698
+ ...grantedTo ? { granted_to: grantedTo } : {}
6699
+ });
6700
+ if (status !== 200) {
6701
+ throw new Error(
6702
+ `skill list failed (HTTP ${status}): ${String(body.error ?? "")}`
6703
+ );
6704
+ }
6705
+ return body.skills ?? [];
6706
+ },
6707
+ async get(uid) {
6708
+ const { status, body } = await call({ action: "get", uid });
6709
+ if (status === 404) return null;
6710
+ if (status !== 200) {
6711
+ throw new Error(
6712
+ `skill get failed (HTTP ${status}): ${String(body.error ?? "")}`
6713
+ );
6714
+ }
6715
+ const props = body.props ?? {};
6716
+ return {
6717
+ uid: String(body.uid ?? uid),
6718
+ name: String(props.name ?? ""),
6719
+ description: String(props.description ?? ""),
6720
+ content: String(props.content ?? ""),
6721
+ license: props.license ?? null,
6722
+ status: String(props.status ?? "candidate"),
6723
+ status_reason: props.status_reason ?? null,
6724
+ content_hash: String(props.content_hash ?? ""),
6725
+ version: Number(body.version ?? 0)
6726
+ };
6727
+ },
6728
+ send: call
6729
+ };
6730
+ }
6731
+ function isValidSkillName(name) {
6732
+ return name.length >= 1 && name.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
6733
+ }
6734
+ function renderSkillMd(skill) {
6735
+ const lines = [
6736
+ "---",
6737
+ `name: ${JSON.stringify(skill.name)}`,
6738
+ `description: ${JSON.stringify(skill.description)}`
6739
+ ];
6740
+ if (skill.license) {
6741
+ lines.push(`license: ${JSON.stringify(skill.license)}`);
6742
+ }
6743
+ lines.push(
6744
+ "metadata:",
6745
+ ` mindgraph-uid: ${JSON.stringify(skill.uid)}`,
6746
+ ` mindgraph-version: ${JSON.stringify(String(skill.version))}`,
6747
+ ` mindgraph-content-hash: ${JSON.stringify(skill.content_hash)}`,
6748
+ "---",
6749
+ "",
6750
+ skill.content
6751
+ );
6752
+ return `${lines.join("\n")}
6753
+ `;
6754
+ }
6755
+ function scalar(raw) {
6756
+ const trimmed = raw.trim();
6757
+ try {
6758
+ const parsed = JSON.parse(trimmed);
6759
+ if (typeof parsed === "string") return parsed;
6760
+ } catch {
6761
+ }
6762
+ return trimmed;
6763
+ }
6764
+ function parseSkillMd(text) {
6765
+ if (!text.startsWith("---\n")) return null;
6766
+ const end = text.indexOf("\n---\n", 4);
6767
+ if (end < 0) return null;
6768
+ const frontmatter = text.slice(4, end + 1);
6769
+ const body = text.slice(end + 5).replace(/^\n/, "").replace(/\n$/, "");
6770
+ const entries = [];
6771
+ for (const line of frontmatter.split("\n")) {
6772
+ const top = /^([A-Za-z0-9_-]+):(.*)$/.exec(line);
6773
+ if (top) {
6774
+ entries.push({ key: top[1], raw: top[2] });
6775
+ } else if (entries.length > 0) {
6776
+ entries[entries.length - 1].raw += `
6777
+ ${line}`;
6778
+ }
6779
+ }
6780
+ const result = { unknown: {}, body };
6781
+ for (const { key, raw } of entries) {
6782
+ switch (key) {
6783
+ case "name":
6784
+ result.name = scalar(raw);
6785
+ break;
6786
+ case "description":
6787
+ result.description = scalar(raw);
6788
+ break;
6789
+ case "license":
6790
+ result.license = scalar(raw);
6791
+ break;
6792
+ case "metadata": {
6793
+ const uid = /mindgraph-uid:\s*(.+)/.exec(raw);
6794
+ const version = /mindgraph-version:\s*(.+)/.exec(raw);
6795
+ const hash = /mindgraph-content-hash:\s*(.+)/.exec(raw);
6796
+ if (uid) {
6797
+ result.pin = {
6798
+ uid: scalar(uid[1]),
6799
+ version: version ? scalar(version[1]) : void 0,
6800
+ contentHash: hash ? scalar(hash[1]) : void 0
6801
+ };
6802
+ } else if (raw.trim().length > 0) {
6803
+ result.unknown[key] = raw;
6006
6804
  }
6007
6805
  break;
6806
+ }
6807
+ default:
6808
+ result.unknown[key] = raw;
6809
+ break;
6810
+ }
6811
+ }
6812
+ return result;
6813
+ }
6814
+ var STATE_FILE = ".mindgraph-skills-state.json";
6815
+ function sha256(text) {
6816
+ return (0, import_node_crypto2.createHash)("sha256").update(text).digest("hex");
6817
+ }
6818
+ function loadState(dir) {
6819
+ try {
6820
+ return JSON.parse(
6821
+ fs2.readFileSync(path2.join(dir, STATE_FILE), "utf8")
6822
+ );
6823
+ } catch {
6824
+ return {};
6825
+ }
6826
+ }
6827
+ function saveState(dir, state) {
6828
+ fs2.writeFileSync(
6829
+ path2.join(dir, STATE_FILE),
6830
+ `${JSON.stringify(state, null, 2)}
6831
+ `
6832
+ );
6833
+ }
6834
+ function isSymlink(target) {
6835
+ try {
6836
+ return fs2.lstatSync(target).isSymbolicLink();
6837
+ } catch {
6838
+ return false;
6839
+ }
6840
+ }
6841
+ function safeSkillPaths(dir, name) {
6842
+ if (!isValidSkillName(name)) {
6843
+ return `invalid skill name ${JSON.stringify(name)}`;
6844
+ }
6845
+ const root = path2.resolve(dir);
6846
+ const skillDir = path2.resolve(root, name);
6847
+ if (skillDir !== path2.join(root, name) || !skillDir.startsWith(root + path2.sep)) {
6848
+ return "resolved path escapes the target directory";
6849
+ }
6850
+ if (isSymlink(skillDir)) {
6851
+ return "target directory is a symlink";
6852
+ }
6853
+ const skillFile = path2.join(skillDir, "SKILL.md");
6854
+ if (isSymlink(skillFile)) {
6855
+ return "target SKILL.md is a symlink";
6856
+ }
6857
+ return { skillDir, skillFile };
6858
+ }
6859
+ async function skillsPull(api, dir, grantedTo) {
6860
+ const report = {
6861
+ written: [],
6862
+ unchanged: [],
6863
+ locallyEdited: [],
6864
+ conflicts: [],
6865
+ removed: [],
6866
+ refused: []
6867
+ };
6868
+ fs2.mkdirSync(dir, { recursive: true });
6869
+ const state = loadState(dir);
6870
+ const summaries = await api.list(grantedTo);
6871
+ const fetched = /* @__PURE__ */ new Set();
6872
+ for (const summary of summaries) {
6873
+ const detail = await api.get(summary.uid);
6874
+ if (!detail || detail.status !== "published") continue;
6875
+ fetched.add(detail.name);
6876
+ const paths = safeSkillPaths(dir, detail.name);
6877
+ if (typeof paths === "string") {
6878
+ report.refused.push({ name: detail.name, reason: paths });
6879
+ continue;
6880
+ }
6881
+ const { skillDir, skillFile } = paths;
6882
+ const rendered2 = renderSkillMd(detail);
6883
+ let existing = null;
6884
+ try {
6885
+ existing = fs2.readFileSync(skillFile, "utf8");
6886
+ } catch {
6887
+ existing = null;
6888
+ }
6889
+ if (existing === rendered2) {
6890
+ state[detail.name] = {
6891
+ uid: detail.uid,
6892
+ content_hash: detail.content_hash,
6893
+ file_sha256: sha256(rendered2)
6894
+ };
6895
+ report.unchanged.push(detail.name);
6896
+ continue;
6897
+ }
6898
+ const recorded = state[detail.name];
6899
+ const locallyModified = existing !== null && (!recorded || sha256(existing) !== recorded.file_sha256);
6900
+ if (locallyModified) {
6901
+ if (recorded && recorded.content_hash === detail.content_hash) {
6902
+ report.locallyEdited.push(detail.name);
6903
+ continue;
6904
+ }
6905
+ fs2.writeFileSync(`${skillFile}.conflict`, rendered2);
6906
+ report.conflicts.push(detail.name);
6907
+ continue;
6008
6908
  }
6909
+ fs2.mkdirSync(skillDir, { recursive: true });
6910
+ fs2.writeFileSync(skillFile, rendered2);
6911
+ state[detail.name] = {
6912
+ uid: detail.uid,
6913
+ content_hash: detail.content_hash,
6914
+ file_sha256: sha256(rendered2)
6915
+ };
6916
+ report.written.push(detail.name);
6009
6917
  }
6010
- return { command: command2, apiKey, baseUrl, scope, projectDir, harness, hooks, agentId };
6011
- }
6012
- function mcpAddFailureOutput(e) {
6013
- const err5 = e;
6014
- return `${err5.stdout ?? ""}${err5.stderr ?? ""}`.trim();
6918
+ for (const [name, recorded] of Object.entries(state)) {
6919
+ if (fetched.has(name)) continue;
6920
+ const paths = safeSkillPaths(dir, name);
6921
+ if (typeof paths === "string") {
6922
+ delete state[name];
6923
+ continue;
6924
+ }
6925
+ const { skillDir, skillFile } = paths;
6926
+ let statusReason = null;
6927
+ try {
6928
+ const gone = await api.get(recorded.uid);
6929
+ statusReason = gone?.status_reason ?? (gone ? `status: ${gone.status}` : null);
6930
+ } catch {
6931
+ statusReason = null;
6932
+ }
6933
+ let existing = null;
6934
+ try {
6935
+ existing = fs2.readFileSync(skillFile, "utf8");
6936
+ } catch {
6937
+ existing = null;
6938
+ }
6939
+ if (existing === null) {
6940
+ delete state[name];
6941
+ continue;
6942
+ }
6943
+ const locallyModified = sha256(existing) !== recorded.file_sha256;
6944
+ if (locallyModified) {
6945
+ fs2.renameSync(skillFile, `${skillFile}.conflict`);
6946
+ report.removed.push({ name, statusReason, kept: true });
6947
+ } else {
6948
+ fs2.rmSync(skillDir, { recursive: true, force: true });
6949
+ report.removed.push({ name, statusReason, kept: false });
6950
+ }
6951
+ delete state[name];
6952
+ }
6953
+ saveState(dir, state);
6954
+ return report;
6015
6955
  }
6016
- function classifyMcpAddFailure(e) {
6017
- if (e?.code === "ENOENT") return "missing-cli";
6018
- if (mcpAddFailureOutput(e).toLowerCase().includes("already exists")) {
6019
- return "already-exists";
6956
+ async function skillsPush(api, skillPath) {
6957
+ const resolved = path2.resolve(skillPath);
6958
+ const file = resolved.endsWith("SKILL.md") ? resolved : path2.join(resolved, "SKILL.md");
6959
+ if (isSymlink(file)) {
6960
+ return { outcome: "error", message: `${file} is a symlink; refusing` };
6020
6961
  }
6021
- return "other";
6962
+ let text;
6963
+ try {
6964
+ text = fs2.readFileSync(file, "utf8");
6965
+ } catch {
6966
+ return { outcome: "error", message: `could not read ${file}` };
6967
+ }
6968
+ const parsed = parseSkillMd(text);
6969
+ if (!parsed) {
6970
+ return {
6971
+ outcome: "error",
6972
+ message: "SKILL.md must start with a --- frontmatter block"
6973
+ };
6974
+ }
6975
+ if (!parsed.name || !parsed.description) {
6976
+ return {
6977
+ outcome: "error",
6978
+ message: "SKILL.md frontmatter requires name and description"
6979
+ };
6980
+ }
6981
+ if (parsed.pin?.uid) {
6982
+ const { status: status2, body: body2 } = await api.send({
6983
+ action: "update",
6984
+ uid: parsed.pin.uid,
6985
+ name: parsed.name,
6986
+ description: parsed.description,
6987
+ content: parsed.body,
6988
+ ...parsed.license ? { license: parsed.license } : {}
6989
+ });
6990
+ if (status2 === 200) {
6991
+ return {
6992
+ outcome: "updated",
6993
+ message: `updated candidate ${parsed.name} (${parsed.pin.uid})`,
6994
+ uid: parsed.pin.uid
6995
+ };
6996
+ }
6997
+ return {
6998
+ outcome: status2 === 409 ? "conflict" : "error",
6999
+ message: `update failed (HTTP ${status2}): ${String(body2.error ?? "")}`,
7000
+ existingUid: body2.existing_uid ?? void 0,
7001
+ existingStatus: body2.existing_status ?? void 0
7002
+ };
7003
+ }
7004
+ const { status, body } = await api.send({
7005
+ action: "create",
7006
+ skill_source: "imported",
7007
+ name: parsed.name,
7008
+ description: parsed.description,
7009
+ content: parsed.body,
7010
+ ...parsed.license ? { license: parsed.license } : {},
7011
+ ...Object.keys(parsed.unknown).length > 0 ? { imported_frontmatter: parsed.unknown } : {},
7012
+ import_origin: `skills-push:${path2.basename(resolved)}`
7013
+ });
7014
+ if (status === 200 && body.replayed === true) {
7015
+ return {
7016
+ outcome: "replayed",
7017
+ message: `unchanged: ${parsed.name} already imported (${String(body.uid)})`,
7018
+ uid: String(body.uid)
7019
+ };
7020
+ }
7021
+ if (status === 200) {
7022
+ return {
7023
+ outcome: "created",
7024
+ message: `imported ${parsed.name} as a candidate (${String(body.uid)}) \u2014 review and publish in the dashboard`,
7025
+ uid: String(body.uid)
7026
+ };
7027
+ }
7028
+ return {
7029
+ outcome: status === 409 ? "conflict" : "error",
7030
+ message: `import failed (HTTP ${status}): ${String(body.error ?? "")}`,
7031
+ existingUid: body.existing_uid ?? void 0,
7032
+ existingStatus: body.existing_status ?? void 0
7033
+ };
6022
7034
  }
6023
7035
 
6024
7036
  // src/hook-core.ts
6025
- var import_node_crypto3 = __toESM(require("crypto"));
7037
+ var import_node_crypto4 = __toESM(require("crypto"));
6026
7038
  var import_node_fs3 = __toESM(require("fs"));
6027
7039
  var import_node_os3 = __toESM(require("os"));
6028
7040
  var import_node_path3 = __toESM(require("path"));
6029
7041
  var import_node_child_process2 = require("child_process");
6030
7042
  init_codegraph();
6031
7043
  init_code_tool();
7044
+ init_error_detail();
6032
7045
  init_hook_env();
6033
7046
  var NOOP = { kind: "noop" };
6034
7047
  function defaultLedger(sessionId, options) {
@@ -6051,7 +7064,7 @@ function runtimeDir(options) {
6051
7064
  }
6052
7065
  function ledgerPath(sessionId, options) {
6053
7066
  const harness = "harness" in options ? options.harness : "claude-code";
6054
- const key = import_node_crypto3.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
7067
+ const key = import_node_crypto4.default.createHash("sha256").update(`${harness}\0${sessionId}`).digest("hex");
6055
7068
  return import_node_path3.default.join(runtimeDir(options), `${key}.json`);
6056
7069
  }
6057
7070
  function waitBriefly(ms) {
@@ -6093,7 +7106,7 @@ function withLedger(sessionId, options, update) {
6093
7106
  }
6094
7107
  const result = update(ledger);
6095
7108
  ledger.updatedAt = Date.now();
6096
- const temporary = `${file}.${process.pid}.${import_node_crypto3.default.randomUUID()}.tmp`;
7109
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
6097
7110
  import_node_fs3.default.writeFileSync(temporary, `${JSON.stringify(ledger)}
6098
7111
  `, { mode: 384 });
6099
7112
  import_node_fs3.default.renameSync(temporary, file);
@@ -6360,6 +7373,51 @@ function isOwnPriorWork(brief, agentId, harness) {
6360
7373
  const owner = string(object(brief?.lease)?.lease_owner_agent_id);
6361
7374
  return owner === agentId || owner === harness;
6362
7375
  }
7376
+ function hasMindgraphServer(servers) {
7377
+ if (!servers || typeof servers !== "object") return false;
7378
+ for (const [name, entry] of Object.entries(servers)) {
7379
+ if (name === "mindgraph") return true;
7380
+ const cfg = entry && typeof entry === "object" ? entry : void 0;
7381
+ const text = [
7382
+ String(cfg?.command ?? ""),
7383
+ Array.isArray(cfg?.args) ? cfg.args.join(" ") : "",
7384
+ String(cfg?.url ?? "")
7385
+ ].join(" ");
7386
+ if (text.includes("mindgraph-mcp")) return true;
7387
+ }
7388
+ return false;
7389
+ }
7390
+ function pathIsWithin(ancestor, descendant) {
7391
+ const base = import_node_path3.default.resolve(ancestor);
7392
+ const target = import_node_path3.default.resolve(descendant);
7393
+ return target === base || target.startsWith(base + import_node_path3.default.sep);
7394
+ }
7395
+ function claudeMcpRegistrationNote(input, options) {
7396
+ if (options.harness !== "claude-code") return void 0;
7397
+ if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
7398
+ try {
7399
+ const configPath = options.claudeConfigPath || import_node_path3.default.join(import_node_os3.default.homedir(), ".claude.json");
7400
+ if (!import_node_fs3.default.existsSync(configPath)) return void 0;
7401
+ const config = JSON.parse(import_node_fs3.default.readFileSync(configPath, "utf8"));
7402
+ if (hasMindgraphServer(config.mcpServers)) return void 0;
7403
+ const cwd = import_node_path3.default.resolve(input.cwd || ".");
7404
+ for (const [projectDir, project] of Object.entries(config.projects || {})) {
7405
+ if (!pathIsWithin(projectDir, cwd)) continue;
7406
+ if (hasMindgraphServer(project?.mcpServers)) return void 0;
7407
+ }
7408
+ for (let dir = cwd; ; dir = import_node_path3.default.dirname(dir)) {
7409
+ const candidate = import_node_path3.default.join(dir, ".mcp.json");
7410
+ if (import_node_fs3.default.existsSync(candidate)) {
7411
+ const parsed = JSON.parse(import_node_fs3.default.readFileSync(candidate, "utf8"));
7412
+ if (hasMindgraphServer(parsed.mcpServers)) return void 0;
7413
+ }
7414
+ if (dir === import_node_path3.default.dirname(dir)) break;
7415
+ }
7416
+ } catch {
7417
+ return void 0;
7418
+ }
7419
+ return "NOTE: no MindGraph MCP server appears to be registered for Claude Code, so this brief's continuity actions (claim, checkpoint, capture) have no mcp__mindgraph__* tools to run through and durable memory cannot be written this session. Tell the user to run: npx -y mindgraph-mcp@latest install-code \u2014 then start a new session.";
7420
+ }
6363
7421
  async function sessionStart(input, client2, options) {
6364
7422
  const agentId = resolveAgentId(options);
6365
7423
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
@@ -6449,7 +7507,14 @@ async function sessionStart(input, client2, options) {
6449
7507
  claimFailed: claimFailed && !stillHeld,
6450
7508
  scopeNote
6451
7509
  });
6452
- const briefHash = import_node_crypto3.default.createHash("sha256").update(briefText).digest("hex");
7510
+ const registrationNote = claudeMcpRegistrationNote(input, options);
7511
+ if (registrationNote) {
7512
+ console.error(`mindgraph-hook: ${registrationNote}`);
7513
+ }
7514
+ const contextText = registrationNote ? `${registrationNote}
7515
+
7516
+ ${briefText}` : briefText;
7517
+ const briefHash = import_node_crypto4.default.createHash("sha256").update(contextText).digest("hex");
6453
7518
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
6454
7519
  const changed = ledger.lastBriefHash !== briefHash;
6455
7520
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
@@ -6482,7 +7547,7 @@ async function sessionStart(input, client2, options) {
6482
7547
  ledger.lastBriefHash = briefHash;
6483
7548
  return changed || reinjectCompact;
6484
7549
  });
6485
- return shouldInject ? { kind: "context", context: briefText } : NOOP;
7550
+ return shouldInject ? { kind: "context", context: contextText } : NOOP;
6486
7551
  }
6487
7552
  async function preTool(input, options) {
6488
7553
  const original = { ...input.tool_input || {} };
@@ -6548,7 +7613,7 @@ async function postTool(input, client2, options) {
6548
7613
  ledger.mutatingOperations += 1;
6549
7614
  }
6550
7615
  if (!payload?.error) {
6551
- if (name.includes("mindgraph_capture") && ["lesson", "journal"].includes(action || "") || name.includes("mindgraph_commit") && action === "resolve_decision" || name.includes("mindgraph_plan") && action === "assess_risk") {
7616
+ if (name.includes("mindgraph_capture") && ["lesson", "journal", "skill"].includes(action || "") || name.includes("mindgraph_commit") && action === "resolve_decision" || name.includes("mindgraph_plan") && action === "assess_risk") {
6552
7617
  ledger.memoryWritten = true;
6553
7618
  }
6554
7619
  }
@@ -6589,9 +7654,7 @@ async function postTool(input, client2, options) {
6589
7654
  }
6590
7655
  }
6591
7656
  } else if (aboutLedgerTask) {
6592
- ledger.taskVersion = number(payload?.current_version) ?? ledger.taskVersion;
6593
- ledger.leaseEpoch = number(payload?.current_epoch) ?? ledger.leaseEpoch;
6594
- ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? ledger.leaseExpiresAt;
7657
+ adoptConflictFence(ledger, payload);
6595
7658
  }
6596
7659
  }
6597
7660
  if (ledger.taskUid && ledger.sessionUid && ledger.taskVersion !== void 0 && ledger.leaseEpoch !== void 0 && ledger.leaseExpiresAt !== void 0 && // Only ledgers whose lease state was written by a claim THIS code made
@@ -6648,19 +7711,35 @@ async function postTool(input, client2, options) {
6648
7711
  })
6649
7712
  );
6650
7713
  withLedger(input.session_id, options, (ledger) => {
6651
- if (payloadIsError(response)) return void 0;
6652
7714
  if (ledger.taskUid !== active.taskUid) return void 0;
7715
+ if (payloadIsError(response)) {
7716
+ adoptConflictFence(ledger, response);
7717
+ return void 0;
7718
+ }
6653
7719
  ledger.taskVersion = number(response?.task_version) ?? ledger.taskVersion;
6654
7720
  ledger.leaseEpoch = number(response?.lease_epoch) ?? ledger.leaseEpoch;
6655
7721
  ledger.leaseExpiresAt = number(response?.lease_expires_at) ?? ledger.leaseExpiresAt;
6656
7722
  ledger.lastLeaseRenewalAt = nowSeconds;
6657
7723
  return void 0;
6658
7724
  });
6659
- } catch {
7725
+ } catch (cause) {
7726
+ const conflict = conflictState(cause);
7727
+ if (Object.keys(conflict).length > 0) {
7728
+ withLedger(input.session_id, options, (ledger) => {
7729
+ if (ledger.taskUid !== active.taskUid) return void 0;
7730
+ adoptConflictFence(ledger, conflict);
7731
+ return void 0;
7732
+ });
7733
+ }
6660
7734
  }
6661
7735
  }
6662
7736
  return NOOP;
6663
7737
  }
7738
+ function adoptConflictFence(ledger, payload) {
7739
+ ledger.taskVersion = number(payload?.current_version) ?? ledger.taskVersion;
7740
+ ledger.leaseEpoch = number(payload?.current_epoch) ?? ledger.leaseEpoch;
7741
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? ledger.leaseExpiresAt;
7742
+ }
6664
7743
  function payloadIsError(payload) {
6665
7744
  return Boolean(payload && payload.error);
6666
7745
  }
@@ -6813,195 +7892,19 @@ async function runCodexHook(input, client2, options = {}) {
6813
7892
  }
6814
7893
  }
6815
7894
 
6816
- // src/hook-installer.ts
6817
- var import_node_fs4 = __toESM(require("fs"));
6818
- var import_node_os4 = __toESM(require("os"));
6819
- var import_node_path4 = __toESM(require("path"));
6820
- var import_node_crypto4 = __toESM(require("crypto"));
6821
- var OWNER_MARKER = "--owner mindgraph";
6822
- var RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
6823
- function command(harness) {
6824
- return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
6825
- }
6826
- function windowsCommand(harness) {
6827
- return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
6828
- }
6829
- function installHookRunner(sourceFile, homeDir) {
6830
- const target = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_RELATIVE);
6831
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
6832
- import_node_fs4.default.copyFileSync(sourceFile, target);
6833
- return target;
6834
- }
6835
- function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os4.default.homedir(), ".codex")) {
6836
- if (harness === "codex") {
6837
- return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
6838
- }
6839
- return scope === "user" ? import_node_path4.default.join(import_node_os4.default.homedir(), ".claude", "settings.json") : import_node_path4.default.join(projectDir, ".claude", "settings.json");
6840
- }
6841
- function readSettings(file) {
6842
- if (!import_node_fs4.default.existsSync(file)) return {};
6843
- return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
6844
- }
6845
- function ownedHook(harness, timeout, additionalContextLimit) {
6846
- return {
6847
- type: "command",
6848
- command: command(harness),
6849
- ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
6850
- timeout,
6851
- statusMessage: "Syncing MindGraph session context",
6852
- ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
6853
- };
6854
- }
6855
- function desiredEntries(harness) {
6856
- const common = {
6857
- SessionStart: [
6858
- {
6859
- matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
6860
- // B7 live acceptance caught cold SessionStart calls crossing the
6861
- // inherited 20s edge in both harnesses: the hooks correctly failed
6862
- // open, but no brief reached the model. The same installed runner
6863
- // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
6864
- // The declared Codex context limit must not undercut the brief's own
6865
- // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
6866
- // truncation the renderer knew nothing about.
6867
- hooks: [
6868
- ownedHook(
6869
- harness,
6870
- 30,
6871
- harness === "codex" ? 1e4 : void 0
6872
- )
6873
- ]
6874
- }
6875
- ],
6876
- PreToolUse: [
6877
- {
6878
- matcher: "mcp__mindgraph__.*",
6879
- // Repository resolution over a large multi-repo workspace plus three
6880
- // git probes can exceed 3s on the first (uncached) call; a killed
6881
- // PreToolUse means the call goes through untagged.
6882
- hooks: [ownedHook(harness, 5)]
6883
- }
6884
- ],
6885
- // PostToolUse awaits the lease renewal round-trip, which fires exactly
6886
- // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
6887
- PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
6888
- Stop: [{ hooks: [ownedHook(harness, 5)] }],
6889
- // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
6890
- // session close) — the SAME cold-tenant latency SessionStart needs 30s
6891
- // for. Claude honors the configured budget; Codex clamps SessionEnd to a
6892
- // 3s platform cap regardless of this value (see codex-hooks.ts wire
6893
- // map), so the codex adapter instead makes at most ONE call there.
6894
- SessionEnd: [
6895
- { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
6896
- ]
6897
- };
6898
- return harness === "claude-code" ? {
6899
- ...common,
6900
- TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
6901
- TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
6902
- } : common;
6903
- }
6904
- function hookCommand(value) {
6905
- if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
6906
- const command2 = value.command;
6907
- return typeof command2 === "string" ? command2 : void 0;
6908
- }
6909
- function entryHasOwnedHook(value) {
6910
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
6911
- const hooks = value.hooks;
6912
- return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
6913
- }
6914
- function writeSettings(file, settings) {
6915
- import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
6916
- const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
6917
- import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
6918
- `, {
6919
- mode: 384
6920
- });
6921
- import_node_fs4.default.renameSync(temporary, file);
6922
- }
6923
- function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
6924
- const file = settingsPath(harness, scope, projectDir, codexHomeDir);
6925
- const settings = readSettings(file);
6926
- const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
6927
- let added = 0;
6928
- let updated = 0;
6929
- for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
6930
- const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
6931
- const kept = current.filter((entry) => !entryHasOwnedHook(entry));
6932
- const hadOwned = kept.length !== current.length;
6933
- for (const entry of wanted) {
6934
- kept.push(entry);
6935
- if (hadOwned) updated += 1;
6936
- else added += 1;
6937
- }
6938
- hooks[event] = kept;
6939
- }
6940
- settings.hooks = hooks;
6941
- writeSettings(file, settings);
6942
- return { path: file, added, updated };
6943
- }
6944
- function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
6945
- const file = settingsPath(harness, scope, projectDir, codexHomeDir);
6946
- if (!import_node_fs4.default.existsSync(file)) return { path: file, removed: 0 };
6947
- const settings = readSettings(file);
6948
- const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
6949
- let removed = 0;
6950
- for (const [event, value] of Object.entries(hooks)) {
6951
- if (!Array.isArray(value)) continue;
6952
- const retainedEntries = [];
6953
- for (const rawEntry of value) {
6954
- if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
6955
- retainedEntries.push(rawEntry);
6956
- continue;
6957
- }
6958
- const entry = { ...rawEntry };
6959
- if (!Array.isArray(entry.hooks)) {
6960
- retainedEntries.push(entry);
6961
- continue;
6962
- }
6963
- const retainedHooks = entry.hooks.filter((hook) => {
6964
- const owned = hookCommand(hook)?.includes(OWNER_MARKER) || false;
6965
- if (owned) removed += 1;
6966
- return !owned;
6967
- });
6968
- if (retainedHooks.length > 0) {
6969
- entry.hooks = retainedHooks;
6970
- retainedEntries.push(entry);
6971
- }
6972
- }
6973
- if (retainedEntries.length > 0) hooks[event] = retainedEntries;
6974
- else delete hooks[event];
6975
- }
6976
- settings.hooks = hooks;
6977
- writeSettings(file, settings);
6978
- return { path: file, removed };
6979
- }
6980
- function installClaudeHooks(scope, projectDir = process.cwd()) {
6981
- return installHooks("claude-code", scope, projectDir);
6982
- }
6983
- function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
6984
- return uninstallHooks("claude-code", scope, projectDir);
6985
- }
6986
- function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
6987
- return installHooks("codex", scope, projectDir, codexHomeDir);
6988
- }
6989
- function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
6990
- return uninstallHooks("codex", scope, projectDir, codexHomeDir);
6991
- }
6992
-
6993
7895
  // src/cli.ts
7896
+ init_hook_installer();
6994
7897
  function recordHookHealth(harness, error) {
6995
7898
  try {
6996
- const dir = process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime");
6997
- fs5.mkdirSync(dir, { recursive: true, mode: 448 });
6998
- const file = path5.join(dir, "hook-health.json");
7899
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime");
7900
+ fs6.mkdirSync(dir, { recursive: true, mode: 448 });
7901
+ const file = path7.join(dir, "hook-health.json");
6999
7902
  let failures = 0;
7000
7903
  try {
7001
- failures = Number(JSON.parse(fs5.readFileSync(file, "utf8")).consecutiveFailures) || 0;
7904
+ failures = Number(JSON.parse(fs6.readFileSync(file, "utf8")).consecutiveFailures) || 0;
7002
7905
  } catch {
7003
7906
  }
7004
- fs5.writeFileSync(
7907
+ fs6.writeFileSync(
7005
7908
  file,
7006
7909
  `${JSON.stringify({
7007
7910
  harness,
@@ -7017,14 +7920,14 @@ function recordHookHealth(harness, error) {
7017
7920
  }
7018
7921
  function clearHookHealth(harness) {
7019
7922
  try {
7020
- const file = path5.join(
7021
- process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
7923
+ const file = path7.join(
7924
+ process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
7022
7925
  "hook-health.json"
7023
7926
  );
7024
- if (!fs5.existsSync(file)) return;
7025
- const current = JSON.parse(fs5.readFileSync(file, "utf8"));
7927
+ if (!fs6.existsSync(file)) return;
7928
+ const current = JSON.parse(fs6.readFileSync(file, "utf8"));
7026
7929
  if (!Number(current.consecutiveFailures)) return;
7027
- fs5.writeFileSync(
7930
+ fs6.writeFileSync(
7028
7931
  file,
7029
7932
  `${JSON.stringify({
7030
7933
  harness,
@@ -7040,7 +7943,7 @@ function clearHookHealth(harness) {
7040
7943
  function getClaudeDesktopConfigPath() {
7041
7944
  switch (process.platform) {
7042
7945
  case "darwin":
7043
- return path5.join(
7946
+ return path7.join(
7044
7947
  os4.homedir(),
7045
7948
  "Library",
7046
7949
  "Application Support",
@@ -7048,13 +7951,13 @@ function getClaudeDesktopConfigPath() {
7048
7951
  "claude_desktop_config.json"
7049
7952
  );
7050
7953
  case "win32":
7051
- return path5.join(
7052
- process.env.APPDATA || path5.join(os4.homedir(), "AppData", "Roaming"),
7954
+ return path7.join(
7955
+ process.env.APPDATA || path7.join(os4.homedir(), "AppData", "Roaming"),
7053
7956
  "Claude",
7054
7957
  "claude_desktop_config.json"
7055
7958
  );
7056
7959
  case "linux":
7057
- return path5.join(
7960
+ return path7.join(
7058
7961
  os4.homedir(),
7059
7962
  ".config",
7060
7963
  "Claude",
@@ -7065,16 +7968,16 @@ function getClaudeDesktopConfigPath() {
7065
7968
  }
7066
7969
  }
7067
7970
  function readJsonFile(filePath) {
7068
- if (!fs5.existsSync(filePath)) return {};
7069
- const content = fs5.readFileSync(filePath, "utf-8");
7971
+ if (!fs6.existsSync(filePath)) return {};
7972
+ const content = fs6.readFileSync(filePath, "utf-8");
7070
7973
  return JSON.parse(content);
7071
7974
  }
7072
7975
  function writeJsonFile(filePath, data) {
7073
- const dir = path5.dirname(filePath);
7074
- if (!fs5.existsSync(dir)) {
7075
- fs5.mkdirSync(dir, { recursive: true });
7976
+ const dir = path7.dirname(filePath);
7977
+ if (!fs6.existsSync(dir)) {
7978
+ fs6.mkdirSync(dir, { recursive: true });
7076
7979
  }
7077
- fs5.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
7980
+ fs6.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
7078
7981
  }
7079
7982
  function getMcpServerEntry(apiKey, baseUrl) {
7080
7983
  const env = { MINDGRAPH_API_KEY: apiKey };
@@ -7161,7 +8064,7 @@ function installClaudeCode(apiKey, baseUrl, agentId) {
7161
8064
  }
7162
8065
  function uninstallClaudeDesktop() {
7163
8066
  const configPath = getClaudeDesktopConfigPath();
7164
- if (!fs5.existsSync(configPath)) {
8067
+ if (!fs6.existsSync(configPath)) {
7165
8068
  console.log("Claude Desktop config not found. Nothing to remove.");
7166
8069
  return;
7167
8070
  }
@@ -7188,7 +8091,7 @@ function uninstallClaudeCode() {
7188
8091
  }
7189
8092
  function printStatus() {
7190
8093
  const desktopPath = getClaudeDesktopConfigPath();
7191
- if (fs5.existsSync(desktopPath)) {
8094
+ if (fs6.existsSync(desktopPath)) {
7192
8095
  const config = readJsonFile(desktopPath);
7193
8096
  const servers = config.mcpServers;
7194
8097
  if (servers && "mindgraph" in servers) {
@@ -7206,12 +8109,12 @@ function printStatus() {
7206
8109
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
7207
8110
  );
7208
8111
  try {
7209
- const healthFile = path5.join(
7210
- process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
8112
+ const healthFile = path7.join(
8113
+ process.env.MINDGRAPH_RUNTIME_DIR || path7.join(os4.homedir(), ".mindgraph", "runtime"),
7211
8114
  "hook-health.json"
7212
8115
  );
7213
- if (fs5.existsSync(healthFile)) {
7214
- const health = JSON.parse(fs5.readFileSync(healthFile, "utf8"));
8116
+ if (fs6.existsSync(healthFile)) {
8117
+ const health = JSON.parse(fs6.readFileSync(healthFile, "utf8"));
7215
8118
  const failures = Number(health.consecutiveFailures) || 0;
7216
8119
  console.log(
7217
8120
  failures > 0 ? `Hooks: FAILING (${failures} consecutive; last: ${health.lastError} at ${health.lastFailureAt})` : `Hooks: healthy (last recovery ${health.lastOkAt || "unknown"})`
@@ -7228,7 +8131,7 @@ function withHooksFlag() {
7228
8131
  }
7229
8132
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
7230
8133
  if (withHooks) {
7231
- const runner = installHookRunner(__filename);
8134
+ const runner = installHookRunner(__filename, void 0, "0.19.0");
7232
8135
  console.log(`Installed pinned hook runner at ${runner}`);
7233
8136
  const result = installClaudeHooks("user");
7234
8137
  console.log(
@@ -7276,6 +8179,20 @@ USAGE:
7276
8179
  mindgraph-mcp install-hooks --harness claude-code|codex [--scope user|project]
7277
8180
  mindgraph-mcp uninstall-hooks --harness claude-code|codex [--scope user|project]
7278
8181
  mindgraph-mcp status Show installation status
8182
+ mindgraph-mcp skills pull [--dir .claude/skills]
8183
+ Write your org's published+granted skills as
8184
+ SKILL.md directories (allowlisted frontmatter
8185
+ only). Revoked/archived skills are withdrawn;
8186
+ local edits are never overwritten (.conflict
8187
+ sidecars). NOTE: coding harnesses hot-reload
8188
+ skill files \u2014 running sessions adopt pulled
8189
+ changes immediately; session pinning exists
8190
+ on the managed-agent leg only.
8191
+ mindgraph-mcp skills push <path>
8192
+ Import a SKILL.md directory as a review
8193
+ candidate (nothing auto-publishes). A
8194
+ previously pulled directory routes to the
8195
+ candidate-only update path.
7279
8196
 
7280
8197
  OPTIONS:
7281
8198
  --api-key <key> MindGraph API key (or set MINDGRAPH_API_KEY env var)
@@ -7339,7 +8256,7 @@ async function pollForKey(code) {
7339
8256
  } catch (e) {
7340
8257
  if (e instanceof Error && e.message.includes("Session")) throw e;
7341
8258
  }
7342
- await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL));
8259
+ await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL));
7343
8260
  }
7344
8261
  throw new Error("Timed out waiting for authorization.");
7345
8262
  }
@@ -7349,7 +8266,7 @@ async function interactiveInit(baseUrl) {
7349
8266
  input: process.stdin,
7350
8267
  output: process.stdout
7351
8268
  });
7352
- const ask = (q) => new Promise((resolve2) => rl.question(q, resolve2));
8269
+ const ask = (q) => new Promise((resolve3) => rl.question(q, resolve3));
7353
8270
  console.log("\nMindGraph MCP Server Setup\n");
7354
8271
  console.log(" 1. Sign in via browser (recommended)");
7355
8272
  console.log(" 2. Enter API key manually");
@@ -7420,8 +8337,79 @@ async function interactiveInit(baseUrl) {
7420
8337
  console.log("\nDone! Your knowledge graph is now connected.");
7421
8338
  }
7422
8339
  async function main2() {
7423
- const { command: command2, apiKey, baseUrl, scope, projectDir, harness, hooks, agentId } = parseArgs(process.argv);
8340
+ const {
8341
+ command: command2,
8342
+ apiKey,
8343
+ baseUrl,
8344
+ scope,
8345
+ projectDir,
8346
+ harness,
8347
+ hooks,
8348
+ agentId,
8349
+ positionals,
8350
+ dir
8351
+ } = parseArgs(process.argv);
7424
8352
  switch (command2) {
8353
+ case "skills": {
8354
+ if (!apiKey) {
8355
+ console.error(
8356
+ "API key required. Use --api-key or set MINDGRAPH_API_KEY."
8357
+ );
8358
+ process.exit(1);
8359
+ }
8360
+ const sub = positionals[0];
8361
+ const api = httpSkillsApi(
8362
+ baseUrl ?? "https://api.mindgraph.cloud",
8363
+ apiKey,
8364
+ agentId
8365
+ );
8366
+ if (sub === "pull") {
8367
+ if (!agentId) {
8368
+ console.error(
8369
+ "note: no --agent-id/MINDGRAPH_AGENT_ID \u2014 pulling ALL published skills; bind an agent identity to scope the set by its grants."
8370
+ );
8371
+ }
8372
+ const report = await skillsPull(api, dir ?? ".claude/skills", agentId);
8373
+ for (const name of report.written) console.log(` written ${name}`);
8374
+ for (const name of report.unchanged)
8375
+ console.log(` unchanged ${name}`);
8376
+ for (const name of report.locallyEdited)
8377
+ console.log(` local-edit ${name} \u2014 upstream unchanged; left alone`);
8378
+ for (const name of report.conflicts)
8379
+ console.log(
8380
+ ` CONFLICT ${name} \u2014 local edits kept; new upstream saved as SKILL.md.conflict`
8381
+ );
8382
+ for (const removal of report.removed)
8383
+ console.log(
8384
+ ` withdrawn ${removal.name}${removal.kept ? " (local edits kept as SKILL.md.conflict)" : ""}${removal.statusReason ? ` \u2014 ${removal.statusReason}` : ""}`
8385
+ );
8386
+ for (const refusal of report.refused)
8387
+ console.log(` REFUSED ${refusal.name} \u2014 ${refusal.reason}`);
8388
+ const total = report.written.length + report.unchanged.length + report.locallyEdited.length + report.conflicts.length;
8389
+ console.log(`
8390
+ ${total} skill(s) synced.`);
8391
+ } else if (sub === "push") {
8392
+ const target = positionals[1];
8393
+ if (!target) {
8394
+ console.error("Usage: mindgraph-mcp skills push <path>");
8395
+ process.exit(1);
8396
+ }
8397
+ const report = await skillsPush(api, target);
8398
+ console.log(report.message);
8399
+ if (report.existingUid) {
8400
+ console.log(
8401
+ ` existing skill: ${report.existingUid}${report.existingStatus ? ` (${report.existingStatus})` : ""}`
8402
+ );
8403
+ }
8404
+ if (report.outcome === "error" || report.outcome === "conflict") {
8405
+ process.exit(1);
8406
+ }
8407
+ } else {
8408
+ console.error("Usage: mindgraph-mcp skills pull|push [...]");
8409
+ process.exit(1);
8410
+ }
8411
+ break;
8412
+ }
7425
8413
  case "help":
7426
8414
  printUsage();
7427
8415
  break;
@@ -7458,7 +8446,7 @@ async function main2() {
7458
8446
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
7459
8447
  throw new Error("--harness must be claude-code or codex");
7460
8448
  }
7461
- const runner = installHookRunner(__filename);
8449
+ const runner = installHookRunner(__filename, void 0, "0.19.0");
7462
8450
  console.log(`Installed pinned hook runner at ${runner}`);
7463
8451
  const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir);
7464
8452
  console.log(