mindgraph-mcp 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +53 -0
  2. package/dist/cli.js +573 -257
  3. package/dist/index.js +418 -46
  4. package/package.json +1 -1
package/dist/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, path7, body) {
69
+ const url = `${this.baseUrl}${path7}`;
70
70
  const requestId = createRequestId();
71
71
  const init = {
72
72
  method,
@@ -91,7 +91,7 @@ var init_dist = __esm({
91
91
  parsed = text2;
92
92
  }
93
93
  const err5 = new MindGraphError(
94
- `${method} ${path6} failed: ${res.status}`,
94
+ `${method} ${path7} failed: ${res.status}`,
95
95
  res.status,
96
96
  parsed
97
97
  );
@@ -109,17 +109,17 @@ var init_dist = __esm({
109
109
  }
110
110
  throw lastError;
111
111
  }
112
- get(path6) {
113
- return this.request("GET", path6);
112
+ get(path7) {
113
+ return this.request("GET", path7);
114
114
  }
115
- post(path6, body) {
116
- return this.request("POST", path6, body);
115
+ post(path7, body) {
116
+ return this.request("POST", path7, body);
117
117
  }
118
- patch(path6, body) {
119
- return this.request("PATCH", path6, body);
118
+ patch(path7, body) {
119
+ return this.request("PATCH", path7, body);
120
120
  }
121
- del(path6) {
122
- return this.request("DELETE", path6);
121
+ del(path7) {
122
+ return this.request("DELETE", path7);
123
123
  }
124
124
  // ---- Health ----
125
125
  async health() {
@@ -971,6 +971,14 @@ var init_hook_env = __esm({
971
971
  });
972
972
 
973
973
  // src/codegraph.ts
974
+ function invocationCwd(args) {
975
+ const context = args.invocation_context;
976
+ if (context && typeof context === "object" && !Array.isArray(context)) {
977
+ const cwd = context.cwd;
978
+ if (typeof cwd === "string" && cwd) return cwd;
979
+ }
980
+ return process.cwd();
981
+ }
974
982
  function unavailabilityCaveats(reason, message) {
975
983
  const caveats = [message || "codegraph status failed"];
976
984
  if (reason === "absent") {
@@ -980,14 +988,14 @@ function unavailabilityCaveats(reason, message) {
980
988
  }
981
989
  return caveats;
982
990
  }
983
- function normalizePath(path6) {
984
- return path6.replaceAll("\\", "/").replace(/^\.\//, "");
991
+ function normalizePath(path7) {
992
+ return path7.replaceAll("\\", "/").replace(/^\.\//, "");
985
993
  }
986
994
  function normalizeSignature(signature) {
987
995
  return signature.replace(/\s+/g, " ").trim();
988
996
  }
989
- function languageFromPath(path6) {
990
- const extension = (0, import_node_path2.extname)(path6).toLowerCase();
997
+ function languageFromPath(path7) {
998
+ const extension = (0, import_node_path2.extname)(path7).toLowerCase();
991
999
  return {
992
1000
  ".c": "c",
993
1001
  ".cc": "cpp",
@@ -1150,7 +1158,7 @@ function codeRefIdentityKey(ref) {
1150
1158
  }
1151
1159
  };
1152
1160
  }
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;
1161
+ var import_node_child_process, import_node_fs2, import_promises, import_node_os2, import_node_path2, import_node_crypto2, UnknownRepositoryError, DEFAULT_TIMEOUT_MS, DEFAULT_STATUS_TTL_MS, MAX_OUTPUT_BYTES, CodegraphAdapter;
1154
1162
  var init_codegraph = __esm({
1155
1163
  "src/codegraph.ts"() {
1156
1164
  "use strict";
@@ -1160,6 +1168,16 @@ var init_codegraph = __esm({
1160
1168
  import_node_os2 = require("os");
1161
1169
  import_node_path2 = require("path");
1162
1170
  import_node_crypto2 = require("crypto");
1171
+ UnknownRepositoryError = class extends Error {
1172
+ constructor(repo) {
1173
+ super(
1174
+ `unknown repository "${repo}": not declared in .mindgraph/workspace.json, not the current repository, and no such path exists`
1175
+ );
1176
+ this.repo = repo;
1177
+ this.name = "UnknownRepositoryError";
1178
+ }
1179
+ repo;
1180
+ };
1163
1181
  DEFAULT_TIMEOUT_MS = 5e3;
1164
1182
  DEFAULT_STATUS_TTL_MS = 6e4;
1165
1183
  MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
@@ -1195,7 +1213,26 @@ var init_codegraph = __esm({
1195
1213
  selected.spaceUid
1196
1214
  ) : selected;
1197
1215
  }
1198
- const requestedRoot = repo || this.env.MINDGRAPH_REPO_ROOT || this.cwd;
1216
+ if (repo) {
1217
+ const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, repo)).catch(
1218
+ () => (0, import_node_path2.resolve)(this.cwd, repo)
1219
+ );
1220
+ if (!(0, import_node_fs2.existsSync)(requestedPath)) {
1221
+ const current = await this.currentRepository(fallbackSpaceUid);
1222
+ if (current.repoId === repo) return current;
1223
+ throw new UnknownRepositoryError(repo);
1224
+ }
1225
+ const repoRoot = await this.gitRoot(requestedPath) ?? requestedPath;
1226
+ return this.resolveRepositoryIdentity(
1227
+ repoRoot,
1228
+ this.env.MINDGRAPH_REPO_ID,
1229
+ this.env.MINDGRAPH_CODE_SPACE_UID || fallbackSpaceUid
1230
+ );
1231
+ }
1232
+ return this.currentRepository(fallbackSpaceUid);
1233
+ }
1234
+ async currentRepository(fallbackSpaceUid) {
1235
+ const requestedRoot = this.env.MINDGRAPH_REPO_ROOT || this.cwd;
1199
1236
  const requestedPath = await (0, import_promises.realpath)((0, import_node_path2.resolve)(this.cwd, requestedRoot)).catch(
1200
1237
  () => (0, import_node_path2.resolve)(this.cwd, requestedRoot)
1201
1238
  );
@@ -1285,6 +1322,14 @@ var init_codegraph = __esm({
1285
1322
  ).catch(() => "");
1286
1323
  return raw.trim() ? (0, import_node_path2.resolve)(start, raw.trim()) : void 0;
1287
1324
  }
1325
+ /** Why the workspace map yielded no repositories — for callers that were
1326
+ * explicitly asked to fan out and would otherwise return silently empty. */
1327
+ workspaceDiagnostic() {
1328
+ if (!this.workspaceFile || !(0, import_node_fs2.existsSync)(this.workspaceFile)) {
1329
+ return "no .mindgraph/workspace.json was found between the working directory and the filesystem root";
1330
+ }
1331
+ return `${this.workspaceFile} is malformed or declares no repositories`;
1332
+ }
1288
1333
  async availability(repository) {
1289
1334
  const cached = this.statusCache.get(repository.repoRoot);
1290
1335
  if (cached && cached.expiresAt > this.now()) return cached.availability;
@@ -1698,13 +1743,13 @@ var init_error_detail = __esm({
1698
1743
  });
1699
1744
 
1700
1745
  // src/code-tool.ts
1701
- async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1746
+ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter({ cwd: invocationCwd(args) })) {
1702
1747
  const codeClient = client2;
1703
1748
  const action = args.action;
1704
1749
  const refs = Array.isArray(args.code_refs) ? args.code_refs : [];
1705
1750
  const anchorUids = Array.isArray(args.anchor_uids) ? args.anchor_uids : [];
1706
1751
  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}`;
1752
+ const fallbackSpaceUid = typeof args.repo_space_uid === "string" ? args.repo_space_uid : void 0;
1708
1753
  const options = {
1709
1754
  repo: typeof args.repo === "string" ? args.repo : void 0,
1710
1755
  fallbackSpaceUid
@@ -1828,6 +1873,7 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1828
1873
  const targetUid = typeof args.task_uid === "string" ? args.task_uid : typeof args.execution_uid === "string" ? args.execution_uid : void 0;
1829
1874
  let graphContext;
1830
1875
  const graphRefs = [];
1876
+ let unavailableRepositories = [];
1831
1877
  if (targetUid) {
1832
1878
  graphContext = await codeClient.traverse({
1833
1879
  action: "neighborhood",
@@ -1835,9 +1881,13 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1835
1881
  depth: 2,
1836
1882
  max_nodes: 50
1837
1883
  });
1838
- graphRefs.push(
1839
- ...await codeRefsFromGraphContext(codeClient, adapter, graphContext)
1884
+ const fromGraph = await codeRefsFromGraphContext(
1885
+ codeClient,
1886
+ adapter,
1887
+ graphContext
1840
1888
  );
1889
+ graphRefs.push(...fromGraph.refs);
1890
+ unavailableRepositories = fromGraph.unavailable;
1841
1891
  }
1842
1892
  if (refs.length > 0 || anchorUids.length > 0) {
1843
1893
  const target = await resolveSingleTarget(
@@ -1856,7 +1906,8 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1856
1906
  target_uid: targetUid,
1857
1907
  graph_context: graphContext,
1858
1908
  affected: [],
1859
- degradation: "no materialized code anchors were attached"
1909
+ degradation: unavailableRepositories.length > 0 ? `attached code anchors reference repositories unavailable here: ${unavailableRepositories.join(", ")}` : "no materialized code anchors were attached",
1910
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1860
1911
  });
1861
1912
  }
1862
1913
  const impacts = await Promise.all(
@@ -1873,13 +1924,17 @@ async function handleCodeTool(client2, args, adapter = new CodegraphAdapter()) {
1873
1924
  target_uid: targetUid,
1874
1925
  graph_context: graphContext,
1875
1926
  impacts,
1876
- materialized: joined
1927
+ materialized: joined,
1928
+ ...unavailableRepositories.length > 0 ? { unavailable_repositories: unavailableRepositories } : {}
1877
1929
  });
1878
1930
  }
1879
1931
  default:
1880
1932
  return err("unknown_action", `unknown mindgraph_code action: ${action}`);
1881
1933
  }
1882
1934
  } catch (cause) {
1935
+ if (cause instanceof UnknownRepositoryError) {
1936
+ return err("unknown_repository", cause.message);
1937
+ }
1883
1938
  return err("code_tool_failed", errorDetail(cause));
1884
1939
  }
1885
1940
  }
@@ -1905,7 +1960,10 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1905
1960
  code_refs: args.code_refs,
1906
1961
  repo: args.repo,
1907
1962
  repo_space_uid: args.repo_space_uid,
1908
- agent_id: args.agent_id
1963
+ agent_id: args.agent_id,
1964
+ // Keeps the default adapter anchored to the live session directory
1965
+ // rather than wherever the MCP server process was launched.
1966
+ invocation_context: args.invocation_context
1909
1967
  },
1910
1968
  adapter
1911
1969
  );
@@ -1915,17 +1973,33 @@ async function attachCodeRefsToToolResult(client2, result, args, adapter) {
1915
1973
  const anchorUids = (anchors.anchors ?? []).flatMap(
1916
1974
  (anchor) => anchor.uid ? [anchor.uid] : []
1917
1975
  );
1976
+ const relateFailures = [];
1918
1977
  for (const targetUid of targetUids) {
1919
1978
  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
- });
1979
+ try {
1980
+ await codeClient.entity({
1981
+ action: "relate",
1982
+ source_uid: targetUid,
1983
+ target_uid: anchorUid,
1984
+ edge_type: args.action === "lesson" ? "ABOUT" : "RELEVANT_TO",
1985
+ agent_id: args.agent_id
1986
+ });
1987
+ } catch (cause) {
1988
+ relateFailures.push(
1989
+ `${targetUid} -> ${anchorUid}: ${errorDetail(cause)}`
1990
+ );
1991
+ }
1927
1992
  }
1928
1993
  }
1994
+ if (relateFailures.length > 0) {
1995
+ return ok({
1996
+ result: original,
1997
+ code_anchors: {
1998
+ ...anchors,
1999
+ caveat: `anchors written, but ${relateFailures.length} relate edge(s) failed: ${relateFailures.join("; ")}`
2000
+ }
2001
+ });
2002
+ }
1929
2003
  }
1930
2004
  return ok({ result: original, code_anchors: anchors });
1931
2005
  }
@@ -1998,16 +2072,34 @@ async function codeRefFromAnchor(client2, adapter, uid) {
1998
2072
  if (!attributes || typeof attributes !== "object") return void 0;
1999
2073
  const codeRef = attributes.code_ref;
2000
2074
  if (!isStoredCodeRef(codeRef)) return void 0;
2001
- const repository = await adapter.resolveRepository(codeRef.repoId);
2002
- return { ...codeRef, repoRoot: repository.repoRoot };
2075
+ try {
2076
+ const repository = await adapter.resolveRepository(codeRef.repoId);
2077
+ return { ref: { ...codeRef, repoRoot: repository.repoRoot } };
2078
+ } catch (cause) {
2079
+ if (cause instanceof UnknownRepositoryError) {
2080
+ return { missingRepo: codeRef.repoId };
2081
+ }
2082
+ throw cause;
2083
+ }
2003
2084
  }
2004
2085
  async function resolveSingleTarget(client2, adapter, refs, anchorUids, options) {
2005
2086
  if (refs.length > 1 || anchorUids.length > 1) {
2006
2087
  return { error: err("ambiguous_target", "expand/affected accepts exactly one code target") };
2007
2088
  }
2008
2089
  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") };
2090
+ const outcome = await codeRefFromAnchor(client2, adapter, anchorUids[0]);
2091
+ if (!outcome) {
2092
+ return { error: err("invalid_anchor", "anchor UID does not contain a typed code_ref") };
2093
+ }
2094
+ if ("missingRepo" in outcome) {
2095
+ return {
2096
+ error: err(
2097
+ "repository_unavailable",
2098
+ `repository ${outcome.missingRepo} is not checked out here or declared in .mindgraph/workspace.json; expand/affected need its local index`
2099
+ )
2100
+ };
2101
+ }
2102
+ return { ref: outcome.ref };
2011
2103
  }
2012
2104
  if (refs.length !== 1) {
2013
2105
  return { error: err("missing_code_ref", "one code_ref or anchor_uid is required") };
@@ -2046,7 +2138,7 @@ async function joinMaterialized(client2, refs, agentId, limit) {
2046
2138
  async function codeRefsFromGraphContext(client2, adapter, context) {
2047
2139
  const uids = /* @__PURE__ */ new Set();
2048
2140
  collectUids(context, uids);
2049
- if (uids.size === 0) return [];
2141
+ if (uids.size === 0) return { refs: [], unavailable: [] };
2050
2142
  const nodes = client2.getNodesBatch ? await client2.getNodesBatch([...uids]) : await Promise.all([...uids].map((uid) => client2.getNode(uid).catch(() => void 0)));
2051
2143
  const stored = nodes.flatMap((node) => {
2052
2144
  if (!node) return [];
@@ -2055,12 +2147,21 @@ async function codeRefsFromGraphContext(client2, adapter, context) {
2055
2147
  const codeRef = attributes.code_ref;
2056
2148
  return isStoredCodeRef(codeRef) ? [codeRef] : [];
2057
2149
  });
2058
- return Promise.all(
2059
- stored.map(async (ref) => {
2150
+ const refs = [];
2151
+ const unavailable = /* @__PURE__ */ new Set();
2152
+ for (const ref of stored) {
2153
+ try {
2060
2154
  const repository = await adapter.resolveRepository(ref.repoId);
2061
- return { ...ref, repoRoot: repository.repoRoot };
2062
- })
2063
- );
2155
+ refs.push({ ...ref, repoRoot: repository.repoRoot });
2156
+ } catch (cause) {
2157
+ if (cause instanceof UnknownRepositoryError) {
2158
+ unavailable.add(ref.repoId);
2159
+ } else {
2160
+ throw cause;
2161
+ }
2162
+ }
2163
+ }
2164
+ return { refs, unavailable: [...unavailable].sort() };
2064
2165
  }
2065
2166
  function collectUids(value, uids) {
2066
2167
  if (Array.isArray(value)) {
@@ -2136,7 +2237,7 @@ var init_code_tool = __esm({
2136
2237
  });
2137
2238
  CODE_TOOL = {
2138
2239
  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.",
2240
+ 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
2241
  inputSchema: {
2141
2242
  type: "object",
2142
2243
  properties: {
@@ -2178,7 +2279,7 @@ var init_code_tool = __esm({
2178
2279
  },
2179
2280
  repo_space_uid: {
2180
2281
  type: "string",
2181
- description: "Writable shared Project/repository Space for new anchors"
2282
+ description: "Writable shared Project/repository Space for new anchors (anchor requires this, a workspace space_uid, or MINDGRAPH_CODE_SPACE_UID)"
2182
2283
  },
2183
2284
  task_uid: { type: "string" },
2184
2285
  execution_uid: { type: "string" },
@@ -2197,6 +2298,276 @@ var init_code_tool = __esm({
2197
2298
  }
2198
2299
  });
2199
2300
 
2301
+ // src/hook-installer.ts
2302
+ function command(harness) {
2303
+ return `node "$HOME/${RUNNER_RELATIVE}" hook --harness ${harness} --owner mindgraph`;
2304
+ }
2305
+ function windowsCommand(harness) {
2306
+ return `node "%USERPROFILE%\\.mindgraph\\bin\\mindgraph-hook.cjs" hook --harness ${harness} --owner mindgraph`;
2307
+ }
2308
+ function installHookRunner(sourceFile, homeDir, version) {
2309
+ const target = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_RELATIVE);
2310
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
2311
+ const temporary = `${target}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
2312
+ import_node_fs4.default.copyFileSync(sourceFile, temporary);
2313
+ import_node_fs4.default.renameSync(temporary, target);
2314
+ const versionPath = import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE);
2315
+ if (version) {
2316
+ import_node_fs4.default.writeFileSync(versionPath, `${version}
2317
+ `);
2318
+ } else {
2319
+ import_node_fs4.default.rmSync(versionPath, { force: true });
2320
+ }
2321
+ return target;
2322
+ }
2323
+ function installedHookRunnerVersion(homeDir) {
2324
+ try {
2325
+ const raw = import_node_fs4.default.readFileSync(
2326
+ import_node_path4.default.join(homeDir || import_node_os4.default.homedir(), RUNNER_VERSION_RELATIVE),
2327
+ "utf8"
2328
+ ).trim();
2329
+ return raw || void 0;
2330
+ } catch {
2331
+ return void 0;
2332
+ }
2333
+ }
2334
+ function parseSemver(value) {
2335
+ if (!value) return void 0;
2336
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value.trim());
2337
+ if (!match) return void 0;
2338
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
2339
+ }
2340
+ function semverNewer(a, b) {
2341
+ for (let index = 0; index < 3; index += 1) {
2342
+ if (a[index] !== b[index]) return a[index] > b[index];
2343
+ }
2344
+ return false;
2345
+ }
2346
+ function refreshHookRunner(serverVersion, sourceBundle, homeDir) {
2347
+ try {
2348
+ if (process.env.MINDGRAPH_HOOK_AUTOUPDATE === "off") {
2349
+ return { refreshed: false, reason: "disabled" };
2350
+ }
2351
+ const home = homeDir || import_node_os4.default.homedir();
2352
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(home, RUNNER_RELATIVE))) {
2353
+ return { refreshed: false, reason: "no_runner" };
2354
+ }
2355
+ if (!import_node_fs4.default.existsSync(sourceBundle)) {
2356
+ return { refreshed: false, reason: "no_source" };
2357
+ }
2358
+ const server2 = parseSemver(serverVersion);
2359
+ if (!server2) return { refreshed: false, reason: "unparseable" };
2360
+ const runnerVersion = installedHookRunnerVersion(home);
2361
+ if (runnerVersion !== void 0) {
2362
+ const pinned = parseSemver(runnerVersion);
2363
+ if (!pinned) {
2364
+ return { refreshed: false, reason: "unparseable", runnerVersion };
2365
+ }
2366
+ if (!semverNewer(server2, pinned)) {
2367
+ return {
2368
+ refreshed: false,
2369
+ reason: runnerVersion === serverVersion ? "current" : "downgrade",
2370
+ runnerVersion
2371
+ };
2372
+ }
2373
+ }
2374
+ installHookRunner(sourceBundle, home, serverVersion);
2375
+ return { refreshed: true, reason: "updated", runnerVersion };
2376
+ } catch {
2377
+ return { refreshed: false, reason: "error" };
2378
+ }
2379
+ }
2380
+ function refreshOwnedClaudeHooks(projectDir = process.cwd(), homeDir) {
2381
+ const refreshed = [];
2382
+ const scopes = ["user", "project"];
2383
+ for (const scope of scopes) {
2384
+ try {
2385
+ const file = settingsPath("claude-code", scope, projectDir, void 0, homeDir);
2386
+ if (!import_node_fs4.default.existsSync(file)) continue;
2387
+ if (!import_node_fs4.default.readFileSync(file, "utf8").includes(OWNER_MARKER)) continue;
2388
+ installHooks("claude-code", scope, projectDir, void 0, homeDir);
2389
+ refreshed.push(file);
2390
+ } catch {
2391
+ }
2392
+ }
2393
+ return refreshed;
2394
+ }
2395
+ function versionSkewNote(serverVersion, homeDir) {
2396
+ const runnerVersion = installedHookRunnerVersion(homeDir);
2397
+ if (!runnerVersion || runnerVersion === serverVersion) return void 0;
2398
+ 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).`;
2399
+ }
2400
+ function settingsPath(harness, scope, projectDir = process.cwd(), codexHomeDir = process.env.CODEX_HOME || import_node_path4.default.join(import_node_os4.default.homedir(), ".codex"), homeDir) {
2401
+ if (harness === "codex") {
2402
+ return scope === "user" ? import_node_path4.default.join(codexHomeDir, "hooks.json") : import_node_path4.default.join(projectDir, ".codex", "hooks.json");
2403
+ }
2404
+ 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");
2405
+ }
2406
+ function readSettings(file) {
2407
+ if (!import_node_fs4.default.existsSync(file)) return {};
2408
+ return JSON.parse(import_node_fs4.default.readFileSync(file, "utf8"));
2409
+ }
2410
+ function ownedHook(harness, timeout, additionalContextLimit) {
2411
+ return {
2412
+ type: "command",
2413
+ command: command(harness),
2414
+ ...harness === "codex" ? { commandWindows: windowsCommand(harness) } : {},
2415
+ timeout,
2416
+ statusMessage: "Syncing MindGraph session context",
2417
+ ...additionalContextLimit === void 0 ? {} : { additionalContextLimit }
2418
+ };
2419
+ }
2420
+ function desiredEntries(harness) {
2421
+ const common = {
2422
+ SessionStart: [
2423
+ {
2424
+ matcher: harness === "claude-code" ? "startup|resume|clear|compact|fork" : "startup|resume|clear|compact",
2425
+ // B7 live acceptance caught cold SessionStart calls crossing the
2426
+ // inherited 20s edge in both harnesses: the hooks correctly failed
2427
+ // open, but no brief reached the model. The same installed runner
2428
+ // completed in 16.84s once warm. Keep a 30s cold-tenant margin.
2429
+ // The declared Codex context limit must not undercut the brief's own
2430
+ // 9,000-char budget: at 3,000 the harness applied a SECOND, tighter
2431
+ // truncation the renderer knew nothing about.
2432
+ hooks: [
2433
+ ownedHook(
2434
+ harness,
2435
+ 30,
2436
+ harness === "codex" ? 1e4 : void 0
2437
+ )
2438
+ ]
2439
+ }
2440
+ ],
2441
+ PreToolUse: [
2442
+ {
2443
+ matcher: "mcp__mindgraph__.*",
2444
+ // Repository resolution over a large multi-repo workspace plus three
2445
+ // git probes can exceed 3s on the first (uncached) call; a killed
2446
+ // PreToolUse means the call goes through untagged.
2447
+ hooks: [ownedHook(harness, 5)]
2448
+ }
2449
+ ],
2450
+ // PostToolUse awaits the lease renewal round-trip, which fires exactly
2451
+ // when the tenant is cold (post-idle reclaim) — 5s killed it mid-claim.
2452
+ PostToolUse: [{ matcher: ".*", hooks: [ownedHook(harness, 10)] }],
2453
+ Stop: [{ hooks: [ownedHook(harness, 5)] }],
2454
+ // SessionEnd makes up to two sequential cloud calls (abandon_iteration +
2455
+ // session close) — the SAME cold-tenant latency SessionStart needs 30s
2456
+ // for. Claude honors the configured budget; Codex clamps SessionEnd to a
2457
+ // 3s platform cap regardless of this value (see codex-hooks.ts wire
2458
+ // map), so the codex adapter instead makes at most ONE call there.
2459
+ SessionEnd: [
2460
+ { hooks: [ownedHook(harness, harness === "codex" ? 3 : 30)] }
2461
+ ]
2462
+ };
2463
+ return harness === "claude-code" ? {
2464
+ ...common,
2465
+ TaskCreated: [{ hooks: [ownedHook(harness, 3)] }],
2466
+ TaskCompleted: [{ hooks: [ownedHook(harness, 3)] }]
2467
+ } : common;
2468
+ }
2469
+ function hookCommand(value) {
2470
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
2471
+ const command2 = value.command;
2472
+ return typeof command2 === "string" ? command2 : void 0;
2473
+ }
2474
+ function entryHasOwnedHook(value) {
2475
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
2476
+ const hooks = value.hooks;
2477
+ return Array.isArray(hooks) && hooks.some((hook) => hookCommand(hook)?.includes(OWNER_MARKER));
2478
+ }
2479
+ function writeSettings(file, settings) {
2480
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(file), { recursive: true });
2481
+ const temporary = `${file}.${process.pid}.${import_node_crypto4.default.randomUUID()}.tmp`;
2482
+ import_node_fs4.default.writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}
2483
+ `, {
2484
+ mode: 384
2485
+ });
2486
+ import_node_fs4.default.renameSync(temporary, file);
2487
+ }
2488
+ function installHooks(harness, scope, projectDir = process.cwd(), codexHomeDir, homeDir) {
2489
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir, homeDir);
2490
+ const settings = readSettings(file);
2491
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2492
+ let added = 0;
2493
+ let updated = 0;
2494
+ for (const [event, wanted] of Object.entries(desiredEntries(harness))) {
2495
+ const current = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
2496
+ const kept = current.filter((entry) => !entryHasOwnedHook(entry));
2497
+ const hadOwned = kept.length !== current.length;
2498
+ for (const entry of wanted) {
2499
+ kept.push(entry);
2500
+ if (hadOwned) updated += 1;
2501
+ else added += 1;
2502
+ }
2503
+ hooks[event] = kept;
2504
+ }
2505
+ settings.hooks = hooks;
2506
+ writeSettings(file, settings);
2507
+ return { path: file, added, updated };
2508
+ }
2509
+ function uninstallHooks(harness, scope, projectDir = process.cwd(), codexHomeDir) {
2510
+ const file = settingsPath(harness, scope, projectDir, codexHomeDir);
2511
+ if (!import_node_fs4.default.existsSync(file)) return { path: file, removed: 0 };
2512
+ const settings = readSettings(file);
2513
+ const hooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
2514
+ let removed = 0;
2515
+ for (const [event, value] of Object.entries(hooks)) {
2516
+ if (!Array.isArray(value)) continue;
2517
+ const retainedEntries = [];
2518
+ for (const rawEntry of value) {
2519
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
2520
+ retainedEntries.push(rawEntry);
2521
+ continue;
2522
+ }
2523
+ const entry = { ...rawEntry };
2524
+ if (!Array.isArray(entry.hooks)) {
2525
+ retainedEntries.push(entry);
2526
+ continue;
2527
+ }
2528
+ const retainedHooks = entry.hooks.filter((hook) => {
2529
+ const owned = hookCommand(hook)?.includes(OWNER_MARKER) || false;
2530
+ if (owned) removed += 1;
2531
+ return !owned;
2532
+ });
2533
+ if (retainedHooks.length > 0) {
2534
+ entry.hooks = retainedHooks;
2535
+ retainedEntries.push(entry);
2536
+ }
2537
+ }
2538
+ if (retainedEntries.length > 0) hooks[event] = retainedEntries;
2539
+ else delete hooks[event];
2540
+ }
2541
+ settings.hooks = hooks;
2542
+ writeSettings(file, settings);
2543
+ return { path: file, removed };
2544
+ }
2545
+ function installClaudeHooks(scope, projectDir = process.cwd()) {
2546
+ return installHooks("claude-code", scope, projectDir);
2547
+ }
2548
+ function uninstallClaudeHooks(scope, projectDir = process.cwd()) {
2549
+ return uninstallHooks("claude-code", scope, projectDir);
2550
+ }
2551
+ function installCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2552
+ return installHooks("codex", scope, projectDir, codexHomeDir);
2553
+ }
2554
+ function uninstallCodexHooks(scope, projectDir = process.cwd(), codexHomeDir) {
2555
+ return uninstallHooks("codex", scope, projectDir, codexHomeDir);
2556
+ }
2557
+ var import_node_fs4, import_node_os4, import_node_path4, import_node_crypto4, OWNER_MARKER, RUNNER_RELATIVE, RUNNER_VERSION_RELATIVE;
2558
+ var init_hook_installer = __esm({
2559
+ "src/hook-installer.ts"() {
2560
+ "use strict";
2561
+ import_node_fs4 = __toESM(require("fs"));
2562
+ import_node_os4 = __toESM(require("os"));
2563
+ import_node_path4 = __toESM(require("path"));
2564
+ import_node_crypto4 = __toESM(require("crypto"));
2565
+ OWNER_MARKER = "--owner mindgraph";
2566
+ RUNNER_RELATIVE = ".mindgraph/bin/mindgraph-hook.cjs";
2567
+ RUNNER_VERSION_RELATIVE = `${RUNNER_RELATIVE}.version`;
2568
+ }
2569
+ });
2570
+
2200
2571
  // src/sync-tool.ts
2201
2572
  function safePath(root, candidate) {
2202
2573
  const absolute = import_node_path5.default.resolve(root, candidate);
@@ -2204,14 +2575,29 @@ function safePath(root, candidate) {
2204
2575
  if (relative2.startsWith("..") || import_node_path5.default.isAbsolute(relative2)) {
2205
2576
  throw new Error(`path escapes repository root: ${candidate}`);
2206
2577
  }
2578
+ if (import_node_fs5.default.existsSync(absolute)) {
2579
+ if (!pathContains(realPathOf(root), realPathOf(absolute))) {
2580
+ throw new Error(`path escapes repository root via symlink: ${candidate}`);
2581
+ }
2582
+ }
2207
2583
  return absolute;
2208
2584
  }
2209
- function markdownFiles(target) {
2585
+ function realPathOf(target) {
2586
+ try {
2587
+ return import_node_fs5.default.realpathSync(target);
2588
+ } catch {
2589
+ return import_node_path5.default.resolve(target);
2590
+ }
2591
+ }
2592
+ function markdownFiles(target, realRoot, visited = /* @__PURE__ */ new Set()) {
2210
2593
  if (!import_node_fs5.default.existsSync(target)) return [];
2594
+ const real = realPathOf(target);
2595
+ if (!pathContains(realRoot, real)) return [];
2211
2596
  const stat = import_node_fs5.default.statSync(target);
2212
2597
  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)));
2598
+ if (!stat.isDirectory() || visited.has(real)) return [];
2599
+ visited.add(real);
2600
+ return import_node_fs5.default.readdirSync(target, { withFileTypes: true }).flatMap((entry) => markdownFiles(import_node_path5.default.join(target, entry.name), realRoot, visited));
2215
2601
  }
2216
2602
  function fileDescriptor(root, absolute) {
2217
2603
  const content = import_node_fs5.default.readFileSync(absolute, "utf8");
@@ -2226,9 +2612,7 @@ async function handleSyncTool(client2, args) {
2226
2612
  try {
2227
2613
  const action = String(args.action || "");
2228
2614
  const context = args.invocation_context || {};
2229
- const adapter = new CodegraphAdapter({
2230
- cwd: context.cwd || process.cwd()
2231
- });
2615
+ const adapter = new CodegraphAdapter({ cwd: invocationCwd(args) });
2232
2616
  const requestedRepo = typeof args.repo === "string" ? args.repo : typeof args.repo_id === "string" ? args.repo_id : context.repoId;
2233
2617
  const repository = await adapter.resolveRepository(requestedRepo);
2234
2618
  const root = repository.repoRoot;
@@ -2237,6 +2621,12 @@ async function handleSyncTool(client2, args) {
2237
2621
  const remote = client2;
2238
2622
  if (action === "scan") {
2239
2623
  const repositories = args.workspace === true ? await adapter.resolveRepositories() : [repository];
2624
+ if (args.workspace === true && repositories.length === 0) {
2625
+ return err2(
2626
+ "workspace_not_configured",
2627
+ `workspace scan found no declared repositories: ${adapter.workspaceDiagnostic()}`
2628
+ );
2629
+ }
2240
2630
  const requested = Array.isArray(args.paths) && args.paths.length > 0 ? args.paths.map(String) : [".claude/agent-memory", "memory"];
2241
2631
  const scans = [];
2242
2632
  for (const selected of repositories) {
@@ -2293,10 +2683,11 @@ async function handleSyncTool(client2, args) {
2293
2683
  }
2294
2684
  }
2295
2685
  async function scanRepository(remote, repository, requested, provider, agentId) {
2686
+ const realRoot = realPathOf(repository.repoRoot);
2296
2687
  const files = [
2297
2688
  ...new Set(
2298
2689
  requested.flatMap(
2299
- (candidate) => markdownFiles(safePath(repository.repoRoot, candidate))
2690
+ (candidate) => markdownFiles(safePath(repository.repoRoot, candidate), realRoot)
2300
2691
  )
2301
2692
  )
2302
2693
  ].sort();
@@ -2730,15 +3121,15 @@ function renderTraversal(response) {
2730
3121
  const labelByUid = /* @__PURE__ */ new Map();
2731
3122
  for (const node of nodes) labelByUid.set(str(node.uid), str(node.label) || "?");
2732
3123
  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) => {
3124
+ const items = paths.map((raw) => obj(raw)).filter((path7) => Boolean(path7)).map((path7, index) => {
3125
+ const uids = Array.isArray(path7.node_uids) ? path7.node_uids.map(String) : [];
3126
+ const labels = Array.isArray(path7.labels) ? path7.labels.map(String) : Array.isArray(path7.nodes) ? path7.nodes.map((hop) => {
2736
3127
  const hopNode = obj(hop);
2737
3128
  return str(hopNode?.label) || labelByUid.get(str(hopNode?.uid) || "") || str(hop) || "?";
2738
3129
  }) : [];
2739
3130
  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);
3131
+ const cost = num(path7.cost) ?? num(path7.path_cost);
3132
+ const confidence = num(path7.path_confidence);
2742
3133
  const meta = [
2743
3134
  cost !== void 0 ? `cost ${cost}` : void 0,
2744
3135
  confidence !== void 0 ? `confidence ${confidence}` : void 0
@@ -5343,7 +5734,7 @@ async function checkMcpGovernance(toolName, args, config) {
5343
5734
  if (!response) {
5344
5735
  return {
5345
5736
  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.`
5737
+ 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
5738
  };
5348
5739
  }
5349
5740
  if (!response.ok) {
@@ -5587,10 +5978,10 @@ async function main() {
5587
5978
  const transport = new import_stdio.StdioServerTransport();
5588
5979
  await server.connect(transport);
5589
5980
  console.error(
5590
- `MindGraph MCP server v${"0.17.0"} running on stdio (${BASE_URL})`
5981
+ `MindGraph MCP server v${"0.18.0"} running on stdio (${BASE_URL})`
5591
5982
  );
5592
5983
  }
5593
- var import_server, import_stdio, import_types, API_KEY, BASE_URL, AGENT_ID, ORG_ID, PROFILE, HARNESS, client, INSTRUCTIONS, server, GOVERNANCE_CONFIG, PROMPTS;
5984
+ 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
5985
  var init_src = __esm({
5595
5986
  "src/index.ts"() {
5596
5987
  "use strict";
@@ -5603,6 +5994,8 @@ var init_src = __esm({
5603
5994
  init_generated_tools();
5604
5995
  init_governance();
5605
5996
  init_resources();
5997
+ import_node_path6 = __toESM(require("path"));
5998
+ init_hook_installer();
5606
5999
  API_KEY = process.env.MINDGRAPH_API_KEY;
5607
6000
  BASE_URL = process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud";
5608
6001
  AGENT_ID = process.env.MINDGRAPH_AGENT_ID || loadHookEnv().agentId || stableAgentId();
@@ -5724,10 +6117,42 @@ Capture knowledge when the user shares something worth remembering:
5724
6117
  (line) => !line.includes("mindgraph_code") && !line.includes("`code`") && !line.includes("Typed code anchors")
5725
6118
  ).join("\n");
5726
6119
  }
6120
+ RUNNER_REFRESH = (() => {
6121
+ try {
6122
+ return refreshHookRunner("0.18.0", import_node_path6.default.join(__dirname, "cli.js"));
6123
+ } catch {
6124
+ return void 0;
6125
+ }
6126
+ })();
6127
+ if (RUNNER_REFRESH?.refreshed) {
6128
+ const refreshedSettings = (() => {
6129
+ try {
6130
+ return refreshOwnedClaudeHooks();
6131
+ } catch {
6132
+ return [];
6133
+ }
6134
+ })();
6135
+ console.error(
6136
+ `[mindgraph-mcp] refreshed pinned hook runner ${RUNNER_REFRESH.runnerVersion ? `v${RUNNER_REFRESH.runnerVersion}` : "(pre-sidecar)"} \u2192 v${"0.18.0"}` + (refreshedSettings.length > 0 ? `; refreshed owned hook entries in ${refreshedSettings.join(", ")}` : "")
6137
+ );
6138
+ }
6139
+ HOOK_SKEW_NOTE = (() => {
6140
+ try {
6141
+ return versionSkewNote("0.18.0");
6142
+ } catch {
6143
+ return void 0;
6144
+ }
6145
+ })();
6146
+ if (HOOK_SKEW_NOTE) {
6147
+ INSTRUCTIONS += `
6148
+
6149
+ ${HOOK_SKEW_NOTE}`;
6150
+ console.error(`[mindgraph-mcp] ${HOOK_SKEW_NOTE}`);
6151
+ }
5727
6152
  server = new import_server.Server(
5728
6153
  {
5729
6154
  name: "mindgraph",
5730
- version: "0.17.0"
6155
+ version: "0.18.0"
5731
6156
  },
5732
6157
  {
5733
6158
  capabilities: {
@@ -5946,7 +6371,7 @@ ${conversation}`
5946
6371
 
5947
6372
  // src/cli.ts
5948
6373
  var fs5 = __toESM(require("fs"));
5949
- var path5 = __toESM(require("path"));
6374
+ var path6 = __toESM(require("path"));
5950
6375
  var os4 = __toESM(require("os"));
5951
6376
  var crypto5 = __toESM(require("crypto"));
5952
6377
  var import_child_process = require("child_process");
@@ -6029,6 +6454,7 @@ var import_node_path3 = __toESM(require("path"));
6029
6454
  var import_node_child_process2 = require("child_process");
6030
6455
  init_codegraph();
6031
6456
  init_code_tool();
6457
+ init_error_detail();
6032
6458
  init_hook_env();
6033
6459
  var NOOP = { kind: "noop" };
6034
6460
  function defaultLedger(sessionId, options) {
@@ -6360,6 +6786,51 @@ function isOwnPriorWork(brief, agentId, harness) {
6360
6786
  const owner = string(object(brief?.lease)?.lease_owner_agent_id);
6361
6787
  return owner === agentId || owner === harness;
6362
6788
  }
6789
+ function hasMindgraphServer(servers) {
6790
+ if (!servers || typeof servers !== "object") return false;
6791
+ for (const [name, entry] of Object.entries(servers)) {
6792
+ if (name === "mindgraph") return true;
6793
+ const cfg = entry && typeof entry === "object" ? entry : void 0;
6794
+ const text = [
6795
+ String(cfg?.command ?? ""),
6796
+ Array.isArray(cfg?.args) ? cfg.args.join(" ") : "",
6797
+ String(cfg?.url ?? "")
6798
+ ].join(" ");
6799
+ if (text.includes("mindgraph-mcp")) return true;
6800
+ }
6801
+ return false;
6802
+ }
6803
+ function pathIsWithin(ancestor, descendant) {
6804
+ const base = import_node_path3.default.resolve(ancestor);
6805
+ const target = import_node_path3.default.resolve(descendant);
6806
+ return target === base || target.startsWith(base + import_node_path3.default.sep);
6807
+ }
6808
+ function claudeMcpRegistrationNote(input, options) {
6809
+ if (options.harness !== "claude-code") return void 0;
6810
+ if (process.env.MINDGRAPH_SKIP_MCP_REGISTRATION_CHECK === "1") return void 0;
6811
+ try {
6812
+ const configPath = options.claudeConfigPath || import_node_path3.default.join(import_node_os3.default.homedir(), ".claude.json");
6813
+ if (!import_node_fs3.default.existsSync(configPath)) return void 0;
6814
+ const config = JSON.parse(import_node_fs3.default.readFileSync(configPath, "utf8"));
6815
+ if (hasMindgraphServer(config.mcpServers)) return void 0;
6816
+ const cwd = import_node_path3.default.resolve(input.cwd || ".");
6817
+ for (const [projectDir, project] of Object.entries(config.projects || {})) {
6818
+ if (!pathIsWithin(projectDir, cwd)) continue;
6819
+ if (hasMindgraphServer(project?.mcpServers)) return void 0;
6820
+ }
6821
+ for (let dir = cwd; ; dir = import_node_path3.default.dirname(dir)) {
6822
+ const candidate = import_node_path3.default.join(dir, ".mcp.json");
6823
+ if (import_node_fs3.default.existsSync(candidate)) {
6824
+ const parsed = JSON.parse(import_node_fs3.default.readFileSync(candidate, "utf8"));
6825
+ if (hasMindgraphServer(parsed.mcpServers)) return void 0;
6826
+ }
6827
+ if (dir === import_node_path3.default.dirname(dir)) break;
6828
+ }
6829
+ } catch {
6830
+ return void 0;
6831
+ }
6832
+ 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.";
6833
+ }
6363
6834
  async function sessionStart(input, client2, options) {
6364
6835
  const agentId = resolveAgentId(options);
6365
6836
  const harnessLabel = options.harness === "claude-code" ? "Claude Code" : "Codex";
@@ -6449,7 +6920,14 @@ async function sessionStart(input, client2, options) {
6449
6920
  claimFailed: claimFailed && !stillHeld,
6450
6921
  scopeNote
6451
6922
  });
6452
- const briefHash = import_node_crypto3.default.createHash("sha256").update(briefText).digest("hex");
6923
+ const registrationNote = claudeMcpRegistrationNote(input, options);
6924
+ if (registrationNote) {
6925
+ console.error(`mindgraph-hook: ${registrationNote}`);
6926
+ }
6927
+ const contextText = registrationNote ? `${registrationNote}
6928
+
6929
+ ${briefText}` : briefText;
6930
+ const briefHash = import_node_crypto3.default.createHash("sha256").update(contextText).digest("hex");
6453
6931
  const shouldInject = withLedger(input.session_id, options, (ledger) => {
6454
6932
  const changed = ledger.lastBriefHash !== briefHash;
6455
6933
  const reinjectCompact = options.reinjectUnchangedCompact && input.source === "compact";
@@ -6482,7 +6960,7 @@ async function sessionStart(input, client2, options) {
6482
6960
  ledger.lastBriefHash = briefHash;
6483
6961
  return changed || reinjectCompact;
6484
6962
  });
6485
- return shouldInject ? { kind: "context", context: briefText } : NOOP;
6963
+ return shouldInject ? { kind: "context", context: contextText } : NOOP;
6486
6964
  }
6487
6965
  async function preTool(input, options) {
6488
6966
  const original = { ...input.tool_input || {} };
@@ -6589,9 +7067,7 @@ async function postTool(input, client2, options) {
6589
7067
  }
6590
7068
  }
6591
7069
  } 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;
7070
+ adoptConflictFence(ledger, payload);
6595
7071
  }
6596
7072
  }
6597
7073
  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 +7124,35 @@ async function postTool(input, client2, options) {
6648
7124
  })
6649
7125
  );
6650
7126
  withLedger(input.session_id, options, (ledger) => {
6651
- if (payloadIsError(response)) return void 0;
6652
7127
  if (ledger.taskUid !== active.taskUid) return void 0;
7128
+ if (payloadIsError(response)) {
7129
+ adoptConflictFence(ledger, response);
7130
+ return void 0;
7131
+ }
6653
7132
  ledger.taskVersion = number(response?.task_version) ?? ledger.taskVersion;
6654
7133
  ledger.leaseEpoch = number(response?.lease_epoch) ?? ledger.leaseEpoch;
6655
7134
  ledger.leaseExpiresAt = number(response?.lease_expires_at) ?? ledger.leaseExpiresAt;
6656
7135
  ledger.lastLeaseRenewalAt = nowSeconds;
6657
7136
  return void 0;
6658
7137
  });
6659
- } catch {
7138
+ } catch (cause) {
7139
+ const conflict = conflictState(cause);
7140
+ if (Object.keys(conflict).length > 0) {
7141
+ withLedger(input.session_id, options, (ledger) => {
7142
+ if (ledger.taskUid !== active.taskUid) return void 0;
7143
+ adoptConflictFence(ledger, conflict);
7144
+ return void 0;
7145
+ });
7146
+ }
6660
7147
  }
6661
7148
  }
6662
7149
  return NOOP;
6663
7150
  }
7151
+ function adoptConflictFence(ledger, payload) {
7152
+ ledger.taskVersion = number(payload?.current_version) ?? ledger.taskVersion;
7153
+ ledger.leaseEpoch = number(payload?.current_epoch) ?? ledger.leaseEpoch;
7154
+ ledger.leaseExpiresAt = number(payload?.lease_expires_at) ?? ledger.leaseExpiresAt;
7155
+ }
6664
7156
  function payloadIsError(payload) {
6665
7157
  return Boolean(payload && payload.error);
6666
7158
  }
@@ -6813,189 +7305,13 @@ async function runCodexHook(input, client2, options = {}) {
6813
7305
  }
6814
7306
  }
6815
7307
 
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
7308
  // src/cli.ts
7309
+ init_hook_installer();
6994
7310
  function recordHookHealth(harness, error) {
6995
7311
  try {
6996
- const dir = process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime");
7312
+ const dir = process.env.MINDGRAPH_RUNTIME_DIR || path6.join(os4.homedir(), ".mindgraph", "runtime");
6997
7313
  fs5.mkdirSync(dir, { recursive: true, mode: 448 });
6998
- const file = path5.join(dir, "hook-health.json");
7314
+ const file = path6.join(dir, "hook-health.json");
6999
7315
  let failures = 0;
7000
7316
  try {
7001
7317
  failures = Number(JSON.parse(fs5.readFileSync(file, "utf8")).consecutiveFailures) || 0;
@@ -7017,8 +7333,8 @@ function recordHookHealth(harness, error) {
7017
7333
  }
7018
7334
  function clearHookHealth(harness) {
7019
7335
  try {
7020
- const file = path5.join(
7021
- process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
7336
+ const file = path6.join(
7337
+ process.env.MINDGRAPH_RUNTIME_DIR || path6.join(os4.homedir(), ".mindgraph", "runtime"),
7022
7338
  "hook-health.json"
7023
7339
  );
7024
7340
  if (!fs5.existsSync(file)) return;
@@ -7040,7 +7356,7 @@ function clearHookHealth(harness) {
7040
7356
  function getClaudeDesktopConfigPath() {
7041
7357
  switch (process.platform) {
7042
7358
  case "darwin":
7043
- return path5.join(
7359
+ return path6.join(
7044
7360
  os4.homedir(),
7045
7361
  "Library",
7046
7362
  "Application Support",
@@ -7048,13 +7364,13 @@ function getClaudeDesktopConfigPath() {
7048
7364
  "claude_desktop_config.json"
7049
7365
  );
7050
7366
  case "win32":
7051
- return path5.join(
7052
- process.env.APPDATA || path5.join(os4.homedir(), "AppData", "Roaming"),
7367
+ return path6.join(
7368
+ process.env.APPDATA || path6.join(os4.homedir(), "AppData", "Roaming"),
7053
7369
  "Claude",
7054
7370
  "claude_desktop_config.json"
7055
7371
  );
7056
7372
  case "linux":
7057
- return path5.join(
7373
+ return path6.join(
7058
7374
  os4.homedir(),
7059
7375
  ".config",
7060
7376
  "Claude",
@@ -7070,7 +7386,7 @@ function readJsonFile(filePath) {
7070
7386
  return JSON.parse(content);
7071
7387
  }
7072
7388
  function writeJsonFile(filePath, data) {
7073
- const dir = path5.dirname(filePath);
7389
+ const dir = path6.dirname(filePath);
7074
7390
  if (!fs5.existsSync(dir)) {
7075
7391
  fs5.mkdirSync(dir, { recursive: true });
7076
7392
  }
@@ -7206,8 +7522,8 @@ function printStatus() {
7206
7522
  `MINDGRAPH_BASE_URL: ${process.env.MINDGRAPH_BASE_URL || "https://api.mindgraph.cloud (default)"}`
7207
7523
  );
7208
7524
  try {
7209
- const healthFile = path5.join(
7210
- process.env.MINDGRAPH_RUNTIME_DIR || path5.join(os4.homedir(), ".mindgraph", "runtime"),
7525
+ const healthFile = path6.join(
7526
+ process.env.MINDGRAPH_RUNTIME_DIR || path6.join(os4.homedir(), ".mindgraph", "runtime"),
7211
7527
  "hook-health.json"
7212
7528
  );
7213
7529
  if (fs5.existsSync(healthFile)) {
@@ -7228,7 +7544,7 @@ function withHooksFlag() {
7228
7544
  }
7229
7545
  function finishCodeInstall(apiKey, baseUrl, withHooks, agentId) {
7230
7546
  if (withHooks) {
7231
- const runner = installHookRunner(__filename);
7547
+ const runner = installHookRunner(__filename, void 0, "0.18.0");
7232
7548
  console.log(`Installed pinned hook runner at ${runner}`);
7233
7549
  const result = installClaudeHooks("user");
7234
7550
  console.log(
@@ -7458,7 +7774,7 @@ async function main2() {
7458
7774
  if (hookHarness !== "claude-code" && hookHarness !== "codex") {
7459
7775
  throw new Error("--harness must be claude-code or codex");
7460
7776
  }
7461
- const runner = installHookRunner(__filename);
7777
+ const runner = installHookRunner(__filename, void 0, "0.18.0");
7462
7778
  console.log(`Installed pinned hook runner at ${runner}`);
7463
7779
  const result = hookHarness === "codex" ? installCodexHooks(scope, projectDir) : installClaudeHooks(scope, projectDir);
7464
7780
  console.log(