grepmax 0.26.27 → 0.26.30

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 (50) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +40 -0
  3. package/dist/commands/audit.js +28 -260
  4. package/dist/commands/context.js +42 -26
  5. package/dist/commands/dead.js +37 -35
  6. package/dist/commands/doctor.js +280 -10
  7. package/dist/commands/extract.js +79 -49
  8. package/dist/commands/impact.js +74 -20
  9. package/dist/commands/log.js +25 -25
  10. package/dist/commands/mcp.js +639 -475
  11. package/dist/commands/peek.js +49 -47
  12. package/dist/commands/project.js +29 -107
  13. package/dist/commands/related.js +43 -52
  14. package/dist/commands/search-run.js +23 -8
  15. package/dist/commands/search.js +7 -2
  16. package/dist/commands/similar.js +50 -76
  17. package/dist/commands/skeleton.js +137 -83
  18. package/dist/commands/status.js +90 -25
  19. package/dist/commands/surprises.js +73 -16
  20. package/dist/commands/symbols.js +58 -77
  21. package/dist/commands/test-find.js +51 -17
  22. package/dist/commands/trace.js +26 -11
  23. package/dist/commands/watch.js +1 -1
  24. package/dist/config.js +8 -2
  25. package/dist/lib/analysis/surprising-connections.js +135 -15
  26. package/dist/lib/daemon/daemon.js +157 -13
  27. package/dist/lib/daemon/graph-handler.js +859 -0
  28. package/dist/lib/daemon/ipc-handler.js +36 -4
  29. package/dist/lib/daemon/read-verbs.js +156 -0
  30. package/dist/lib/daemon/rows-handler.js +665 -0
  31. package/dist/lib/daemon/search-handler.js +7 -4
  32. package/dist/lib/daemon/vector-handler.js +199 -0
  33. package/dist/lib/daemon/watch-leases.js +179 -0
  34. package/dist/lib/index/batch-processor.js +7 -1
  35. package/dist/lib/index/watcher.js +4 -0
  36. package/dist/lib/search/pagerank.js +80 -19
  37. package/dist/lib/store/vector-db.js +35 -1
  38. package/dist/lib/utils/async-semaphore.js +141 -0
  39. package/dist/lib/utils/daemon-client.js +11 -0
  40. package/dist/lib/utils/process-footprint.js +51 -0
  41. package/dist/lib/utils/store-access.js +381 -0
  42. package/dist/lib/utils/tests-footer.js +19 -2
  43. package/dist/lib/utils/watcher-launcher.js +46 -19
  44. package/dist/lib/workers/pool.js +72 -2
  45. package/package.json +1 -1
  46. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  47. package/plugins/grepmax/hooks/cwd-changed.js +19 -36
  48. package/plugins/grepmax/hooks/start.js +15 -19
  49. package/plugins/grepmax/hooks/stop.js +13 -4
  50. package/plugins/grepmax/hooks/watch-lease.js +163 -0
@@ -53,6 +53,7 @@ exports.mcp = void 0;
53
53
  exports.toStringArray = toStringArray;
54
54
  exports.ok = ok;
55
55
  exports.err = err;
56
+ exports.isMcpSessionFallback = isMcpSessionFallback;
56
57
  exports.shouldFallbackMcpDaemonSearch = shouldFallbackMcpDaemonSearch;
57
58
  exports.isExplicitCrossProjectSearch = isExplicitCrossProjectSearch;
58
59
  exports.resolveMcpProject = resolveMcpProject;
@@ -74,32 +75,30 @@ const commander_1 = require("commander");
74
75
  const zod_1 = require("zod");
75
76
  const config_1 = require("../config");
76
77
  const surprising_connections_1 = require("../lib/analysis/surprising-connections");
77
- const languages_1 = require("../lib/core/languages");
78
- const graph_builder_1 = require("../lib/graph/graph-builder");
78
+ const graph_handler_1 = require("../lib/daemon/graph-handler");
79
+ const rows_handler_1 = require("../lib/daemon/rows-handler");
80
+ const vector_handler_1 = require("../lib/daemon/vector-handler");
79
81
  const embedding_status_1 = require("../lib/index/embedding-status");
80
82
  const index_config_1 = require("../lib/index/index-config");
81
83
  const syncer_1 = require("../lib/index/syncer");
82
84
  const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
83
85
  const searcher_1 = require("../lib/search/searcher");
84
86
  const annotator_1 = require("../lib/skeleton/annotator");
85
- const retriever_1 = require("../lib/skeleton/retriever");
86
87
  const skeletonizer_1 = require("../lib/skeleton/skeletonizer");
87
88
  const symbol_extractor_1 = require("../lib/skeleton/symbol-extractor");
88
- const ann_config_1 = require("../lib/store/ann-config");
89
89
  const meta_cache_1 = require("../lib/store/meta-cache");
90
- const vector_db_1 = require("../lib/store/vector-db");
91
90
  const cross_project_1 = require("../lib/utils/cross-project");
91
+ const daemon_client_1 = require("../lib/utils/daemon-client");
92
92
  const file_utils_1 = require("../lib/utils/file-utils");
93
- const filter_builder_1 = require("../lib/utils/filter-builder");
94
93
  const format_helpers_1 = require("../lib/utils/format-helpers");
95
94
  const import_extractor_1 = require("../lib/utils/import-extractor");
96
95
  const path_containment_1 = require("../lib/utils/path-containment");
97
96
  const project_registry_1 = require("../lib/utils/project-registry");
98
97
  const project_root_1 = require("../lib/utils/project-root");
99
98
  const scope_filter_1 = require("../lib/utils/scope-filter");
99
+ const store_access_1 = require("../lib/utils/store-access");
100
100
  const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
101
101
  const watcher_store_1 = require("../lib/utils/watcher-store");
102
- const audit_1 = require("./audit");
103
102
  // ---------------------------------------------------------------------------
104
103
  // Helpers
105
104
  // ---------------------------------------------------------------------------
@@ -123,14 +122,56 @@ function ok(text) {
123
122
  function err(text) {
124
123
  return { content: [{ type: "text", text }], isError: true };
125
124
  }
126
- function shouldFallbackMcpDaemonSearch(error) {
125
+ /**
126
+ * The two daemon answers that mean "not usable *yet*", as opposed to "your
127
+ * request was wrong".
128
+ *
129
+ * `project not watched` is the daemon still picking a project up; `daemon not
130
+ * ready` is its store still opening. A CLI command reports both — there is a
131
+ * user at a prompt who can wait or re-run — but an MCP tool call gets no second
132
+ * attempt, so it degrades to its own store read for the life of that one call.
133
+ * That is why these live here and not in `withStoreRead`'s built-in set.
134
+ */
135
+ function isMcpSessionFallback(error) {
127
136
  const message = String(error !== null && error !== void 0 ? error : "").toLowerCase();
128
- return (message === "enoent" ||
129
- message === "econnrefused" ||
130
- message.includes("project not watched") ||
131
- message.includes("daemon not ready") ||
132
- message.includes("oversize") ||
133
- message.includes("unknown command"));
137
+ return (message.includes("project not watched") ||
138
+ message.includes("daemon not ready"));
139
+ }
140
+ /**
141
+ * MCP's read-tool fallback policy, in one object so every tool shares it.
142
+ *
143
+ * - `fallbackOnUnknownVerb` — a daemon older than this build does not know the
144
+ * read verbs (the live 0.26.27 daemon does not); answer from the store rather
145
+ * than failing the tool call. Removed one release after the verbs ship.
146
+ * - `fallbackOnOversize` — the daemon's 2 MB response cap. See
147
+ * `isOversizeError` for why MCP opts in and the CLI does not.
148
+ * - `extraFallback` — the two session-readiness answers above.
149
+ *
150
+ * Everything else (`DAEMON_BUSY`, timeouts, scope rejections, store failures) is
151
+ * a live daemon saying no, and is reported. Falling back there would put a
152
+ * second opener on the store while the daemon is working.
153
+ */
154
+ /**
155
+ * Watch-lease cadence. Renewal is cheap (one IPC line), and the TTL is three
156
+ * renewals long so a slow tick or a daemon restart does not drop the project.
157
+ */
158
+ const MCP_WATCH_LEASE_RENEW_MS = 5 * 60 * 1000;
159
+ const MCP_WATCH_LEASE_TTL_MS = 3 * MCP_WATCH_LEASE_RENEW_MS;
160
+ const MCP_STORE_FALLBACK = {
161
+ fallbackOnUnknownVerb: true,
162
+ fallbackOnOversize: true,
163
+ extraFallback: isMcpSessionFallback,
164
+ };
165
+ /**
166
+ * Kept as a named predicate because `daemonSearch`'s callers and its tests read
167
+ * as one rule; the rule itself is now assembled from `withStoreRead`'s
168
+ * classification rather than restated as a second string table.
169
+ */
170
+ function shouldFallbackMcpDaemonSearch(error) {
171
+ return ((0, store_access_1.classifyDaemonError)(error) === "no-daemon" ||
172
+ (0, store_access_1.isUnknownCommandError)(error) ||
173
+ (0, store_access_1.isOversizeError)(error) ||
174
+ isMcpSessionFallback(error));
134
175
  }
135
176
  function isExplicitCrossProjectSearch(args, isSearchAll = false) {
136
177
  return (isSearchAll ||
@@ -239,6 +280,11 @@ function formatMcpPointerSearchResults(data, displayRoot, options = {}) {
239
280
  query: options.query,
240
281
  });
241
282
  }
283
+ /**
284
+ * Takes summary + findings rather than a whole `SurpriseAnalysisResult`: the
285
+ * `vector.surprises` verb drops the raw `pairs` array (the bulk of the object,
286
+ * and nothing renders it), so this is the shape both paths can supply.
287
+ */
242
288
  function formatMcpSurprisingConnections(result, top = 10) {
243
289
  const { summary, findings } = result;
244
290
  const lines = [
@@ -300,24 +346,15 @@ exports.mcp = new commander_1.Command("mcp")
300
346
  .action((_optsArg, _cmd) => __awaiter(void 0, void 0, void 0, function* () {
301
347
  process.title = "gmax-mcp";
302
348
  // --- Lifecycle ---
303
- let _vectorDb = null;
304
- let _searcher = null;
349
+ // No per-session VectorDB. Every read tool goes through the daemon's read
350
+ // verbs, and the only store this process may open is the short-lived one
351
+ // `withLocalStore` opens and closes inside a `withStoreRead` fallback — so
352
+ // an MCP session holds no reader marker under ~/.gmax/lancedb.lease/readers/
353
+ // while a daemon serving the verbs is up.
305
354
  let _skeletonizer = null;
306
355
  let _indexReady = false;
307
- const cleanup = () => __awaiter(void 0, void 0, void 0, function* () {
308
- if (_vectorDb) {
309
- try {
310
- yield _vectorDb.close();
311
- }
312
- catch (_a) { }
313
- _vectorDb = null;
314
- _searcher = null;
315
- }
316
- });
317
- const exit = () => __awaiter(void 0, void 0, void 0, function* () {
318
- yield cleanup();
319
- process.exit(0);
320
- });
356
+ // Nothing to close: the tools open no store of their own.
357
+ const exit = () => process.exit(0);
321
358
  process.on("SIGINT", exit);
322
359
  process.on("SIGTERM", exit);
323
360
  // MCP SDK doesn't handle stdin close — exit when the client disconnects
@@ -339,38 +376,185 @@ exports.mcp = new commander_1.Command("mcp")
339
376
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
340
377
  // Propagate project root to worker processes
341
378
  process.env.GMAX_PROJECT_ROOT = paths.root;
342
- // Lazy resource accessors — all use centralized store
343
- function getVectorDb() {
344
- if (!_vectorDb)
345
- _vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
346
- return _vectorDb;
379
+ // --- Store access ---
380
+ /**
381
+ * One read through the access policy, with MCP's fallback allowances. The
382
+ * `inProcess` branch is entered only when no daemon serves the verb.
383
+ */
384
+ function readStore(name, opts) {
385
+ return (0, store_access_1.withStoreRead)(name, Object.assign(Object.assign({}, opts), MCP_STORE_FALLBACK));
347
386
  }
348
- function getSearcher() {
349
- if (!_searcher)
350
- _searcher = new searcher_1.Searcher(getVectorDb());
351
- return _searcher;
387
+ /** Open the shared store for the duration of one fallback, then close it. */
388
+ function localStore(fn) {
389
+ return (0, rows_handler_1.withLocalStore)(paths.lancedbDir, fn);
352
390
  }
353
- function daemonSearch(args) {
354
- return __awaiter(this, void 0, void 0, function* () {
355
- var _a;
356
- const { sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
357
- const response = yield sendDaemonCommand(Object.assign({ cmd: ((_a = args.filters) === null || _a === void 0 ? void 0 : _a.projectRoots) ? "search-v2" : "search" }, args), { timeoutMs: 60000 });
358
- if (!response.ok) {
359
- if (shouldFallbackMcpDaemonSearch(response.error))
360
- return null;
361
- const detail = [response.error, response.hint]
362
- .filter((value) => typeof value === "string" && value)
363
- .join(": ");
364
- throw new Error(detail || "daemon search failed");
365
- }
366
- return {
367
- data: Array.isArray(response.data)
368
- ? response.data
369
- : [],
370
- warnings: Array.isArray(response.warnings)
371
- ? response.warnings.filter((warning) => typeof warning === "string")
391
+ /** A graph verb with MCP's fallback allowances. */
392
+ function graphVerb(verb, call) {
393
+ return (0, graph_handler_1.callGraphVerb)(verb, Object.assign(Object.assign({}, call), { fallback: MCP_STORE_FALLBACK }));
394
+ }
395
+ /**
396
+ * MCP tools take no `--in`/`--exclude`, so a tool's scope is always the
397
+ * whole project. Building it through `resolveScope` anyway keeps the wire
398
+ * shape identical to the CLI's.
399
+ */
400
+ function projectScope(root) {
401
+ return (0, scope_filter_1.resolveScope)({ projectRoot: root });
402
+ }
403
+ /**
404
+ * Turn a thrown error into a tool result. A store-access refusal is already
405
+ * one actionable line, so it is passed through verbatim rather than buried
406
+ * under a `<Tool> failed:` prefix. It should never fire here — an MCP server
407
+ * is spawned by the editor, not from inside a sandboxed Bash tool — but the
408
+ * path exists so the message is right if it ever does.
409
+ */
410
+ function toolError(prefix, e) {
411
+ if ((0, store_access_1.isStoreAccessRefused)(e))
412
+ return err(e.message);
413
+ return err(`${prefix}: ${e instanceof Error ? e.message : String(e)}`);
414
+ }
415
+ /**
416
+ * Search through the daemon, falling back to a Searcher over a store this
417
+ * process opens only for the duration of the call. Replaces the old
418
+ * `daemonSearch(...) ?? getSearcher().search(...)` pair, whose null return
419
+ * encoded the same fallback rule `withStoreRead` now owns.
420
+ */
421
+ function searchStore(args) {
422
+ return readStore("search", {
423
+ daemon: () => {
424
+ var _a;
425
+ return (0, daemon_client_1.sendDaemonCommand)(Object.assign({ cmd: ((_a = args.filters) === null || _a === void 0 ? void 0 : _a.projectRoots) ? "search-v2" : "search" }, args), { timeoutMs: 60000 });
426
+ },
427
+ render: (resp) => ({
428
+ data: Array.isArray(resp.data) ? resp.data : [],
429
+ warnings: Array.isArray(resp.warnings)
430
+ ? resp.warnings.filter((warning) => typeof warning === "string")
372
431
  : undefined,
373
- };
432
+ }),
433
+ inProcess: () => localStore((deps) => __awaiter(this, void 0, void 0, function* () {
434
+ const searcher = new searcher_1.Searcher(deps.vectorDb);
435
+ return searcher.search(args.query, args.limit, { rerank: args.rerank, seeds: args.seeds }, args.filters, args.pathPrefix);
436
+ })),
437
+ daemonErrorMessage: (resp) => [resp.error, resp.hint]
438
+ .filter((value) => typeof value === "string" && value)
439
+ .join(": ") || "daemon search failed",
440
+ });
441
+ }
442
+ /**
443
+ * `graph.peek`: the symbol's single-hop graph plus its defining chunk's
444
+ * metadata, and the multi-hop caller tree when `depth > 1`. Shared by
445
+ * `peek_symbol` and `semantic_search`'s symbol mode, which used to issue the
446
+ * same `buildGraph` call through their own GraphBuilder.
447
+ */
448
+ function peekSymbolGraph(root, symbol, depth) {
449
+ const scope = projectScope(root);
450
+ return graphVerb("graph.peek", {
451
+ projectRoot: root,
452
+ scope,
453
+ payload: { target: symbol, depth, includeTests: false },
454
+ render: (resp) => resp.peek,
455
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphPeek)(deps.vectorDb, {
456
+ symbol,
457
+ depth,
458
+ scope,
459
+ includeTests: false,
460
+ })),
461
+ });
462
+ }
463
+ /**
464
+ * `graph.resolve`: a target (symbol name or file path) to the symbols it
465
+ * names. Shared by `find_tests` and `impact_analysis`.
466
+ */
467
+ function resolveTarget(root, target) {
468
+ const scope = projectScope(root);
469
+ return graphVerb("graph.resolve", {
470
+ projectRoot: root,
471
+ scope,
472
+ payload: { target },
473
+ render: (resp) => {
474
+ var _a, _b;
475
+ return ({
476
+ symbols: (_a = resp.symbols) !== null && _a !== void 0 ? _a : [],
477
+ resolvedAsFile: resp.resolvedAsFile === true,
478
+ symbolFamilies: (_b = resp.symbolFamilies) !== null && _b !== void 0 ? _b : null,
479
+ });
480
+ },
481
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphResolve)(deps.vectorDb, { target, projectRoot: root })),
482
+ });
483
+ }
484
+ /** `graph.tests`: the reverse call graph's test hits for a symbol set. */
485
+ function findTestsForSymbols(root, symbols, depth, families) {
486
+ const scope = projectScope(root);
487
+ return graphVerb("graph.tests", {
488
+ projectRoot: root,
489
+ scope,
490
+ payload: { symbols, depth, families },
491
+ render: (resp) => { var _a; return (_a = resp.hits) !== null && _a !== void 0 ? _a : []; },
492
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphTests)(deps.vectorDb, {
493
+ symbols,
494
+ queryRoot: root,
495
+ depth,
496
+ excludePrefixes: scope.excludePrefixes,
497
+ families: (0, graph_handler_1.decodeSymbolFamilies)(families),
498
+ })),
499
+ });
500
+ }
501
+ /**
502
+ * `rows.locate`: a batch of row selects, one result array per matcher in
503
+ * order. Batched so a per-symbol loop costs one round trip, not hundreds.
504
+ */
505
+ function locateRows(root, req) {
506
+ return (0, rows_handler_1.readRows)(Object.assign({ name: "locate", projectRoot: root, lancedbDir: paths.lancedbDir, scope: projectScope(root), fallback: MCP_STORE_FALLBACK }, req));
507
+ }
508
+ /**
509
+ * Chunk and file totals for `index_status`, summed per registered project.
510
+ *
511
+ * The old code asked the store for `countRows()` and a distinct-path scan
512
+ * over the whole table. There is no whole-table read verb — deliberately:
513
+ * the daemon's `project-stats` is per project, and summing it keeps the
514
+ * daemon and in-process paths on the *same* per-prefix counts instead of
515
+ * one counting orphaned rows the other cannot see. A failed per-project
516
+ * call contributes nothing rather than failing the tool, exactly as
517
+ * `gmax status` degrades to its cached counts.
518
+ */
519
+ function indexTotals(projects) {
520
+ return __awaiter(this, void 0, void 0, function* () {
521
+ let chunks = 0;
522
+ let files = 0;
523
+ for (const project of projects) {
524
+ const totals = yield readStore("project-stats", {
525
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "project-stats", root: project.root }, { timeoutMs: 30000 }),
526
+ render: (resp) => ({
527
+ chunks: typeof resp.chunks === "number" ? resp.chunks : 0,
528
+ files: typeof resp.files === "number" ? resp.files : 0,
529
+ }),
530
+ inProcess: () => localStore((deps) => __awaiter(this, void 0, void 0, function* () {
531
+ const prefix = project.root.endsWith("/")
532
+ ? project.root
533
+ : `${project.root}/`;
534
+ return {
535
+ chunks: yield deps.vectorDb.countRowsForPath(prefix),
536
+ files: yield deps.vectorDb.countDistinctFilesForPath(prefix),
537
+ };
538
+ })),
539
+ }).catch(() => ({ chunks: 0, files: 0 }));
540
+ chunks += totals.chunks;
541
+ files += totals.files;
542
+ }
543
+ return { chunks, files };
544
+ });
545
+ }
546
+ /** `rows.skeleton`: the stored per-file skeleton (and the symbol lookup). */
547
+ function lookupSkeleton(root, req) {
548
+ return readStore("skeleton", {
549
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)(Object.assign({ cmd: "rows.skeleton", projectRoot: root }, req), { timeoutMs: 30000 }),
550
+ render: (resp) => {
551
+ var _a, _b;
552
+ return ({
553
+ path: ((_a = resp.path) !== null && _a !== void 0 ? _a : null),
554
+ skeleton: ((_b = resp.skeleton) !== null && _b !== void 0 ? _b : null),
555
+ });
556
+ },
557
+ inProcess: () => localStore((deps) => (0, rows_handler_1.runSkeleton)(deps, Object.assign({ projectRoot: root }, req))),
374
558
  });
375
559
  }
376
560
  function getSkeletonizer() {
@@ -395,6 +579,13 @@ exports.mcp = new commander_1.Command("mcp")
395
579
  });
396
580
  }
397
581
  // --- Background watcher ---
582
+ // This server's watch lease. It names our PID, so the daemon drops it the
583
+ // minute this session exits; the TTL only covers a wedged server.
584
+ const WATCH_LEASE = {
585
+ holder: `mcp:${process.pid}`,
586
+ pid: process.pid,
587
+ ttlMs: MCP_WATCH_LEASE_TTL_MS,
588
+ };
398
589
  function ensureWatcher(root) {
399
590
  return __awaiter(this, void 0, void 0, function* () {
400
591
  var _a;
@@ -402,7 +593,7 @@ exports.mcp = new commander_1.Command("mcp")
402
593
  const watchRoot = root !== null && root !== void 0 ? root : (_a = resolveMcpProject(undefined, projectRoot)) === null || _a === void 0 ? void 0 : _a.root;
403
594
  if (!watchRoot)
404
595
  return;
405
- const result = yield (0, watcher_launcher_1.launchWatcher)(watchRoot);
596
+ const result = yield (0, watcher_launcher_1.launchWatcher)(watchRoot, WATCH_LEASE);
406
597
  if (result.ok && !result.reused) {
407
598
  console.log(`[MCP] Started background watcher for ${watchRoot} (PID: ${result.pid})`);
408
599
  }
@@ -428,7 +619,7 @@ exports.mcp = new commander_1.Command("mcp")
428
619
  // --- Tool handlers ---
429
620
  function handleSemanticSearch(args_1) {
430
621
  return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
431
- var _a, _b, _c;
622
+ var _a, _b;
432
623
  const query = String(args.query || "");
433
624
  if (!query)
434
625
  return err("Missing required parameter: query");
@@ -538,10 +729,10 @@ exports.mcp = new commander_1.Command("mcp")
538
729
  rerank: process.env.GMAX_RERANK === "1",
539
730
  seeds,
540
731
  };
541
- const result = (_b = (yield daemonSearch(Object.assign({ projectRoot: resolvedRoot, query,
542
- limit, filters: effectiveFilters, pathPrefix }, searchOptions)))) !== null && _b !== void 0 ? _b : (yield getSearcher().search(query, limit, searchOptions, effectiveFilters, pathPrefix));
732
+ const result = yield searchStore(Object.assign({ projectRoot: resolvedRoot, query,
733
+ limit, filters: effectiveFilters, pathPrefix }, searchOptions));
543
734
  const allowedRoots = searchAll
544
- ? ((_c = filters.projectRoots) !== null && _c !== void 0 ? _c : [])
735
+ ? ((_b = filters.projectRoots) !== null && _b !== void 0 ? _b : [])
545
736
  : [resolvedRoot];
546
737
  result.data = result.data.filter((chunk) => {
547
738
  const candidate = chunkAbsPath(chunk);
@@ -684,7 +875,7 @@ exports.mcp = new commander_1.Command("mcp")
684
875
  const regex = new RegExp(namePattern, "i");
685
876
  results = results.filter((r) => r.symbols.some((s) => regex.test(s)));
686
877
  }
687
- catch (_d) {
878
+ catch (_c) {
688
879
  // Invalid regex — skip filter
689
880
  }
690
881
  }
@@ -692,9 +883,10 @@ exports.mcp = new commander_1.Command("mcp")
692
883
  // Symbol mode: append call graph
693
884
  if (mode === "symbol" && !searchAll) {
694
885
  try {
695
- const db = getVectorDb();
696
- const builder = new graph_builder_1.GraphBuilder(db, resolvedRoot);
697
- const graph = yield builder.buildGraph(query);
886
+ // `graph.peek` is the single-hop `buildGraph` this used to call,
887
+ // plus metadata this branch ignores — one verb rather than a second
888
+ // shape for the same query.
889
+ const { graph } = yield peekSymbolGraph(resolvedRoot, query, 1);
698
890
  if (graph.center) {
699
891
  const traceLines = ["", "--- Call graph ---"];
700
892
  const centerRel = graph.center.file.startsWith(resolvedRoot)
@@ -727,15 +919,14 @@ exports.mcp = new commander_1.Command("mcp")
727
919
  output += `\n${traceLines.join("\n")}`;
728
920
  }
729
921
  }
730
- catch (_e) {
922
+ catch (_d) {
731
923
  // Trace failed — return search results without trace
732
924
  }
733
925
  }
734
926
  return ok(prefixNotes(output));
735
927
  }
736
928
  catch (e) {
737
- const msg = e instanceof Error ? e.message : String(e);
738
- return err(`Search failed: ${msg}`);
929
+ return toolError("Search failed", e);
739
930
  }
740
931
  });
741
932
  }
@@ -800,10 +991,10 @@ exports.mcp = new commander_1.Command("mcp")
800
991
  let skeleton = "";
801
992
  let language = "";
802
993
  let tokenEstimate = 0;
803
- // Try cached skeleton first
994
+ // Try the stored skeleton first, via the daemon's warm store.
804
995
  try {
805
- const db = getVectorDb();
806
- const cached = yield (0, retriever_1.getStoredSkeleton)(db, absPath);
996
+ const cached = (yield lookupSkeleton(root, { path: absPath }))
997
+ .skeleton;
807
998
  if (cached) {
808
999
  skeleton = cached;
809
1000
  tokenEstimate = Math.ceil(cached.length / 4);
@@ -871,10 +1062,15 @@ exports.mcp = new commander_1.Command("mcp")
871
1062
  }
872
1063
  void ensureWatcher(root);
873
1064
  try {
874
- const db = getVectorDb();
875
- const builder = new graph_builder_1.GraphBuilder(db, root);
876
1065
  const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
877
- const graph = yield builder.buildGraphMultiHop(symbol, depth);
1066
+ const scope = projectScope(root);
1067
+ const graph = yield graphVerb("graph.trace", {
1068
+ projectRoot: root,
1069
+ scope,
1070
+ payload: { target: symbol, hops: depth },
1071
+ render: (resp) => resp.graph,
1072
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphTrace)(deps.vectorDb, { symbol, hops: depth, scope })),
1073
+ });
878
1074
  if (!graph.center) {
879
1075
  return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
880
1076
  }
@@ -938,8 +1134,7 @@ exports.mcp = new commander_1.Command("mcp")
938
1134
  return ok(lines.join("\n"));
939
1135
  }
940
1136
  catch (e) {
941
- const msg = e instanceof Error ? e.message : String(e);
942
- return err(`Trace failed: ${msg}`);
1137
+ return toolError("Trace failed", e);
943
1138
  }
944
1139
  });
945
1140
  }
@@ -953,23 +1148,21 @@ exports.mcp = new commander_1.Command("mcp")
953
1148
  const { proj, root } = resolveRegisteredProject(args.root);
954
1149
  if (!proj)
955
1150
  return err("Project not added to gmax yet.");
956
- const db = getVectorDb();
957
- const table = yield db.ensureTable();
958
- const prefix = root.endsWith("/") ? root : `${root}/`;
959
- const rows = yield table
960
- .query()
961
- .select([
962
- "path",
963
- "start_line",
964
- "end_line",
965
- "role",
966
- "is_exported",
967
- "defined_symbols",
968
- ])
969
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
970
- .limit(10)
971
- .toArray();
972
- if (rows.length === 0) {
1151
+ // Locations come from the daemon; the body is read below, from this
1152
+ // process's own filesystem view — same split the `extract` CLI makes.
1153
+ const [rows] = yield locateRows(root, {
1154
+ select: [
1155
+ "path",
1156
+ "start_line",
1157
+ "end_line",
1158
+ "role",
1159
+ "is_exported",
1160
+ "defined_symbols",
1161
+ ],
1162
+ matches: [{ kind: "definedSymbol", symbol }],
1163
+ limit: 10,
1164
+ });
1165
+ if (!rows || rows.length === 0) {
973
1166
  return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
974
1167
  }
975
1168
  // Pick best match: prefer exact first-defined, then highest role
@@ -978,7 +1171,7 @@ exports.mcp = new commander_1.Command("mcp")
978
1171
  DEFINITION: 2,
979
1172
  IMPLEMENTATION: 1,
980
1173
  };
981
- const sorted = rows.sort((a, b) => {
1174
+ const sorted = [...rows].sort((a, b) => {
982
1175
  const aDefs = Array.isArray(a.defined_symbols)
983
1176
  ? a.defined_symbols
984
1177
  : [];
@@ -1031,13 +1224,13 @@ exports.mcp = new commander_1.Command("mcp")
1031
1224
  return ok(parts.join("\n"));
1032
1225
  }
1033
1226
  catch (e) {
1034
- const msg = e instanceof Error ? e.message : String(e);
1035
- return err(`Extract failed: ${msg}`);
1227
+ return toolError("Extract failed", e);
1036
1228
  }
1037
1229
  });
1038
1230
  }
1039
1231
  function handlePeekSymbol(args) {
1040
1232
  return __awaiter(this, void 0, void 0, function* () {
1233
+ var _a, _b, _c, _d, _e, _f, _g;
1041
1234
  ensureWatcher();
1042
1235
  const symbol = String(args.symbol || "");
1043
1236
  if (!symbol)
@@ -1047,31 +1240,21 @@ exports.mcp = new commander_1.Command("mcp")
1047
1240
  if (!proj)
1048
1241
  return err("Project not added to gmax yet.");
1049
1242
  const depth = Math.min(Math.max(Number(args.depth || 1), 1), 3);
1050
- const db = getVectorDb();
1051
- const { GraphBuilder } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/graph-builder")));
1052
- const builder = new GraphBuilder(db, root);
1053
- const graph = yield builder.buildGraph(symbol);
1243
+ // One verb for the whole tool: the graph, the defining chunk's
1244
+ // is_exported/start_line/end_line, and (when depth > 1) the multi-hop
1245
+ // caller tree. Four queries became one round trip.
1246
+ const peek = yield peekSymbolGraph(root, symbol, depth);
1247
+ const graph = peek.graph;
1054
1248
  if (!graph.center) {
1055
1249
  return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
1056
1250
  }
1057
1251
  const center = graph.center;
1058
1252
  const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
1059
- // Get chunk metadata for is_exported and end_line
1060
- const table = yield db.ensureTable();
1061
- const prefix = root.endsWith("/") ? root : `${root}/`;
1062
- const metaRows = yield table
1063
- .query()
1064
- .select(["is_exported", "start_line", "end_line"])
1065
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
1066
- .limit(1)
1067
- .toArray();
1068
- const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
1069
- const startLine = metaRows.length > 0
1070
- ? Number(metaRows[0].start_line || 0)
1071
- : center.line;
1072
- const endLine = metaRows.length > 0
1073
- ? Number(metaRows[0].end_line || 0)
1074
- : center.line;
1253
+ // Chunk metadata for is_exported and end_line; null when the symbol has
1254
+ // no defining chunk, in which case the center's own line stands in.
1255
+ const exported = (_b = (_a = peek.meta) === null || _a === void 0 ? void 0 : _a.isExported) !== null && _b !== void 0 ? _b : false;
1256
+ const startLine = (_d = (_c = peek.meta) === null || _c === void 0 ? void 0 : _c.startLine) !== null && _d !== void 0 ? _d : center.line;
1257
+ const endLine = (_f = (_e = peek.meta) === null || _e === void 0 ? void 0 : _e.endLine) !== null && _f !== void 0 ? _f : center.line;
1075
1258
  // Get signature from source
1076
1259
  const fs = yield Promise.resolve().then(() => __importStar(require("node:fs")));
1077
1260
  let sigText = "(source not available)";
@@ -1090,11 +1273,10 @@ exports.mcp = new commander_1.Command("mcp")
1090
1273
  }
1091
1274
  sigText = sigLines.join("\n").trim();
1092
1275
  }
1093
- catch (_a) { }
1276
+ catch (_h) { }
1094
1277
  // Get callers
1095
1278
  let callerList;
1096
1279
  if (depth > 1) {
1097
- const multiHop = yield builder.buildGraphMultiHop(symbol, depth);
1098
1280
  const flat = [];
1099
1281
  function walkCallers(tree) {
1100
1282
  for (const t of tree) {
@@ -1107,7 +1289,7 @@ exports.mcp = new commander_1.Command("mcp")
1107
1289
  walkCallers(t.callers);
1108
1290
  }
1109
1291
  }
1110
- walkCallers(multiHop.callerTree);
1292
+ walkCallers((_g = peek.callerTree) !== null && _g !== void 0 ? _g : []);
1111
1293
  callerList = flat;
1112
1294
  }
1113
1295
  else {
@@ -1165,8 +1347,7 @@ exports.mcp = new commander_1.Command("mcp")
1165
1347
  return ok(parts.join("\n"));
1166
1348
  }
1167
1349
  catch (e) {
1168
- const msg = e instanceof Error ? e.message : String(e);
1169
- return err(`Peek failed: ${msg}`);
1350
+ return toolError("Peek failed", e);
1170
1351
  }
1171
1352
  });
1172
1353
  }
@@ -1180,35 +1361,28 @@ exports.mcp = new commander_1.Command("mcp")
1180
1361
  const { proj, root } = resolveRegisteredProject(args.root);
1181
1362
  if (!proj)
1182
1363
  return err("Project not added to gmax yet.");
1183
- const db = getVectorDb();
1184
- const table = yield db.ensureTable();
1185
- const prefix = root.endsWith("/") ? root : `${root}/`;
1186
- const defRows = yield table
1187
- .query()
1188
- .select(["path", "start_line", "is_exported"])
1189
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
1190
- .limit(1)
1191
- .toArray();
1192
- if (defRows.length === 0) {
1364
+ const scope = projectScope(root);
1365
+ const facts = yield graphVerb("graph.dead", {
1366
+ projectRoot: root,
1367
+ scope,
1368
+ payload: { target: symbol },
1369
+ render: (resp) => resp.dead,
1370
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphDead)(deps.vectorDb, { symbol, scope })),
1371
+ });
1372
+ if (!facts.found) {
1193
1373
  return ok(`Symbol '${symbol}' not found in the index. Check \`gmax status\` to see which projects are indexed, or try \`gmax search ${symbol}\` to find similar symbols.`);
1194
1374
  }
1195
- const defRow = defRows[0];
1196
- const defPath = String(defRow.path || "");
1197
- const defLine = Number(defRow.start_line || 0);
1198
- const isExported = Boolean(defRow.is_exported);
1199
- const builder = new graph_builder_1.GraphBuilder(db, root);
1200
- const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
1201
1375
  const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
1202
- const defLoc = `${rel(defPath)}:${defLine + 1}`;
1203
- if (callers.length === 0) {
1204
- if (isExported) {
1376
+ const defLoc = `${rel(facts.defPath)}:${facts.defLine + 1}`;
1377
+ if (facts.callerCount === 0) {
1378
+ if (facts.isExported) {
1205
1379
  return ok(`PUBLIC EXPORT ${defLoc} defines ${symbol} — no internal callers found; check external usage`);
1206
1380
  }
1207
1381
  return ok(`DEAD ${defLoc} defines ${symbol}`);
1208
1382
  }
1209
- const top = callers.slice(0, 3);
1383
+ const top = facts.topCallers;
1210
1384
  const lines = [
1211
- `LIVE ${defLoc} defines ${symbol} — ${callers.length} inbound caller${callers.length === 1 ? "" : "s"} (top ${top.length}):`,
1385
+ `LIVE ${defLoc} defines ${symbol} — ${facts.callerCount} inbound caller${facts.callerCount === 1 ? "" : "s"} (top ${top.length}):`,
1212
1386
  ];
1213
1387
  for (const c of top) {
1214
1388
  lines.push(` ${rel(c.file)}:${c.line + 1}`);
@@ -1216,8 +1390,7 @@ exports.mcp = new commander_1.Command("mcp")
1216
1390
  return ok(lines.join("\n"));
1217
1391
  }
1218
1392
  catch (e) {
1219
- const msg = e instanceof Error ? e.message : String(e);
1220
- return err(`Dead check failed: ${msg}`);
1393
+ return toolError("Dead check failed", e);
1221
1394
  }
1222
1395
  });
1223
1396
  }
@@ -1227,39 +1400,25 @@ exports.mcp = new commander_1.Command("mcp")
1227
1400
  const { proj, root } = resolveRegisteredProject(args.root);
1228
1401
  if (!proj)
1229
1402
  return err("Project not added to gmax yet.");
1230
- const prefix = root.endsWith("/") ? root : `${root}/`;
1231
1403
  const top = Math.min(Math.max(Number(args.top) || 10, 1), 50);
1232
1404
  try {
1233
- const db = getVectorDb();
1234
- const table = yield db.ensureTable();
1235
- const rows = yield table
1236
- .query()
1237
- .select([
1238
- "path",
1239
- "start_line",
1240
- "defined_symbols",
1241
- "referenced_symbols",
1242
- "type_referenced_symbols",
1243
- "is_exported",
1244
- ])
1245
- .where((0, filter_builder_1.pathStartsWith)(prefix))
1246
- .limit(500000)
1247
- .toArray();
1248
- if (rows.length === 0) {
1405
+ // The 500k-row read and its aggregation stay wherever the store is
1406
+ // open; what crosses the socket is the finished report.
1407
+ const scope = projectScope(root);
1408
+ const audit = yield graphVerb("graph.audit", {
1409
+ projectRoot: root,
1410
+ scope,
1411
+ payload: { top },
1412
+ render: (resp) => { var _a; return (_a = resp.audit) !== null && _a !== void 0 ? _a : null; },
1413
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphAudit)(deps.vectorDb, {
1414
+ projectRoot: root,
1415
+ scope,
1416
+ top,
1417
+ })),
1418
+ });
1419
+ if (!audit) {
1249
1420
  return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
1250
1421
  }
1251
- const audit = (0, audit_1.computeAudit)(rows.map((r) => ({
1252
- path: String(r.path || ""),
1253
- start_line: Number(r.start_line || 0),
1254
- is_exported: Boolean(r.is_exported),
1255
- defined_symbols: toStringArray(r.defined_symbols),
1256
- referenced_symbols: [
1257
- ...new Set([
1258
- ...toStringArray(r.referenced_symbols),
1259
- ...toStringArray(r.type_referenced_symbols),
1260
- ]),
1261
- ],
1262
- })), prefix, top);
1263
1422
  const lines = [];
1264
1423
  lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
1265
1424
  lines.push("");
@@ -1298,8 +1457,7 @@ exports.mcp = new commander_1.Command("mcp")
1298
1457
  return ok(lines.join("\n"));
1299
1458
  }
1300
1459
  catch (e) {
1301
- const msg = e instanceof Error ? e.message : String(e);
1302
- return err(`Audit failed: ${msg}`);
1460
+ return toolError("Audit failed", e);
1303
1461
  }
1304
1462
  });
1305
1463
  }
@@ -1318,25 +1476,35 @@ exports.mcp = new commander_1.Command("mcp")
1318
1476
  const dirDepth = Math.min(Math.max(Number(args.dir_depth) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 1), 20);
1319
1477
  const minSimilarity = Math.max(Number(args.min_similarity) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity, 0);
1320
1478
  const maxRows = Math.min(Math.max(Number(args.max_rows) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, 1), surprising_connections_1.MAX_SURPRISE_ROWS);
1479
+ const options = {
1480
+ sample,
1481
+ neighbors,
1482
+ dirDepth,
1483
+ minSimilarity,
1484
+ maxRows,
1485
+ includeTests: Boolean(args.include_tests),
1486
+ includeEval: Boolean(args.include_eval),
1487
+ in: typeof args.in === "string" ? [args.in] : undefined,
1488
+ exclude: typeof args.exclude === "string" ? [args.exclude] : undefined,
1489
+ };
1321
1490
  try {
1322
- const db = getVectorDb();
1323
- const table = yield db.ensureTable();
1324
- const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, root, {
1325
- sample,
1326
- neighbors,
1327
- dirDepth,
1328
- minSimilarity,
1329
- maxRows,
1330
- includeTests: Boolean(args.include_tests),
1331
- includeEval: Boolean(args.include_eval),
1332
- in: typeof args.in === "string" ? args.in : undefined,
1333
- exclude: typeof args.exclude === "string" ? args.exclude : undefined,
1491
+ // The scan can touch 50k rows and every one of their vectors; the verb
1492
+ // returns the summary plus the findings this will print, and drops the
1493
+ // raw `pairs` array entirely.
1494
+ const result = yield readStore("surprises", {
1495
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
1496
+ cmd: "vector.surprises",
1497
+ projectRoot: root,
1498
+ options,
1499
+ top,
1500
+ }, { timeoutMs: 120000 }),
1501
+ render: (resp) => resp,
1502
+ inProcess: () => localStore((deps) => (0, vector_handler_1.runSurprises)(deps, { projectRoot: root, options, top })),
1334
1503
  });
1335
1504
  return ok(formatMcpSurprisingConnections(result, top));
1336
1505
  }
1337
1506
  catch (e) {
1338
- const msg = e instanceof Error ? e.message : String(e);
1339
- return err(`Surprising connections failed: ${msg}`);
1507
+ return toolError("Surprising connections failed", e);
1340
1508
  }
1341
1509
  });
1342
1510
  }
@@ -1352,8 +1520,19 @@ exports.mcp = new commander_1.Command("mcp")
1352
1520
  const { proj, root } = resolveRegisteredProject(args.root);
1353
1521
  if (!proj)
1354
1522
  return err("Project not added to gmax yet.");
1355
- const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1356
- const hits = yield builder.getNeighbors(symbol, direction, maxHops);
1523
+ const scope = projectScope(root);
1524
+ const hits = yield graphVerb("graph.neighbors", {
1525
+ projectRoot: root,
1526
+ scope,
1527
+ payload: { symbol, direction, maxHops },
1528
+ render: (resp) => { var _a; return (_a = resp.hits) !== null && _a !== void 0 ? _a : []; },
1529
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphNeighbors)(deps.vectorDb, {
1530
+ symbol,
1531
+ direction,
1532
+ maxHops,
1533
+ scope,
1534
+ })),
1535
+ });
1357
1536
  if (hits.length === 0) {
1358
1537
  return ok(`No ${direction} found for '${symbol}' within ${maxHops} hop(s). Check it is indexed (\`gmax status\`) or try the \`dead\` tool.`);
1359
1538
  }
@@ -1370,8 +1549,7 @@ exports.mcp = new commander_1.Command("mcp")
1370
1549
  return ok(lines.join("\n"));
1371
1550
  }
1372
1551
  catch (e) {
1373
- const msg = e instanceof Error ? e.message : String(e);
1374
- return err(`get_neighbors failed: ${msg}`);
1552
+ return toolError("get_neighbors failed", e);
1375
1553
  }
1376
1554
  });
1377
1555
  }
@@ -1389,8 +1567,20 @@ exports.mcp = new commander_1.Command("mcp")
1389
1567
  const { proj, root } = resolveRegisteredProject(args.root);
1390
1568
  if (!proj)
1391
1569
  return err("Project not added to gmax yet.");
1392
- const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1393
- const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
1570
+ const scope = projectScope(root);
1571
+ const pathSyms = yield graphVerb("graph.paths", {
1572
+ projectRoot: root,
1573
+ scope,
1574
+ payload: { from, to, direction, maxHops },
1575
+ render: (resp) => { var _a; return (_a = resp.path) !== null && _a !== void 0 ? _a : null; },
1576
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphPaths)(deps.vectorDb, {
1577
+ from,
1578
+ to,
1579
+ direction,
1580
+ maxHops,
1581
+ scope,
1582
+ })),
1583
+ });
1394
1584
  if (!pathSyms) {
1395
1585
  return ok(`No ${direction} path from '${from}' to '${to}' within ${maxHops} hops.`);
1396
1586
  }
@@ -1399,8 +1589,7 @@ exports.mcp = new commander_1.Command("mcp")
1399
1589
  return ok(`Path (${hops} hop${hops === 1 ? "" : "s"}): ${pathSyms.join(arrow)}`);
1400
1590
  }
1401
1591
  catch (e) {
1402
- const msg = e instanceof Error ? e.message : String(e);
1403
- return err(`find_paths failed: ${msg}`);
1592
+ return toolError("find_paths failed", e);
1404
1593
  }
1405
1594
  });
1406
1595
  }
@@ -1417,9 +1606,18 @@ exports.mcp = new commander_1.Command("mcp")
1417
1606
  const { proj, root } = resolveRegisteredProject(args.root);
1418
1607
  if (!proj)
1419
1608
  return err("Project not added to gmax yet.");
1609
+ // Resolved here, not daemon-side: verifyExistingTarget looks at this
1610
+ // process's filesystem view, and a path outside the project must fail
1611
+ // with the containment error before anything reaches the store.
1420
1612
  const abs = filesIn.map((f) => (0, path_containment_1.resolveContainedPath)(root, f, { verifyExistingTarget: true }));
1421
- const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1422
- const sg = yield builder.subgraphForFiles(abs);
1613
+ const scope = projectScope(root);
1614
+ const sg = yield graphVerb("graph.subgraph", {
1615
+ projectRoot: root,
1616
+ scope,
1617
+ payload: { files: abs },
1618
+ render: (resp) => resp.subgraph,
1619
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphSubgraph)(deps.vectorDb, { files: abs, scope })),
1620
+ });
1423
1621
  if (sg.symbols.length === 0) {
1424
1622
  return ok(`No indexed symbols found in: ${filesIn.join(", ")}. Check the paths and \`gmax status\`.`);
1425
1623
  }
@@ -1447,8 +1645,7 @@ exports.mcp = new commander_1.Command("mcp")
1447
1645
  return ok(lines.join("\n"));
1448
1646
  }
1449
1647
  catch (e) {
1450
- const msg = e instanceof Error ? e.message : String(e);
1451
- return err(`subgraph_for_files failed: ${msg}`);
1648
+ return toolError("subgraph_for_files failed", e);
1452
1649
  }
1453
1650
  });
1454
1651
  }
@@ -1463,60 +1660,29 @@ exports.mcp = new commander_1.Command("mcp")
1463
1660
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
1464
1661
  }
1465
1662
  try {
1466
- const db = getVectorDb();
1467
- const table = yield db.ensureTable();
1468
- const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1469
- let where = `array_length(defined_symbols) > 0 AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`;
1470
- if (pathPrefix) {
1471
- const absPrefix = (0, path_containment_1.resolveContainedPath)(root, pathPrefix);
1472
- where += ` AND ${(0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix))}`;
1473
- }
1474
- const query = table
1475
- .query()
1476
- .select([
1477
- "defined_symbols",
1478
- "path",
1479
- "start_line",
1480
- "role",
1481
- "is_exported",
1482
- ])
1483
- .where(where)
1484
- .limit(pattern ? 10000 : Math.max(limit * 50, 2000));
1485
- const rows = yield query.toArray();
1486
- const map = new Map();
1487
- for (const row of rows) {
1488
- const defs = toStringArray(row.defined_symbols);
1489
- const rowPath = String(row.path || "");
1490
- const line = Number(row.start_line || 0);
1491
- const role = String(row.role || "");
1492
- const exported = Boolean(row.is_exported);
1493
- for (const sym of defs) {
1494
- if (pattern && !sym.toLowerCase().includes(pattern.toLowerCase())) {
1495
- continue;
1496
- }
1497
- const existing = map.get(sym);
1498
- if (existing) {
1499
- existing.count += 1;
1500
- }
1501
- else {
1502
- map.set(sym, {
1503
- symbol: sym,
1504
- count: 1,
1505
- path: rowPath,
1506
- line: Math.max(1, line + 1),
1507
- role,
1508
- exported,
1509
- });
1510
- }
1511
- }
1512
- }
1513
- const entries = Array.from(map.values())
1514
- .sort((a, b) => {
1515
- if (b.count !== a.count)
1516
- return b.count - a.count;
1517
- return a.symbol.localeCompare(b.symbol);
1518
- })
1519
- .slice(0, limit);
1663
+ // A single absolute prefix carries both scopes: the sub-path when one
1664
+ // was asked for (it is already inside the root), the root otherwise.
1665
+ const absPrefix = pathPrefix
1666
+ ? (0, path_containment_1.resolveContainedPath)(root, pathPrefix)
1667
+ : root.endsWith("/")
1668
+ ? root
1669
+ : `${root}/`;
1670
+ const entries = yield readStore("symbols", {
1671
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
1672
+ cmd: "rows.symbols",
1673
+ projectRoot: root,
1674
+ pathPrefix: absPrefix,
1675
+ pattern,
1676
+ limit,
1677
+ }, { timeoutMs: 60000 }),
1678
+ render: (resp) => { var _a; return ((_a = resp.entries) !== null && _a !== void 0 ? _a : []); },
1679
+ inProcess: () => localStore((deps) => (0, rows_handler_1.runSymbols)(deps, {
1680
+ projectRoot: root,
1681
+ pathPrefix: absPrefix,
1682
+ pattern,
1683
+ limit,
1684
+ })),
1685
+ });
1520
1686
  if (entries.length === 0) {
1521
1687
  return ok("No symbols found. Run `gmax status` to verify the project is indexed, or `gmax index` to rebuild.");
1522
1688
  }
@@ -1526,13 +1692,14 @@ exports.mcp = new commander_1.Command("mcp")
1526
1692
  : e.path;
1527
1693
  const roleTag = e.role ? ` [${e.role.slice(0, 4)}]` : "";
1528
1694
  const expTag = e.exported ? " exported" : "";
1529
- return `${e.symbol}${roleTag}${expTag}\t${rel}:${e.line}`;
1695
+ // SymbolEntry.line is the 0-based start_line the store holds; this
1696
+ // tool has always printed a 1-based line, floored at 1.
1697
+ return `${e.symbol}${roleTag}${expTag}\t${rel}:${Math.max(1, e.line + 1)}`;
1530
1698
  });
1531
1699
  return ok(lines.join("\n"));
1532
1700
  }
1533
1701
  catch (e) {
1534
- const msg = e instanceof Error ? e.message : String(e);
1535
- return err(`Symbol listing failed: ${msg}`);
1702
+ return toolError("Symbol listing failed", e);
1536
1703
  }
1537
1704
  });
1538
1705
  }
@@ -1572,9 +1739,7 @@ exports.mcp = new commander_1.Command("mcp")
1572
1739
  const projects = (0, project_registry_1.listProjects)();
1573
1740
  const currentProject = resolveMcpProject(undefined, projectRoot);
1574
1741
  const identity = (0, embedding_status_1.projectEmbeddingStatus)(currentProject, globalConfig);
1575
- const db = getVectorDb();
1576
- const stats = yield db.getStats();
1577
- const fileCount = yield db.getDistinctFileCount();
1742
+ const { chunks, files } = yield indexTotals(projects);
1578
1743
  // Watcher status
1579
1744
  const watcher = (0, watcher_store_1.getWatcherCoveringPath)(projectRoot);
1580
1745
  let watcherLine = "Watcher: not running";
@@ -1593,7 +1758,7 @@ exports.mcp = new commander_1.Command("mcp")
1593
1758
  // per-project chunk counts) lives in `list_projects` — keep this tool
1594
1759
  // focused on index health and avoid the N-project LanceDB scans.
1595
1760
  const lines = [
1596
- `Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
1761
+ `Index: ~/.gmax/lancedb (${chunks} chunks, ${files} files)`,
1597
1762
  `Configured embedding: ${identity.configured.tier} ${identity.configured.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.configured.fingerprint)}] (${globalConfig.embedMode})`,
1598
1763
  identity.built
1599
1764
  ? `Built embedding: ${identity.built.tier} ${identity.built.vectorDim}d [${(0, embedding_status_1.embeddingFingerprintLabel)(identity.built.fingerprint)}] (${identity.state})`
@@ -1607,8 +1772,7 @@ exports.mcp = new commander_1.Command("mcp")
1607
1772
  return ok(lines.join("\n"));
1608
1773
  }
1609
1774
  catch (e) {
1610
- const msg = e instanceof Error ? e.message : String(e);
1611
- return err(`Status check failed: ${msg}`);
1775
+ return toolError("Status check failed", e);
1612
1776
  }
1613
1777
  });
1614
1778
  }
@@ -1623,8 +1787,15 @@ exports.mcp = new commander_1.Command("mcp")
1623
1787
  const prefix = dir.endsWith("/") ? dir : `${dir}/`;
1624
1788
  const limit = Math.min(Math.max(Number(args.limit) || 200, 1), 5000);
1625
1789
  try {
1626
- const db = getVectorDb();
1627
- const { summarized, remaining } = yield (0, syncer_1.generateSummaries)(db, prefix, (done, total) => {
1790
+ // `generateSummaries` is a deliberate no-op stub — the summarizer is
1791
+ // decommissioned (see the HARD STOP at the top of CLAUDE.md) — and it
1792
+ // touches neither the VectorDB it is handed nor any other store.
1793
+ // Opening one here would take a reader lease under ~/.gmax purely to
1794
+ // reach a function that returns {0, 0}, which is precisely the
1795
+ // per-session reader this tool set no longer holds. It is still called
1796
+ // rather than inlined so the stub stays the single source of truth.
1797
+ const noStoreNeeded = null;
1798
+ const { summarized, remaining } = yield (0, syncer_1.generateSummaries)(noStoreNeeded, prefix, (done, total) => {
1628
1799
  console.log(`[summarize] ${done}/${total} chunks`);
1629
1800
  }, limit);
1630
1801
  if (summarized === 0) {
@@ -1636,8 +1807,7 @@ exports.mcp = new commander_1.Command("mcp")
1636
1807
  return ok(`Summarized ${summarized} chunks in ${path.basename(dir)}/${remainMsg}`);
1637
1808
  }
1638
1809
  catch (e) {
1639
- const msg = e instanceof Error ? e.message : String(e);
1640
- return err(`Summarization failed: ${msg}`);
1810
+ return toolError("Summarization failed", e);
1641
1811
  }
1642
1812
  });
1643
1813
  }
@@ -1647,117 +1817,54 @@ exports.mcp = new commander_1.Command("mcp")
1647
1817
  const { proj, root } = resolveRegisteredProject(args.root);
1648
1818
  if (!proj)
1649
1819
  return err("Project not added to gmax yet.");
1650
- const prefix = root.endsWith("/") ? root : `${root}/`;
1651
1820
  const projectName = path.basename(root);
1652
1821
  try {
1653
- const db = getVectorDb();
1654
- const table = yield db.ensureTable();
1655
- const rows = yield table
1656
- .query()
1657
- .select([
1658
- "path",
1659
- "role",
1660
- "is_exported",
1661
- "complexity",
1662
- "defined_symbols",
1663
- "referenced_symbols",
1664
- ])
1665
- .where((0, filter_builder_1.pathStartsWith)(prefix))
1666
- .limit(200000)
1667
- .toArray();
1668
- if (rows.length === 0) {
1822
+ // `rows.project` returns the finished overview, not the up-to-200 000
1823
+ // rows it was computed from — the same aggregation `gmax project` runs,
1824
+ // in whichever process holds the store.
1825
+ const overview = yield readStore("project", {
1826
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({ cmd: "rows.project", projectRoot: root }, { timeoutMs: 120000 }),
1827
+ render: (resp) => resp.overview,
1828
+ inProcess: () => localStore((deps) => (0, rows_handler_1.runProject)(deps, root)),
1829
+ });
1830
+ if (overview.chunks === 0) {
1669
1831
  return ok(`No indexed data found for ${root}. Run: gmax index --path ${root}`);
1670
1832
  }
1671
- const files = new Set();
1672
- const extCounts = new Map();
1673
- const dirCounts = new Map();
1674
- const roleCounts = new Map();
1675
- const symbolRefs = new Map();
1676
- const entryPoints = [];
1677
- for (const row of rows) {
1678
- const p = String(row.path || "");
1679
- const role = String(row.role || "IMPLEMENTATION");
1680
- const exported = Boolean(row.is_exported);
1681
- const complexity = Number(row.complexity || 0);
1682
- const defs = toStringArray(row.defined_symbols);
1683
- const refs = toStringArray(row.referenced_symbols);
1684
- files.add(p);
1685
- const ext = path.extname(p).toLowerCase() || path.basename(p);
1686
- extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
1687
- const rel = p.startsWith(prefix) ? p.slice(prefix.length) : p;
1688
- const parts = rel.split("/");
1689
- const dir = parts.length > 2
1690
- ? `${parts.slice(0, 2).join("/")}/`
1691
- : parts.length > 1
1692
- ? `${parts[0]}/`
1693
- : "(root)";
1694
- if (!dirCounts.has(dir)) {
1695
- dirCounts.set(dir, { files: new Set(), chunks: 0 });
1696
- }
1697
- const dc = dirCounts.get(dir);
1698
- dc.files.add(p);
1699
- dc.chunks++;
1700
- roleCounts.set(role, (roleCounts.get(role) || 0) + 1);
1701
- for (const ref of refs) {
1702
- symbolRefs.set(ref, (symbolRefs.get(ref) || 0) + 1);
1703
- }
1704
- if (exported &&
1705
- role === "ORCHESTRATION" &&
1706
- complexity >= 5 &&
1707
- defs.length > 0) {
1708
- const relPath = p.startsWith(prefix) ? p.slice(prefix.length) : p;
1709
- entryPoints.push({ symbol: defs[0], path: relPath });
1710
- }
1711
- }
1833
+ const pct = (count) => Math.round((count / overview.chunks) * 100);
1712
1834
  const lines = [];
1713
- const projects = (0, project_registry_1.listProjects)();
1714
- const proj = projects.find((p) => p.root === root);
1715
1835
  lines.push(`Project: ${projectName} (${root})`);
1716
- lines.push(`Last indexed: ${(_a = proj === null || proj === void 0 ? void 0 : proj.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"} • ${rows.length} chunks • ${files.size} files`);
1836
+ lines.push(`Last indexed: ${(_a = proj.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"} • ${overview.chunks} chunks • ${overview.files} files`);
1717
1837
  lines.push("");
1718
- const extEntries = Array.from(extCounts.entries())
1719
- .sort((a, b) => b[1] - a[1])
1720
- .slice(0, 8);
1721
- const langLine = extEntries
1722
- .map(([ext, count]) => `${ext} (${Math.round((count / rows.length) * 100)}%)`)
1723
- .join(", ");
1724
- lines.push(`Languages: ${langLine}`);
1838
+ lines.push(`Languages: ${overview.extEntries
1839
+ .map(([ext, count]) => `${ext} (${pct(count)}%)`)
1840
+ .join(", ")}`);
1725
1841
  lines.push("");
1726
1842
  lines.push("Directory structure:");
1727
- const dirEntries = Array.from(dirCounts.entries())
1728
- .sort((a, b) => b[1].chunks - a[1].chunks)
1729
- .slice(0, 12);
1730
- for (const [dir, data] of dirEntries) {
1731
- lines.push(` ${dir.padEnd(25)} (${data.files.size} files, ${data.chunks} chunks)`);
1843
+ for (const [dir, data] of overview.dirEntries) {
1844
+ lines.push(` ${dir.padEnd(25)} (${data.files} files, ${data.chunks} chunks)`);
1732
1845
  }
1733
1846
  lines.push("");
1734
- const roleEntries = Array.from(roleCounts.entries()).sort((a, b) => b[1] - a[1]);
1735
- const roleLine = roleEntries
1736
- .map(([role, count]) => `${Math.round((count / rows.length) * 100)}% ${role}`)
1737
- .join(", ");
1738
- lines.push(`Roles: ${roleLine}`);
1847
+ lines.push(`Roles: ${overview.roleEntries
1848
+ .map(([role, count]) => `${pct(count)}% ${role}`)
1849
+ .join(", ")}`);
1739
1850
  lines.push("");
1740
- const topSymbols = Array.from(symbolRefs.entries())
1741
- .sort((a, b) => b[1] - a[1])
1742
- .slice(0, 8);
1743
- if (topSymbols.length > 0) {
1851
+ if (overview.topSymbols.length > 0) {
1744
1852
  lines.push("Key symbols (by reference count):");
1745
- for (const [sym, count] of topSymbols) {
1853
+ for (const [sym, count] of overview.topSymbols) {
1746
1854
  lines.push(` ${sym.padEnd(25)} (referenced ${count}x)`);
1747
1855
  }
1748
1856
  lines.push("");
1749
1857
  }
1750
- if (entryPoints.length > 0) {
1858
+ if (overview.entryPoints.length > 0) {
1751
1859
  lines.push("Entry points (exported orchestration):");
1752
- for (const ep of entryPoints.slice(0, 10)) {
1860
+ for (const ep of overview.entryPoints) {
1753
1861
  lines.push(` ${ep.symbol.padEnd(25)} ${ep.path}`);
1754
1862
  }
1755
1863
  }
1756
1864
  return ok(lines.join("\n"));
1757
1865
  }
1758
1866
  catch (e) {
1759
- const msg = e instanceof Error ? e.message : String(e);
1760
- return err(`Project summary failed: ${msg}`);
1867
+ return toolError("Project summary failed", e);
1761
1868
  }
1762
1869
  });
1763
1870
  }
@@ -1776,14 +1883,13 @@ exports.mcp = new commander_1.Command("mcp")
1776
1883
  });
1777
1884
  const rootPrefix = root.endsWith("/") ? root : `${root}/`;
1778
1885
  try {
1779
- const db = getVectorDb();
1780
- const table = yield db.ensureTable();
1781
- const fileChunks = yield table
1782
- .query()
1783
- .select(["defined_symbols", "referenced_symbols"])
1784
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
1785
- .toArray();
1786
- if (fileChunks.length === 0) {
1886
+ // The file's own chunks, unscoped: the file was named explicitly.
1887
+ const [fileChunks] = yield locateRows(root, {
1888
+ select: ["defined_symbols", "referenced_symbols"],
1889
+ matches: [{ kind: "path", path: absPath }],
1890
+ scoped: false,
1891
+ });
1892
+ if (!fileChunks || fileChunks.length === 0) {
1787
1893
  return ok(`File not found in index: ${file}. Check that the path is relative to the project root. Run \`gmax status\` to see indexed projects.`);
1788
1894
  }
1789
1895
  const definedHere = new Set();
@@ -1794,17 +1900,19 @@ exports.mcp = new commander_1.Command("mcp")
1794
1900
  for (const s of toStringArray(chunk.referenced_symbols))
1795
1901
  referencedHere.add(s);
1796
1902
  }
1797
- // Dependencies: files that DEFINE symbols this file REFERENCES
1903
+ // Dependencies: files that DEFINE symbols this file REFERENCES. One
1904
+ // batched round trip instead of one per symbol — the daemon runs the
1905
+ // same N selects, the socket carries one request.
1906
+ const depSymbols = [...referencedHere].filter((s) => !definedHere.has(s));
1798
1907
  const depCounts = new Map();
1799
- for (const sym of referencedHere) {
1800
- if (definedHere.has(sym))
1801
- continue;
1802
- const rows = yield table
1803
- .query()
1804
- .select(["path"])
1805
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1806
- .limit(3)
1807
- .toArray();
1908
+ for (const rows of yield locateRows(root, {
1909
+ select: ["path"],
1910
+ matches: depSymbols.map((symbol) => ({
1911
+ kind: "definedSymbol",
1912
+ symbol,
1913
+ })),
1914
+ limit: 3,
1915
+ })) {
1808
1916
  for (const row of rows) {
1809
1917
  const p = String(row.path || "");
1810
1918
  if (p === absPath)
@@ -1812,15 +1920,16 @@ exports.mcp = new commander_1.Command("mcp")
1812
1920
  depCounts.set(p, (depCounts.get(p) || 0) + 1);
1813
1921
  }
1814
1922
  }
1815
- // Dependents: files that REFERENCE symbols this file DEFINES
1923
+ // Dependents: files that REFERENCE symbols this file DEFINES.
1816
1924
  const revCounts = new Map();
1817
- for (const sym of definedHere) {
1818
- const rows = yield table
1819
- .query()
1820
- .select(["path"])
1821
- .where(`array_contains(referenced_symbols, '${(0, filter_builder_1.escapeSqlString)(sym)}') AND ${(0, filter_builder_1.pathStartsWith)(rootPrefix)}`)
1822
- .limit(20)
1823
- .toArray();
1925
+ for (const rows of yield locateRows(root, {
1926
+ select: ["path"],
1927
+ matches: [...definedHere].map((symbol) => ({
1928
+ kind: "referencedSymbol",
1929
+ symbol,
1930
+ })),
1931
+ limit: 20,
1932
+ })) {
1824
1933
  for (const row of rows) {
1825
1934
  const p = String(row.path || "");
1826
1935
  if (p === absPath)
@@ -1864,8 +1973,7 @@ exports.mcp = new commander_1.Command("mcp")
1864
1973
  return ok(lines.join("\n"));
1865
1974
  }
1866
1975
  catch (e) {
1867
- const msg = e instanceof Error ? e.message : String(e);
1868
- return err(`Related files failed: ${msg}`);
1976
+ return toolError("Related files failed", e);
1869
1977
  }
1870
1978
  });
1871
1979
  }
@@ -1930,7 +2038,6 @@ exports.mcp = new commander_1.Command("mcp")
1930
2038
  // --- New command handlers (Phase 4) ---
1931
2039
  function handleDiffChanges(args) {
1932
2040
  return __awaiter(this, void 0, void 0, function* () {
1933
- var _a;
1934
2041
  ensureWatcher();
1935
2042
  const { getChangedFiles } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/git")));
1936
2043
  const ref = typeof args.ref === "string" ? args.ref : undefined;
@@ -1953,8 +2060,8 @@ exports.mcp = new commander_1.Command("mcp")
1953
2060
  const searchOptions = {
1954
2061
  rerank: process.env.GMAX_RERANK === "1",
1955
2062
  };
1956
- const response = (_a = (yield daemonSearch(Object.assign({ projectRoot: root, query,
1957
- limit, filters: {}, pathPrefix: root }, searchOptions)))) !== null && _a !== void 0 ? _a : (yield getSearcher().search(query, limit, searchOptions, {}, root));
2063
+ const response = yield searchStore(Object.assign({ projectRoot: root, query,
2064
+ limit, filters: {}, pathPrefix: root }, searchOptions));
1958
2065
  const changedSet = new Set(changedFiles);
1959
2066
  // searcher.search() returns mapped chunks (path under metadata.path);
1960
2067
  // changedFiles are absolute, so match on the resolved absolute path.
@@ -1972,25 +2079,32 @@ exports.mcp = new commander_1.Command("mcp")
1972
2079
  });
1973
2080
  return ok(lines.join("\n"));
1974
2081
  }
1975
- const db = getVectorDb();
1976
- const table = yield db.ensureTable();
1977
- const lines = [];
1978
- for (const file of changedFiles) {
1979
- const chunks = yield table
1980
- .query()
1981
- .select(["defined_symbols", "role"])
1982
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(file)}'`)
1983
- .limit(50)
1984
- .toArray();
1985
- const symbols = chunks.flatMap((c) => toStringArray(c.defined_symbols));
1986
- lines.push(symbols.length > 0
2082
+ // One matcher per changed file, batched. `rows.locate` accepts up to
2083
+ // 2000 matchers per request, so a very large diff is sent in chunks
2084
+ // rather than one oversized line.
2085
+ const MATCH_BATCH = 500;
2086
+ const perFile = [];
2087
+ for (let i = 0; i < changedFiles.length; i += MATCH_BATCH) {
2088
+ perFile.push(...(yield locateRows(root, {
2089
+ select: ["defined_symbols", "role"],
2090
+ matches: changedFiles
2091
+ .slice(i, i + MATCH_BATCH)
2092
+ .map((file) => ({ kind: "path", path: file })),
2093
+ limit: 50,
2094
+ scoped: false,
2095
+ })));
2096
+ }
2097
+ const lines = changedFiles.map((file, i) => {
2098
+ var _a;
2099
+ const symbols = ((_a = perFile[i]) !== null && _a !== void 0 ? _a : []).flatMap((c) => toStringArray(c.defined_symbols));
2100
+ return symbols.length > 0
1987
2101
  ? `${rel(file)} (${symbols.slice(0, 5).join(", ")}${symbols.length > 5 ? "..." : ""})`
1988
- : rel(file));
1989
- }
2102
+ : rel(file);
2103
+ });
1990
2104
  return ok(`${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"}${ref ? ` (vs ${ref})` : ""}:\n${lines.join("\n")}`);
1991
2105
  }
1992
2106
  catch (e) {
1993
- return err(`Diff failed: ${e instanceof Error ? e.message : String(e)}`);
2107
+ return toolError("Diff failed", e);
1994
2108
  }
1995
2109
  });
1996
2110
  }
@@ -2006,12 +2120,10 @@ exports.mcp = new commander_1.Command("mcp")
2006
2120
  if (!proj)
2007
2121
  return err("Project not added to gmax yet.");
2008
2122
  const scopedTarget = containMcpTarget(root, target);
2009
- const { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
2010
- const db = getVectorDb();
2011
- const { symbols, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, mcpRootPrefix(root));
2123
+ const { symbols, symbolFamilies } = yield resolveTarget(root, scopedTarget);
2012
2124
  if (symbols.length === 0)
2013
2125
  return ok(`No symbols found for: ${target}`);
2014
- const tests = yield findTests(symbols, db, root, depth, undefined, symbolFamilies);
2126
+ const tests = yield findTestsForSymbols(root, symbols, depth, symbolFamilies);
2015
2127
  if (tests.length === 0)
2016
2128
  return ok(`No tests found for ${target}.`);
2017
2129
  const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
@@ -2022,7 +2134,7 @@ exports.mcp = new commander_1.Command("mcp")
2022
2134
  return ok(`Tests for ${target}:\n${lines.join("\n")}`);
2023
2135
  }
2024
2136
  catch (e) {
2025
- return err(`Find tests failed: ${e instanceof Error ? e.message : String(e)}`);
2137
+ return toolError("Find tests failed", e);
2026
2138
  }
2027
2139
  });
2028
2140
  }
@@ -2040,22 +2152,47 @@ exports.mcp = new commander_1.Command("mcp")
2040
2152
  if (!proj)
2041
2153
  return err("Project not added to gmax yet.");
2042
2154
  const scopedTarget = containMcpTarget(root, target);
2043
- const { resolveTargetSymbols, findTests, findDependents, findDependentsDetailed, isTestPath, } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
2155
+ const { isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
2044
2156
  const { buildImpactRollup, formatImpactRollupAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact-rollup")));
2045
- const db = getVectorDb();
2046
- const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(scopedTarget, db, root);
2157
+ const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTarget(root, scopedTarget);
2047
2158
  if (symbols.length === 0)
2048
2159
  return ok(`No symbols found for: ${target}`);
2049
2160
  const targetPath = resolvedAsFile
2050
2161
  ? (0, path_containment_1.resolveContainedPath)(root, scopedTarget)
2051
2162
  : undefined;
2052
- const excludePaths = targetPath ? new Set([targetPath]) : undefined;
2053
- const rollupLimit = Math.min(Math.max(top * 10, 100), 500);
2163
+ const excludePaths = targetPath ? [targetPath] : undefined;
2164
+ // The flat (non-rollup) call keeps findDependents' own default of 10;
2165
+ // `undefined` on the wire is what preserves it.
2166
+ const limit = rollup
2167
+ ? Math.min(Math.max(top * 10, 100), 500)
2168
+ : undefined;
2169
+ const scope = projectScope(root);
2054
2170
  const [dependents, tests] = yield Promise.all([
2055
- rollup
2056
- ? findDependentsDetailed(symbols, db, root, excludePaths, rollupLimit, undefined, symbolFamilies)
2057
- : findDependents(symbols, db, root, excludePaths, undefined, undefined, symbolFamilies),
2058
- findTests(symbols, db, root, depth, undefined, symbolFamilies),
2171
+ graphVerb("graph.dependents", {
2172
+ projectRoot: root,
2173
+ scope,
2174
+ payload: {
2175
+ symbols,
2176
+ detailed: rollup,
2177
+ excludePaths,
2178
+ limit,
2179
+ families: symbolFamilies,
2180
+ },
2181
+ render: (resp) => {
2182
+ var _a;
2183
+ return (_a = resp.dependents) !== null && _a !== void 0 ? _a : [];
2184
+ },
2185
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphDependents)(deps.vectorDb, {
2186
+ symbols,
2187
+ queryRoot: root,
2188
+ detailed: rollup,
2189
+ excludePaths,
2190
+ limit,
2191
+ excludePrefixes: scope.excludePrefixes,
2192
+ families: (0, graph_handler_1.decodeSymbolFamilies)(symbolFamilies),
2193
+ })),
2194
+ }),
2195
+ findTestsForSymbols(root, symbols, depth, symbolFamilies),
2059
2196
  ]);
2060
2197
  if (rollup) {
2061
2198
  const detailedDependents = dependents;
@@ -2097,7 +2234,7 @@ exports.mcp = new commander_1.Command("mcp")
2097
2234
  return ok(sections.join("\n"));
2098
2235
  }
2099
2236
  catch (e) {
2100
- return err(`Impact analysis failed: ${e instanceof Error ? e.message : String(e)}`);
2237
+ return toolError("Impact analysis failed", e);
2101
2238
  }
2102
2239
  });
2103
2240
  }
@@ -2114,67 +2251,60 @@ exports.mcp = new commander_1.Command("mcp")
2114
2251
  if (!proj)
2115
2252
  return err("Project not added to gmax yet.");
2116
2253
  (0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
2117
- const db = getVectorDb();
2118
- const table = yield db.ensureTable();
2119
2254
  const isFile = target.includes("/") ||
2120
2255
  (target.includes(".") && !target.includes(" "));
2121
- let sourceRows;
2122
- if (isFile) {
2123
- const absPath = (0, path_containment_1.resolveContainedPath)(root, target);
2124
- sourceRows = yield table
2125
- .query()
2126
- .select(["vector", "path", "start_line"])
2127
- .where(`path = '${(0, filter_builder_1.escapeSqlString)(absPath)}'`)
2128
- .limit(1)
2129
- .toArray();
2130
- }
2131
- else {
2132
- sourceRows = yield table
2133
- .query()
2134
- .select(["vector", "path", "start_line"])
2135
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(target)}') AND ${(0, filter_builder_1.pathStartsWith)(`${root}/`)}`)
2136
- .limit(1)
2137
- .toArray();
2138
- }
2139
- if (sourceRows.length === 0)
2256
+ // Resolved here: containment is this process's filesystem question.
2257
+ const absPath = isFile ? (0, path_containment_1.resolveContainedPath)(root, target) : undefined;
2258
+ const scope = projectScope(root);
2259
+ // The source chunk's vector never leaves the store: the daemon looks it
2260
+ // up, runs the vector search, and returns the ranked chunks.
2261
+ const result = yield readStore("similar", {
2262
+ daemon: () => (0, daemon_client_1.sendDaemonCommand)({
2263
+ cmd: "vector.similar",
2264
+ projectRoot: root,
2265
+ absPath,
2266
+ symbol: isFile ? undefined : target,
2267
+ scope: (0, rows_handler_1.scopeToWire)(scope),
2268
+ limit,
2269
+ threshold,
2270
+ }, { timeoutMs: 60000 }),
2271
+ render: (resp) => resp,
2272
+ inProcess: () => localStore((deps) => (0, vector_handler_1.runSimilar)(deps, {
2273
+ projectRoot: root,
2274
+ absPath,
2275
+ symbol: isFile ? undefined : target,
2276
+ scope,
2277
+ limit,
2278
+ threshold,
2279
+ })),
2280
+ });
2281
+ if (result.status === "not-found") {
2140
2282
  return ok(isFile
2141
2283
  ? `File not found: ${target}`
2142
2284
  : `Symbol not found: ${target}`);
2143
- const source = sourceRows[0];
2144
- if (!source.vector || source.vector.length === 0)
2285
+ }
2286
+ if (result.status === "no-vector") {
2145
2287
  return ok("Source chunk has no embedding.");
2146
- const results = yield (0, ann_config_1.configureAnnVectorQuery)(table.vectorSearch(source.vector))
2147
- .select([
2148
- "path",
2149
- "start_line",
2150
- "defined_symbols",
2151
- "role",
2152
- "_distance",
2153
- ])
2154
- .where((0, filter_builder_1.pathStartsWith)(`${root}/`))
2155
- .limit(limit + 5)
2156
- .toArray();
2157
- let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
2158
- if (threshold > 0)
2159
- filtered = filtered.filter((r) => { var _a; return 1 / (1 + ((_a = r._distance) !== null && _a !== void 0 ? _a : 0)) >= threshold; });
2160
- if (filtered.length === 0)
2288
+ }
2289
+ if (result.results.length === 0) {
2161
2290
  return ok(`No similar code found for ${target}.`);
2291
+ }
2162
2292
  const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
2163
- const lines = filtered.slice(0, limit).map((r) => {
2164
- var _a, _b, _c, _d;
2165
- const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2166
- return `${rel(r.path)}:${Number((_c = r.start_line) !== null && _c !== void 0 ? _c : 0) + 1} ${sym} [${r.role || "IMPL"}] d=${((_d = r._distance) !== null && _d !== void 0 ? _d : 0).toFixed(3)}`;
2293
+ const lines = result.results.slice(0, limit).map((r) => {
2294
+ var _a;
2295
+ const sym = (_a = r.defined_symbols[0]) !== null && _a !== void 0 ? _a : "";
2296
+ return `${rel(r.path)}:${r.start_line + 1} ${sym} [${r.role || "IMPL"}] d=${r._distance.toFixed(3)}`;
2167
2297
  });
2168
2298
  return ok(`Similar to ${target}:\n${lines.join("\n")}`);
2169
2299
  }
2170
2300
  catch (e) {
2171
- return err(`Similar search failed: ${e instanceof Error ? e.message : String(e)}`);
2301
+ return toolError("Similar search failed", e);
2172
2302
  }
2173
2303
  });
2174
2304
  }
2175
2305
  function handleBuildContext(args) {
2176
2306
  return __awaiter(this, void 0, void 0, function* () {
2177
- var _a, _b, _c;
2307
+ var _a, _b;
2178
2308
  ensureWatcher();
2179
2309
  const topic = String(args.topic || "");
2180
2310
  if (!topic)
@@ -2187,7 +2317,7 @@ exports.mcp = new commander_1.Command("mcp")
2187
2317
  return err("Project not added to gmax yet.");
2188
2318
  (0, embedding_status_1.assertEmbeddingSearchCompatible)([proj], (0, index_config_1.readGlobalConfig)());
2189
2319
  const searchOptions = { rerank: process.env.GMAX_RERANK === "1" };
2190
- const response = (_a = (yield daemonSearch(Object.assign({ projectRoot: root, query: topic, limit, filters: {}, pathPrefix: root }, searchOptions)))) !== null && _a !== void 0 ? _a : (yield getSearcher().search(topic, limit, searchOptions, {}, root));
2320
+ const response = yield searchStore(Object.assign({ projectRoot: root, query: topic, limit, filters: {}, pathPrefix: root }, searchOptions));
2191
2321
  if (response.data.length === 0)
2192
2322
  return ok(`No results found for "${topic}".`);
2193
2323
  const rel = (p) => p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p;
@@ -2209,7 +2339,7 @@ exports.mcp = new commander_1.Command("mcp")
2209
2339
  const absP = searchResultPath(r);
2210
2340
  const startLine = searchResultStartLine(r);
2211
2341
  const endLine = searchResultEndLine(r, startLine);
2212
- const sym = (_c = (_b = toStringArray(r.defined_symbols)) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : "";
2342
+ const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2213
2343
  try {
2214
2344
  const sourcePath = (0, path_containment_1.resolveContainedFile)(root, absP);
2215
2345
  const content = fs.readFileSync(sourcePath, "utf-8");
@@ -2223,7 +2353,7 @@ exports.mcp = new commander_1.Command("mcp")
2223
2353
  sections.push(blob);
2224
2354
  tokensUsed += estTokens(blob);
2225
2355
  }
2226
- catch (_d) {
2356
+ catch (_c) {
2227
2357
  /* skip unreadable */
2228
2358
  }
2229
2359
  }
@@ -2231,7 +2361,7 @@ exports.mcp = new commander_1.Command("mcp")
2231
2361
  return ok(sections.join("\n"));
2232
2362
  }
2233
2363
  catch (e) {
2234
- return err(`Context generation failed: ${e instanceof Error ? e.message : String(e)}`);
2364
+ return toolError("Context generation failed", e);
2235
2365
  }
2236
2366
  });
2237
2367
  }
@@ -2301,20 +2431,51 @@ exports.mcp = new commander_1.Command("mcp")
2301
2431
  ensureWatcher();
2302
2432
  const commitRef = String(args.commit || "HEAD");
2303
2433
  try {
2304
- const db = getVectorDb();
2305
- const builder = new graph_builder_1.GraphBuilder(db, projectRoot);
2306
- const { gatherRiskInputs, computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
2307
- const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
2308
- vectorDb: db,
2309
- graphBuilder: builder,
2434
+ // `gatherRiskInputs` mixes git and the graph. The git half stays here —
2435
+ // the diff and the churn count both need a working tree — and only the
2436
+ // graph half (callers, definition, tests) goes to the daemon, the same
2437
+ // split `extract` makes when it reads a body locally after the daemon
2438
+ // returns the location.
2439
+ const { extractDiff, extractSymbols, fileChurn } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/diff")));
2440
+ const { computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
2441
+ const diff = extractDiff(commitRef, projectRoot);
2442
+ const symbols = diff ? extractSymbols(diff) : [];
2443
+ if (symbols.length === 0) {
2444
+ return ok("(no changed symbols in this diff)");
2445
+ }
2446
+ const scope = projectScope(projectRoot);
2447
+ const facts = yield graphVerb("graph.risk", {
2448
+ projectRoot,
2449
+ scope,
2450
+ payload: { symbols },
2451
+ render: (resp) => { var _a; return (_a = resp.facts) !== null && _a !== void 0 ? _a : []; },
2452
+ inProcess: () => localStore((deps) => (0, graph_handler_1.runGraphRisk)(deps.vectorDb, {
2453
+ symbols,
2454
+ queryRoot: projectRoot,
2455
+ scope,
2456
+ })),
2310
2457
  });
2311
- const rows = computeRiskTable(inputs);
2458
+ const prefix = projectRoot.endsWith("/")
2459
+ ? projectRoot
2460
+ : `${projectRoot}/`;
2461
+ const rows = computeRiskTable(facts.map((fact) => ({
2462
+ symbol: fact.symbol,
2463
+ file: fact.file
2464
+ ? fact.file.startsWith(prefix)
2465
+ ? fact.file.slice(prefix.length)
2466
+ : fact.file
2467
+ : "(unindexed)",
2468
+ line: fact.line,
2469
+ callerCount: fact.callerCount,
2470
+ hasTests: fact.hasTests,
2471
+ churn: fileChurn(fact.file, projectRoot),
2472
+ })));
2312
2473
  return rows.length === 0
2313
2474
  ? ok("(no changed symbols in this diff)")
2314
2475
  : ok(formatRiskTable(rows, { agent: true }));
2315
2476
  }
2316
2477
  catch (e) {
2317
- return err(`Risk ranking failed: ${e instanceof Error ? e.message : String(e)}`);
2478
+ return toolError("Risk ranking failed", e);
2318
2479
  }
2319
2480
  });
2320
2481
  }
@@ -2817,4 +2978,7 @@ exports.mcp = new commander_1.Command("mcp")
2817
2978
  // Kick off index readiness check and watcher in background
2818
2979
  ensureIndexReady().catch((e) => console.error("[MCP] Index readiness check failed:", e));
2819
2980
  ensureWatcher();
2981
+ // Keep this session's project leased while the session lives. The daemon
2982
+ // only watches projects some session holds.
2983
+ setInterval(() => void ensureWatcher(), MCP_WATCH_LEASE_RENEW_MS).unref();
2820
2984
  }));