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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +40 -0
- package/dist/commands/audit.js +28 -260
- package/dist/commands/context.js +42 -26
- package/dist/commands/dead.js +37 -35
- package/dist/commands/doctor.js +280 -10
- package/dist/commands/extract.js +79 -49
- package/dist/commands/impact.js +74 -20
- package/dist/commands/log.js +25 -25
- package/dist/commands/mcp.js +639 -475
- package/dist/commands/peek.js +49 -47
- package/dist/commands/project.js +29 -107
- package/dist/commands/related.js +43 -52
- package/dist/commands/search-run.js +23 -8
- package/dist/commands/search.js +7 -2
- package/dist/commands/similar.js +50 -76
- package/dist/commands/skeleton.js +137 -83
- package/dist/commands/status.js +90 -25
- package/dist/commands/surprises.js +73 -16
- package/dist/commands/symbols.js +58 -77
- package/dist/commands/test-find.js +51 -17
- package/dist/commands/trace.js +26 -11
- package/dist/commands/watch.js +1 -1
- package/dist/config.js +8 -2
- package/dist/lib/analysis/surprising-connections.js +135 -15
- package/dist/lib/daemon/daemon.js +157 -13
- package/dist/lib/daemon/graph-handler.js +859 -0
- package/dist/lib/daemon/ipc-handler.js +36 -4
- package/dist/lib/daemon/read-verbs.js +156 -0
- package/dist/lib/daemon/rows-handler.js +665 -0
- package/dist/lib/daemon/search-handler.js +7 -4
- package/dist/lib/daemon/vector-handler.js +199 -0
- package/dist/lib/daemon/watch-leases.js +179 -0
- package/dist/lib/index/batch-processor.js +7 -1
- package/dist/lib/index/watcher.js +4 -0
- package/dist/lib/search/pagerank.js +80 -19
- package/dist/lib/store/vector-db.js +35 -1
- package/dist/lib/utils/async-semaphore.js +141 -0
- package/dist/lib/utils/daemon-client.js +11 -0
- package/dist/lib/utils/process-footprint.js +51 -0
- package/dist/lib/utils/store-access.js +381 -0
- package/dist/lib/utils/tests-footer.js +19 -2
- package/dist/lib/utils/watcher-launcher.js +46 -19
- package/dist/lib/workers/pool.js +72 -2
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
- package/plugins/grepmax/hooks/cwd-changed.js +19 -36
- package/plugins/grepmax/hooks/start.js +15 -19
- package/plugins/grepmax/hooks/stop.js +13 -4
- package/plugins/grepmax/hooks/watch-lease.js +163 -0
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
78
|
-
const
|
|
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
|
-
|
|
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
|
|
129
|
-
message
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
308
|
-
|
|
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
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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 =
|
|
542
|
-
limit, filters: effectiveFilters, pathPrefix }, searchOptions))
|
|
732
|
+
const result = yield searchStore(Object.assign({ projectRoot: resolvedRoot, query,
|
|
733
|
+
limit, filters: effectiveFilters, pathPrefix }, searchOptions));
|
|
543
734
|
const allowedRoots = searchAll
|
|
544
|
-
? ((
|
|
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 (
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
994
|
+
// Try the stored skeleton first, via the daemon's warm store.
|
|
804
995
|
try {
|
|
805
|
-
const
|
|
806
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
"
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const
|
|
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
|
-
//
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
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
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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 (
|
|
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 =
|
|
1383
|
+
const top = facts.topCallers;
|
|
1210
1384
|
const lines = [
|
|
1211
|
-
`LIVE ${defLoc} defines ${symbol} — ${
|
|
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
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
const
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
-
|
|
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
|
|
1356
|
-
const hits = yield
|
|
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
|
-
|
|
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
|
|
1393
|
-
const pathSyms = yield
|
|
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
|
-
|
|
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
|
|
1422
|
-
const sg = yield
|
|
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
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
const
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (${
|
|
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
|
-
|
|
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
|
-
|
|
1627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
.
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1719
|
-
.
|
|
1720
|
-
.
|
|
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
|
|
1728
|
-
.
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1780
|
-
const
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
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
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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
|
-
|
|
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 =
|
|
1957
|
-
limit, filters: {}, pathPrefix: root }, searchOptions))
|
|
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
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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 ?
|
|
2053
|
-
|
|
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
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
|
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
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
.
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
-
|
|
2144
|
-
if (
|
|
2285
|
+
}
|
|
2286
|
+
if (result.status === "no-vector") {
|
|
2145
2287
|
return ok("Source chunk has no embedding.");
|
|
2146
|
-
|
|
2147
|
-
|
|
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 =
|
|
2164
|
-
var _a
|
|
2165
|
-
const sym = (
|
|
2166
|
-
return `${rel(r.path)}:${
|
|
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
|
|
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
|
|
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 =
|
|
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 = (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
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
|
|
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
|
|
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
|
}));
|