grepmax 0.24.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 (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +11 -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/index.js +23 -24
  9. package/dist/commands/list.js +23 -14
  10. package/dist/commands/mcp.js +278 -143
  11. package/dist/commands/peek.js +16 -5
  12. package/dist/commands/repair.js +54 -120
  13. package/dist/commands/search-output.js +30 -9
  14. package/dist/commands/search-run.js +75 -47
  15. package/dist/commands/search-skeletons.js +28 -18
  16. package/dist/commands/search.js +45 -49
  17. package/dist/commands/serve.js +415 -373
  18. package/dist/commands/setup.js +2 -2
  19. package/dist/commands/similar.js +5 -5
  20. package/dist/commands/skeleton.js +67 -41
  21. package/dist/commands/status.js +5 -2
  22. package/dist/commands/summarize.js +6 -0
  23. package/dist/commands/watch.js +102 -38
  24. package/dist/config.js +3 -0
  25. package/dist/lib/daemon/daemon.js +1101 -379
  26. package/dist/lib/daemon/ipc-handler.js +122 -13
  27. package/dist/lib/daemon/mlx-server-manager.js +268 -125
  28. package/dist/lib/daemon/process-manager.js +7 -4
  29. package/dist/lib/daemon/search-handler.js +23 -9
  30. package/dist/lib/daemon/watcher-manager.js +353 -110
  31. package/dist/lib/index/batch-processor.js +231 -67
  32. package/dist/lib/index/embedding-generation.js +109 -0
  33. package/dist/lib/index/embedding-status.js +23 -0
  34. package/dist/lib/index/file-policy.js +273 -0
  35. package/dist/lib/index/ignore-patterns.js +4 -0
  36. package/dist/lib/index/index-config.js +18 -4
  37. package/dist/lib/index/syncer.js +256 -79
  38. package/dist/lib/index/walker.js +66 -86
  39. package/dist/lib/index/watcher-batch.js +9 -0
  40. package/dist/lib/index/watcher.js +129 -3
  41. package/dist/lib/llm/server.js +6 -0
  42. package/dist/lib/search/searcher.js +30 -19
  43. package/dist/lib/store/store-lease.js +473 -0
  44. package/dist/lib/store/vector-db.js +302 -57
  45. package/dist/lib/utils/blocked-roots.js +63 -0
  46. package/dist/lib/utils/cross-project.js +7 -3
  47. package/dist/lib/utils/daemon-client.js +24 -1
  48. package/dist/lib/utils/daemon-launcher.js +38 -13
  49. package/dist/lib/utils/doctor-status.js +76 -0
  50. package/dist/lib/utils/file-utils.js +74 -4
  51. package/dist/lib/utils/keyed-mutex.js +101 -0
  52. package/dist/lib/utils/logger.js +57 -1
  53. package/dist/lib/utils/mlx-hf-cache.js +114 -0
  54. package/dist/lib/utils/operation-coordinator.js +146 -0
  55. package/dist/lib/utils/path-containment.js +106 -0
  56. package/dist/lib/utils/process.js +44 -0
  57. package/dist/lib/utils/project-registry.js +351 -3
  58. package/dist/lib/utils/scope-filter.js +3 -9
  59. package/dist/lib/utils/stale-hint.js +12 -19
  60. package/dist/lib/utils/watcher-launcher.js +5 -1
  61. package/dist/lib/utils/watcher-store.js +2 -2
  62. package/dist/lib/workers/colbert-math.js +15 -12
  63. package/dist/lib/workers/embeddings/colbert.js +3 -2
  64. package/dist/lib/workers/embeddings/granite.js +4 -3
  65. package/dist/lib/workers/embeddings/mlx-client.js +59 -16
  66. package/dist/lib/workers/orchestrator.js +39 -8
  67. package/dist/lib/workers/pool.js +146 -82
  68. package/dist/lib/workers/process-child.js +11 -2
  69. package/dist/lib/workers/serialized-handler.js +10 -0
  70. package/dist/lib/workers/worker.js +13 -4
  71. package/mlx-embed-server/server.py +21 -3
  72. package/mlx-embed-server/summarizer.py +8 -0
  73. package/package.json +4 -3
  74. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  75. package/plugins/grepmax/hooks/start.js +3 -170
@@ -54,6 +54,10 @@ exports.toStringArray = toStringArray;
54
54
  exports.ok = ok;
55
55
  exports.err = err;
56
56
  exports.isExplicitCrossProjectSearch = isExplicitCrossProjectSearch;
57
+ exports.resolveMcpProject = resolveMcpProject;
58
+ exports.containMcpTarget = containMcpTarget;
59
+ exports.mcpRootPrefix = mcpRootPrefix;
60
+ exports.resolveMcpSourceFile = resolveMcpSourceFile;
57
61
  exports.searchResultPath = searchResultPath;
58
62
  exports.searchResultStartLine = searchResultStartLine;
59
63
  exports.searchResultEndLine = searchResultEndLine;
@@ -71,6 +75,7 @@ const config_1 = require("../config");
71
75
  const surprising_connections_1 = require("../lib/analysis/surprising-connections");
72
76
  const languages_1 = require("../lib/core/languages");
73
77
  const graph_builder_1 = require("../lib/graph/graph-builder");
78
+ const embedding_status_1 = require("../lib/index/embedding-status");
74
79
  const index_config_1 = require("../lib/index/index-config");
75
80
  const syncer_1 = require("../lib/index/syncer");
76
81
  const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
@@ -81,12 +86,15 @@ const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
81
86
  const symbol_extractor_1 = require("../lib/skeleton/symbol-extractor");
82
87
  const meta_cache_1 = require("../lib/store/meta-cache");
83
88
  const vector_db_1 = require("../lib/store/vector-db");
89
+ const cross_project_1 = require("../lib/utils/cross-project");
84
90
  const file_utils_1 = require("../lib/utils/file-utils");
85
91
  const filter_builder_1 = require("../lib/utils/filter-builder");
86
92
  const format_helpers_1 = require("../lib/utils/format-helpers");
87
93
  const import_extractor_1 = require("../lib/utils/import-extractor");
94
+ const path_containment_1 = require("../lib/utils/path-containment");
88
95
  const project_registry_1 = require("../lib/utils/project-registry");
89
96
  const project_root_1 = require("../lib/utils/project-root");
97
+ const scope_filter_1 = require("../lib/utils/scope-filter");
90
98
  const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
91
99
  const watcher_store_1 = require("../lib/utils/watcher-store");
92
100
  const audit_1 = require("./audit");
@@ -120,6 +128,44 @@ function isExplicitCrossProjectSearch(args, isSearchAll = false) {
120
128
  (typeof args.exclude_projects === "string" &&
121
129
  args.exclude_projects.trim() !== ""));
122
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
+ }
123
169
  function chunkAbsPath(chunk) {
124
170
  const metadata = chunk.metadata;
125
171
  return String(chunk.path || (metadata === null || metadata === void 0 ? void 0 : metadata.path) || "");
@@ -315,12 +361,16 @@ exports.mcp = new commander_1.Command("mcp")
315
361
  });
316
362
  }
317
363
  // --- Background watcher ---
318
- function ensureWatcher() {
364
+ function ensureWatcher(root) {
319
365
  return __awaiter(this, void 0, void 0, function* () {
366
+ var _a;
320
367
  try {
321
- 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);
322
372
  if (result.ok && !result.reused) {
323
- console.log(`[MCP] Started background watcher for ${projectRoot} (PID: ${result.pid})`);
373
+ console.log(`[MCP] Started background watcher for ${watchRoot} (PID: ${result.pid})`);
324
374
  }
325
375
  }
326
376
  catch (err) {
@@ -335,29 +385,32 @@ exports.mcp = new commander_1.Command("mcp")
335
385
  // exact registry match; otherwise walk the registry for an ancestor that
336
386
  // covers the cwd, and scope to it instead of silently widening to the whole
337
387
  // index. `root` is the registered project root to scope to.
338
- function resolveRegisteredProject() {
339
- const exact = (0, project_registry_1.getProject)(projectRoot);
340
- if (exact)
341
- return { proj: exact, root: projectRoot };
342
- const parent = (0, project_registry_1.getParentProject)(projectRoot);
343
- if (parent)
344
- return { proj: parent, root: parent.root };
388
+ function resolveRegisteredProject(requested) {
389
+ const project = resolveMcpProject(requested, projectRoot);
390
+ if (project)
391
+ return { proj: project, root: project.root };
345
392
  return { proj: undefined, root: projectRoot };
346
393
  }
347
394
  // --- Tool handlers ---
348
395
  function handleSemanticSearch(args_1) {
349
396
  return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
397
+ var _a, _b;
350
398
  const query = String(args.query || "");
351
399
  if (!query)
352
400
  return err("Missing required parameter: query");
353
401
  const searchAll = isExplicitCrossProjectSearch(args, isSearchAll);
354
402
  const limit = Math.min(Math.max(Number(args.limit) || 3, 1), 50);
355
- ensureWatcher();
356
403
  // Project resolution. The server is pinned to whatever cwd it launched in
357
404
  // (resolved once at startup). Resolve that cwd to its registered project —
358
405
  // exact match, or a registered ancestor when the cwd is a subdirectory of
359
406
  // an umbrella project. Searches scope to `resolvedRoot`.
360
- const { proj, root: resolvedRoot } = resolveRegisteredProject();
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
+ }
361
414
  // Cross-project search (the whole index) is opt-in: it happens only when
362
415
  // the caller passes scope:"all" or projects:"…". Otherwise we require a
363
416
  // resolved project and error loudly when there isn't one — never silently
@@ -378,34 +431,25 @@ exports.mcp = new commander_1.Command("mcp")
378
431
  if (proj.status === "pending" || proj.chunkCount === 0) {
379
432
  return err("Project not indexed yet. Run `gmax add` to index it first.");
380
433
  }
434
+ void ensureWatcher(resolvedRoot);
381
435
  }
382
436
  try {
383
- const searcher = getSearcher();
384
437
  // Determine path prefix and display root for relative paths
385
438
  let pathPrefix;
386
- let displayRoot = projectRoot;
439
+ const displayRoot = resolvedRoot;
440
+ let projectScope;
387
441
  if (!searchAll) {
388
- const searchRoot = typeof args.root === "string"
389
- ? path.resolve(args.root)
390
- : path.resolve(resolvedRoot);
391
- if (typeof args.root === "string" && !fs.existsSync(searchRoot)) {
392
- return err(`Directory not found: ${args.root}`);
393
- }
394
- displayRoot = searchRoot;
395
- pathPrefix = searchRoot.endsWith("/") ? searchRoot : `${searchRoot}/`;
396
- if (typeof args.path === "string") {
397
- pathPrefix = path.join(searchRoot, args.path);
398
- if (!pathPrefix.endsWith("/"))
399
- pathPrefix += "/";
400
- }
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;
401
448
  }
402
449
  const filters = {};
403
450
  if (typeof args.file === "string" && args.file) {
404
451
  filters.file = args.file;
405
452
  }
406
- if (typeof args.exclude === "string" && args.exclude) {
407
- filters.exclude = args.exclude;
408
- }
409
453
  if (typeof args.language === "string" && args.language) {
410
454
  filters.language = args.language;
411
455
  }
@@ -413,29 +457,32 @@ exports.mcp = new commander_1.Command("mcp")
413
457
  filters.role = args.role;
414
458
  }
415
459
  if (searchAll) {
416
- const allProjects = (0, project_registry_1.listProjects)();
417
- if (typeof args.projects === "string" && args.projects) {
418
- const names = args.projects.split(",").map((s) => s.trim());
419
- const roots = names
420
- .map((n) => { var _a; return (_a = allProjects.find((p) => p.name === n)) === null || _a === void 0 ? void 0 : _a.root; })
421
- .filter(Boolean);
422
- if (roots.length > 0) {
423
- filters.project_roots = roots.join(",");
424
- }
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.");
425
470
  }
426
- if (typeof args.exclude_projects === "string" &&
427
- args.exclude_projects) {
428
- const names = args.exclude_projects
429
- .split(",")
430
- .map((s) => s.trim());
431
- const roots = names
432
- .map((n) => { var _a; return (_a = allProjects.find((p) => p.name === n)) === null || _a === void 0 ? void 0 : _a.root; })
433
- .filter(Boolean);
434
- if (roots.length > 0) {
435
- filters.exclude_project_roots = roots.join(",");
436
- }
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;
437
479
  }
438
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();
439
486
  // Aider-style seeding: the agent passes its open files / discussed
440
487
  // symbols; the searcher biases candidate generation toward them.
441
488
  const parseSeedList = (v) => {
@@ -454,6 +501,14 @@ exports.mcp = new commander_1.Command("mcp")
454
501
  ? { files: seedFiles, symbols: seedSymbols }
455
502
  : undefined;
456
503
  const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1", seeds }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
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
+ });
457
512
  // Prepend any searcher warnings to whatever body we return.
458
513
  const prefixNotes = (body) => {
459
514
  var _a;
@@ -474,8 +529,11 @@ exports.mcp = new commander_1.Command("mcp")
474
529
  var _a;
475
530
  if (!includeImports || !absPath)
476
531
  return "";
532
+ const sourcePath = resolveMcpSourceFile(allowedRoots, absPath);
533
+ if (!sourcePath)
534
+ return "";
477
535
  if (!importCache.has(absPath)) {
478
- importCache.set(absPath, (0, import_extractor_1.extractImports)(absPath));
536
+ importCache.set(absPath, (0, import_extractor_1.extractImports)(sourcePath));
479
537
  }
480
538
  return (_a = importCache.get(absPath)) !== null && _a !== void 0 ? _a : "";
481
539
  };
@@ -519,7 +577,10 @@ exports.mcp = new commander_1.Command("mcp")
519
577
  if (contextN > 0 && absPath) {
520
578
  // Read surrounding context from file
521
579
  try {
522
- 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");
523
584
  const fileLines = fileContent.split("\n");
524
585
  const ctxStart = Math.max(0, startLine - contextN);
525
586
  const ctxEnd = Math.min(fileLines.length, endLine + 1 + contextN);
@@ -584,7 +645,7 @@ exports.mcp = new commander_1.Command("mcp")
584
645
  const regex = new RegExp(namePattern, "i");
585
646
  results = results.filter((r) => r.symbols.some((s) => regex.test(s)));
586
647
  }
587
- catch (_a) {
648
+ catch (_c) {
588
649
  // Invalid regex — skip filter
589
650
  }
590
651
  }
@@ -593,19 +654,19 @@ exports.mcp = new commander_1.Command("mcp")
593
654
  if (mode === "symbol" && !searchAll) {
594
655
  try {
595
656
  const db = getVectorDb();
596
- const builder = new graph_builder_1.GraphBuilder(db);
657
+ const builder = new graph_builder_1.GraphBuilder(db, resolvedRoot);
597
658
  const graph = yield builder.buildGraph(query);
598
659
  if (graph.center) {
599
660
  const traceLines = ["", "--- Call graph ---"];
600
- const centerRel = graph.center.file.startsWith(projectRoot)
601
- ? graph.center.file.slice(projectRoot.length + 1)
661
+ const centerRel = graph.center.file.startsWith(resolvedRoot)
662
+ ? graph.center.file.slice(resolvedRoot.length + 1)
602
663
  : graph.center.file;
603
664
  traceLines.push(`${graph.center.symbol} [${graph.center.role}] ${centerRel}:${graph.center.line + 1}`);
604
665
  if (graph.callers.length > 0) {
605
666
  traceLines.push("Callers:");
606
667
  for (const caller of graph.callers) {
607
- const rel = caller.file.startsWith(projectRoot)
608
- ? caller.file.slice(projectRoot.length + 1)
668
+ const rel = caller.file.startsWith(resolvedRoot)
669
+ ? caller.file.slice(resolvedRoot.length + 1)
609
670
  : caller.file;
610
671
  traceLines.push(` <- ${caller.symbol} ${rel}:${caller.line + 1}`);
611
672
  }
@@ -614,8 +675,8 @@ exports.mcp = new commander_1.Command("mcp")
614
675
  traceLines.push("Calls:");
615
676
  for (const callee of graph.callees.slice(0, 15)) {
616
677
  if (callee.file) {
617
- const rel = callee.file.startsWith(projectRoot)
618
- ? callee.file.slice(projectRoot.length + 1)
678
+ const rel = callee.file.startsWith(resolvedRoot)
679
+ ? callee.file.slice(resolvedRoot.length + 1)
619
680
  : callee.file;
620
681
  traceLines.push(` -> ${callee.symbol} ${rel}:${callee.line + 1}`);
621
682
  }
@@ -627,7 +688,7 @@ exports.mcp = new commander_1.Command("mcp")
627
688
  output += `\n${traceLines.join("\n")}`;
628
689
  }
629
690
  }
630
- catch (_b) {
691
+ catch (_d) {
631
692
  // Trace failed — return search results without trace
632
693
  }
633
694
  }
@@ -641,10 +702,14 @@ exports.mcp = new commander_1.Command("mcp")
641
702
  }
642
703
  function handleCodeSkeleton(args) {
643
704
  return __awaiter(this, void 0, void 0, function* () {
644
- ensureWatcher();
645
705
  const target = String(args.target || "");
646
706
  if (!target)
647
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);
648
713
  const fileLimit = Math.min(Math.max(Number(args.limit) || 10, 1), 20);
649
714
  // Determine targets: comma-separated, directory, or single file
650
715
  let targets;
@@ -655,12 +720,14 @@ exports.mcp = new commander_1.Command("mcp")
655
720
  .filter(Boolean);
656
721
  }
657
722
  else {
658
- const absPath = path.resolve(projectRoot, target);
723
+ const absPath = (0, path_containment_1.resolveContainedPath)(root, target, {
724
+ verifyExistingTarget: true,
725
+ });
659
726
  if (fs.existsSync(absPath) && fs.statSync(absPath).isDirectory()) {
660
727
  const entries = fs.readdirSync(absPath, { withFileTypes: true });
661
728
  targets = entries
662
729
  .filter((e) => e.isFile() && (0, file_utils_1.isIndexableFile)(path.join(absPath, e.name)))
663
- .map((e) => path.relative(projectRoot, path.join(absPath, e.name)))
730
+ .map((e) => path.relative(root, path.join(absPath, e.name)))
664
731
  .slice(0, fileLimit);
665
732
  if (targets.length === 0) {
666
733
  return err(`No indexable files found in ${target}`);
@@ -676,8 +743,11 @@ exports.mcp = new commander_1.Command("mcp")
676
743
  const jsonFiles = [];
677
744
  const skel = yield getSkeletonizer();
678
745
  for (const t of targets) {
679
- const absPath = path.resolve(projectRoot, t);
680
- if (!fs.existsSync(absPath)) {
746
+ let absPath;
747
+ try {
748
+ absPath = (0, path_containment_1.resolveContainedFile)(root, t);
749
+ }
750
+ catch (_a) {
681
751
  if (fmt !== "json")
682
752
  parts.push(`// ${t} — file not found`);
683
753
  continue;
@@ -687,7 +757,7 @@ exports.mcp = new commander_1.Command("mcp")
687
757
  try {
688
758
  sourceContent = fs.readFileSync(absPath, "utf-8");
689
759
  }
690
- catch (_a) { }
760
+ catch (_b) { }
691
761
  let skeleton = "";
692
762
  let language = "";
693
763
  let tokenEstimate = 0;
@@ -700,7 +770,7 @@ exports.mcp = new commander_1.Command("mcp")
700
770
  tokenEstimate = Math.ceil(cached.length / 4);
701
771
  }
702
772
  }
703
- catch (_b) { }
773
+ catch (_c) { }
704
774
  // Generate live if no cache
705
775
  if (!skeleton) {
706
776
  try {
@@ -753,17 +823,17 @@ exports.mcp = new commander_1.Command("mcp")
753
823
  }
754
824
  function handleTraceCalls(args) {
755
825
  return __awaiter(this, void 0, void 0, function* () {
756
- ensureWatcher();
757
826
  const symbol = String(args.symbol || "");
758
827
  if (!symbol)
759
828
  return err("Missing required parameter: symbol");
760
- const { proj } = resolveRegisteredProject();
829
+ const { proj, root } = resolveRegisteredProject();
761
830
  if (!proj) {
762
831
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
763
832
  }
833
+ void ensureWatcher(root);
764
834
  try {
765
835
  const db = getVectorDb();
766
- const builder = new graph_builder_1.GraphBuilder(db);
836
+ const builder = new graph_builder_1.GraphBuilder(db, root);
767
837
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
768
838
  const graph = yield builder.buildGraphMultiHop(symbol, depth);
769
839
  if (!graph.center) {
@@ -780,8 +850,8 @@ exports.mcp = new commander_1.Command("mcp")
780
850
  if (filteredImporters.length > 0) {
781
851
  lines.push("Imported by:");
782
852
  for (const imp of filteredImporters.slice(0, 10)) {
783
- const rel = imp.startsWith(projectRoot)
784
- ? imp.slice(projectRoot.length + 1)
853
+ const rel = imp.startsWith(root)
854
+ ? imp.slice(root.length + 1)
785
855
  : imp;
786
856
  lines.push(` ${rel}`);
787
857
  }
@@ -790,8 +860,8 @@ exports.mcp = new commander_1.Command("mcp")
790
860
  // Callers (recursive tree)
791
861
  function formatCallerTree(trees, indent) {
792
862
  for (const t of trees) {
793
- const rel = t.node.file.startsWith(projectRoot)
794
- ? t.node.file.slice(projectRoot.length + 1)
863
+ const rel = t.node.file.startsWith(root)
864
+ ? t.node.file.slice(root.length + 1)
795
865
  : t.node.file;
796
866
  const pad = " ".repeat(indent);
797
867
  lines.push(`${pad}<- ${t.node.symbol} ${rel}:${t.node.line + 1}`);
@@ -810,8 +880,8 @@ exports.mcp = new commander_1.Command("mcp")
810
880
  lines.push("Calls:");
811
881
  for (const callee of graph.callees.slice(0, 15)) {
812
882
  if (callee.file) {
813
- const rel = callee.file.startsWith(projectRoot)
814
- ? callee.file.slice(projectRoot.length + 1)
883
+ const rel = callee.file.startsWith(root)
884
+ ? callee.file.slice(root.length + 1)
815
885
  : callee.file;
816
886
  lines.push(` -> ${callee.symbol} ${rel}:${callee.line + 1}`);
817
887
  }
@@ -841,7 +911,9 @@ exports.mcp = new commander_1.Command("mcp")
841
911
  if (!symbol)
842
912
  return err("Missing required parameter: symbol");
843
913
  try {
844
- 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.");
845
917
  const db = getVectorDb();
846
918
  const table = yield db.ensureTable();
847
919
  const prefix = root.endsWith("/") ? root : `${root}/`;
@@ -881,7 +953,7 @@ exports.mcp = new commander_1.Command("mcp")
881
953
  return ((ROLE_PRI[String(b.role)] || 0) - (ROLE_PRI[String(a.role)] || 0));
882
954
  });
883
955
  const best = sorted[0];
884
- const filePath = String(best.path);
956
+ const filePath = (0, path_containment_1.resolveContainedFile)(root, String(best.path));
885
957
  const startLine = Number(best.start_line || 0);
886
958
  const endLine = Number(best.end_line || 0);
887
959
  const role = String(best.role || "IMPLEMENTATION");
@@ -932,7 +1004,9 @@ exports.mcp = new commander_1.Command("mcp")
932
1004
  if (!symbol)
933
1005
  return err("Missing required parameter: symbol");
934
1006
  try {
935
- 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.");
936
1010
  const depth = Math.min(Math.max(Number(args.depth || 1), 1), 3);
937
1011
  const db = getVectorDb();
938
1012
  const { GraphBuilder } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/graph-builder")));
@@ -964,7 +1038,8 @@ exports.mcp = new commander_1.Command("mcp")
964
1038
  let sigText = "(source not available)";
965
1039
  let bodyLines = 0;
966
1040
  try {
967
- 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");
968
1043
  const lines = content.split("\n");
969
1044
  const chunk = lines.slice(startLine, endLine + 1);
970
1045
  bodyLines = chunk.length;
@@ -1063,7 +1138,9 @@ exports.mcp = new commander_1.Command("mcp")
1063
1138
  if (!symbol)
1064
1139
  return err("Missing required parameter: symbol");
1065
1140
  try {
1066
- 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.");
1067
1144
  const db = getVectorDb();
1068
1145
  const table = yield db.ensureTable();
1069
1146
  const prefix = root.endsWith("/") ? root : `${root}/`;
@@ -1108,9 +1185,9 @@ exports.mcp = new commander_1.Command("mcp")
1108
1185
  function handleAudit(args) {
1109
1186
  return __awaiter(this, void 0, void 0, function* () {
1110
1187
  ensureWatcher();
1111
- const root = typeof args.root === "string" && args.root
1112
- ? path.resolve(args.root)
1113
- : projectRoot;
1188
+ const { proj, root } = resolveRegisteredProject(args.root);
1189
+ if (!proj)
1190
+ return err("Project not added to gmax yet.");
1114
1191
  const prefix = root.endsWith("/") ? root : `${root}/`;
1115
1192
  const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
1116
1193
  try {
@@ -1193,9 +1270,9 @@ exports.mcp = new commander_1.Command("mcp")
1193
1270
  if (args.experimental !== true) {
1194
1271
  return err("surprising_connections is experimental; pass experimental:true.");
1195
1272
  }
1196
- const root = typeof args.root === "string" && args.root
1197
- ? path.resolve(args.root)
1198
- : projectRoot;
1273
+ const { proj, root } = resolveRegisteredProject(args.root);
1274
+ if (!proj)
1275
+ return err("Project not added to gmax yet.");
1199
1276
  const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
1200
1277
  const sample = Math.min(Math.max(Number(args.sample) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample, 1), 10000);
1201
1278
  const neighbors = Math.min(Math.max(Number(args.neighbors) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 1), 200);
@@ -1233,7 +1310,9 @@ exports.mcp = new commander_1.Command("mcp")
1233
1310
  const direction = args.direction === "callers" ? "callers" : "callees";
1234
1311
  const maxHops = Math.min(Math.max(Number(args.max_hops) || 2, 1), 5);
1235
1312
  try {
1236
- 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.");
1237
1316
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1238
1317
  const hits = yield builder.getNeighbors(symbol, direction, maxHops);
1239
1318
  if (hits.length === 0) {
@@ -1268,7 +1347,9 @@ exports.mcp = new commander_1.Command("mcp")
1268
1347
  const direction = args.direction === "callers" ? "callers" : "callees";
1269
1348
  const maxHops = Math.min(Math.max(Number(args.max_hops) || 6, 1), 10);
1270
1349
  try {
1271
- 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.");
1272
1353
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1273
1354
  const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
1274
1355
  if (!pathSyms) {
@@ -1294,8 +1375,10 @@ exports.mcp = new commander_1.Command("mcp")
1294
1375
  return err("Missing required parameter: files (non-empty array)");
1295
1376
  }
1296
1377
  try {
1297
- const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1298
- 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 }));
1299
1382
  const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1300
1383
  const sg = yield builder.subgraphForFiles(abs);
1301
1384
  if (sg.symbols.length === 0) {
@@ -1336,14 +1419,20 @@ exports.mcp = new commander_1.Command("mcp")
1336
1419
  const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
1337
1420
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
1338
1421
  const pathPrefix = typeof args.path === "string" ? args.path : undefined;
1339
- const { proj } = resolveRegisteredProject();
1422
+ const { proj, root } = resolveRegisteredProject();
1340
1423
  if (!proj) {
1341
1424
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
1342
1425
  }
1343
1426
  try {
1344
1427
  const db = getVectorDb();
1345
1428
  const table = yield db.ensureTable();
1346
- 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
1347
1436
  .query()
1348
1437
  .select([
1349
1438
  "defined_symbols",
@@ -1352,15 +1441,8 @@ exports.mcp = new commander_1.Command("mcp")
1352
1441
  "role",
1353
1442
  "is_exported",
1354
1443
  ])
1355
- .where("array_length(defined_symbols) > 0")
1444
+ .where(where)
1356
1445
  .limit(pattern ? 10000 : Math.max(limit * 50, 2000));
1357
- if (pathPrefix) {
1358
- // Support both absolute and relative path prefixes
1359
- const absPrefix = path.isAbsolute(pathPrefix)
1360
- ? pathPrefix
1361
- : path.resolve(projectRoot, pathPrefix);
1362
- query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix)));
1363
- }
1364
1446
  const rows = yield query.toArray();
1365
1447
  const map = new Map();
1366
1448
  for (const row of rows) {
@@ -1400,8 +1482,8 @@ exports.mcp = new commander_1.Command("mcp")
1400
1482
  return ok("No symbols found. Run `gmax status` to verify the project is indexed, or `gmax index` to rebuild.");
1401
1483
  }
1402
1484
  const lines = entries.map((e) => {
1403
- const rel = e.path.startsWith(projectRoot)
1404
- ? e.path.slice(projectRoot.length + 1)
1485
+ const rel = e.path.startsWith(root)
1486
+ ? e.path.slice(root.length + 1)
1405
1487
  : e.path;
1406
1488
  const roleTag = e.role ? ` [${e.role.slice(0, 4)}]` : "";
1407
1489
  const expTag = e.exported ? " exported" : "";
@@ -1423,13 +1505,15 @@ exports.mcp = new commander_1.Command("mcp")
1423
1505
  if (projects.length === 0) {
1424
1506
  return ok("No projects indexed yet. cd into a repo and run `gmax add` to index it.");
1425
1507
  }
1426
- 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)();
1427
1510
  const lines = projects.map((p) => {
1428
1511
  const here = p.name === currentName ? " (current)" : "";
1429
1512
  const chunks = typeof p.chunkCount === "number"
1430
1513
  ? `\t(${p.chunkCount} chunks)`
1431
1514
  : "";
1432
- 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}`;
1433
1517
  });
1434
1518
  return ok(`${projects.length} indexed project(s). ` +
1435
1519
  `Pass a name to semantic_search via projects:"name", or use scope:"all".\n\n` +
@@ -1443,11 +1527,12 @@ exports.mcp = new commander_1.Command("mcp")
1443
1527
  }
1444
1528
  function handleIndexStatus() {
1445
1529
  return __awaiter(this, void 0, void 0, function* () {
1446
- var _a, _b, _c, _d, _e, _f;
1530
+ var _a;
1447
1531
  try {
1448
- const config = (0, index_config_1.readIndexConfig)(config_1.PATHS.configPath);
1449
1532
  const globalConfig = (0, index_config_1.readGlobalConfig)();
1450
1533
  const projects = (0, project_registry_1.listProjects)();
1534
+ const currentProject = resolveMcpProject(undefined, projectRoot);
1535
+ const identity = (0, embedding_status_1.projectEmbeddingStatus)(currentProject, globalConfig);
1451
1536
  const db = getVectorDb();
1452
1537
  const stats = yield db.getStats();
1453
1538
  const fileCount = yield db.getDistinctFileCount();
@@ -1470,8 +1555,13 @@ exports.mcp = new commander_1.Command("mcp")
1470
1555
  // focused on index health and avoid the N-project LanceDB scans.
1471
1556
  const lines = [
1472
1557
  `Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
1473
- `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})`,
1474
- (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
+ : "",
1475
1565
  watcherLine,
1476
1566
  `Projects: ${projects.length} indexed (call list_projects for names + per-project chunk counts)`,
1477
1567
  ].filter(Boolean);
@@ -1485,7 +1575,12 @@ exports.mcp = new commander_1.Command("mcp")
1485
1575
  }
1486
1576
  function handleSummarizeDirectory(args) {
1487
1577
  return __awaiter(this, void 0, void 0, function* () {
1488
- 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;
1489
1584
  const prefix = dir.endsWith("/") ? dir : `${dir}/`;
1490
1585
  const limit = Math.min(Math.max(Number(args.limit) || 200, 1), 5000);
1491
1586
  try {
@@ -1510,7 +1605,9 @@ exports.mcp = new commander_1.Command("mcp")
1510
1605
  function handleSummarizeProject(args) {
1511
1606
  return __awaiter(this, void 0, void 0, function* () {
1512
1607
  var _a;
1513
- 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.");
1514
1611
  const prefix = root.endsWith("/") ? root : `${root}/`;
1515
1612
  const projectName = path.basename(root);
1516
1613
  try {
@@ -1632,7 +1729,13 @@ exports.mcp = new commander_1.Command("mcp")
1632
1729
  if (!file)
1633
1730
  return err("Missing required parameter: file");
1634
1731
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
1635
- 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}/`;
1636
1739
  try {
1637
1740
  const db = getVectorDb();
1638
1741
  const table = yield db.ensureTable();
@@ -1660,7 +1763,7 @@ exports.mcp = new commander_1.Command("mcp")
1660
1763
  const rows = yield table
1661
1764
  .query()
1662
1765
  .select(["path"])
1663
- .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)}`)
1664
1767
  .limit(3)
1665
1768
  .toArray();
1666
1769
  for (const row of rows) {
@@ -1676,7 +1779,7 @@ exports.mcp = new commander_1.Command("mcp")
1676
1779
  const rows = yield table
1677
1780
  .query()
1678
1781
  .select(["path"])
1679
- .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)}`)
1680
1783
  .limit(20)
1681
1784
  .toArray();
1682
1785
  for (const row of rows) {
@@ -1694,8 +1797,8 @@ exports.mcp = new commander_1.Command("mcp")
1694
1797
  if (topDeps.length > 0) {
1695
1798
  lines.push("Dependencies (files this imports/calls):");
1696
1799
  for (const [p, count] of topDeps) {
1697
- const rel = p.startsWith(`${projectRoot}/`)
1698
- ? p.slice(projectRoot.length + 1)
1800
+ const rel = p.startsWith(rootPrefix)
1801
+ ? p.slice(rootPrefix.length)
1699
1802
  : p;
1700
1803
  lines.push(` ${rel.padEnd(40)} (${count} shared symbol${count > 1 ? "s" : ""})`);
1701
1804
  }
@@ -1710,8 +1813,8 @@ exports.mcp = new commander_1.Command("mcp")
1710
1813
  if (topRevs.length > 0) {
1711
1814
  lines.push("Dependents (files that call this):");
1712
1815
  for (const [p, count] of topRevs) {
1713
- const rel = p.startsWith(`${projectRoot}/`)
1714
- ? p.slice(projectRoot.length + 1)
1816
+ const rel = p.startsWith(rootPrefix)
1817
+ ? p.slice(rootPrefix.length)
1715
1818
  : p;
1716
1819
  lines.push(` ${rel.padEnd(40)} (${count} shared symbol${count > 1 ? "s" : ""})`);
1717
1820
  }
@@ -1732,7 +1835,9 @@ exports.mcp = new commander_1.Command("mcp")
1732
1835
  var _a, e_1, _b, _c;
1733
1836
  ensureWatcher();
1734
1837
  const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 50);
1735
- 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.");
1736
1841
  const prefix = root.endsWith("/") ? root : `${root}/`;
1737
1842
  try {
1738
1843
  const metaCache = new meta_cache_1.MetaCache(config_1.PATHS.lmdbPath);
@@ -1793,16 +1898,20 @@ exports.mcp = new commander_1.Command("mcp")
1793
1898
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50);
1794
1899
  const role = typeof args.role === "string" ? args.role.toUpperCase() : undefined;
1795
1900
  try {
1796
- 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);
1797
1906
  if (changedFiles.length === 0) {
1798
1907
  return ok(ref
1799
1908
  ? `No changes found relative to ${ref}.`
1800
1909
  : "No uncommitted changes found.");
1801
1910
  }
1802
- 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;
1803
1912
  if (query) {
1804
1913
  const searcher = getSearcher();
1805
- 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);
1806
1915
  const changedSet = new Set(changedFiles);
1807
1916
  // searcher.search() returns mapped chunks (path under metadata.path);
1808
1917
  // changedFiles are absolute, so match on the resolved absolute path.
@@ -1850,15 +1959,19 @@ exports.mcp = new commander_1.Command("mcp")
1850
1959
  return err("Missing required parameter: target");
1851
1960
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
1852
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);
1853
1966
  const { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1854
1967
  const db = getVectorDb();
1855
- const { symbols, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1968
+ const { symbols, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, mcpRootPrefix(root));
1856
1969
  if (symbols.length === 0)
1857
1970
  return ok(`No symbols found for: ${target}`);
1858
- const tests = yield findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies);
1971
+ const tests = yield findTests(symbols, db, root, depth, undefined, symbolFamilies);
1859
1972
  if (tests.length === 0)
1860
1973
  return ok(`No tests found for ${target}.`);
1861
- 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;
1862
1975
  const lines = tests.map((t) => {
1863
1976
  const hop = t.hops === 0 ? "direct" : `${t.hops}-hop`;
1864
1977
  return `${rel(t.file)}:${t.line + 1} ${t.symbol} (${hop})`;
@@ -1880,22 +1993,26 @@ exports.mcp = new commander_1.Command("mcp")
1880
1993
  const rollup = args.rollup === true;
1881
1994
  const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
1882
1995
  try {
1996
+ const { proj, root } = resolveRegisteredProject();
1997
+ if (!proj)
1998
+ return err("Project not added to gmax yet.");
1999
+ const scopedTarget = containMcpTarget(root, target);
1883
2000
  const { resolveTargetSymbols, findTests, findDependents, findDependentsDetailed, isTestPath, } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1884
2001
  const { buildImpactRollup, formatImpactRollupAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact-rollup")));
1885
2002
  const db = getVectorDb();
1886
- const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
2003
+ const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, root);
1887
2004
  if (symbols.length === 0)
1888
2005
  return ok(`No symbols found for: ${target}`);
1889
2006
  const targetPath = resolvedAsFile
1890
- ? path.resolve(projectRoot, target)
2007
+ ? (0, path_containment_1.resolveContainedPath)(root, scopedTarget)
1891
2008
  : undefined;
1892
2009
  const excludePaths = targetPath ? new Set([targetPath]) : undefined;
1893
2010
  const rollupLimit = Math.min(Math.max(top * 10, 100), 500);
1894
2011
  const [dependents, tests] = yield Promise.all([
1895
2012
  rollup
1896
- ? findDependentsDetailed(symbols, db, projectRoot, excludePaths, rollupLimit, undefined, symbolFamilies)
1897
- : findDependents(symbols, db, projectRoot, excludePaths, undefined, undefined, symbolFamilies),
1898
- findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies),
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),
1899
2016
  ]);
1900
2017
  if (rollup) {
1901
2018
  const detailedDependents = dependents;
@@ -1903,17 +2020,17 @@ exports.mcp = new commander_1.Command("mcp")
1903
2020
  targetSymbols: symbols,
1904
2021
  dependents: detailedDependents,
1905
2022
  tests,
1906
- projectRoot,
2023
+ projectRoot: root,
1907
2024
  top,
1908
2025
  });
1909
2026
  return ok(formatImpactRollupAgent(impactRollup, {
1910
2027
  target,
1911
- projectRoot,
2028
+ projectRoot: root,
1912
2029
  includeTests: true,
1913
2030
  }));
1914
2031
  }
1915
2032
  const nonTestDeps = dependents.filter((d) => !isTestPath(d.file));
1916
- 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;
1917
2034
  const sections = [`Impact analysis for ${target}:\n`];
1918
2035
  if (nonTestDeps.length > 0) {
1919
2036
  sections.push(`Dependents (${nonTestDeps.length}):`);
@@ -1950,13 +2067,17 @@ exports.mcp = new commander_1.Command("mcp")
1950
2067
  const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 25);
1951
2068
  const threshold = Number(args.threshold) || 0;
1952
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)());
1953
2074
  const db = getVectorDb();
1954
2075
  const table = yield db.ensureTable();
1955
2076
  const isFile = target.includes("/") ||
1956
2077
  (target.includes(".") && !target.includes(" "));
1957
2078
  let sourceRows;
1958
2079
  if (isFile) {
1959
- const absPath = path.resolve(projectRoot, target);
2080
+ const absPath = (0, path_containment_1.resolveContainedPath)(root, target);
1960
2081
  sourceRows = yield table
1961
2082
  .query()
1962
2083
  .select(["vector", "path", "start_line"])
@@ -1968,7 +2089,7 @@ exports.mcp = new commander_1.Command("mcp")
1968
2089
  sourceRows = yield table
1969
2090
  .query()
1970
2091
  .select(["vector", "path", "start_line"])
1971
- .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}/`)}`)
1972
2093
  .limit(1)
1973
2094
  .toArray();
1974
2095
  }
@@ -1988,7 +2109,7 @@ exports.mcp = new commander_1.Command("mcp")
1988
2109
  "role",
1989
2110
  "_distance",
1990
2111
  ])
1991
- .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
2112
+ .where((0, filter_builder_1.pathStartsWith)(`${root}/`))
1992
2113
  .limit(limit + 5)
1993
2114
  .toArray();
1994
2115
  let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
@@ -1996,7 +2117,7 @@ exports.mcp = new commander_1.Command("mcp")
1996
2117
  filtered = filtered.filter((r) => { var _a; return 1 / (1 + ((_a = r._distance) !== null && _a !== void 0 ? _a : 0)) >= threshold; });
1997
2118
  if (filtered.length === 0)
1998
2119
  return ok(`No similar code found for ${target}.`);
1999
- 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;
2000
2121
  const lines = filtered.slice(0, limit).map((r) => {
2001
2122
  var _a, _b, _c, _d;
2002
2123
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
@@ -2019,11 +2140,15 @@ exports.mcp = new commander_1.Command("mcp")
2019
2140
  const budget = Math.max(Number(args.budget) || 4000, 500);
2020
2141
  const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 25);
2021
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)());
2022
2147
  const searcher = getSearcher();
2023
- 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);
2024
2149
  if (response.data.length === 0)
2025
2150
  return ok(`No results found for "${topic}".`);
2026
- 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;
2027
2152
  const estTokens = (s) => Math.ceil(s.length / 4);
2028
2153
  let tokensUsed = 0;
2029
2154
  const sections = [];
@@ -2044,7 +2169,8 @@ exports.mcp = new commander_1.Command("mcp")
2044
2169
  const endLine = searchResultEndLine(r, startLine);
2045
2170
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2046
2171
  try {
2047
- 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");
2048
2174
  const body = content
2049
2175
  .split("\n")
2050
2176
  .slice(startLine, endLine + 1)
@@ -2216,8 +2342,17 @@ exports.mcp = new commander_1.Command("mcp")
2216
2342
  // the schema before the handler runs); handlers stay defensively coded.
2217
2343
  function tool(name, config, handler) {
2218
2344
  server.registerTool(name, config, (rawArgs) => __awaiter(this, void 0, void 0, function* () {
2219
- const args = (rawArgs !== null && rawArgs !== void 0 ? rawArgs : {});
2345
+ let args = (rawArgs !== null && rawArgs !== void 0 ? rawArgs : {});
2220
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
+ }
2221
2356
  const result = yield handler(args);
2222
2357
  yield logToolCall(name, args, startMs, result);
2223
2358
  return result;
@@ -2235,7 +2370,7 @@ exports.mcp = new commander_1.Command("mcp")
2235
2370
  .optional(),
2236
2371
  root: zod_1.z
2237
2372
  .string()
2238
- .describe("Search a different directory (absolute path)")
2373
+ .describe("Registered project name or exact root")
2239
2374
  .optional(),
2240
2375
  path: zod_1.z
2241
2376
  .string()