grepmax 0.17.11 → 0.17.13

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.
@@ -257,6 +257,65 @@ const TOOLS = [
257
257
  },
258
258
  },
259
259
  },
260
+ {
261
+ name: "get_neighbors",
262
+ 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`.",
263
+ inputSchema: {
264
+ type: "object",
265
+ properties: {
266
+ symbol: { type: "string", description: "Starting symbol" },
267
+ direction: {
268
+ type: "string",
269
+ enum: ["callers", "callees"],
270
+ description: "Edge direction (default callees)",
271
+ },
272
+ max_hops: {
273
+ type: "number",
274
+ description: "Max hops (default 2, max 5)",
275
+ },
276
+ root: { type: "string", description: "Project root (absolute path)" },
277
+ },
278
+ required: ["symbol"],
279
+ },
280
+ },
281
+ {
282
+ name: "find_paths",
283
+ description: "Graph primitive: shortest call-graph path between two symbols, as a symbol sequence. direction 'callees' searches outward from `from` (does `from` transitively call `to`?), 'callers' searches inward. Returns 'no path' if unreachable within max_hops.",
284
+ inputSchema: {
285
+ type: "object",
286
+ properties: {
287
+ from: { type: "string", description: "Start symbol" },
288
+ to: { type: "string", description: "Target symbol" },
289
+ direction: {
290
+ type: "string",
291
+ enum: ["callers", "callees"],
292
+ description: "Search direction (default callees)",
293
+ },
294
+ max_hops: {
295
+ type: "number",
296
+ description: "Max hops (default 6, max 10)",
297
+ },
298
+ root: { type: "string", description: "Project root (absolute path)" },
299
+ },
300
+ required: ["from", "to"],
301
+ },
302
+ },
303
+ {
304
+ name: "subgraph_for_files",
305
+ description: "Graph primitive: the local dependency subgraph for a set of files — symbols they define, the call edges among those symbols, and their outbound external dependencies. Paths may be absolute or project-root-relative.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ files: {
310
+ type: "array",
311
+ items: { type: "string" },
312
+ description: "File paths (absolute or root-relative)",
313
+ },
314
+ root: { type: "string", description: "Project root (absolute path)" },
315
+ },
316
+ required: ["files"],
317
+ },
318
+ },
260
319
  {
261
320
  name: "list_symbols",
262
321
  description: "List indexed symbols with role and export status.",
@@ -1425,6 +1484,112 @@ exports.mcp = new commander_1.Command("mcp")
1425
1484
  }
1426
1485
  });
1427
1486
  }
1487
+ function handleGetNeighbors(args) {
1488
+ return __awaiter(this, void 0, void 0, function* () {
1489
+ ensureWatcher();
1490
+ const symbol = String(args.symbol || "");
1491
+ if (!symbol)
1492
+ return err("Missing required parameter: symbol");
1493
+ const direction = args.direction === "callers" ? "callers" : "callees";
1494
+ const maxHops = Math.min(Math.max(Number(args.max_hops) || 2, 1), 5);
1495
+ try {
1496
+ const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1497
+ const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1498
+ const hits = yield builder.getNeighbors(symbol, direction, maxHops);
1499
+ if (hits.length === 0) {
1500
+ return ok(`No ${direction} found for '${symbol}' within ${maxHops} hop(s). Check it is indexed (\`gmax status\`) or try the \`dead\` tool.`);
1501
+ }
1502
+ const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
1503
+ const lines = [
1504
+ `${direction} of ${symbol} (≤${maxHops} hops, ${hits.length} found):`,
1505
+ ];
1506
+ for (const h of hits.slice(0, 100)) {
1507
+ const loc = h.file ? ` ${rel(h.file)}:${h.line + 1}` : " (external)";
1508
+ lines.push(` [${h.hops}h] ${h.symbol}${loc}`);
1509
+ }
1510
+ if (hits.length > 100)
1511
+ lines.push(` … and ${hits.length - 100} more`);
1512
+ return ok(lines.join("\n"));
1513
+ }
1514
+ catch (e) {
1515
+ const msg = e instanceof Error ? e.message : String(e);
1516
+ return err(`get_neighbors failed: ${msg}`);
1517
+ }
1518
+ });
1519
+ }
1520
+ function handleFindPaths(args) {
1521
+ return __awaiter(this, void 0, void 0, function* () {
1522
+ ensureWatcher();
1523
+ const from = String(args.from || "");
1524
+ const to = String(args.to || "");
1525
+ if (!from || !to) {
1526
+ return err("Missing required parameters: from and to");
1527
+ }
1528
+ const direction = args.direction === "callers" ? "callers" : "callees";
1529
+ const maxHops = Math.min(Math.max(Number(args.max_hops) || 6, 1), 10);
1530
+ try {
1531
+ const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1532
+ const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1533
+ const pathSyms = yield builder.findPaths(from, to, direction, maxHops);
1534
+ if (!pathSyms) {
1535
+ return ok(`No ${direction} path from '${from}' to '${to}' within ${maxHops} hops.`);
1536
+ }
1537
+ const arrow = direction === "callees" ? " → " : " ← ";
1538
+ const hops = pathSyms.length - 1;
1539
+ return ok(`Path (${hops} hop${hops === 1 ? "" : "s"}): ${pathSyms.join(arrow)}`);
1540
+ }
1541
+ catch (e) {
1542
+ const msg = e instanceof Error ? e.message : String(e);
1543
+ return err(`find_paths failed: ${msg}`);
1544
+ }
1545
+ });
1546
+ }
1547
+ function handleSubgraphForFiles(args) {
1548
+ return __awaiter(this, void 0, void 0, function* () {
1549
+ ensureWatcher();
1550
+ const filesIn = Array.isArray(args.files)
1551
+ ? args.files.map((f) => String(f)).filter(Boolean)
1552
+ : [];
1553
+ if (filesIn.length === 0) {
1554
+ return err("Missing required parameter: files (non-empty array)");
1555
+ }
1556
+ try {
1557
+ const root = typeof args.root === "string" && args.root ? args.root : projectRoot;
1558
+ const abs = filesIn.map((f) => path.isAbsolute(f) ? f : path.resolve(root, f));
1559
+ const builder = new graph_builder_1.GraphBuilder(getVectorDb(), root);
1560
+ const sg = yield builder.subgraphForFiles(abs);
1561
+ if (sg.symbols.length === 0) {
1562
+ return ok(`No indexed symbols found in: ${filesIn.join(", ")}. Check the paths and \`gmax status\`.`);
1563
+ }
1564
+ const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
1565
+ const cap = (arr, n) => arr.length > n
1566
+ ? `${arr.slice(0, n).join(", ")} … (+${arr.length - n})`
1567
+ : arr.join(", ");
1568
+ const lines = [];
1569
+ lines.push(`Subgraph for ${sg.files.length} file(s): ${sg.symbols.length} symbols, ${sg.internalEdges.length} internal edges, ${sg.externalDeps.length} external deps`);
1570
+ lines.push(`Files: ${sg.files.map(rel).join(", ")}`);
1571
+ lines.push("");
1572
+ lines.push(`Symbols: ${cap(sg.symbols, 50)}`);
1573
+ lines.push("");
1574
+ lines.push("Internal edges:");
1575
+ for (const e of sg.internalEdges.slice(0, 80)) {
1576
+ lines.push(` ${e.from} → ${e.to}`);
1577
+ }
1578
+ if (sg.internalEdges.length === 0)
1579
+ lines.push(" none");
1580
+ if (sg.internalEdges.length > 80) {
1581
+ lines.push(` … and ${sg.internalEdges.length - 80} more`);
1582
+ }
1583
+ lines.push("");
1584
+ lines.push(`External deps: ${cap(sg.externalDeps, 50) || "none"}`);
1585
+ return ok(lines.join("\n"));
1586
+ }
1587
+ catch (e) {
1588
+ const msg = e instanceof Error ? e.message : String(e);
1589
+ return err(`subgraph_for_files failed: ${msg}`);
1590
+ }
1591
+ });
1592
+ }
1428
1593
  function handleListSymbols(args) {
1429
1594
  return __awaiter(this, void 0, void 0, function* () {
1430
1595
  ensureWatcher();
@@ -2170,6 +2335,15 @@ exports.mcp = new commander_1.Command("mcp")
2170
2335
  case "audit":
2171
2336
  result = yield handleAudit(toolArgs);
2172
2337
  break;
2338
+ case "get_neighbors":
2339
+ result = yield handleGetNeighbors(toolArgs);
2340
+ break;
2341
+ case "find_paths":
2342
+ result = yield handleFindPaths(toolArgs);
2343
+ break;
2344
+ case "subgraph_for_files":
2345
+ result = yield handleSubgraphForFiles(toolArgs);
2346
+ break;
2173
2347
  case "list_symbols":
2174
2348
  result = yield handleListSymbols(toolArgs);
2175
2349
  break;
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.GraphBuilder = void 0;
13
13
  const filter_builder_1 = require("../utils/filter-builder");
14
+ const graph_traversal_1 = require("./graph-traversal");
14
15
  class GraphBuilder {
15
16
  constructor(db, pathPrefix, excludePrefixes) {
16
17
  this.db = db;
@@ -185,6 +186,102 @@ class GraphBuilder {
185
186
  return trees;
186
187
  });
187
188
  }
189
+ // --- MCP graph primitives (Phase 7) -----------------------------------
190
+ /** Symbols the given symbol references (outbound edges). */
191
+ calleesOf(symbol) {
192
+ return __awaiter(this, void 0, void 0, function* () {
193
+ return this.getCallees(symbol);
194
+ });
195
+ }
196
+ /** Distinct symbols that reference the given symbol (inbound edges). */
197
+ callersOf(symbol) {
198
+ return __awaiter(this, void 0, void 0, function* () {
199
+ const nodes = yield this.getCallers(symbol);
200
+ const out = [];
201
+ for (const n of nodes) {
202
+ if (n.symbol && n.symbol !== "unknown" && !out.includes(n.symbol)) {
203
+ out.push(n.symbol);
204
+ }
205
+ }
206
+ return out;
207
+ });
208
+ }
209
+ neighborFn(direction) {
210
+ return direction === "callers"
211
+ ? (s) => this.callersOf(s)
212
+ : (s) => this.calleesOf(s);
213
+ }
214
+ /**
215
+ * Symbols reachable from `symbol` along `direction` within `maxHops`, each
216
+ * annotated with hop distance and resolved to a definition location when one
217
+ * is indexed.
218
+ */
219
+ getNeighbors(symbol, direction, maxHops) {
220
+ return __awaiter(this, void 0, void 0, function* () {
221
+ var _a, _b;
222
+ const hits = yield (0, graph_traversal_1.bfsNeighbors)(symbol, this.neighborFn(direction), maxHops);
223
+ const out = [];
224
+ for (const h of hits) {
225
+ const loc = yield this.resolveLocation(h.symbol);
226
+ out.push(Object.assign(Object.assign({}, h), { file: (_a = loc === null || loc === void 0 ? void 0 : loc.file) !== null && _a !== void 0 ? _a : "", line: (_b = loc === null || loc === void 0 ? void 0 : loc.line) !== null && _b !== void 0 ? _b : 0 }));
227
+ }
228
+ return out;
229
+ });
230
+ }
231
+ /** Shortest path `[from, …, to]` along `direction`, or null. */
232
+ findPaths(from_1, to_1, direction_1) {
233
+ return __awaiter(this, arguments, void 0, function* (from, to, direction, maxHops = 6) {
234
+ return (0, graph_traversal_1.findPath)(from, to, this.neighborFn(direction), maxHops);
235
+ });
236
+ }
237
+ /** Resolve a symbol to its first defining chunk's file:line, if indexed. */
238
+ resolveLocation(symbol) {
239
+ return __awaiter(this, void 0, void 0, function* () {
240
+ const table = yield this.db.ensureTable();
241
+ const escaped = (0, filter_builder_1.escapeSqlString)(symbol);
242
+ const rows = yield table
243
+ .query()
244
+ .select(["path", "start_line"])
245
+ .where(this.scopeWhere(`array_contains(defined_symbols, '${escaped}')`))
246
+ .limit(1)
247
+ .toArray();
248
+ if (rows.length === 0)
249
+ return null;
250
+ const r = rows[0];
251
+ return { file: String(r.path || ""), line: Number(r.start_line || 0) };
252
+ });
253
+ }
254
+ /**
255
+ * Build the local dependency subgraph for a set of files: every symbol they
256
+ * define, the edges among those symbols, and their outbound external deps.
257
+ */
258
+ subgraphForFiles(files) {
259
+ return __awaiter(this, void 0, void 0, function* () {
260
+ if (files.length === 0) {
261
+ return { files: [], symbols: [], internalEdges: [], externalDeps: [] };
262
+ }
263
+ const table = yield this.db.ensureTable();
264
+ const orClause = files
265
+ .map((f) => `path = '${(0, filter_builder_1.escapeSqlString)(f)}'`)
266
+ .join(" OR ");
267
+ const rows = yield table
268
+ .query()
269
+ .select(["path", "defined_symbols", "referenced_symbols"])
270
+ .where(this.scopeWhere(`(${orClause})`))
271
+ .limit(100000)
272
+ .toArray();
273
+ const toArray = (val) => {
274
+ if (val && typeof val.toArray === "function")
275
+ return val.toArray();
276
+ return Array.isArray(val) ? val : [];
277
+ };
278
+ return (0, graph_traversal_1.buildFileSubgraph)(rows.map((r) => ({
279
+ path: String(r.path || ""),
280
+ defined_symbols: toArray(r.defined_symbols),
281
+ referenced_symbols: toArray(r.referenced_symbols),
282
+ })));
283
+ });
284
+ }
188
285
  mapRowToNode(row, targetSymbol, type) {
189
286
  // Helper to convert Arrow Vector to array if needed
190
287
  const toArray = (val) => {
@@ -41,6 +41,24 @@ exports.DEFAULT_IGNORE_PATTERNS = [
41
41
  "*.min.css",
42
42
  "*.map",
43
43
  "*.wasm",
44
+ // Machine-generated source (floods the index + ranks codegen as god nodes).
45
+ // Content-based @generated/DO-NOT-EDIT header sniff (file-utils.ts) catches the
46
+ // rest; these are the unambiguous filename/dir conventions.
47
+ "**/__generated__/**", // Relay / graphql-codegen
48
+ "**/Generated/**", // Apollo iOS, Xcode codegen
49
+ "*.graphql.swift", // Apollo iOS operations
50
+ "*.pb.go", // protobuf (Go)
51
+ "*.pb.cc",
52
+ "*.pb.h", // protobuf (C++)
53
+ "*_pb2.py",
54
+ "*_pb2.pyi",
55
+ "*_pb2_grpc.py", // protobuf (Python)
56
+ "*.g.dart",
57
+ "*.freezed.dart",
58
+ "*.gr.dart", // Dart codegen
59
+ "*.designer.cs", // C# designer
60
+ "*.generated.ts",
61
+ "*.generated.tsx", // graphql-codegen / common TS
44
62
  // Test fixtures and benchmark data
45
63
  "**/fixtures/**",
46
64
  "**/benchmark/**",
@@ -47,6 +47,7 @@ exports.hasNullByte = hasNullByte;
47
47
  exports.readFileSnapshot = readFileSnapshot;
48
48
  exports.isIndexableFile = isIndexableFile;
49
49
  exports.isIndexablePath = isIndexablePath;
50
+ exports.isGeneratedContent = isGeneratedContent;
50
51
  exports.formatDenseSnippet = formatDenseSnippet;
51
52
  exports.isDevelopment = isDevelopment;
52
53
  const node_crypto_1 = require("node:crypto");
@@ -111,6 +112,23 @@ function isIndexableFile(filePath, size) {
111
112
  function isIndexablePath(filePath) {
112
113
  return isIndexableFile(filePath);
113
114
  }
115
+ // Well-known machine-generated markers emitted at the top of codegen output.
116
+ // Tools (protoc, graphql-codegen, Apollo, Relay, Go `stringer`, OpenAPI, etc.)
117
+ // stamp one of these in a banner comment. Matched against only the first few KB
118
+ // because the banner is always at the very top — this keeps a hand-written
119
+ // "do not edit this section" note deep in a real file from being misclassified.
120
+ const GENERATED_MARKER = /@generated\b|\bDO NOT EDIT\b|Code generated by |auto-?generated by |This (?:file|code) (?:is|was) (?:\w+[ -])?generated|machine generated|@autogenerated\b/i;
121
+ const GENERATED_SNIFF_BYTES = 2048;
122
+ /**
123
+ * Detect machine-generated source by its banner. Generated files flood both the
124
+ * vector index and the symbol graph (codegen types rank as god nodes), so they
125
+ * are skipped at index time. Path globs in `ignore-patterns.ts` catch the
126
+ * obvious filenames; this catches the rest by content.
127
+ */
128
+ function isGeneratedContent(buffer) {
129
+ const head = buffer.subarray(0, GENERATED_SNIFF_BYTES).toString("utf-8");
130
+ return GENERATED_MARKER.test(head);
131
+ }
114
132
  function formatDenseSnippet(text, maxLength = 1500) {
115
133
  const clean = text !== null && text !== void 0 ? text : "";
116
134
  if (clean.length <= maxLength)
@@ -252,6 +252,10 @@ class WorkerOrchestrator {
252
252
  (0, logger_1.debug)("orch", `skip ${input.path} (empty or binary)`);
253
253
  return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
254
254
  }
255
+ if ((0, file_utils_1.isGeneratedContent)(buffer)) {
256
+ (0, logger_1.debug)("orch", `skip ${input.path} (machine-generated header)`);
257
+ return { vectors: [], hash, mtimeMs, size, shouldDelete: true };
258
+ }
255
259
  onProgress === null || onProgress === void 0 ? void 0 : onProgress();
256
260
  yield this.ensureReady();
257
261
  onProgress === null || onProgress === void 0 ? void 0 : onProgress();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.11",
3
+ "version": "0.17.13",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -37,7 +37,7 @@
37
37
  "prepublishOnly": "pnpm build",
38
38
  "preversion": "pnpm test && pnpm typecheck",
39
39
  "version": "bash scripts/sync-versions.sh && git add -A",
40
- "postversion": "git push origin main && git push origin v$npm_package_version && gh release create v$npm_package_version --generate-notes --title v$npm_package_version && sleep 5 && gh run watch $(gh run list --workflow=release.yml --branch v$npm_package_version --limit 1 --json databaseId --jq '.[0].databaseId') --exit-status && sleep 30 && npm cache clean --force && npm install -g grepmax@$npm_package_version"
40
+ "postversion": "bash scripts/postrelease.sh"
41
41
  },
42
42
  "keywords": [
43
43
  "grepmax",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.11",
3
+ "version": "0.17.13",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",