grepmax 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +16 -1
- package/dist/commands/audit.js +111 -5
- package/dist/commands/impact.js +33 -1
- package/dist/commands/mcp.js +249 -39
- package/dist/commands/surprises.js +150 -0
- package/dist/eval-surprising-connections.js +191 -0
- package/dist/index.js +2 -0
- package/dist/lib/analysis/surprising-connections.js +600 -0
- package/dist/lib/graph/impact-rollup.js +202 -0
- package/dist/lib/graph/impact.js +15 -1
- package/dist/lib/help/agent-cheatsheet.js +1 -0
- package/dist/lib/skeleton/skeletonizer.js +118 -0
- package/dist/lib/skeleton/summary-formatter.js +8 -1
- package/dist/lib/workers/pool.js +4 -1
- package/package.json +4 -2
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
package/dist/commands/mcp.js
CHANGED
|
@@ -53,11 +53,14 @@ exports.mcp = void 0;
|
|
|
53
53
|
exports.toStringArray = toStringArray;
|
|
54
54
|
exports.ok = ok;
|
|
55
55
|
exports.err = err;
|
|
56
|
+
exports.isExplicitCrossProjectSearch = isExplicitCrossProjectSearch;
|
|
56
57
|
exports.searchResultPath = searchResultPath;
|
|
57
58
|
exports.searchResultStartLine = searchResultStartLine;
|
|
58
59
|
exports.searchResultEndLine = searchResultEndLine;
|
|
59
60
|
exports.filterMcpSearchResults = filterMcpSearchResults;
|
|
60
61
|
exports.formatMcpPointerSearchResults = formatMcpPointerSearchResults;
|
|
62
|
+
exports.formatMcpSurprisingConnections = formatMcpSurprisingConnections;
|
|
63
|
+
exports.mcpLogQuery = mcpLogQuery;
|
|
61
64
|
const fs = __importStar(require("node:fs"));
|
|
62
65
|
const path = __importStar(require("node:path"));
|
|
63
66
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
@@ -65,6 +68,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
65
68
|
const commander_1 = require("commander");
|
|
66
69
|
const zod_1 = require("zod");
|
|
67
70
|
const config_1 = require("../config");
|
|
71
|
+
const surprising_connections_1 = require("../lib/analysis/surprising-connections");
|
|
68
72
|
const languages_1 = require("../lib/core/languages");
|
|
69
73
|
const graph_builder_1 = require("../lib/graph/graph-builder");
|
|
70
74
|
const index_config_1 = require("../lib/index/index-config");
|
|
@@ -109,6 +113,13 @@ function ok(text) {
|
|
|
109
113
|
function err(text) {
|
|
110
114
|
return { content: [{ type: "text", text }], isError: true };
|
|
111
115
|
}
|
|
116
|
+
function isExplicitCrossProjectSearch(args, isSearchAll = false) {
|
|
117
|
+
return (isSearchAll ||
|
|
118
|
+
args.scope === "all" ||
|
|
119
|
+
(typeof args.projects === "string" && args.projects.trim() !== "") ||
|
|
120
|
+
(typeof args.exclude_projects === "string" &&
|
|
121
|
+
args.exclude_projects.trim() !== ""));
|
|
122
|
+
}
|
|
112
123
|
function chunkAbsPath(chunk) {
|
|
113
124
|
const metadata = chunk.metadata;
|
|
114
125
|
return String(chunk.path || (metadata === null || metadata === void 0 ? void 0 : metadata.path) || "");
|
|
@@ -171,6 +182,59 @@ function formatMcpPointerSearchResults(data, displayRoot, options = {}) {
|
|
|
171
182
|
query: options.query,
|
|
172
183
|
});
|
|
173
184
|
}
|
|
185
|
+
function formatMcpSurprisingConnections(result, top = 10) {
|
|
186
|
+
const { summary, findings } = result;
|
|
187
|
+
const lines = [
|
|
188
|
+
[
|
|
189
|
+
"summary",
|
|
190
|
+
`sampled=${summary.sampledAnchors}`,
|
|
191
|
+
`code=${summary.codeRows}`,
|
|
192
|
+
`pairs=${summary.acceptedPairs}`,
|
|
193
|
+
`file_pairs=${summary.acceptedFilePairs}`,
|
|
194
|
+
`score_p90=${summary.actionabilityScore.p90}`,
|
|
195
|
+
].join("\t"),
|
|
196
|
+
];
|
|
197
|
+
for (const finding of findings.slice(0, top)) {
|
|
198
|
+
const pair = finding.representative;
|
|
199
|
+
lines.push([
|
|
200
|
+
"surprise",
|
|
201
|
+
finding.score.toFixed(3),
|
|
202
|
+
finding.maxSimilarity.toFixed(3),
|
|
203
|
+
String(finding.pairCount),
|
|
204
|
+
finding.fileA,
|
|
205
|
+
finding.fileB,
|
|
206
|
+
(0, surprising_connections_1.lineLabel)(pair.source),
|
|
207
|
+
(0, surprising_connections_1.lineLabel)(pair.target),
|
|
208
|
+
finding.reasons.join(","),
|
|
209
|
+
`buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}`,
|
|
210
|
+
`top_sims=${finding.topSimilarities.join(",")}`,
|
|
211
|
+
`penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`,
|
|
212
|
+
`next=${(0, surprising_connections_1.skeletonHint)(finding)}`,
|
|
213
|
+
].join("\t"));
|
|
214
|
+
}
|
|
215
|
+
if (findings.length === 0)
|
|
216
|
+
lines.push("none");
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
function mcpLogQuery(name, args) {
|
|
220
|
+
var _a, _b;
|
|
221
|
+
const direct = (_b = (_a = args.query) !== null && _a !== void 0 ? _a : args.symbol) !== null && _b !== void 0 ? _b : args.target;
|
|
222
|
+
if (direct !== undefined && direct !== null && String(direct) !== "") {
|
|
223
|
+
return String(direct);
|
|
224
|
+
}
|
|
225
|
+
if (name === "surprising_connections") {
|
|
226
|
+
const parts = [
|
|
227
|
+
"surprising_connections",
|
|
228
|
+
typeof args.root === "string" && args.root ? `root=${args.root}` : "",
|
|
229
|
+
typeof args.in === "string" && args.in ? `in=${args.in}` : "",
|
|
230
|
+
typeof args.exclude === "string" && args.exclude
|
|
231
|
+
? `exclude=${args.exclude}`
|
|
232
|
+
: "",
|
|
233
|
+
].filter(Boolean);
|
|
234
|
+
return parts.join(" ");
|
|
235
|
+
}
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
174
238
|
// ---------------------------------------------------------------------------
|
|
175
239
|
// Command
|
|
176
240
|
// ---------------------------------------------------------------------------
|
|
@@ -264,38 +328,56 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
264
328
|
}
|
|
265
329
|
});
|
|
266
330
|
}
|
|
331
|
+
// Resolve the registered project this server scopes to. The server pins to
|
|
332
|
+
// its launch cwd (findProjectRoot, .git-only), but a registered project may
|
|
333
|
+
// be an umbrella with no .git of its own — so a session launched in a
|
|
334
|
+
// subdirectory resolves to a path that isn't the registered root. Prefer an
|
|
335
|
+
// exact registry match; otherwise walk the registry for an ancestor that
|
|
336
|
+
// covers the cwd, and scope to it instead of silently widening to the whole
|
|
337
|
+
// index. `root` is the registered project root to scope to.
|
|
338
|
+
function resolveRegisteredProject() {
|
|
339
|
+
const exact = (0, project_registry_1.getProject)(projectRoot);
|
|
340
|
+
if (exact)
|
|
341
|
+
return { proj: exact, root: projectRoot };
|
|
342
|
+
const parent = (0, project_registry_1.getParentProject)(projectRoot);
|
|
343
|
+
if (parent)
|
|
344
|
+
return { proj: parent, root: parent.root };
|
|
345
|
+
return { proj: undefined, root: projectRoot };
|
|
346
|
+
}
|
|
267
347
|
// --- Tool handlers ---
|
|
268
348
|
function handleSemanticSearch(args_1) {
|
|
269
349
|
return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
|
|
270
350
|
const query = String(args.query || "");
|
|
271
351
|
if (!query)
|
|
272
352
|
return err("Missing required parameter: query");
|
|
273
|
-
|
|
353
|
+
const searchAll = isExplicitCrossProjectSearch(args, isSearchAll);
|
|
274
354
|
const limit = Math.min(Math.max(Number(args.limit) || 3, 1), 50);
|
|
275
355
|
ensureWatcher();
|
|
276
356
|
// Project resolution. The server is pinned to whatever cwd it launched in
|
|
277
|
-
// (resolved once at startup).
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
357
|
+
// (resolved once at startup). Resolve that cwd to its registered project —
|
|
358
|
+
// exact match, or a registered ancestor when the cwd is a subdirectory of
|
|
359
|
+
// an umbrella project. Searches scope to `resolvedRoot`.
|
|
360
|
+
const { proj, root: resolvedRoot } = resolveRegisteredProject();
|
|
361
|
+
// Cross-project search (the whole index) is opt-in: it happens only when
|
|
362
|
+
// the caller passes scope:"all" or projects:"…". Otherwise we require a
|
|
363
|
+
// resolved project and error loudly when there isn't one — never silently
|
|
364
|
+
// widen a scoped query to every indexed project.
|
|
365
|
+
if (!searchAll) {
|
|
366
|
+
if (!proj) {
|
|
367
|
+
if (typeof args.root === "string") {
|
|
368
|
+
return err("Project not added to gmax yet. Run `gmax add` to index it first.");
|
|
369
|
+
}
|
|
370
|
+
const indexed = (0, project_registry_1.listProjects)().filter((p) => { var _a; return p.status !== "pending" && ((_a = p.chunkCount) !== null && _a !== void 0 ? _a : 0) > 0; });
|
|
371
|
+
if (indexed.length === 0) {
|
|
372
|
+
return err("No indexed projects yet. cd into a repo and run `gmax add` to index it first.");
|
|
373
|
+
}
|
|
374
|
+
return err(`${path.basename(projectRoot)}/ isn't an indexed gmax project. ` +
|
|
375
|
+
`Pass scope:"all" to search all ${indexed.length} indexed project(s), ` +
|
|
376
|
+
`projects:"name" to target specific ones, or cd into an indexed project.`);
|
|
377
|
+
}
|
|
378
|
+
if (proj.status === "pending" || proj.chunkCount === 0) {
|
|
379
|
+
return err("Project not indexed yet. Run `gmax add` to index it first.");
|
|
380
|
+
}
|
|
299
381
|
}
|
|
300
382
|
try {
|
|
301
383
|
const searcher = getSearcher();
|
|
@@ -305,7 +387,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
305
387
|
if (!searchAll) {
|
|
306
388
|
const searchRoot = typeof args.root === "string"
|
|
307
389
|
? path.resolve(args.root)
|
|
308
|
-
: path.resolve(
|
|
390
|
+
: path.resolve(resolvedRoot);
|
|
309
391
|
if (typeof args.root === "string" && !fs.existsSync(searchRoot)) {
|
|
310
392
|
return err(`Directory not found: ${args.root}`);
|
|
311
393
|
}
|
|
@@ -372,11 +454,10 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
372
454
|
? { files: seedFiles, symbols: seedSymbols }
|
|
373
455
|
: undefined;
|
|
374
456
|
const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1", seeds }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
|
|
375
|
-
// Prepend
|
|
376
|
-
// whatever body we return, so the agent learns it searched all projects.
|
|
457
|
+
// Prepend any searcher warnings to whatever body we return.
|
|
377
458
|
const prefixNotes = (body) => {
|
|
378
459
|
var _a;
|
|
379
|
-
const notes =
|
|
460
|
+
const notes = ((_a = result.warnings) !== null && _a !== void 0 ? _a : []).filter(Boolean);
|
|
380
461
|
return notes.length ? `${notes.join("\n")}\n\n${body}` : body;
|
|
381
462
|
};
|
|
382
463
|
if (!result.data || result.data.length === 0) {
|
|
@@ -676,7 +757,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
676
757
|
const symbol = String(args.symbol || "");
|
|
677
758
|
if (!symbol)
|
|
678
759
|
return err("Missing required parameter: symbol");
|
|
679
|
-
const proj = (
|
|
760
|
+
const { proj } = resolveRegisteredProject();
|
|
680
761
|
if (!proj) {
|
|
681
762
|
return err("Project not added to gmax yet. Run `gmax add` to index it first.");
|
|
682
763
|
}
|
|
@@ -1042,6 +1123,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1042
1123
|
"start_line",
|
|
1043
1124
|
"defined_symbols",
|
|
1044
1125
|
"referenced_symbols",
|
|
1126
|
+
"type_referenced_symbols",
|
|
1045
1127
|
"is_exported",
|
|
1046
1128
|
])
|
|
1047
1129
|
.where((0, filter_builder_1.pathStartsWith)(prefix))
|
|
@@ -1055,7 +1137,12 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1055
1137
|
start_line: Number(r.start_line || 0),
|
|
1056
1138
|
is_exported: Boolean(r.is_exported),
|
|
1057
1139
|
defined_symbols: toStringArray(r.defined_symbols),
|
|
1058
|
-
referenced_symbols:
|
|
1140
|
+
referenced_symbols: [
|
|
1141
|
+
...new Set([
|
|
1142
|
+
...toStringArray(r.referenced_symbols),
|
|
1143
|
+
...toStringArray(r.type_referenced_symbols),
|
|
1144
|
+
]),
|
|
1145
|
+
],
|
|
1059
1146
|
})), prefix, top);
|
|
1060
1147
|
const lines = [];
|
|
1061
1148
|
lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
|
|
@@ -1074,6 +1161,13 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1074
1161
|
if (audit.hubFiles.length === 0)
|
|
1075
1162
|
lines.push(" none");
|
|
1076
1163
|
lines.push("");
|
|
1164
|
+
lines.push("File dependency cycles (symbol-derived):");
|
|
1165
|
+
for (const c of audit.fileCycles) {
|
|
1166
|
+
lines.push(` ${c.files.join(", ")} - ${c.files.length} files, ${c.edgeCount} internal edges`);
|
|
1167
|
+
}
|
|
1168
|
+
if (audit.fileCycles.length === 0)
|
|
1169
|
+
lines.push(" none");
|
|
1170
|
+
lines.push("");
|
|
1077
1171
|
lines.push(`Dead-code candidates (${audit.deadTotal} non-exported symbols with zero inbound refs):`);
|
|
1078
1172
|
for (const d of audit.deadCandidates) {
|
|
1079
1173
|
lines.push(` ${d.symbol} (${d.file}:${d.line + 1})`);
|
|
@@ -1093,6 +1187,43 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1093
1187
|
}
|
|
1094
1188
|
});
|
|
1095
1189
|
}
|
|
1190
|
+
function handleSurprisingConnections(args) {
|
|
1191
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1192
|
+
ensureWatcher();
|
|
1193
|
+
if (args.experimental !== true) {
|
|
1194
|
+
return err("surprising_connections is experimental; pass experimental:true.");
|
|
1195
|
+
}
|
|
1196
|
+
const root = typeof args.root === "string" && args.root
|
|
1197
|
+
? path.resolve(args.root)
|
|
1198
|
+
: projectRoot;
|
|
1199
|
+
const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
|
|
1200
|
+
const sample = Math.min(Math.max(Number(args.sample) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample, 1), 10000);
|
|
1201
|
+
const neighbors = Math.min(Math.max(Number(args.neighbors) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 1), 200);
|
|
1202
|
+
const dirDepth = Math.min(Math.max(Number(args.dir_depth) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 1), 20);
|
|
1203
|
+
const minSimilarity = Math.max(Number(args.min_similarity) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity, 0);
|
|
1204
|
+
const maxRows = Math.min(Math.max(Number(args.max_rows) || surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, 1), surprising_connections_1.MAX_SURPRISE_ROWS);
|
|
1205
|
+
try {
|
|
1206
|
+
const db = getVectorDb();
|
|
1207
|
+
const table = yield db.ensureTable();
|
|
1208
|
+
const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, root, {
|
|
1209
|
+
sample,
|
|
1210
|
+
neighbors,
|
|
1211
|
+
dirDepth,
|
|
1212
|
+
minSimilarity,
|
|
1213
|
+
maxRows,
|
|
1214
|
+
includeTests: Boolean(args.include_tests),
|
|
1215
|
+
includeEval: Boolean(args.include_eval),
|
|
1216
|
+
in: typeof args.in === "string" ? args.in : undefined,
|
|
1217
|
+
exclude: typeof args.exclude === "string" ? args.exclude : undefined,
|
|
1218
|
+
});
|
|
1219
|
+
return ok(formatMcpSurprisingConnections(result, top));
|
|
1220
|
+
}
|
|
1221
|
+
catch (e) {
|
|
1222
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1223
|
+
return err(`Surprising connections failed: ${msg}`);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1096
1227
|
function handleGetNeighbors(args) {
|
|
1097
1228
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1098
1229
|
ensureWatcher();
|
|
@@ -1205,7 +1336,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1205
1336
|
const pattern = typeof args.pattern === "string" ? args.pattern : undefined;
|
|
1206
1337
|
const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100);
|
|
1207
1338
|
const pathPrefix = typeof args.path === "string" ? args.path : undefined;
|
|
1208
|
-
const proj = (
|
|
1339
|
+
const { proj } = resolveRegisteredProject();
|
|
1209
1340
|
if (!proj) {
|
|
1210
1341
|
return err("Project not added to gmax yet. Run `gmax add` to index it first.");
|
|
1211
1342
|
}
|
|
@@ -1746,8 +1877,11 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1746
1877
|
if (!target)
|
|
1747
1878
|
return err("Missing required parameter: target");
|
|
1748
1879
|
const depth = Math.min(Math.max(Number(args.depth) || 1, 1), 3);
|
|
1880
|
+
const rollup = args.rollup === true;
|
|
1881
|
+
const top = Math.min(Math.max(Number(args.top) || 10, 1), 100);
|
|
1749
1882
|
try {
|
|
1750
|
-
const { resolveTargetSymbols, findTests, findDependents, isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
|
|
1883
|
+
const { resolveTargetSymbols, findTests, findDependents, findDependentsDetailed, isTestPath, } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
|
|
1884
|
+
const { buildImpactRollup, formatImpactRollupAgent } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact-rollup")));
|
|
1751
1885
|
const db = getVectorDb();
|
|
1752
1886
|
const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
|
|
1753
1887
|
if (symbols.length === 0)
|
|
@@ -1756,10 +1890,28 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
1756
1890
|
? path.resolve(projectRoot, target)
|
|
1757
1891
|
: undefined;
|
|
1758
1892
|
const excludePaths = targetPath ? new Set([targetPath]) : undefined;
|
|
1893
|
+
const rollupLimit = Math.min(Math.max(top * 10, 100), 500);
|
|
1759
1894
|
const [dependents, tests] = yield Promise.all([
|
|
1760
|
-
|
|
1895
|
+
rollup
|
|
1896
|
+
? findDependentsDetailed(symbols, db, projectRoot, excludePaths, rollupLimit, undefined, symbolFamilies)
|
|
1897
|
+
: findDependents(symbols, db, projectRoot, excludePaths, undefined, undefined, symbolFamilies),
|
|
1761
1898
|
findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies),
|
|
1762
1899
|
]);
|
|
1900
|
+
if (rollup) {
|
|
1901
|
+
const detailedDependents = dependents;
|
|
1902
|
+
const impactRollup = buildImpactRollup({
|
|
1903
|
+
targetSymbols: symbols,
|
|
1904
|
+
dependents: detailedDependents,
|
|
1905
|
+
tests,
|
|
1906
|
+
projectRoot,
|
|
1907
|
+
top,
|
|
1908
|
+
});
|
|
1909
|
+
return ok(formatImpactRollupAgent(impactRollup, {
|
|
1910
|
+
target,
|
|
1911
|
+
projectRoot,
|
|
1912
|
+
includeTests: true,
|
|
1913
|
+
}));
|
|
1914
|
+
}
|
|
1763
1915
|
const nonTestDeps = dependents.filter((d) => !isTestPath(d.file));
|
|
1764
1916
|
const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
|
|
1765
1917
|
const sections = [`Impact analysis for ${target}:\n`];
|
|
@@ -2032,15 +2184,16 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2032
2184
|
"If you are NOT inside a specific repo, or want a different one: call " +
|
|
2033
2185
|
'`list_projects` to see what\'s indexed, then pass `projects:"name"` (or ' +
|
|
2034
2186
|
'`scope:"all"`) to `semantic_search`. When the working directory isn\'t an ' +
|
|
2035
|
-
"indexed project, `semantic_search`
|
|
2036
|
-
"
|
|
2187
|
+
"indexed project, `semantic_search` errors instead of searching everything " +
|
|
2188
|
+
"implicitly. Prefer `semantic_search` (detail:'pointer') for discovery, then " +
|
|
2037
2189
|
"`extract_symbol`/`peek_symbol`/`code_skeleton` to read, and " +
|
|
2038
|
-
"`trace_calls`/`impact_analysis`/`find_tests` for call-graph questions."
|
|
2190
|
+
"`trace_calls`/`impact_analysis`/`find_tests` for call-graph questions. " +
|
|
2191
|
+
"Use `audit` and experimental `surprising_connections` for project orientation.",
|
|
2039
2192
|
});
|
|
2040
2193
|
// Best-effort query logging, applied uniformly to every tool exactly as the
|
|
2041
2194
|
// old single CallToolRequestSchema dispatch did before each return.
|
|
2042
2195
|
const logToolCall = (name, toolArgs, startMs, result) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2043
|
-
var _a, _b, _c
|
|
2196
|
+
var _a, _b, _c;
|
|
2044
2197
|
try {
|
|
2045
2198
|
const { logQuery } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/query-log")));
|
|
2046
2199
|
const text = (_c = (_b = (_a = result.content) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : "";
|
|
@@ -2049,14 +2202,14 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2049
2202
|
ts: new Date().toISOString(),
|
|
2050
2203
|
source: "mcp",
|
|
2051
2204
|
tool: name,
|
|
2052
|
-
query:
|
|
2205
|
+
query: mcpLogQuery(name, toolArgs),
|
|
2053
2206
|
project: projectRoot,
|
|
2054
2207
|
results: resultLines,
|
|
2055
2208
|
ms: Date.now() - startMs,
|
|
2056
2209
|
error: result.isError ? text.slice(0, 200) : undefined,
|
|
2057
2210
|
});
|
|
2058
2211
|
}
|
|
2059
|
-
catch (
|
|
2212
|
+
catch (_d) { }
|
|
2060
2213
|
});
|
|
2061
2214
|
// Register a tool, wrapping its handler with timing + query logging. Zod raw
|
|
2062
2215
|
// shapes give us free input validation (the SDK rejects calls that violate
|
|
@@ -2204,7 +2357,7 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2204
2357
|
},
|
|
2205
2358
|
}, handleDead);
|
|
2206
2359
|
tool("audit", {
|
|
2207
|
-
description: "Graph-summary of the indexed project in one call: god nodes (most depended-upon symbols), hub files (most depended-upon files), and dead-code candidates (non-exported symbols with zero inbound references). Built from the static
|
|
2360
|
+
description: "Graph-summary of the indexed project in one call: god nodes (most depended-upon symbols), hub files (most depended-upon files), symbol-derived file dependency cycles, and dead-code candidates (non-exported symbols with zero inbound references). Built from the static reference graph; dynamic dispatch, reflection, and eval are invisible, so dead candidates are hypotheses; verify with the `dead` tool before acting.",
|
|
2208
2361
|
inputSchema: {
|
|
2209
2362
|
root: zod_1.z
|
|
2210
2363
|
.string()
|
|
@@ -2216,6 +2369,55 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2216
2369
|
.optional(),
|
|
2217
2370
|
},
|
|
2218
2371
|
}, handleAudit);
|
|
2372
|
+
tool("surprising_connections", {
|
|
2373
|
+
description: "Experimental orientation signal: embedding-similar cross-directory file pairs that are not directly connected by the static symbol graph. Useful for spotting duplicate or parallel logic. Requires experimental:true.",
|
|
2374
|
+
inputSchema: {
|
|
2375
|
+
experimental: zod_1.z
|
|
2376
|
+
.boolean()
|
|
2377
|
+
.describe("Must be true to acknowledge experimental signal quality"),
|
|
2378
|
+
root: zod_1.z
|
|
2379
|
+
.string()
|
|
2380
|
+
.describe("Project root (default: current)")
|
|
2381
|
+
.optional(),
|
|
2382
|
+
sample: zod_1.z
|
|
2383
|
+
.number()
|
|
2384
|
+
.describe("Indexed code chunks to sample (default 160, max 10000)")
|
|
2385
|
+
.optional(),
|
|
2386
|
+
neighbors: zod_1.z
|
|
2387
|
+
.number()
|
|
2388
|
+
.describe("Nearest neighbors per sampled chunk (default 20, max 200)")
|
|
2389
|
+
.optional(),
|
|
2390
|
+
top: zod_1.z
|
|
2391
|
+
.number()
|
|
2392
|
+
.describe("Grouped findings to return (default 10, max 100)")
|
|
2393
|
+
.optional(),
|
|
2394
|
+
dir_depth: zod_1.z
|
|
2395
|
+
.number()
|
|
2396
|
+
.describe("Directory bucket depth considered unsurprising")
|
|
2397
|
+
.optional(),
|
|
2398
|
+
min_similarity: zod_1.z
|
|
2399
|
+
.number()
|
|
2400
|
+
.describe("Minimum similarity 0-1 (default 0)")
|
|
2401
|
+
.optional(),
|
|
2402
|
+
max_rows: zod_1.z
|
|
2403
|
+
.number()
|
|
2404
|
+
.describe(`Maximum indexed rows to scan (default 50000, max ${surprising_connections_1.MAX_SURPRISE_ROWS})`)
|
|
2405
|
+
.optional(),
|
|
2406
|
+
in: zod_1.z
|
|
2407
|
+
.string()
|
|
2408
|
+
.describe("Restrict to a sub-path of the project")
|
|
2409
|
+
.optional(),
|
|
2410
|
+
exclude: zod_1.z
|
|
2411
|
+
.string()
|
|
2412
|
+
.describe("Exclude a sub-path of the project")
|
|
2413
|
+
.optional(),
|
|
2414
|
+
include_tests: zod_1.z.boolean().describe("Include test files").optional(),
|
|
2415
|
+
include_eval: zod_1.z
|
|
2416
|
+
.boolean()
|
|
2417
|
+
.describe("Include eval/experiment/script files")
|
|
2418
|
+
.optional(),
|
|
2419
|
+
},
|
|
2420
|
+
}, handleSurprisingConnections);
|
|
2219
2421
|
tool("get_neighbors", {
|
|
2220
2422
|
description: "Graph primitive: symbols reachable from a node along call edges within N hops. direction 'callees' = what it calls (outbound), 'callers' = what calls it (inbound). Each result carries its hop distance and definition location. Static call graph — same caveats as `dead`/`audit`.",
|
|
2221
2423
|
inputSchema: {
|
|
@@ -2358,6 +2560,14 @@ exports.mcp = new commander_1.Command("mcp")
|
|
|
2358
2560
|
.number()
|
|
2359
2561
|
.describe("Caller traversal depth 1-3 (default 1)")
|
|
2360
2562
|
.optional(),
|
|
2563
|
+
rollup: zod_1.z
|
|
2564
|
+
.boolean()
|
|
2565
|
+
.describe("Return export/package rollup TSV rows")
|
|
2566
|
+
.optional(),
|
|
2567
|
+
top: zod_1.z
|
|
2568
|
+
.number()
|
|
2569
|
+
.describe("Max rows per rollup section (default 10)")
|
|
2570
|
+
.optional(),
|
|
2361
2571
|
},
|
|
2362
2572
|
}, handleImpactAnalysis);
|
|
2363
2573
|
tool("find_similar", {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.surprises = void 0;
|
|
13
|
+
const commander_1 = require("commander");
|
|
14
|
+
const surprising_connections_1 = require("../lib/analysis/surprising-connections");
|
|
15
|
+
const vector_db_1 = require("../lib/store/vector-db");
|
|
16
|
+
const exit_1 = require("../lib/utils/exit");
|
|
17
|
+
const project_registry_1 = require("../lib/utils/project-registry");
|
|
18
|
+
const project_root_1 = require("../lib/utils/project-root");
|
|
19
|
+
const stale_hint_1 = require("../lib/utils/stale-hint");
|
|
20
|
+
const useColors = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
21
|
+
const style = {
|
|
22
|
+
bold: (s) => (useColors ? `\x1b[1m${s}\x1b[22m` : s),
|
|
23
|
+
dim: (s) => (useColors ? `\x1b[2m${s}\x1b[39m` : s),
|
|
24
|
+
cyan: (s) => (useColors ? `\x1b[36m${s}\x1b[39m` : s),
|
|
25
|
+
};
|
|
26
|
+
function parseIntOption(value, fallback, max = 10000) {
|
|
27
|
+
const parsed = Number.parseInt(String(value !== null && value !== void 0 ? value : ""), 10);
|
|
28
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
29
|
+
return fallback;
|
|
30
|
+
return Math.min(parsed, max);
|
|
31
|
+
}
|
|
32
|
+
function parseFloatOption(value, fallback) {
|
|
33
|
+
const parsed = Number.parseFloat(String(value !== null && value !== void 0 ? value : ""));
|
|
34
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
35
|
+
}
|
|
36
|
+
function formatAgent(result, top) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
const { summary, findings } = result;
|
|
39
|
+
lines.push(`summary\t${summary.sampledAnchors}\t${summary.codeRows}\t${summary.acceptedPairs}\t${summary.acceptedFilePairs}`);
|
|
40
|
+
for (const finding of findings.slice(0, top)) {
|
|
41
|
+
const pair = finding.representative;
|
|
42
|
+
lines.push([
|
|
43
|
+
"surprise",
|
|
44
|
+
finding.score.toFixed(3),
|
|
45
|
+
finding.maxSimilarity.toFixed(3),
|
|
46
|
+
String(finding.pairCount),
|
|
47
|
+
finding.fileA,
|
|
48
|
+
finding.fileB,
|
|
49
|
+
(0, surprising_connections_1.lineLabel)(pair.source),
|
|
50
|
+
(0, surprising_connections_1.lineLabel)(pair.target),
|
|
51
|
+
finding.reasons.join(","),
|
|
52
|
+
`buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}`,
|
|
53
|
+
`top_sims=${finding.topSimilarities.join(",")}`,
|
|
54
|
+
`penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`,
|
|
55
|
+
`next=${(0, surprising_connections_1.skeletonHint)(finding)}`,
|
|
56
|
+
].join("\t"));
|
|
57
|
+
}
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
function formatHuman(result, top) {
|
|
61
|
+
const { summary, findings } = result;
|
|
62
|
+
const out = [];
|
|
63
|
+
out.push(`${style.bold("Surprising connections")} ${style.dim("(experimental, embedding-similar but graph-disconnected file pairs)")}`);
|
|
64
|
+
out.push(style.dim(` sampled ${summary.sampledAnchors}/${summary.codeRows} code chunks; accepted ${summary.acceptedPairs} chunk pairs across ${summary.acceptedFilePairs} file pairs`));
|
|
65
|
+
out.push(style.dim(` score p50/p90/max ${summary.actionabilityScore.p50}/${summary.actionabilityScore.p90}/${summary.actionabilityScore.max}; graph file edges filtered ${summary.filters.graphEdge}`));
|
|
66
|
+
out.push("");
|
|
67
|
+
if (findings.length === 0) {
|
|
68
|
+
out.push(style.dim(" none"));
|
|
69
|
+
return out.join("\n");
|
|
70
|
+
}
|
|
71
|
+
for (const finding of findings.slice(0, top)) {
|
|
72
|
+
const pair = finding.representative;
|
|
73
|
+
out.push(` ${style.cyan(`score=${finding.score.toFixed(3)}`)} sim=${finding.maxSimilarity.toFixed(3)} pairs=${finding.pairCount} ${finding.fileA}`);
|
|
74
|
+
out.push(` <-> ${finding.fileB}`);
|
|
75
|
+
out.push(` best: ${(0, surprising_connections_1.lineLabel)(pair.source)} <-> ${(0, surprising_connections_1.lineLabel)(pair.target)}`);
|
|
76
|
+
out.push(` detail: no static file edge; buckets=${(0, surprising_connections_1.findingBucketLabel)(finding, summary.options.dirDepth)}; top_sims=${finding.topSimilarities.join(",")}`);
|
|
77
|
+
out.push(` reasons: ${finding.reasons.join(", ") || "none"}; penalties=${(0, surprising_connections_1.formatPenaltySummary)(pair.scoreParts)}`);
|
|
78
|
+
const examples = (0, surprising_connections_1.findingExamples)(finding, 2);
|
|
79
|
+
if (examples.length > 1) {
|
|
80
|
+
out.push(` examples: ${examples
|
|
81
|
+
.map((example) => `${(0, surprising_connections_1.lineLabel)(example.source)} <-> ${(0, surprising_connections_1.lineLabel)(example.target)}`)
|
|
82
|
+
.join("; ")}`);
|
|
83
|
+
}
|
|
84
|
+
out.push(` next: ${(0, surprising_connections_1.skeletonHint)(finding)}`);
|
|
85
|
+
}
|
|
86
|
+
return out.join("\n");
|
|
87
|
+
}
|
|
88
|
+
exports.surprises = new commander_1.Command("surprises")
|
|
89
|
+
.description("Experimental: find embedding-similar cross-directory file pairs not already connected by the static graph")
|
|
90
|
+
.option("--experimental", "Required acknowledgement for this experimental signal", false)
|
|
91
|
+
.option("--root <dir>", "Project root directory")
|
|
92
|
+
.option("--sample <n>", "How many indexed code chunks to sample", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample))
|
|
93
|
+
.option("--neighbors <n>", "Nearest neighbors to inspect per sampled chunk", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors))
|
|
94
|
+
.option("--top <n>", "How many grouped findings to show", "20")
|
|
95
|
+
.option("--dir-depth <n>", "Directory bucket depth considered unsurprising", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth))
|
|
96
|
+
.option("--min-sim <n>", "Minimum similarity 0-1", String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity))
|
|
97
|
+
.option("--max-rows <n>", `Maximum indexed rows to scan (capped at ${surprising_connections_1.MAX_SURPRISE_ROWS})`, String(surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows))
|
|
98
|
+
.option("--include-tests", "Include test files", false)
|
|
99
|
+
.option("--include-eval", "Include eval/experiment/script files", false)
|
|
100
|
+
.option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
|
|
101
|
+
.option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
|
|
102
|
+
.option("--agent", "Compact TSV output for AI agents", false)
|
|
103
|
+
.action((opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
var _a;
|
|
105
|
+
if (!opts.experimental) {
|
|
106
|
+
const msg = "`gmax surprises` is experimental; rerun with --experimental.";
|
|
107
|
+
console.error(opts.agent ? `error\texperimental_required\t${msg}` : msg);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let vectorDb = null;
|
|
112
|
+
try {
|
|
113
|
+
const root = (0, project_registry_1.resolveRootOrExit)(opts.root);
|
|
114
|
+
if (root === null)
|
|
115
|
+
return;
|
|
116
|
+
const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
|
|
117
|
+
(0, stale_hint_1.maybeWarnStaleChunker)(projectRoot, { agent: opts.agent });
|
|
118
|
+
(0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
|
|
119
|
+
const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
|
|
120
|
+
vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
121
|
+
const table = yield vectorDb.ensureTable();
|
|
122
|
+
const top = parseIntOption(opts.top, 20, 100);
|
|
123
|
+
const result = yield (0, surprising_connections_1.analyzeSurprisingConnections)(table, projectRoot, {
|
|
124
|
+
sample: parseIntOption(opts.sample, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.sample),
|
|
125
|
+
neighbors: parseIntOption(opts.neighbors, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.neighbors, 200),
|
|
126
|
+
dirDepth: parseIntOption(opts.dirDepth, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.dirDepth, 20),
|
|
127
|
+
minSimilarity: parseFloatOption(opts.minSim, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.minSimilarity),
|
|
128
|
+
maxRows: parseIntOption(opts.maxRows, surprising_connections_1.DEFAULT_SURPRISE_OPTIONS.maxRows, surprising_connections_1.MAX_SURPRISE_ROWS),
|
|
129
|
+
includeTests: Boolean(opts.includeTests),
|
|
130
|
+
includeEval: Boolean(opts.includeEval),
|
|
131
|
+
in: opts.in,
|
|
132
|
+
exclude: opts.exclude,
|
|
133
|
+
});
|
|
134
|
+
console.log(opts.agent ? formatAgent(result, top) : formatHuman(result, top));
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
138
|
+
console.error("Surprises failed:", msg);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
if (vectorDb) {
|
|
143
|
+
try {
|
|
144
|
+
yield vectorDb.close();
|
|
145
|
+
}
|
|
146
|
+
catch (_b) { }
|
|
147
|
+
}
|
|
148
|
+
yield (0, exit_1.gracefulExit)();
|
|
149
|
+
}
|
|
150
|
+
}));
|