grepmax 0.23.0 → 0.25.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +25 -6
  3. package/dist/commands/add.js +38 -31
  4. package/dist/commands/config.js +16 -15
  5. package/dist/commands/context.js +38 -13
  6. package/dist/commands/doctor.js +76 -83
  7. package/dist/commands/extract.js +12 -1
  8. package/dist/commands/impact.js +33 -1
  9. package/dist/commands/index.js +23 -24
  10. package/dist/commands/list.js +23 -14
  11. package/dist/commands/mcp.js +494 -162
  12. package/dist/commands/peek.js +16 -5
  13. package/dist/commands/repair.js +54 -120
  14. package/dist/commands/search-output.js +30 -9
  15. package/dist/commands/search-run.js +75 -47
  16. package/dist/commands/search-skeletons.js +28 -18
  17. package/dist/commands/search.js +45 -49
  18. package/dist/commands/serve.js +415 -373
  19. package/dist/commands/setup.js +2 -2
  20. package/dist/commands/similar.js +5 -5
  21. package/dist/commands/skeleton.js +67 -41
  22. package/dist/commands/status.js +5 -2
  23. package/dist/commands/summarize.js +6 -0
  24. package/dist/commands/surprises.js +150 -0
  25. package/dist/commands/watch.js +102 -38
  26. package/dist/config.js +3 -0
  27. package/dist/eval-surprising-connections.js +191 -0
  28. package/dist/index.js +2 -0
  29. package/dist/lib/analysis/surprising-connections.js +600 -0
  30. package/dist/lib/daemon/daemon.js +1101 -379
  31. package/dist/lib/daemon/ipc-handler.js +122 -13
  32. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  33. package/dist/lib/daemon/process-manager.js +7 -4
  34. package/dist/lib/daemon/search-handler.js +23 -9
  35. package/dist/lib/daemon/watcher-manager.js +353 -110
  36. package/dist/lib/graph/impact-rollup.js +202 -0
  37. package/dist/lib/graph/impact.js +15 -1
  38. package/dist/lib/help/agent-cheatsheet.js +1 -0
  39. package/dist/lib/index/batch-processor.js +231 -67
  40. package/dist/lib/index/embedding-generation.js +109 -0
  41. package/dist/lib/index/embedding-status.js +23 -0
  42. package/dist/lib/index/file-policy.js +273 -0
  43. package/dist/lib/index/ignore-patterns.js +4 -0
  44. package/dist/lib/index/index-config.js +18 -4
  45. package/dist/lib/index/syncer.js +256 -79
  46. package/dist/lib/index/walker.js +66 -86
  47. package/dist/lib/index/watcher-batch.js +9 -0
  48. package/dist/lib/index/watcher.js +129 -3
  49. package/dist/lib/llm/server.js +6 -0
  50. package/dist/lib/search/searcher.js +30 -19
  51. package/dist/lib/skeleton/skeletonizer.js +118 -0
  52. package/dist/lib/skeleton/summary-formatter.js +8 -1
  53. package/dist/lib/store/store-lease.js +473 -0
  54. package/dist/lib/store/vector-db.js +302 -57
  55. package/dist/lib/utils/blocked-roots.js +63 -0
  56. package/dist/lib/utils/cross-project.js +7 -3
  57. package/dist/lib/utils/daemon-client.js +24 -1
  58. package/dist/lib/utils/daemon-launcher.js +38 -13
  59. package/dist/lib/utils/doctor-status.js +76 -0
  60. package/dist/lib/utils/file-utils.js +74 -4
  61. package/dist/lib/utils/keyed-mutex.js +101 -0
  62. package/dist/lib/utils/logger.js +57 -1
  63. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  64. package/dist/lib/utils/operation-coordinator.js +146 -0
  65. package/dist/lib/utils/path-containment.js +106 -0
  66. package/dist/lib/utils/process.js +44 -0
  67. package/dist/lib/utils/project-registry.js +351 -3
  68. package/dist/lib/utils/scope-filter.js +3 -9
  69. package/dist/lib/utils/stale-hint.js +12 -19
  70. package/dist/lib/utils/watcher-launcher.js +5 -1
  71. package/dist/lib/utils/watcher-store.js +2 -2
  72. package/dist/lib/workers/colbert-math.js +15 -12
  73. package/dist/lib/workers/embeddings/colbert.js +3 -2
  74. package/dist/lib/workers/embeddings/granite.js +4 -3
  75. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  76. package/dist/lib/workers/orchestrator.js +39 -8
  77. package/dist/lib/workers/pool.js +150 -83
  78. package/dist/lib/workers/process-child.js +11 -2
  79. package/dist/lib/workers/serialized-handler.js +10 -0
  80. package/dist/lib/workers/worker.js +13 -4
  81. package/mlx-embed-server/server.py +21 -3
  82. package/mlx-embed-server/summarizer.py +8 -0
  83. package/package.json +6 -3
  84. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  85. package/plugins/grepmax/hooks/start.js +3 -170
@@ -53,11 +53,18 @@ exports.mcp = void 0;
53
53
  exports.toStringArray = toStringArray;
54
54
  exports.ok = ok;
55
55
  exports.err = err;
56
+ exports.isExplicitCrossProjectSearch = isExplicitCrossProjectSearch;
57
+ exports.resolveMcpProject = resolveMcpProject;
58
+ exports.containMcpTarget = containMcpTarget;
59
+ exports.mcpRootPrefix = mcpRootPrefix;
60
+ exports.resolveMcpSourceFile = resolveMcpSourceFile;
56
61
  exports.searchResultPath = searchResultPath;
57
62
  exports.searchResultStartLine = searchResultStartLine;
58
63
  exports.searchResultEndLine = searchResultEndLine;
59
64
  exports.filterMcpSearchResults = filterMcpSearchResults;
60
65
  exports.formatMcpPointerSearchResults = formatMcpPointerSearchResults;
66
+ exports.formatMcpSurprisingConnections = formatMcpSurprisingConnections;
67
+ exports.mcpLogQuery = mcpLogQuery;
61
68
  const fs = __importStar(require("node:fs"));
62
69
  const path = __importStar(require("node:path"));
63
70
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -65,8 +72,10 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
65
72
  const commander_1 = require("commander");
66
73
  const zod_1 = require("zod");
67
74
  const config_1 = require("../config");
75
+ const surprising_connections_1 = require("../lib/analysis/surprising-connections");
68
76
  const languages_1 = require("../lib/core/languages");
69
77
  const graph_builder_1 = require("../lib/graph/graph-builder");
78
+ const embedding_status_1 = require("../lib/index/embedding-status");
70
79
  const index_config_1 = require("../lib/index/index-config");
71
80
  const syncer_1 = require("../lib/index/syncer");
72
81
  const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
@@ -77,12 +86,15 @@ const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
77
86
  const symbol_extractor_1 = require("../lib/skeleton/symbol-extractor");
78
87
  const meta_cache_1 = require("../lib/store/meta-cache");
79
88
  const vector_db_1 = require("../lib/store/vector-db");
89
+ const cross_project_1 = require("../lib/utils/cross-project");
80
90
  const file_utils_1 = require("../lib/utils/file-utils");
81
91
  const filter_builder_1 = require("../lib/utils/filter-builder");
82
92
  const format_helpers_1 = require("../lib/utils/format-helpers");
83
93
  const import_extractor_1 = require("../lib/utils/import-extractor");
94
+ const path_containment_1 = require("../lib/utils/path-containment");
84
95
  const project_registry_1 = require("../lib/utils/project-registry");
85
96
  const project_root_1 = require("../lib/utils/project-root");
97
+ const scope_filter_1 = require("../lib/utils/scope-filter");
86
98
  const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
87
99
  const watcher_store_1 = require("../lib/utils/watcher-store");
88
100
  const audit_1 = require("./audit");
@@ -109,6 +121,51 @@ function ok(text) {
109
121
  function err(text) {
110
122
  return { content: [{ type: "text", text }], isError: true };
111
123
  }
124
+ function isExplicitCrossProjectSearch(args, isSearchAll = false) {
125
+ return (isSearchAll ||
126
+ args.scope === "all" ||
127
+ (typeof args.projects === "string" && args.projects.trim() !== "") ||
128
+ (typeof args.exclude_projects === "string" &&
129
+ args.exclude_projects.trim() !== ""));
130
+ }
131
+ function resolveMcpProject(requested, currentRoot, projects = (0, project_registry_1.listProjects)()) {
132
+ if (typeof requested === "string" && requested.trim()) {
133
+ const value = requested.trim();
134
+ const byName = projects.filter((project) => project.name === value);
135
+ if (byName.length === 1)
136
+ return byName[0];
137
+ if (byName.length > 1)
138
+ return undefined;
139
+ const resolved = path.resolve(value);
140
+ return projects.find((project) => path.resolve(project.root) === resolved);
141
+ }
142
+ const resolvedCurrent = path.resolve(currentRoot);
143
+ const exact = projects.find((project) => path.resolve(project.root) === resolvedCurrent);
144
+ if (exact)
145
+ return exact;
146
+ return projects
147
+ .filter((project) => (0, path_containment_1.isPathWithin)(project.root, resolvedCurrent))
148
+ .sort((a, b) => b.root.length - a.root.length)[0];
149
+ }
150
+ function containMcpTarget(root, target) {
151
+ return path.isAbsolute(target) ||
152
+ target.includes("/") ||
153
+ target.includes("\\")
154
+ ? (0, path_containment_1.resolveContainedPath)(root, target)
155
+ : target;
156
+ }
157
+ function mcpRootPrefix(root) {
158
+ return root.endsWith(path.sep) ? root : `${root}${path.sep}`;
159
+ }
160
+ function resolveMcpSourceFile(roots, candidate) {
161
+ for (const root of roots) {
162
+ try {
163
+ return (0, path_containment_1.resolveContainedFile)(root, candidate);
164
+ }
165
+ catch (_a) { }
166
+ }
167
+ return undefined;
168
+ }
112
169
  function chunkAbsPath(chunk) {
113
170
  const metadata = chunk.metadata;
114
171
  return String(chunk.path || (metadata === null || metadata === void 0 ? void 0 : metadata.path) || "");
@@ -171,6 +228,59 @@ function formatMcpPointerSearchResults(data, displayRoot, options = {}) {
171
228
  query: options.query,
172
229
  });
173
230
  }
231
+ function formatMcpSurprisingConnections(result, top = 10) {
232
+ const { summary, findings } = result;
233
+ const lines = [
234
+ [
235
+ "summary",
236
+ `sampled=${summary.sampledAnchors}`,
237
+ `code=${summary.codeRows}`,
238
+ `pairs=${summary.acceptedPairs}`,
239
+ `file_pairs=${summary.acceptedFilePairs}`,
240
+ `score_p90=${summary.actionabilityScore.p90}`,
241
+ ].join("\t"),
242
+ ];
243
+ for (const finding of findings.slice(0, top)) {
244
+ const pair = finding.representative;
245
+ lines.push([
246
+ "surprise",
247
+ finding.score.toFixed(3),
248
+ finding.maxSimilarity.toFixed(3),
249
+ String(finding.pairCount),
250
+ finding.fileA,
251
+ finding.fileB,
252
+ (0, surprising_connections_1.lineLabel)(pair.source),
253
+ (0, surprising_connections_1.lineLabel)(pair.target),
254
+ finding.reasons.join(","),
255
+ `buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}`,
256
+ `top_sims=${finding.topSimilarities.join(",")}`,
257
+ `penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`,
258
+ `next=${(0, surprising_connections_1.skeletonHint)(finding)}`,
259
+ ].join("\t"));
260
+ }
261
+ if (findings.length === 0)
262
+ lines.push("none");
263
+ return lines.join("\n");
264
+ }
265
+ function mcpLogQuery(name, args) {
266
+ var _a, _b;
267
+ const direct = (_b = (_a = args.query) !== null && _a !== void 0 ? _a : args.symbol) !== null && _b !== void 0 ? _b : args.target;
268
+ if (direct !== undefined && direct !== null && String(direct) !== "") {
269
+ return String(direct);
270
+ }
271
+ if (name === "surprising_connections") {
272
+ const parts = [
273
+ "surprising_connections",
274
+ typeof args.root === "string" && args.root ? `root=${args.root}` : "",
275
+ typeof args.in === "string" && args.in ? `in=${args.in}` : "",
276
+ typeof args.exclude === "string" && args.exclude
277
+ ? `exclude=${args.exclude}`
278
+ : "",
279
+ ].filter(Boolean);
280
+ return parts.join(" ");
281
+ }
282
+ return "";
283
+ }
174
284
  // ---------------------------------------------------------------------------
175
285
  // Command
176
286
  // ---------------------------------------------------------------------------
@@ -251,12 +361,16 @@ exports.mcp = new commander_1.Command("mcp")
251
361
  });
252
362
  }
253
363
  // --- Background watcher ---
254
- function ensureWatcher() {
364
+ function ensureWatcher(root) {
255
365
  return __awaiter(this, void 0, void 0, function* () {
366
+ var _a;
256
367
  try {
257
- const result = yield (0, watcher_launcher_1.launchWatcher)(projectRoot);
368
+ const watchRoot = root !== null && root !== void 0 ? root : (_a = resolveMcpProject(undefined, projectRoot)) === null || _a === void 0 ? void 0 : _a.root;
369
+ if (!watchRoot)
370
+ return;
371
+ const result = yield (0, watcher_launcher_1.launchWatcher)(watchRoot);
258
372
  if (result.ok && !result.reused) {
259
- console.log(`[MCP] Started background watcher for ${projectRoot} (PID: ${result.pid})`);
373
+ console.log(`[MCP] Started background watcher for ${watchRoot} (PID: ${result.pid})`);
260
374
  }
261
375
  }
262
376
  catch (err) {
@@ -264,66 +378,78 @@ exports.mcp = new commander_1.Command("mcp")
264
378
  }
265
379
  });
266
380
  }
381
+ // Resolve the registered project this server scopes to. The server pins to
382
+ // its launch cwd (findProjectRoot, .git-only), but a registered project may
383
+ // be an umbrella with no .git of its own — so a session launched in a
384
+ // subdirectory resolves to a path that isn't the registered root. Prefer an
385
+ // exact registry match; otherwise walk the registry for an ancestor that
386
+ // covers the cwd, and scope to it instead of silently widening to the whole
387
+ // index. `root` is the registered project root to scope to.
388
+ function resolveRegisteredProject(requested) {
389
+ const project = resolveMcpProject(requested, projectRoot);
390
+ if (project)
391
+ return { proj: project, root: project.root };
392
+ return { proj: undefined, root: projectRoot };
393
+ }
267
394
  // --- Tool handlers ---
268
395
  function handleSemanticSearch(args_1) {
269
396
  return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
397
+ var _a, _b;
270
398
  const query = String(args.query || "");
271
399
  if (!query)
272
400
  return err("Missing required parameter: query");
273
- let searchAll = isSearchAll || args.scope === "all";
401
+ const searchAll = isExplicitCrossProjectSearch(args, isSearchAll);
274
402
  const limit = Math.min(Math.max(Number(args.limit) || 3, 1), 50);
275
- ensureWatcher();
276
403
  // Project resolution. The server is pinned to whatever cwd it launched in
277
- // (resolved once at startup). When that cwd ISN'T an indexed project and
278
- // the caller hasn't pinned an explicit --root, fall back to cross-project
279
- // search instead of dead-ending this is what makes the server usable
280
- // from a session that isn't sitting inside a registered repo.
281
- let scopeNote = "";
282
- const proj = (0, project_registry_1.getProject)(projectRoot);
283
- if (!proj && typeof args.root !== "string") {
284
- const indexed = (0, project_registry_1.listProjects)().filter((p) => { var _a; return p.status !== "pending" && ((_a = p.chunkCount) !== null && _a !== void 0 ? _a : 0) > 0; });
285
- if (indexed.length === 0) {
286
- return err("No indexed projects yet. cd into a repo and run `gmax add` to index it first.");
287
- }
288
- searchAll = true;
289
- scopeNote =
290
- `Note: ${path.basename(projectRoot)}/ isn't an indexed gmax project ` +
291
- `searching all ${indexed.length} indexed project(s). ` +
292
- `Call list_projects to see them, then pass projects:"name" to narrow.`;
293
- }
294
- else if (!proj) {
295
- return err("Project not added to gmax yet. Run `gmax add` to index it first.");
296
- }
297
- else if (proj.status === "pending" || proj.chunkCount === 0) {
298
- return err("Project not indexed yet. Run `gmax add` to index it first.");
404
+ // (resolved once at startup). Resolve that cwd to its registered project
405
+ // exact match, or a registered ancestor when the cwd is a subdirectory of
406
+ // an umbrella project. Searches scope to `resolvedRoot`.
407
+ const { proj, root: resolvedRoot } = resolveRegisteredProject(args.root);
408
+ if (searchAll &&
409
+ (args.root !== undefined ||
410
+ args.path !== undefined ||
411
+ args.exclude !== undefined)) {
412
+ return err("root, path, and exclude are single-project filters; omit them for cross-project search.");
413
+ }
414
+ // Cross-project search (the whole index) is opt-in: it happens only when
415
+ // the caller passes scope:"all" or projects:"…". Otherwise we require a
416
+ // resolved project and error loudly when there isn't one — never silently
417
+ // widen a scoped query to every indexed project.
418
+ if (!searchAll) {
419
+ if (!proj) {
420
+ if (typeof args.root === "string") {
421
+ return err("Project not added to gmax yet. Run `gmax add` to index it first.");
422
+ }
423
+ const indexed = (0, project_registry_1.listProjects)().filter((p) => { var _a; return p.status !== "pending" && ((_a = p.chunkCount) !== null && _a !== void 0 ? _a : 0) > 0; });
424
+ if (indexed.length === 0) {
425
+ return err("No indexed projects yet. cd into a repo and run `gmax add` to index it first.");
426
+ }
427
+ return err(`${path.basename(projectRoot)}/ isn't an indexed gmax project. ` +
428
+ `Pass scope:"all" to search all ${indexed.length} indexed project(s), ` +
429
+ `projects:"name" to target specific ones, or cd into an indexed project.`);
430
+ }
431
+ if (proj.status === "pending" || proj.chunkCount === 0) {
432
+ return err("Project not indexed yet. Run `gmax add` to index it first.");
433
+ }
434
+ void ensureWatcher(resolvedRoot);
299
435
  }
300
436
  try {
301
- const searcher = getSearcher();
302
437
  // Determine path prefix and display root for relative paths
303
438
  let pathPrefix;
304
- let displayRoot = projectRoot;
439
+ const displayRoot = resolvedRoot;
440
+ let projectScope;
305
441
  if (!searchAll) {
306
- const searchRoot = typeof args.root === "string"
307
- ? path.resolve(args.root)
308
- : path.resolve(projectRoot);
309
- if (typeof args.root === "string" && !fs.existsSync(searchRoot)) {
310
- return err(`Directory not found: ${args.root}`);
311
- }
312
- displayRoot = searchRoot;
313
- pathPrefix = searchRoot.endsWith("/") ? searchRoot : `${searchRoot}/`;
314
- if (typeof args.path === "string") {
315
- pathPrefix = path.join(searchRoot, args.path);
316
- if (!pathPrefix.endsWith("/"))
317
- pathPrefix += "/";
318
- }
442
+ projectScope = (0, scope_filter_1.resolveScope)({
443
+ projectRoot: resolvedRoot,
444
+ in: typeof args.path === "string" ? args.path : undefined,
445
+ exclude: typeof args.exclude === "string" ? args.exclude : undefined,
446
+ });
447
+ pathPrefix = projectScope.pathPrefix;
319
448
  }
320
449
  const filters = {};
321
450
  if (typeof args.file === "string" && args.file) {
322
451
  filters.file = args.file;
323
452
  }
324
- if (typeof args.exclude === "string" && args.exclude) {
325
- filters.exclude = args.exclude;
326
- }
327
453
  if (typeof args.language === "string" && args.language) {
328
454
  filters.language = args.language;
329
455
  }
@@ -331,29 +457,32 @@ exports.mcp = new commander_1.Command("mcp")
331
457
  filters.role = args.role;
332
458
  }
333
459
  if (searchAll) {
334
- const allProjects = (0, project_registry_1.listProjects)();
335
- if (typeof args.projects === "string" && args.projects) {
336
- const names = args.projects.split(",").map((s) => s.trim());
337
- const roots = names
338
- .map((n) => { var _a; return (_a = allProjects.find((p) => p.name === n)) === null || _a === void 0 ? void 0 : _a.root; })
339
- .filter(Boolean);
340
- if (roots.length > 0) {
341
- filters.project_roots = roots.join(",");
342
- }
460
+ const crossScope = (0, cross_project_1.resolveCrossProjectScope)({
461
+ allProjects: searchAll &&
462
+ !(typeof args.projects === "string" && args.projects.trim()),
463
+ projects: typeof args.projects === "string" ? args.projects : undefined,
464
+ excludeProjects: typeof args.exclude_projects === "string"
465
+ ? args.exclude_projects
466
+ : undefined,
467
+ });
468
+ if (crossScope.projectRoots.length === 0) {
469
+ return err("No matching indexed projects.");
343
470
  }
344
- if (typeof args.exclude_projects === "string" &&
345
- args.exclude_projects) {
346
- const names = args.exclude_projects
347
- .split(",")
348
- .map((s) => s.trim());
349
- const roots = names
350
- .map((n) => { var _a; return (_a = allProjects.find((p) => p.name === n)) === null || _a === void 0 ? void 0 : _a.root; })
351
- .filter(Boolean);
352
- if (roots.length > 0) {
353
- filters.exclude_project_roots = roots.join(",");
354
- }
471
+ filters.projectRoots = crossScope.projectRoots;
472
+ }
473
+ else if (projectScope) {
474
+ if (projectScope.inPrefixes.length > 0) {
475
+ filters.inPrefixes = projectScope.inPrefixes;
476
+ }
477
+ if (projectScope.excludePrefixes.length > 0) {
478
+ filters.excludePrefixes = projectScope.excludePrefixes;
355
479
  }
356
480
  }
481
+ const selectedRoots = searchAll
482
+ ? new Set((_a = filters.projectRoots) !== null && _a !== void 0 ? _a : [])
483
+ : new Set([resolvedRoot]);
484
+ (0, embedding_status_1.assertEmbeddingSearchCompatible)((0, project_registry_1.listProjects)().filter((project) => selectedRoots.has(project.root)), (0, index_config_1.readGlobalConfig)());
485
+ const searcher = getSearcher();
357
486
  // Aider-style seeding: the agent passes its open files / discussed
358
487
  // symbols; the searcher biases candidate generation toward them.
359
488
  const parseSeedList = (v) => {
@@ -372,11 +501,18 @@ exports.mcp = new commander_1.Command("mcp")
372
501
  ? { files: seedFiles, symbols: seedSymbols }
373
502
  : undefined;
374
503
  const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1", seeds }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
375
- // Prepend the scope-fallback note (if any) + searcher warnings to
376
- // whatever body we return, so the agent learns it searched all projects.
504
+ const allowedRoots = searchAll
505
+ ? ((_b = filters.projectRoots) !== null && _b !== void 0 ? _b : [])
506
+ : [resolvedRoot];
507
+ result.data = result.data.filter((chunk) => {
508
+ const candidate = chunkAbsPath(chunk);
509
+ return (candidate.length > 0 &&
510
+ allowedRoots.some((root) => (0, path_containment_1.isPathWithin)(root, candidate)));
511
+ });
512
+ // Prepend any searcher warnings to whatever body we return.
377
513
  const prefixNotes = (body) => {
378
514
  var _a;
379
- const notes = [scopeNote, ...((_a = result.warnings) !== null && _a !== void 0 ? _a : [])].filter(Boolean);
515
+ const notes = ((_a = result.warnings) !== null && _a !== void 0 ? _a : []).filter(Boolean);
380
516
  return notes.length ? `${notes.join("\n")}\n\n${body}` : body;
381
517
  };
382
518
  if (!result.data || result.data.length === 0) {
@@ -393,8 +529,11 @@ exports.mcp = new commander_1.Command("mcp")
393
529
  var _a;
394
530
  if (!includeImports || !absPath)
395
531
  return "";
532
+ const sourcePath = resolveMcpSourceFile(allowedRoots, absPath);
533
+ if (!sourcePath)
534
+ return "";
396
535
  if (!importCache.has(absPath)) {
397
- importCache.set(absPath, (0, import_extractor_1.extractImports)(absPath));
536
+ importCache.set(absPath, (0, import_extractor_1.extractImports)(sourcePath));
398
537
  }
399
538
  return (_a = importCache.get(absPath)) !== null && _a !== void 0 ? _a : "";
400
539
  };
@@ -438,7 +577,10 @@ exports.mcp = new commander_1.Command("mcp")
438
577
  if (contextN > 0 && absPath) {
439
578
  // Read surrounding context from file
440
579
  try {
441
- const fileContent = fs.readFileSync(absPath, "utf-8");
580
+ const sourcePath = resolveMcpSourceFile(allowedRoots, absPath);
581
+ if (!sourcePath)
582
+ throw new Error("indexed path is outside scope");
583
+ const fileContent = fs.readFileSync(sourcePath, "utf-8");
442
584
  const fileLines = fileContent.split("\n");
443
585
  const ctxStart = Math.max(0, startLine - contextN);
444
586
  const ctxEnd = Math.min(fileLines.length, endLine + 1 + contextN);
@@ -503,7 +645,7 @@ exports.mcp = new commander_1.Command("mcp")
503
645
  const regex = new RegExp(namePattern, "i");
504
646
  results = results.filter((r) => r.symbols.some((s) => regex.test(s)));
505
647
  }
506
- catch (_a) {
648
+ catch (_c) {
507
649
  // Invalid regex — skip filter
508
650
  }
509
651
  }
@@ -512,19 +654,19 @@ exports.mcp = new commander_1.Command("mcp")
512
654
  if (mode === "symbol" && !searchAll) {
513
655
  try {
514
656
  const db = getVectorDb();
515
- const builder = new graph_builder_1.GraphBuilder(db);
657
+ const builder = new graph_builder_1.GraphBuilder(db, resolvedRoot);
516
658
  const graph = yield builder.buildGraph(query);
517
659
  if (graph.center) {
518
660
  const traceLines = ["", "--- Call graph ---"];
519
- const centerRel = graph.center.file.startsWith(projectRoot)
520
- ? graph.center.file.slice(projectRoot.length + 1)
661
+ const centerRel = graph.center.file.startsWith(resolvedRoot)
662
+ ? graph.center.file.slice(resolvedRoot.length + 1)
521
663
  : graph.center.file;
522
664
  traceLines.push(`${graph.center.symbol} [${graph.center.role}] ${centerRel}:${graph.center.line + 1}`);
523
665
  if (graph.callers.length > 0) {
524
666
  traceLines.push("Callers:");
525
667
  for (const caller of graph.callers) {
526
- const rel = caller.file.startsWith(projectRoot)
527
- ? caller.file.slice(projectRoot.length + 1)
668
+ const rel = caller.file.startsWith(resolvedRoot)
669
+ ? caller.file.slice(resolvedRoot.length + 1)
528
670
  : caller.file;
529
671
  traceLines.push(` <- ${caller.symbol} ${rel}:${caller.line + 1}`);
530
672
  }
@@ -533,8 +675,8 @@ exports.mcp = new commander_1.Command("mcp")
533
675
  traceLines.push("Calls:");
534
676
  for (const callee of graph.callees.slice(0, 15)) {
535
677
  if (callee.file) {
536
- const rel = callee.file.startsWith(projectRoot)
537
- ? callee.file.slice(projectRoot.length + 1)
678
+ const rel = callee.file.startsWith(resolvedRoot)
679
+ ? callee.file.slice(resolvedRoot.length + 1)
538
680
  : callee.file;
539
681
  traceLines.push(` -> ${callee.symbol} ${rel}:${callee.line + 1}`);
540
682
  }
@@ -546,7 +688,7 @@ exports.mcp = new commander_1.Command("mcp")
546
688
  output += `\n${traceLines.join("\n")}`;
547
689
  }
548
690
  }
549
- catch (_b) {
691
+ catch (_d) {
550
692
  // Trace failed — return search results without trace
551
693
  }
552
694
  }
@@ -560,10 +702,14 @@ exports.mcp = new commander_1.Command("mcp")
560
702
  }
561
703
  function handleCodeSkeleton(args) {
562
704
  return __awaiter(this, void 0, void 0, function* () {
563
- ensureWatcher();
564
705
  const target = String(args.target || "");
565
706
  if (!target)
566
707
  return err("Missing required parameter: target");
708
+ const { proj, root } = resolveRegisteredProject();
709
+ if (!proj) {
710
+ return err("Project not added to gmax yet. Run `gmax add` to index it first.");
711
+ }
712
+ void ensureWatcher(root);
567
713
  const fileLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 20);
568
714
  // Determine targets: comma-separated, directory, or single file
569
715
  let targets;
@@ -574,12 +720,14 @@ exports.mcp = new commander_1.Command("mcp")
574
720
  .filter(Boolean);
575
721
  }
576
722
  else {
577
- const absPath = path.resolve(projectRoot, target);
723
+ const absPath = (0, path_containment_1.resolveContainedPath)(root, target, {
724
+ verifyExistingTarget: true,
725
+ });
578
726
  if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
579
727
  const entries = fs.readdirSync(absPath, { withFileTypes: true });
580
728
  targets = entries
581
729
  .filter((e) => e.isFile() && (0, file_utils_1.isIndexableFile)(path.join(absPath, e.name)))
582
- .map((e) => path.relative(projectRoot, path.join(absPath, e.name)))
730
+ .map((e) => path.relative(root, path.join(absPath, e.name)))
583
731
  .slice(0, fileLimit);
584
732
  if (targets.length === 0) {
585
733
  return err(`No indexable files found in ${target}`);
@@ -595,8 +743,11 @@ exports.mcp = new commander_1.Command("mcp")
595
743
  const jsonFiles = [];
596
744
  const skel = yield getSkeletonizer();
597
745
  for (const t of targets) {
598
- const absPath = path.resolve(projectRoot, t);
599
- if (!fs.existsSync(absPath)) {
746
+ let absPath;
747
+ try {
748
+ absPath = (0, path_containment_1.resolveContainedFile)(root, t);
749
+ }
750
+ catch (_a) {
600
751
  if (fmt !== "json")
601
752
  parts.push(`// ${t} — file not found`);
602
753
  continue;
@@ -606,7 +757,7 @@ exports.mcp = new commander_1.Command("mcp")
606
757
  try {
607
758
  sourceContent = fs.readFileSync(absPath, "utf-8");
608
759
  }
609
- catch (_a) { }
760
+ catch (_b) { }
610
761
  let skeleton = "";
611
762
  let language = "";
612
763
  let tokenEstimate = 0;
@@ -619,7 +770,7 @@ exports.mcp = new commander_1.Command("mcp")
619
770
  tokenEstimate = Math.ceil(cached.length / 4);
620
771
  }
621
772
  }
622
- catch (_b) { }
773
+ catch (_c) { }
623
774
  // Generate live if no cache
624
775
  if (!skeleton) {
625
776
  try {
@@ -672,17 +823,17 @@ exports.mcp = new commander_1.Command("mcp")
672
823
  }
673
824
  function handleTraceCalls(args) {
674
825
  return __awaiter(this, void 0, void 0, function* () {
675
- ensureWatcher();
676
826
  const symbol = String(args.symbol || "");
677
827
  if (!symbol)
678
828
  return err("Missing required parameter: symbol");
679
- const proj = (0, project_registry_1.getProject)(projectRoot);
829
+ const { proj, root } = resolveRegisteredProject();
680
830
  if (!proj) {
681
831
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
682
832
  }
833
+ void ensureWatcher(root);
683
834
  try {
684
835
  const db = getVectorDb();
685
- const builder = new graph_builder_1.GraphBuilder(db);
836
+ const builder = new graph_builder_1.GraphBuilder(db, root);
686
837
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
687
838
  const graph = yield builder.buildGraphMultiHop(symbol, depth);
688
839
  if (!graph.center) {
@@ -699,8 +850,8 @@ exports.mcp = new commander_1.Command("mcp")
699
850
  if (filteredImporters.length > 0) {
700
851
  lines.push("Imported by:");
701
852
  for (const imp of filteredImporters.slice(0, 10)) {
702
- const rel = imp.startsWith(projectRoot)
703
- ? imp.slice(projectRoot.length + 1)
853
+ const rel = imp.startsWith(root)
854
+ ? imp.slice(root.length + 1)
704
855
  : imp;
705
856
  lines.push(` ${rel}`);
706
857
  }
@@ -709,8 +860,8 @@ exports.mcp = new commander_1.Command("mcp")
709
860
  // Callers (recursive tree)
710
861
  function formatCallerTree(trees, indent) {
711
862
  for (const t of trees) {
712
- const rel = t.node.file.startsWith(projectRoot)
713
- ? t.node.file.slice(projectRoot.length + 1)
863
+ const rel = t.node.file.startsWith(root)
864
+ ? t.node.file.slice(root.length + 1)
714
865
  : t.node.file;
715
866
  const pad = " ".repeat(indent);
716
867
  lines.push(`${pad}<- ${t.node.symbol} ${rel}:${t.node.line + 1}`);
@@ -729,8 +880,8 @@ exports.mcp = new commander_1.Command("mcp")
729
880
  lines.push("Calls:");
730
881
  for (const callee of graph.callees.slice(0, 15)) {
731
882
  if (callee.file) {
732
- const rel = callee.file.startsWith(projectRoot)
733
- ? callee.file.slice(projectRoot.length + 1)
883
+ const rel = callee.file.startsWith(root)
884
+ ? callee.file.slice(root.length + 1)
734
885
  : callee.file;
735
886
  lines.push(` -> ${callee.symbol} ${rel}:${callee.line + 1}`);
736
887
  }
@@ -760,7 +911,9 @@ exports.mcp = new commander_1.Command("mcp")
760
911
  if (!symbol)
761
912
  return err("Missing required parameter: symbol");
762
913
  try {
763
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
914
+ const { proj, root } = resolveRegisteredProject(args.root);
915
+ if (!proj)
916
+ return err("Project not added to gmax yet.");
764
917
  const db = getVectorDb();
765
918
  const table = yield db.ensureTable();
766
919
  const prefix = root.endsWith("/") ? root : `${root}/`;
@@ -800,7 +953,7 @@ exports.mcp = new commander_1.Command("mcp")
800
953
  return ((ROLE_PRI[String(b.role)] || 0) - (ROLE_PRI[String(a.role)] || 0));
801
954
  });
802
955
  const best = sorted[0];
803
- const filePath = String(best.path);
956
+ const filePath = (0, path_containment_1.resolveContainedFile)(root, String(best.path));
804
957
  const startLine = Number(best.start_line || 0);
805
958
  const endLine = Number(best.end_line || 0);
806
959
  const role = String(best.role || "IMPLEMENTATION");
@@ -851,7 +1004,9 @@ exports.mcp = new commander_1.Command("mcp")
851
1004
  if (!symbol)
852
1005
  return err("Missing required parameter: symbol");
853
1006
  try {
854
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1007
+ const { proj, root } = resolveRegisteredProject(args.root);
1008
+ if (!proj)
1009
+ return err("Project not added to gmax yet.");
855
1010
  const depth = Math.min(Math.max(Number(args.depth || 1), 1), 3);
856
1011
  const db = getVectorDb();
857
1012
  const { GraphBuilder } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/graph-builder")));
@@ -883,7 +1038,8 @@ exports.mcp = new commander_1.Command("mcp")
883
1038
  let sigText = "(source not available)";
884
1039
  let bodyLines = 0;
885
1040
  try {
886
- const content = fs.readFileSync(center.file, "utf-8");
1041
+ const centerPath = (0, path_containment_1.resolveContainedFile)(root, center.file);
1042
+ const content = fs.readFileSync(centerPath, "utf-8");
887
1043
  const lines = content.split("\n");
888
1044
  const chunk = lines.slice(startLine, endLine + 1);
889
1045
  bodyLines = chunk.length;
@@ -982,7 +1138,9 @@ exports.mcp = new commander_1.Command("mcp")
982
1138
  if (!symbol)
983
1139
  return err("Missing required parameter: symbol");
984
1140
  try {
985
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1141
+ const { proj, root } = resolveRegisteredProject(args.root);
1142
+ if (!proj)
1143
+ return err("Project not added to gmax yet.");
986
1144
  const db = getVectorDb();
987
1145
  const table = yield db.ensureTable();
988
1146
  const prefix = root.endsWith("/") ? root : `${root}/`;
@@ -1027,9 +1185,9 @@ exports.mcp = new commander_1.Command("mcp")
1027
1185
  function handleAudit(args) {
1028
1186
  return __awaiter(this, void 0, void 0, function* () {
1029
1187
  ensureWatcher();
1030
- const root = typeof args.root === "string" && args.root
1031
- ? path.resolve(args.root)
1032
- : projectRoot;
1188
+ const { proj, root } = resolveRegisteredProject(args.root);
1189
+ if (!proj)
1190
+ return err("Project not added to gmax yet.");
1033
1191
  const prefix = root.endsWith("/") ? root : `${root}/`;
1034
1192
  const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
1035
1193
  try {
@@ -1106,6 +1264,43 @@ exports.mcp = new commander_1.Command("mcp")
1106
1264
  }
1107
1265
  });
1108
1266
  }
1267
+ function handleSurprisingConnections(args) {
1268
+ return __awaiter(this, void 0, void 0, function* () {
1269
+ ensureWatcher();
1270
+ if (args.experimental !== true) {
1271
+ return err("surprising_connections is experimental; pass experimental:true.");
1272
+ }
1273
+ const { proj, root } = resolveRegisteredProject(args.root);
1274
+ if (!proj)
1275
+ return err("Project not added to gmax yet.");
1276
+ const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
1277
+ const sample = Math.min(Math.max(Number(args.sample) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample, 1), 10000);
1278
+ const neighbors = Math.min(Math.max(Number(args.neighbors) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 1), 200);
1279
+ const dirDepth = Math.min(Math.max(Number(args.dir_depth) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 1), 20);
1280
+ const minSimilarity = Math.max(Number(args.min_similarity) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity, 0);
1281
+ const maxRows = Math.min(Math.max(Number(args.max_rows) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, 1), surprising_connections_1.MAX_SURPRISE_ROWS);
1282
+ try {
1283
+ const db = getVectorDb();
1284
+ const table = yield db.ensureTable();
1285
+ const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, root, {
1286
+ sample,
1287
+ neighbors,
1288
+ dirDepth,
1289
+ minSimilarity,
1290
+ maxRows,
1291
+ includeTests: Boolean(args.include_tests),
1292
+ includeEval: Boolean(args.include_eval),
1293
+ in: typeof args.in === "string" ? args.in : undefined,
1294
+ exclude: typeof args.exclude === "string" ? args.exclude : undefined,
1295
+ });
1296
+ return ok(formatMcpSurprisingConnections(result, top));
1297
+ }
1298
+ catch (e) {
1299
+ const msg = e instanceof Error ? e.message : String(e);
1300
+ return err(`Surprising connections failed: ${msg}`);
1301
+ }
1302
+ });
1303
+ }
1109
1304
  function handleGetNeighbors(args) {
1110
1305
  return __awaiter(this, void 0, void 0, function* () {
1111
1306
  ensureWatcher();
@@ -1115,7 +1310,9 @@ exports.mcp = new commander_1.Command("mcp")
1115
1310
  const direction = args.direction === "callers" ? "callers" : "callees";
1116
1311
  const maxHops = Math.min(Math.max(Number(args.max_hops) || 2, 1), 5);
1117
1312
  try {
1118
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1313
+ const { proj, root } = resolveRegisteredProject(args.root);
1314
+ if (!proj)
1315
+ return err("Project not added to gmax yet.");
1119
1316
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1120
1317
  const hits = yield builder.getNeighbors(symbol, direction, maxHops);
1121
1318
  if (hits.length === 0) {
@@ -1150,7 +1347,9 @@ exports.mcp = new commander_1.Command("mcp")
1150
1347
  const direction = args.direction === "callers" ? "callers" : "callees";
1151
1348
  const maxHops = Math.min(Math.max(Number(args.max_hops) || 6, 1), 10);
1152
1349
  try {
1153
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1350
+ const { proj, root } = resolveRegisteredProject(args.root);
1351
+ if (!proj)
1352
+ return err("Project not added to gmax yet.");
1154
1353
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1155
1354
  const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
1156
1355
  if (!pathSyms) {
@@ -1176,8 +1375,10 @@ exports.mcp = new commander_1.Command("mcp")
1176
1375
  return err("Missing required parameter: files (non-empty array)");
1177
1376
  }
1178
1377
  try {
1179
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1180
- const abs = filesIn.map((f) => path.isAbsolute(f) ? f : path.resolve(root, f));
1378
+ const { proj, root } = resolveRegisteredProject(args.root);
1379
+ if (!proj)
1380
+ return err("Project not added to gmax yet.");
1381
+ const abs = filesIn.map((f) => (0, path_containment_1.resolveContainedPath)(root, f, { verifyExistingTarget: true }));
1181
1382
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1182
1383
  const sg = yield builder.subgraphForFiles(abs);
1183
1384
  if (sg.symbols.length === 0) {
@@ -1218,14 +1419,20 @@ exports.mcp = new commander_1.Command("mcp")
1218
1419
  const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
1219
1420
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
1220
1421
  const pathPrefix = typeof args.path === "string" ? args.path : undefined;
1221
- const proj = (0, project_registry_1.getProject)(projectRoot);
1422
+ const { proj, root } = resolveRegisteredProject();
1222
1423
  if (!proj) {
1223
1424
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
1224
1425
  }
1225
1426
  try {
1226
1427
  const db = getVectorDb();
1227
1428
  const table = yield db.ensureTable();
1228
- let query = table
1429
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1430
+ let where = `array_length(defined_symbols) > 0 AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`;
1431
+ if (pathPrefix) {
1432
+ const absPrefix = (0, path_containment_1.resolveContainedPath)(root, pathPrefix);
1433
+ where += ` AND ${(0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix))}`;
1434
+ }
1435
+ const query = table
1229
1436
  .query()
1230
1437
  .select([
1231
1438
  "defined_symbols",
@@ -1234,15 +1441,8 @@ exports.mcp = new commander_1.Command("mcp")
1234
1441
  "role",
1235
1442
  "is_exported",
1236
1443
  ])
1237
- .where("array_length(defined_symbols) > 0")
1444
+ .where(where)
1238
1445
  .limit(pattern ? 10000 : Math.max(limit * 50, 2000));
1239
- if (pathPrefix) {
1240
- // Support both absolute and relative path prefixes
1241
- const absPrefix = path.isAbsolute(pathPrefix)
1242
- ? pathPrefix
1243
- : path.resolve(projectRoot, pathPrefix);
1244
- query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix)));
1245
- }
1246
1446
  const rows = yield query.toArray();
1247
1447
  const map = new Map();
1248
1448
  for (const row of rows) {
@@ -1282,8 +1482,8 @@ exports.mcp = new commander_1.Command("mcp")
1282
1482
  return ok("No symbols found. Run `gmax status` to verify the project is indexed, or `gmax index` to rebuild.");
1283
1483
  }
1284
1484
  const lines = entries.map((e) => {
1285
- const rel = e.path.startsWith(projectRoot)
1286
- ? e.path.slice(projectRoot.length + 1)
1485
+ const rel = e.path.startsWith(root)
1486
+ ? e.path.slice(root.length + 1)
1287
1487
  : e.path;
1288
1488
  const roleTag = e.role ? ` [${e.role.slice(0, 4)}]` : "";
1289
1489
  const expTag = e.exported ? " exported" : "";
@@ -1305,13 +1505,15 @@ exports.mcp = new commander_1.Command("mcp")
1305
1505
  if (projects.length === 0) {
1306
1506
  return ok("No projects indexed yet. cd into a repo and run `gmax add` to index it.");
1307
1507
  }
1308
- const currentName = (_a = (0, project_registry_1.getProject)(projectRoot)) === null || _a === void 0 ? void 0 : _a.name;
1508
+ const currentName = (_a = resolveMcpProject(undefined, projectRoot)) === null || _a === void 0 ? void 0 : _a.name;
1509
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
1309
1510
  const lines = projects.map((p) => {
1310
1511
  const here = p.name === currentName ? " (current)" : "";
1311
1512
  const chunks = typeof p.chunkCount === "number"
1312
1513
  ? `\t(${p.chunkCount} chunks)`
1313
1514
  : "";
1314
- return `${p.name}${here}\t${p.root}\t${p.status}${chunks}`;
1515
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(p, globalConfig);
1516
+ return `${p.name}${here}\t${p.root}\t${p.status}\tembedding=${identity.state}${chunks}`;
1315
1517
  });
1316
1518
  return ok(`${projects.length} indexed project(s). ` +
1317
1519
  `Pass a name to semantic_search via projects:"name", or use scope:"all".\n\n` +
@@ -1325,11 +1527,12 @@ exports.mcp = new commander_1.Command("mcp")
1325
1527
  }
1326
1528
  function handleIndexStatus() {
1327
1529
  return __awaiter(this, void 0, void 0, function* () {
1328
- var _a, _b, _c, _d, _e, _f;
1530
+ var _a;
1329
1531
  try {
1330
- const config = (0, index_config_1.readIndexConfig)(config_1.PATHS.configPath);
1331
1532
  const globalConfig = (0, index_config_1.readGlobalConfig)();
1332
1533
  const projects = (0, project_registry_1.listProjects)();
1534
+ const currentProject = resolveMcpProject(undefined, projectRoot);
1535
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(currentProject, globalConfig);
1333
1536
  const db = getVectorDb();
1334
1537
  const stats = yield db.getStats();
1335
1538
  const fileCount = yield db.getDistinctFileCount();
@@ -1352,8 +1555,13 @@ exports.mcp = new commander_1.Command("mcp")
1352
1555
  // focused on index health and avoid the N-project LanceDB scans.
1353
1556
  const lines = [
1354
1557
  `Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
1355
- `Model: ${globalConfig.embedMode === "gpu" ? ((_d = (_c = (_b = config_1.MODEL_TIERS[globalConfig.modelTier]) === null || _b === void 0 ? void 0 : _b.mlxModel) !== null && _c !== void 0 ? _c : config === null || config === void 0 ? void 0 : config.embedModel) !== null && _d !== void 0 ? _d : "unknown") : ((_e = config === null || config === void 0 ? void 0 : config.embedModel) !== null && _e !== void 0 ? _e : "unknown")} (${(_f = config === null || config === void 0 ? void 0 : config.vectorDim) !== null && _f !== void 0 ? _f : "?"}d, ${globalConfig.embedMode})`,
1356
- (config === null || config === void 0 ? void 0 : config.indexedAt) ? `Last indexed: ${config.indexedAt}` : "",
1558
+ `Configured embedding: ${identity.configured.tier} ${identity.configured.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.configured.fingerprint)}] (${globalConfig.embedMode})`,
1559
+ identity.built
1560
+ ? `Built embedding: ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`
1561
+ : "Built embedding: unbuilt",
1562
+ (currentProject === null || currentProject === void 0 ? void 0 : currentProject.lastIndexed)
1563
+ ? `Last indexed: ${currentProject.lastIndexed}`
1564
+ : "",
1357
1565
  watcherLine,
1358
1566
  `Projects: ${projects.length} indexed (call list_projects for names + per-project chunk counts)`,
1359
1567
  ].filter(Boolean);
@@ -1367,7 +1575,12 @@ exports.mcp = new commander_1.Command("mcp")
1367
1575
  }
1368
1576
  function handleSummarizeDirectory(args) {
1369
1577
  return __awaiter(this, void 0, void 0, function* () {
1370
- const dir = typeof args.path === "string" ? path.resolve(args.path) : projectRoot;
1578
+ const { proj, root } = resolveRegisteredProject();
1579
+ if (!proj)
1580
+ return err("Project not added to gmax yet.");
1581
+ const dir = typeof args.path === "string"
1582
+ ? (0, path_containment_1.resolveContainedPath)(root, args.path)
1583
+ : root;
1371
1584
  const prefix = dir.endsWith("/") ? dir : `${dir}/`;
1372
1585
  const limit = Math.min(Math.max(Number(args.limit) || 200, 1), 5000);
1373
1586
  try {
@@ -1392,7 +1605,9 @@ exports.mcp = new commander_1.Command("mcp")
1392
1605
  function handleSummarizeProject(args) {
1393
1606
  return __awaiter(this, void 0, void 0, function* () {
1394
1607
  var _a;
1395
- const root = typeof args.root === "string" ? path.resolve(args.root) : projectRoot;
1608
+ const { proj, root } = resolveRegisteredProject(args.root);
1609
+ if (!proj)
1610
+ return err("Project not added to gmax yet.");
1396
1611
  const prefix = root.endsWith("/") ? root : `${root}/`;
1397
1612
  const projectName = path.basename(root);
1398
1613
  try {
@@ -1514,7 +1729,13 @@ exports.mcp = new commander_1.Command("mcp")
1514
1729
  if (!file)
1515
1730
  return err("Missing required parameter: file");
1516
1731
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
1517
- const absPath = path.resolve(projectRoot, file);
1732
+ const { proj, root } = resolveRegisteredProject();
1733
+ if (!proj)
1734
+ return err("Project not added to gmax yet.");
1735
+ const absPath = (0, path_containment_1.resolveContainedPath)(root, file, {
1736
+ verifyExistingTarget: true,
1737
+ });
1738
+ const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1518
1739
  try {
1519
1740
  const db = getVectorDb();
1520
1741
  const table = yield db.ensureTable();
@@ -1542,7 +1763,7 @@ exports.mcp = new commander_1.Command("mcp")
1542
1763
  const rows = yield table
1543
1764
  .query()
1544
1765
  .select(["path"])
1545
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')`)
1766
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1546
1767
  .limit(3)
1547
1768
  .toArray();
1548
1769
  for (const row of rows) {
@@ -1558,7 +1779,7 @@ exports.mcp = new commander_1.Command("mcp")
1558
1779
  const rows = yield table
1559
1780
  .query()
1560
1781
  .select(["path"])
1561
- .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}')`)
1782
+ .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1562
1783
  .limit(20)
1563
1784
  .toArray();
1564
1785
  for (const row of rows) {
@@ -1576,8 +1797,8 @@ exports.mcp = new commander_1.Command("mcp")
1576
1797
  if (topDeps.length > 0) {
1577
1798
  lines.push("Dependencies (files this imports/calls):");
1578
1799
  for (const [p, count] of topDeps) {
1579
- const rel = p.startsWith(`${projectRoot}/`)
1580
- ? p.slice(projectRoot.length + 1)
1800
+ const rel = p.startsWith(rootPrefix)
1801
+ ? p.slice(rootPrefix.length)
1581
1802
  : p;
1582
1803
  lines.push(` ${rel.padEnd(40)} (${count} shared symbol${count > 1 ? "s" : ""})`);
1583
1804
  }
@@ -1592,8 +1813,8 @@ exports.mcp = new commander_1.Command("mcp")
1592
1813
  if (topRevs.length > 0) {
1593
1814
  lines.push("Dependents (files that call this):");
1594
1815
  for (const [p, count] of topRevs) {
1595
- const rel = p.startsWith(`${projectRoot}/`)
1596
- ? p.slice(projectRoot.length + 1)
1816
+ const rel = p.startsWith(rootPrefix)
1817
+ ? p.slice(rootPrefix.length)
1597
1818
  : p;
1598
1819
  lines.push(` ${rel.padEnd(40)} (${count} shared symbol${count > 1 ? "s" : ""})`);
1599
1820
  }
@@ -1614,7 +1835,9 @@ exports.mcp = new commander_1.Command("mcp")
1614
1835
  var _a, e_1, _b, _c;
1615
1836
  ensureWatcher();
1616
1837
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 50);
1617
- const root = typeof args.root === "string" ? path.resolve(args.root) : projectRoot;
1838
+ const { proj, root } = resolveRegisteredProject(args.root);
1839
+ if (!proj)
1840
+ return err("Project not added to gmax yet.");
1618
1841
  const prefix = root.endsWith("/") ? root : `${root}/`;
1619
1842
  try {
1620
1843
  const metaCache = new meta_cache_1.MetaCache(config_1.PATHS.lmdbPath);
@@ -1675,16 +1898,20 @@ exports.mcp = new commander_1.Command("mcp")
1675
1898
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50);
1676
1899
  const role = typeof args.role === "string" ? args.role.toUpperCase() : undefined;
1677
1900
  try {
1678
- const changedFiles = getChangedFiles(ref, projectRoot);
1901
+ const { proj, root } = resolveRegisteredProject();
1902
+ if (!proj)
1903
+ return err("Project not added to gmax yet.");
1904
+ (0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
1905
+ const changedFiles = getChangedFiles(ref, root);
1679
1906
  if (changedFiles.length === 0) {
1680
1907
  return ok(ref
1681
1908
  ? `No changes found relative to ${ref}.`
1682
1909
  : "No uncommitted changes found.");
1683
1910
  }
1684
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
1911
+ const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
1685
1912
  if (query) {
1686
1913
  const searcher = getSearcher();
1687
- const response = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
1914
+ const response = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, root);
1688
1915
  const changedSet = new Set(changedFiles);
1689
1916
  // searcher.search() returns mapped chunks (path under metadata.path);
1690
1917
  // changedFiles are absolute, so match on the resolved absolute path.
@@ -1732,15 +1959,19 @@ exports.mcp = new commander_1.Command("mcp")
1732
1959
  return err("Missing required parameter: target");
1733
1960
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
1734
1961
  try {
1962
+ const { proj, root } = resolveRegisteredProject();
1963
+ if (!proj)
1964
+ return err("Project not added to gmax yet.");
1965
+ const scopedTarget = containMcpTarget(root, target);
1735
1966
  const { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1736
1967
  const db = getVectorDb();
1737
- const { symbols, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1968
+ const { symbols, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, mcpRootPrefix(root));
1738
1969
  if (symbols.length === 0)
1739
1970
  return ok(`No symbols found for: ${target}`);
1740
- const tests = yield findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies);
1971
+ const tests = yield findTests(symbols, db, root, depth, undefined, symbolFamilies);
1741
1972
  if (tests.length === 0)
1742
1973
  return ok(`No tests found for ${target}.`);
1743
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
1974
+ const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
1744
1975
  const lines = tests.map((t) => {
1745
1976
  const hop = t.hops === 0 ? "direct" : `${t.hops}-hop`;
1746
1977
  return `${rel(t.file)}:${t.line + 1} ${t.symbol} (${hop})`;
@@ -1759,22 +1990,47 @@ exports.mcp = new commander_1.Command("mcp")
1759
1990
  if (!target)
1760
1991
  return err("Missing required parameter: target");
1761
1992
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
1993
+ const rollup = args.rollup === true;
1994
+ const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
1762
1995
  try {
1763
- const { resolveTargetSymbols, findTests, findDependents, isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1996
+ const { proj, root } = resolveRegisteredProject();
1997
+ if (!proj)
1998
+ return err("Project not added to gmax yet.");
1999
+ const scopedTarget = containMcpTarget(root, target);
2000
+ const { resolveTargetSymbols, findTests, findDependents, findDependentsDetailed, isTestPath, } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
2001
+ const { buildImpactRollup, formatImpactRollupAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact-rollup")));
1764
2002
  const db = getVectorDb();
1765
- const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
2003
+ const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, root);
1766
2004
  if (symbols.length === 0)
1767
2005
  return ok(`No symbols found for: ${target}`);
1768
2006
  const targetPath = resolvedAsFile
1769
- ? path.resolve(projectRoot, target)
2007
+ ? (0, path_containment_1.resolveContainedPath)(root, scopedTarget)
1770
2008
  : undefined;
1771
2009
  const excludePaths = targetPath ? new Set([targetPath]) : undefined;
2010
+ const rollupLimit = Math.min(Math.max(top * 10, 100), 500);
1772
2011
  const [dependents, tests] = yield Promise.all([
1773
- findDependents(symbols, db, projectRoot, excludePaths, undefined, undefined, symbolFamilies),
1774
- findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies),
2012
+ rollup
2013
+ ? findDependentsDetailed(symbols, db, root, excludePaths, rollupLimit, undefined, symbolFamilies)
2014
+ : findDependents(symbols, db, root, excludePaths, undefined, undefined, symbolFamilies),
2015
+ findTests(symbols, db, root, depth, undefined, symbolFamilies),
1775
2016
  ]);
2017
+ if (rollup) {
2018
+ const detailedDependents = dependents;
2019
+ const impactRollup = buildImpactRollup({
2020
+ targetSymbols: symbols,
2021
+ dependents: detailedDependents,
2022
+ tests,
2023
+ projectRoot: root,
2024
+ top,
2025
+ });
2026
+ return ok(formatImpactRollupAgent(impactRollup, {
2027
+ target,
2028
+ projectRoot: root,
2029
+ includeTests: true,
2030
+ }));
2031
+ }
1776
2032
  const nonTestDeps = dependents.filter((d) => !isTestPath(d.file));
1777
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
2033
+ const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
1778
2034
  const sections = [`Impact analysis for ${target}:\n`];
1779
2035
  if (nonTestDeps.length > 0) {
1780
2036
  sections.push(`Dependents (${nonTestDeps.length}):`);
@@ -1811,13 +2067,17 @@ exports.mcp = new commander_1.Command("mcp")
1811
2067
  const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 25);
1812
2068
  const threshold = Number(args.threshold) || 0;
1813
2069
  try {
2070
+ const { proj, root } = resolveRegisteredProject();
2071
+ if (!proj)
2072
+ return err("Project not added to gmax yet.");
2073
+ (0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
1814
2074
  const db = getVectorDb();
1815
2075
  const table = yield db.ensureTable();
1816
2076
  const isFile = target.includes("/") ||
1817
2077
  (target.includes(".") && !target.includes(" "));
1818
2078
  let sourceRows;
1819
2079
  if (isFile) {
1820
- const absPath = path.resolve(projectRoot, target);
2080
+ const absPath = (0, path_containment_1.resolveContainedPath)(root, target);
1821
2081
  sourceRows = yield table
1822
2082
  .query()
1823
2083
  .select(["vector", "path", "start_line"])
@@ -1829,7 +2089,7 @@ exports.mcp = new commander_1.Command("mcp")
1829
2089
  sourceRows = yield table
1830
2090
  .query()
1831
2091
  .select(["vector", "path", "start_line"])
1832
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}')`)
2092
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${root}/`)}`)
1833
2093
  .limit(1)
1834
2094
  .toArray();
1835
2095
  }
@@ -1849,7 +2109,7 @@ exports.mcp = new commander_1.Command("mcp")
1849
2109
  "role",
1850
2110
  "_distance",
1851
2111
  ])
1852
- .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
2112
+ .where((0, filter_builder_1.pathStartsWith)(`${root}/`))
1853
2113
  .limit(limit + 5)
1854
2114
  .toArray();
1855
2115
  let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
@@ -1857,7 +2117,7 @@ exports.mcp = new commander_1.Command("mcp")
1857
2117
  filtered = filtered.filter((r) => { var _a; return 1 / (1 + ((_a = r._distance) !== null && _a !== void 0 ? _a : 0)) >= threshold; });
1858
2118
  if (filtered.length === 0)
1859
2119
  return ok(`No similar code found for ${target}.`);
1860
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
2120
+ const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
1861
2121
  const lines = filtered.slice(0, limit).map((r) => {
1862
2122
  var _a, _b, _c, _d;
1863
2123
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
@@ -1880,11 +2140,15 @@ exports.mcp = new commander_1.Command("mcp")
1880
2140
  const budget = Math.max(Number(args.budget) || 4000, 500);
1881
2141
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
1882
2142
  try {
2143
+ const { proj, root } = resolveRegisteredProject();
2144
+ if (!proj)
2145
+ return err("Project not added to gmax yet.");
2146
+ (0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
1883
2147
  const searcher = getSearcher();
1884
- const response = yield searcher.search(topic, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
2148
+ const response = yield searcher.search(topic, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, root);
1885
2149
  if (response.data.length === 0)
1886
2150
  return ok(`No results found for "${topic}".`);
1887
- const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
2151
+ const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
1888
2152
  const estTokens = (s) => Math.ceil(s.length / 4);
1889
2153
  let tokensUsed = 0;
1890
2154
  const sections = [];
@@ -1905,7 +2169,8 @@ exports.mcp = new commander_1.Command("mcp")
1905
2169
  const endLine = searchResultEndLine(r, startLine);
1906
2170
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
1907
2171
  try {
1908
- const content = fs.readFileSync(absP, "utf-8");
2172
+ const sourcePath = (0, path_containment_1.resolveContainedFile)(root, absP);
2173
+ const content = fs.readFileSync(sourcePath, "utf-8");
1909
2174
  const body = content
1910
2175
  .split("\n")
1911
2176
  .slice(startLine, endLine + 1)
@@ -2045,15 +2310,16 @@ exports.mcp = new commander_1.Command("mcp")
2045
2310
  "If you are NOT inside a specific repo, or want a different one: call " +
2046
2311
  '`list_projects` to see what\'s indexed, then pass `projects:"name"` (or ' +
2047
2312
  '`scope:"all"`) to `semantic_search`. When the working directory isn\'t an ' +
2048
- "indexed project, `semantic_search` automatically searches all of them and " +
2049
- "says so. Prefer `semantic_search` (detail:'pointer') for discovery, then " +
2313
+ "indexed project, `semantic_search` errors instead of searching everything " +
2314
+ "implicitly. Prefer `semantic_search` (detail:'pointer') for discovery, then " +
2050
2315
  "`extract_symbol`/`peek_symbol`/`code_skeleton` to read, and " +
2051
- "`trace_calls`/`impact_analysis`/`find_tests` for call-graph questions.",
2316
+ "`trace_calls`/`impact_analysis`/`find_tests` for call-graph questions. " +
2317
+ "Use `audit` and experimental `surprising_connections` for project orientation.",
2052
2318
  });
2053
2319
  // Best-effort query logging, applied uniformly to every tool exactly as the
2054
2320
  // old single CallToolRequestSchema dispatch did before each return.
2055
2321
  const logToolCall = (name, toolArgs, startMs, result) => __awaiter(void 0, void 0, void 0, function* () {
2056
- var _a, _b, _c, _d, _e, _f;
2322
+ var _a, _b, _c;
2057
2323
  try {
2058
2324
  const { logQuery } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/query-log")));
2059
2325
  const text = (_c = (_b = (_a = result.content) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : "";
@@ -2062,22 +2328,31 @@ exports.mcp = new commander_1.Command("mcp")
2062
2328
  ts: new Date().toISOString(),
2063
2329
  source: "mcp",
2064
2330
  tool: name,
2065
- query: String((_f = (_e = (_d = toolArgs.query) !== null && _d !== void 0 ? _d : toolArgs.symbol) !== null && _e !== void 0 ? _e : toolArgs.target) !== null && _f !== void 0 ? _f : ""),
2331
+ query: mcpLogQuery(name, toolArgs),
2066
2332
  project: projectRoot,
2067
2333
  results: resultLines,
2068
2334
  ms: Date.now() - startMs,
2069
2335
  error: result.isError ? text.slice(0, 200) : undefined,
2070
2336
  });
2071
2337
  }
2072
- catch (_g) { }
2338
+ catch (_d) { }
2073
2339
  });
2074
2340
  // Register a tool, wrapping its handler with timing + query logging. Zod raw
2075
2341
  // shapes give us free input validation (the SDK rejects calls that violate
2076
2342
  // the schema before the handler runs); handlers stay defensively coded.
2077
2343
  function tool(name, config, handler) {
2078
2344
  server.registerTool(name, config, (rawArgs) => __awaiter(this, void 0, void 0, function* () {
2079
- const args = (rawArgs !== null && rawArgs !== void 0 ? rawArgs : {});
2345
+ let args = (rawArgs !== null && rawArgs !== void 0 ? rawArgs : {});
2080
2346
  const startMs = Date.now();
2347
+ if (typeof args.root === "string" && args.root.trim()) {
2348
+ const selected = resolveMcpProject(args.root, projectRoot);
2349
+ if (!selected) {
2350
+ const result = err(`Unknown registered project: ${args.root}. Use list_projects to select a project name or exact root.`);
2351
+ yield logToolCall(name, args, startMs, result);
2352
+ return result;
2353
+ }
2354
+ args = Object.assign(Object.assign({}, args), { root: selected.root });
2355
+ }
2081
2356
  const result = yield handler(args);
2082
2357
  yield logToolCall(name, args, startMs, result);
2083
2358
  return result;
@@ -2095,7 +2370,7 @@ exports.mcp = new commander_1.Command("mcp")
2095
2370
  .optional(),
2096
2371
  root: zod_1.z
2097
2372
  .string()
2098
- .describe("Search a different directory (absolute path)")
2373
+ .describe("Registered project name or exact root")
2099
2374
  .optional(),
2100
2375
  path: zod_1.z
2101
2376
  .string()
@@ -2229,6 +2504,55 @@ exports.mcp = new commander_1.Command("mcp")
2229
2504
  .optional(),
2230
2505
  },
2231
2506
  }, handleAudit);
2507
+ tool("surprising_connections", {
2508
+ description: "Experimental orientation signal: embedding-similar cross-directory file pairs that are not directly connected by the static symbol graph. Useful for spotting duplicate or parallel logic. Requires experimental:true.",
2509
+ inputSchema: {
2510
+ experimental: zod_1.z
2511
+ .boolean()
2512
+ .describe("Must be true to acknowledge experimental signal quality"),
2513
+ root: zod_1.z
2514
+ .string()
2515
+ .describe("Project root (default: current)")
2516
+ .optional(),
2517
+ sample: zod_1.z
2518
+ .number()
2519
+ .describe("Indexed code chunks to sample (default 160, max 10000)")
2520
+ .optional(),
2521
+ neighbors: zod_1.z
2522
+ .number()
2523
+ .describe("Nearest neighbors per sampled chunk (default 20, max 200)")
2524
+ .optional(),
2525
+ top: zod_1.z
2526
+ .number()
2527
+ .describe("Grouped findings to return (default 10, max 100)")
2528
+ .optional(),
2529
+ dir_depth: zod_1.z
2530
+ .number()
2531
+ .describe("Directory bucket depth considered unsurprising")
2532
+ .optional(),
2533
+ min_similarity: zod_1.z
2534
+ .number()
2535
+ .describe("Minimum similarity 0-1 (default 0)")
2536
+ .optional(),
2537
+ max_rows: zod_1.z
2538
+ .number()
2539
+ .describe(`Maximum indexed rows to scan (default 50000, max ${surprising_connections_1.MAX_SURPRISE_ROWS})`)
2540
+ .optional(),
2541
+ in: zod_1.z
2542
+ .string()
2543
+ .describe("Restrict to a sub-path of the project")
2544
+ .optional(),
2545
+ exclude: zod_1.z
2546
+ .string()
2547
+ .describe("Exclude a sub-path of the project")
2548
+ .optional(),
2549
+ include_tests: zod_1.z.boolean().describe("Include test files").optional(),
2550
+ include_eval: zod_1.z
2551
+ .boolean()
2552
+ .describe("Include eval/experiment/script files")
2553
+ .optional(),
2554
+ },
2555
+ }, handleSurprisingConnections);
2232
2556
  tool("get_neighbors", {
2233
2557
  description: "Graph primitive: symbols reachable from a node along call edges within N hops. direction 'callees' = what it calls (outbound), 'callers' = what calls it (inbound). Each result carries its hop distance and definition location. Static call graph — same caveats as `dead`/`audit`.",
2234
2558
  inputSchema: {
@@ -2371,6 +2695,14 @@ exports.mcp = new commander_1.Command("mcp")
2371
2695
  .number()
2372
2696
  .describe("Caller traversal depth 1-3 (default 1)")
2373
2697
  .optional(),
2698
+ rollup: zod_1.z
2699
+ .boolean()
2700
+ .describe("Return export/package rollup TSV rows")
2701
+ .optional(),
2702
+ top: zod_1.z
2703
+ .number()
2704
+ .describe("Max rows per rollup section (default 10)")
2705
+ .optional(),
2374
2706
  },
2375
2707
  }, handleImpactAnalysis);
2376
2708
  tool("find_similar", {