grepmax 0.21.3 → 0.23.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.
@@ -10,14 +10,12 @@
10
10
  "name": "grepmax",
11
11
  "source": "./plugins/grepmax",
12
12
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
- "version": "0.21.3",
13
+ "version": "0.23.0",
14
14
  "author": {
15
15
  "name": "Robert Owens",
16
16
  "email": "robowens@me.com"
17
17
  },
18
- "skills": [
19
- "./skills/grepmax"
20
- ]
18
+ "skills": ["./skills/grepmax"]
21
19
  }
22
20
  ]
23
21
  }
package/README.md CHANGED
@@ -72,6 +72,7 @@ gmax log src/lib/auth.ts # Git commit history for a path or symb
72
72
  gmax test handleAuth # Find tests via reverse call graph
73
73
  gmax impact handleAuth # Dependents + affected tests
74
74
  gmax similar handleAuth # Find similar code patterns
75
+ gmax audit --top 10 # God nodes, hub files, file cycles, dead candidates
75
76
  gmax dead handleAuth # Unused-symbol check via call graph (DEAD / PUBLIC EXPORT / LIVE)
76
77
  gmax context "auth system" --budget 4000 # Token-budgeted topic summary
77
78
  gmax context src/lib/auth.ts --budget 4000 # Deterministic file/path context
@@ -132,7 +133,7 @@ Plugins auto-update when you run `npm install -g grepmax@latest` — no need to
132
133
  | `extract_symbol` | Complete function/class body by symbol name. |
133
134
  | `peek_symbol` | Compact overview: signature + callers + callees. |
134
135
  | `dead` | Unused-symbol check via call graph. Returns `DEAD`, `PUBLIC EXPORT`, or `LIVE` with caller count. |
135
- | `audit` | Graph summary: god nodes, hub files, and dead-code candidates in one call. |
136
+ | `audit` | Graph summary: god nodes, hub files, symbol-derived file dependency cycles, and dead-code candidates. |
136
137
  | `get_neighbors` | Graph primitive: symbols reachable from a node along call edges within N hops. |
137
138
  | `find_paths` | Graph primitive: shortest call-graph path between two symbols. |
138
139
  | `subgraph_for_files` | Graph primitive: local dependency subgraph for a set of files. |
@@ -64,6 +64,73 @@ const MIN_GOD_NAME_LEN = 3;
64
64
  function rel(p, prefix) {
65
65
  return p.startsWith(prefix) ? p.slice(prefix.length) : p;
66
66
  }
67
+ function findFileCycles(deps, prefix, top) {
68
+ const indexByFile = new Map();
69
+ const lowlink = new Map();
70
+ const stack = [];
71
+ const onStack = new Set();
72
+ const components = [];
73
+ let index = 0;
74
+ const strongConnect = (file) => {
75
+ var _a, _b, _c, _d, _e;
76
+ indexByFile.set(file, index);
77
+ lowlink.set(file, index);
78
+ index++;
79
+ stack.push(file);
80
+ onStack.add(file);
81
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
82
+ if (!indexByFile.has(dep)) {
83
+ strongConnect(dep);
84
+ lowlink.set(file, Math.min((_b = lowlink.get(file)) !== null && _b !== void 0 ? _b : 0, (_c = lowlink.get(dep)) !== null && _c !== void 0 ? _c : 0));
85
+ }
86
+ else if (onStack.has(dep)) {
87
+ lowlink.set(file, Math.min((_d = lowlink.get(file)) !== null && _d !== void 0 ? _d : 0, (_e = indexByFile.get(dep)) !== null && _e !== void 0 ? _e : 0));
88
+ }
89
+ }
90
+ if (lowlink.get(file) !== indexByFile.get(file))
91
+ return;
92
+ const component = [];
93
+ while (stack.length > 0) {
94
+ const dep = stack.pop();
95
+ onStack.delete(dep);
96
+ component.push(dep);
97
+ if (dep === file)
98
+ break;
99
+ }
100
+ if (component.length > 1)
101
+ components.push(component);
102
+ };
103
+ const files = new Set();
104
+ for (const [file, targets] of deps) {
105
+ files.add(file);
106
+ for (const target of targets)
107
+ files.add(target);
108
+ }
109
+ for (const file of [...files].sort()) {
110
+ if (!indexByFile.has(file))
111
+ strongConnect(file);
112
+ }
113
+ return components
114
+ .map((component) => {
115
+ var _a;
116
+ const set = new Set(component);
117
+ let edgeCount = 0;
118
+ for (const file of component) {
119
+ for (const dep of (_a = deps.get(file)) !== null && _a !== void 0 ? _a : []) {
120
+ if (set.has(dep))
121
+ edgeCount++;
122
+ }
123
+ }
124
+ return {
125
+ files: component.map((f) => rel(f, prefix)).sort(),
126
+ edgeCount,
127
+ };
128
+ })
129
+ .sort((a, b) => b.files.length - a.files.length ||
130
+ b.edgeCount - a.edgeCount ||
131
+ a.files.join("\0").localeCompare(b.files.join("\0")))
132
+ .slice(0, top);
133
+ }
67
134
  /**
68
135
  * Pure aggregation over chunk rows — no DB, no I/O. Builds the symbol→def map,
69
136
  * cross-file inbound edges, and per-file fan-in/out, then derives god nodes
@@ -170,6 +237,23 @@ function computeAudit(rows, prefix, top) {
170
237
  });
171
238
  }
172
239
  hubFiles.sort((a, b) => b.dependents - a.dependents || b.defines - a.defines);
240
+ const fileDeps = new Map();
241
+ for (const [file, refs] of fileOutRefs) {
242
+ for (const s of refs) {
243
+ if ((0, callsites_1.isBuiltinCallee)(s))
244
+ continue;
245
+ const defFiles = defFileCounts.get(s);
246
+ if (!defFiles || defFiles.size !== 1)
247
+ continue;
248
+ const [depFile] = defFiles;
249
+ if (!depFile || depFile === file)
250
+ continue;
251
+ if (!fileDeps.has(file))
252
+ fileDeps.set(file, new Set());
253
+ fileDeps.get(file).add(depFile);
254
+ }
255
+ }
256
+ const fileCycles = findFileCycles(fileDeps, prefix, top);
173
257
  // Dead candidates — non-exported in-project symbols with zero inbound
174
258
  // references anywhere (including their own file).
175
259
  const deadAll = [];
@@ -186,10 +270,17 @@ function computeAudit(rows, prefix, top) {
186
270
  scannedFiles: files.size,
187
271
  godNodes: godNodes.slice(0, top),
188
272
  hubFiles: hubFiles.filter((h) => h.dependents > 0).slice(0, top),
273
+ fileCycles,
189
274
  deadCandidates: deadAll.slice(0, top),
190
275
  deadTotal: deadAll.length,
191
276
  };
192
277
  }
278
+ function formatCycle(files) {
279
+ const shown = files.length > 6
280
+ ? [...files.slice(0, 6), `... ${files.length - 6} more`]
281
+ : files;
282
+ return shown.join(", ");
283
+ }
193
284
  function formatHuman(r) {
194
285
  const out = [];
195
286
  out.push(`${style.bold("Audit")} ${style.dim(`— ${r.scannedChunks} chunks across ${r.scannedFiles} files`)}`);
@@ -217,6 +308,17 @@ function formatHuman(r) {
217
308
  }
218
309
  }
219
310
  out.push("");
311
+ out.push(style.bold("File dependency cycles") +
312
+ style.dim(" (symbol-derived, bounded SCCs)"));
313
+ if (r.fileCycles.length === 0) {
314
+ out.push(style.dim(" none"));
315
+ }
316
+ else {
317
+ for (const c of r.fileCycles) {
318
+ out.push(` ${formatCycle(c.files)} ${style.dim(`${c.files.length} files, ${c.edgeCount} internal edges`)}`);
319
+ }
320
+ }
321
+ out.push("");
220
322
  out.push(style.bold("Dead-code candidates") +
221
323
  style.dim(` (${r.deadTotal} non-exported symbols with zero inbound refs)`));
222
324
  if (r.deadCandidates.length === 0) {
@@ -246,6 +348,9 @@ function formatAgent(r) {
246
348
  for (const h of r.hubFiles) {
247
349
  lines.push(`hub\t${h.file}\t${h.dependents}\t${h.defines}\t${h.fanOut}`);
248
350
  }
351
+ for (const c of r.fileCycles) {
352
+ lines.push(`cycle\t${c.files.join(",")}\t${c.files.length}\t${c.edgeCount}`);
353
+ }
249
354
  for (const d of r.deadCandidates) {
250
355
  lines.push(`dead\t${d.symbol}\t${d.file}:${d.line + 1}`);
251
356
  }
@@ -253,11 +358,12 @@ function formatAgent(r) {
253
358
  return lines.join("\n");
254
359
  }
255
360
  exports.audit = new commander_1.Command("audit")
256
- .description("Graph-summary of the indexed project god nodes (most depended-upon " +
257
- "symbols), hub files (most depended-upon files), and dead-code candidates " +
258
- "(non-exported symbols with zero inbound references). One pass over the " +
259
- "static call graph; dynamic dispatch / reflection / eval are invisible, so " +
260
- "dead candidates are hypotheses, not proof.")
361
+ .description("Graph-summary of the indexed project: god nodes (most depended-upon " +
362
+ "symbols), hub files (most depended-upon files), symbol-derived file " +
363
+ "dependency cycles, and dead-code candidates (non-exported symbols with " +
364
+ "zero inbound references). One pass over the static reference graph; " +
365
+ "dynamic dispatch / reflection / eval are invisible, so dead candidates " +
366
+ "are hypotheses, not proof.")
261
367
  .option("--root <dir>", "Project root directory")
262
368
  .option("--in <subpath>", "Restrict to a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
263
369
  .option("--exclude <subpath>", "Exclude a sub-path of the project (repeatable)", (value, prev) => prev ? [...prev, value] : [value])
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.dead = void 0;
46
46
  const commander_1 = require("commander");
47
+ const languages_1 = require("../lib/core/languages");
47
48
  const graph_builder_1 = require("../lib/graph/graph-builder");
48
49
  const vector_db_1 = require("../lib/store/vector-db");
49
50
  const agent_errors_1 = require("../lib/utils/agent-errors");
@@ -147,7 +148,7 @@ exports.dead = new commander_1.Command("dead")
147
148
  const defLine = Number(defRow.start_line || 0);
148
149
  const isExported = Boolean(defRow.is_exported);
149
150
  const builder = new graph_builder_1.GraphBuilder(vectorDb, scope.pathPrefix, scope.excludePrefixes);
150
- const callers = yield builder.getCallers(symbol);
151
+ const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
151
152
  const status = callers.length === 0 ? (isExported ? "PUBLIC_EXPORT" : "DEAD") : "LIVE";
152
153
  const topCallers = callers
153
154
  .slice(0, TOP_CALLERS)
@@ -77,7 +77,7 @@ exports.impact = new commander_1.Command("impact")
77
77
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
78
78
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
79
79
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
80
- const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
80
+ const { symbols, resolvedAsFile, symbolFamilies } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
81
81
  if (symbols.length === 0) {
82
82
  console.log(resolvedAsFile
83
83
  ? `No symbols found in file: ${target}`
@@ -109,9 +109,9 @@ exports.impact = new commander_1.Command("impact")
109
109
  // traversal entirely so the affected-tests section is omitted (not just
110
110
  // empty) below.
111
111
  const [dependents, tests] = yield Promise.all([
112
- (0, impact_1.findDependents)(symbols, vectorDb, queryRoot, excludePaths, undefined, scope.excludePrefixes),
112
+ (0, impact_1.findDependents)(symbols, vectorDb, queryRoot, excludePaths, undefined, scope.excludePrefixes, symbolFamilies),
113
113
  includeTests
114
- ? (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes)
114
+ ? (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes, symbolFamilies)
115
115
  : Promise.resolve([]),
116
116
  ]);
117
117
  // Separate test files from non-test dependents
@@ -65,6 +65,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
65
65
  const commander_1 = require("commander");
66
66
  const zod_1 = require("zod");
67
67
  const config_1 = require("../config");
68
+ const languages_1 = require("../lib/core/languages");
68
69
  const graph_builder_1 = require("../lib/graph/graph-builder");
69
70
  const index_config_1 = require("../lib/index/index-config");
70
71
  const syncer_1 = require("../lib/index/syncer");
@@ -906,6 +907,7 @@ exports.mcp = new commander_1.Command("mcp")
906
907
  symbol: t.node.symbol,
907
908
  file: t.node.file,
908
909
  line: t.node.line,
910
+ edgeKind: t.node.edgeKind,
909
911
  });
910
912
  walkCallers(t.callers);
911
913
  }
@@ -933,7 +935,12 @@ exports.mcp = new commander_1.Command("mcp")
933
935
  const loc = c.file
934
936
  ? `${rel(c.file)}:${c.line + 1}`
935
937
  : "(not indexed)";
936
- parts.push(` <- ${c.symbol} ${loc}`);
938
+ // `(member)`/`(type)` flag inferred edges (receiver-unverified call
939
+ // or a type-position ref) so they don't read as confirmed callers.
940
+ const tag = c.edgeKind === "member" || c.edgeKind === "type"
941
+ ? ` (${c.edgeKind})`
942
+ : "";
943
+ parts.push(` <- ${c.symbol} ${loc}${tag}`);
937
944
  }
938
945
  if (callerList.length > maxCallers) {
939
946
  parts.push(` ... and ${callerList.length - maxCallers} more`);
@@ -993,7 +1000,7 @@ exports.mcp = new commander_1.Command("mcp")
993
1000
  const defLine = Number(defRow.start_line || 0);
994
1001
  const isExported = Boolean(defRow.is_exported);
995
1002
  const builder = new graph_builder_1.GraphBuilder(db, root);
996
- const callers = yield builder.getCallers(symbol);
1003
+ const callers = yield builder.getCallers(symbol, (0, languages_1.languageFamilyForPath)(defPath));
997
1004
  const rel = (p) => p.startsWith(root) ? p.slice(root.length + 1) : p;
998
1005
  const defLoc = `${rel(defPath)}:${defLine + 1}`;
999
1006
  if (callers.length === 0) {
@@ -1035,6 +1042,7 @@ exports.mcp = new commander_1.Command("mcp")
1035
1042
  "start_line",
1036
1043
  "defined_symbols",
1037
1044
  "referenced_symbols",
1045
+ "type_referenced_symbols",
1038
1046
  "is_exported",
1039
1047
  ])
1040
1048
  .where((0, filter_builder_1.pathStartsWith)(prefix))
@@ -1048,7 +1056,12 @@ exports.mcp = new commander_1.Command("mcp")
1048
1056
  start_line: Number(r.start_line || 0),
1049
1057
  is_exported: Boolean(r.is_exported),
1050
1058
  defined_symbols: toStringArray(r.defined_symbols),
1051
- referenced_symbols: toStringArray(r.referenced_symbols),
1059
+ referenced_symbols: [
1060
+ ...new Set([
1061
+ ...toStringArray(r.referenced_symbols),
1062
+ ...toStringArray(r.type_referenced_symbols),
1063
+ ]),
1064
+ ],
1052
1065
  })), prefix, top);
1053
1066
  const lines = [];
1054
1067
  lines.push(`Audit — ${audit.scannedChunks} chunks across ${audit.scannedFiles} files`);
@@ -1067,6 +1080,13 @@ exports.mcp = new commander_1.Command("mcp")
1067
1080
  if (audit.hubFiles.length === 0)
1068
1081
  lines.push(" none");
1069
1082
  lines.push("");
1083
+ lines.push("File dependency cycles (symbol-derived):");
1084
+ for (const c of audit.fileCycles) {
1085
+ lines.push(` ${c.files.join(", ")} - ${c.files.length} files, ${c.edgeCount} internal edges`);
1086
+ }
1087
+ if (audit.fileCycles.length === 0)
1088
+ lines.push(" none");
1089
+ lines.push("");
1070
1090
  lines.push(`Dead-code candidates (${audit.deadTotal} non-exported symbols with zero inbound refs):`);
1071
1091
  for (const d of audit.deadCandidates) {
1072
1092
  lines.push(` ${d.symbol} (${d.file}:${d.line + 1})`);
@@ -1714,10 +1734,10 @@ exports.mcp = new commander_1.Command("mcp")
1714
1734
  try {
1715
1735
  const { resolveTargetSymbols, findTests } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1716
1736
  const db = getVectorDb();
1717
- const { symbols } = yield resolveTargetSymbols(target, db, projectRoot);
1737
+ const { symbols, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1718
1738
  if (symbols.length === 0)
1719
1739
  return ok(`No symbols found for: ${target}`);
1720
- const tests = yield findTests(symbols, db, projectRoot, depth);
1740
+ const tests = yield findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies);
1721
1741
  if (tests.length === 0)
1722
1742
  return ok(`No tests found for ${target}.`);
1723
1743
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -1742,7 +1762,7 @@ exports.mcp = new commander_1.Command("mcp")
1742
1762
  try {
1743
1763
  const { resolveTargetSymbols, findTests, findDependents, isTestPath } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/impact")));
1744
1764
  const db = getVectorDb();
1745
- const { symbols, resolvedAsFile } = yield resolveTargetSymbols(target, db, projectRoot);
1765
+ const { symbols, resolvedAsFile, symbolFamilies } = yield resolveTargetSymbols(target, db, projectRoot);
1746
1766
  if (symbols.length === 0)
1747
1767
  return ok(`No symbols found for: ${target}`);
1748
1768
  const targetPath = resolvedAsFile
@@ -1750,8 +1770,8 @@ exports.mcp = new commander_1.Command("mcp")
1750
1770
  : undefined;
1751
1771
  const excludePaths = targetPath ? new Set([targetPath]) : undefined;
1752
1772
  const [dependents, tests] = yield Promise.all([
1753
- findDependents(symbols, db, projectRoot, excludePaths),
1754
- findTests(symbols, db, projectRoot, depth),
1773
+ findDependents(symbols, db, projectRoot, excludePaths, undefined, undefined, symbolFamilies),
1774
+ findTests(symbols, db, projectRoot, depth, undefined, symbolFamilies),
1755
1775
  ]);
1756
1776
  const nonTestDeps = dependents.filter((d) => !isTestPath(d.file));
1757
1777
  const rel = (p) => p.startsWith(`${projectRoot}/`) ? p.slice(projectRoot.length + 1) : p;
@@ -2197,7 +2217,7 @@ exports.mcp = new commander_1.Command("mcp")
2197
2217
  },
2198
2218
  }, handleDead);
2199
2219
  tool("audit", {
2200
- 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 call graph dynamic dispatch, reflection, eval, and type-position-only references are invisible, so dead candidates are hypotheses; verify with the `dead` tool before acting.",
2220
+ 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.",
2201
2221
  inputSchema: {
2202
2222
  root: zod_1.z
2203
2223
  .string()
@@ -212,7 +212,6 @@ exports.peek = new commander_1.Command("peek")
212
212
  const endLine = metaRows.length > 0
213
213
  ? Number(metaRows[0].end_line || 0)
214
214
  : center.line;
215
- // Get multi-hop callers if depth > 1
216
215
  let callerList;
217
216
  if (depth > 1) {
218
217
  const multiHop = yield graphBuilder.buildGraphMultiHop(symbol, depth);
@@ -224,6 +223,7 @@ exports.peek = new commander_1.Command("peek")
224
223
  symbol: t.node.symbol,
225
224
  file: t.node.file,
226
225
  line: t.node.line,
226
+ edgeKind: t.node.edgeKind,
227
227
  });
228
228
  walkCallers(t.callers);
229
229
  }
@@ -236,8 +236,16 @@ exports.peek = new commander_1.Command("peek")
236
236
  symbol: c.symbol,
237
237
  file: c.file,
238
238
  line: c.line,
239
+ edgeKind: c.edgeKind,
239
240
  }));
240
241
  }
242
+ // Confidence per caller symbol: getCallers sorts free calls first, so the
243
+ // first edgeKind seen for a symbol is its highest-confidence one.
244
+ const edgeKindBySym = new Map();
245
+ for (const c of callerList) {
246
+ if (!edgeKindBySym.has(c.symbol))
247
+ edgeKindBySym.set(c.symbol, c.edgeKind);
248
+ }
241
249
  // Re-anchor chunk-level caller rows to actual call sites and dedupe —
242
250
  // getCallers() returns one row per chunk, which multiplies callers for
243
251
  // classes split across many chunks (verified: 3 real call sites → 66).
@@ -247,6 +255,7 @@ exports.peek = new commander_1.Command("peek")
247
255
  symbol: c.symbol,
248
256
  file: c.file,
249
257
  line: (_a = c.snippetLine) !== null && _a !== void 0 ? _a : c.line,
258
+ edgeKind: edgeKindBySym.get(c.symbol),
250
259
  });
251
260
  });
252
261
  // Builtins listed as "(not indexed)" callees (trunc, now, filter, …)
@@ -266,6 +275,10 @@ exports.peek = new commander_1.Command("peek")
266
275
  file: c.file,
267
276
  line: c.line,
268
277
  }));
278
+ // Inferred-caller marker: `member` = receiver-unverified `x.T()`, `type` =
279
+ // a type-position reference (not a call). Free calls render clean — the
280
+ // trustworthy default — so only guesses carry a tag.
281
+ const kindTag = (k) => k === "member" || k === "type" ? ` (${k})` : "";
269
282
  if (opts.agent) {
270
283
  // Compact TSV output
271
284
  const exportedStr = exported ? "exported" : "";
@@ -290,7 +303,7 @@ exports.peek = new commander_1.Command("peek")
290
303
  console.log(`sig: ${sigOnly}`);
291
304
  // Callers
292
305
  for (const c of resolvedCallers.slice(0, MAX_CALLERS)) {
293
- console.log(`<- ${c.symbol}\t${c.file ? `${rel(c.file)}:${c.line + 1}` : "(not indexed)"}`);
306
+ console.log(`<- ${c.symbol}\t${c.file ? `${rel(c.file)}:${c.line + 1}` : "(not indexed)"}${kindTag(c.edgeKind)}`);
294
307
  }
295
308
  if (resolvedCallers.length > MAX_CALLERS) {
296
309
  console.log(`<- ... ${resolvedCallers.length - MAX_CALLERS} more`);
@@ -338,10 +351,10 @@ exports.peek = new commander_1.Command("peek")
338
351
  console.log(style.bold(`callers (${resolvedCallers.length}):`));
339
352
  for (const c of shown) {
340
353
  if (c.file) {
341
- console.log(` ${style.blue("\u2190")} ${style.green(c.symbol.padEnd(25))} ${style.dim(`${rel(c.file)}:${c.line + 1}`)}`);
354
+ console.log(` ${style.blue("\u2190")} ${style.green(c.symbol.padEnd(25))} ${style.dim(`${rel(c.file)}:${c.line + 1}`)}${style.dim(kindTag(c.edgeKind))}`);
342
355
  }
343
356
  else {
344
- console.log(` ${style.blue("\u2190")} ${c.symbol.padEnd(25)} ${style.dim("(not indexed)")}`);
357
+ console.log(` ${style.blue("\u2190")} ${c.symbol.padEnd(25)} ${style.dim("(not indexed)")}${style.dim(kindTag(c.edgeKind))}`);
345
358
  }
346
359
  }
347
360
  if (resolvedCallers.length > MAX_CALLERS) {
@@ -174,33 +174,40 @@ function installAll() {
174
174
  });
175
175
  }
176
176
  // --- Subcommands ---
177
- const addCmd = new commander_1.Command("add")
178
- .description("Install or update gmax plugins")
179
- .argument("[client]", "Client to install (claude, opencode, codex, droid, all)")
180
- .action((clientArg) => __awaiter(void 0, void 0, void 0, function* () {
181
- const clients = getClients();
182
- const onlyId = clientArg && clientArg !== "all" ? clientArg : undefined;
183
- if (onlyId) {
184
- const client = clients.find((c) => c.id === onlyId);
185
- if (!client) {
186
- console.error(`Unknown client: ${onlyId}`);
187
- console.error(`Available: ${clients.map((c) => c.id).join(", ")}`);
188
- yield (0, exit_1.gracefulExit)(1);
189
- return;
190
- }
191
- if (!client.detect()) {
192
- console.error(`${client.name} not found on this system`);
193
- yield (0, exit_1.gracefulExit)(1);
177
+ function installAction(clientArg) {
178
+ return __awaiter(this, void 0, void 0, function* () {
179
+ const clients = getClients();
180
+ const onlyId = clientArg && clientArg !== "all" ? clientArg : undefined;
181
+ if (onlyId) {
182
+ const client = clients.find((c) => c.id === onlyId);
183
+ if (!client) {
184
+ console.error(`Unknown client: ${onlyId}`);
185
+ console.error(`Available: ${clients.map((c) => c.id).join(", ")}`);
186
+ yield (0, exit_1.gracefulExit)(1);
187
+ return;
188
+ }
189
+ if (!client.detect()) {
190
+ console.error(`${client.name} not found on this system`);
191
+ yield (0, exit_1.gracefulExit)(1);
192
+ return;
193
+ }
194
+ yield client.install();
195
+ yield (0, exit_1.gracefulExit)();
194
196
  return;
195
197
  }
196
- yield client.install();
198
+ // Install all detected
199
+ yield installAll();
197
200
  yield (0, exit_1.gracefulExit)();
198
- return;
199
- }
200
- // Install all detected
201
- yield installAll();
202
- yield (0, exit_1.gracefulExit)();
203
- }));
201
+ });
202
+ }
203
+ const addCmd = new commander_1.Command("add")
204
+ .description("Install or update gmax plugins")
205
+ .argument("[client]", "Client to install (claude, opencode, codex, droid, all)")
206
+ .action(installAction);
207
+ const updateCmd = new commander_1.Command("update")
208
+ .description("Update gmax plugins")
209
+ .argument("[client]", "Client to update (claude, opencode, codex, droid, all)")
210
+ .action(installAction);
204
211
  const removeCmd = new commander_1.Command("remove")
205
212
  .description("Remove gmax plugins")
206
213
  .argument("[client]", "Client to remove (claude, opencode, codex, droid, all)")
@@ -261,6 +268,8 @@ function statusAction() {
261
268
  console.log("\nCommands:");
262
269
  console.log(" gmax plugin add Install all detected clients");
263
270
  console.log(" gmax plugin add <client> Install a specific client");
271
+ console.log(" gmax plugin update Update all detected clients");
272
+ console.log(" gmax plugin update <client> Update a specific client");
264
273
  console.log(" gmax plugin remove Remove all installed plugins");
265
274
  console.log(" gmax plugin remove <client> Remove a specific plugin");
266
275
  yield (0, exit_1.gracefulExit)();
@@ -270,4 +279,5 @@ exports.plugin = new commander_1.Command("plugin")
270
279
  .description("Manage gmax plugins for AI coding clients")
271
280
  .action(statusAction)
272
281
  .addCommand(addCmd)
282
+ .addCommand(updateCmd)
273
283
  .addCommand(removeCmd);
@@ -44,6 +44,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.runSearch = runSearch;
46
46
  const path = __importStar(require("node:path"));
47
+ const config_1 = require("../config");
47
48
  const grammar_loader_1 = require("../lib/index/grammar-loader");
48
49
  const sync_helpers_1 = require("../lib/index/sync-helpers");
49
50
  const syncer_1 = require("../lib/index/syncer");
@@ -188,6 +189,7 @@ function runSearch(params) {
188
189
  lastIndexed: new Date().toISOString(),
189
190
  chunkCount: result.indexed,
190
191
  status: "indexed",
192
+ chunkerVersion: config_1.CONFIG.CHUNKER_VERSION,
191
193
  });
192
194
  const failedSuffix = result.failedFiles > 0 ? ` • ${result.failedFiles} failed` : "";
193
195
  spinner.succeed(`${options.sync ? "Indexing" : "Initial indexing"} complete (${result.processed}/${result.total}) • indexed ${result.indexed}${failedSuffix}`);
@@ -144,7 +144,17 @@ Examples:
144
144
  // Check for running server. The per-project HTTP server can't answer
145
145
  // cross-project queries, so cross-project mode skips it and uses the
146
146
  // daemon-mediated / in-process path (both query the shared table).
147
- const execPathForServer = exec_path ? path.resolve(exec_path) : root;
147
+ let resolvedOptionRoot;
148
+ if (options.root && !crossProject.active) {
149
+ resolvedOptionRoot = (0, project_registry_1.resolveRootOrExit)(options.root);
150
+ if (resolvedOptionRoot === null)
151
+ return;
152
+ }
153
+ const execPathForServer = resolvedOptionRoot
154
+ ? resolvedOptionRoot
155
+ : exec_path
156
+ ? path.resolve(exec_path)
157
+ : root;
148
158
  const projectRootForServer = (_a = (0, project_root_1.findProjectRoot)(execPathForServer)) !== null && _a !== void 0 ? _a : execPathForServer;
149
159
  const server = crossProject.active
150
160
  ? null
@@ -167,21 +177,21 @@ Examples:
167
177
  try {
168
178
  yield (0, setup_helpers_1.ensureSetup)();
169
179
  const searchRoot = exec_path ? path.resolve(exec_path) : root;
170
- const projectRoot = (_b = (0, project_root_1.findProjectRoot)(searchRoot)) !== null && _b !== void 0 ? _b : searchRoot;
171
- const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
172
- // Propagate project root to worker processes
173
- process.env.GMAX_PROJECT_ROOT = projectRoot;
174
- // Check if project is registered
175
- let checkRoot;
180
+ const cwdProjectRoot = (_b = (0, project_root_1.findProjectRoot)(searchRoot)) !== null && _b !== void 0 ? _b : searchRoot;
181
+ let checkRoot = cwdProjectRoot;
176
182
  if (options.root) {
177
- const resolved = (0, project_registry_1.resolveRootOrExit)(options.root);
183
+ const resolved = resolvedOptionRoot !== undefined
184
+ ? resolvedOptionRoot
185
+ : (0, project_registry_1.resolveRootOrExit)(options.root);
178
186
  if (resolved === null)
179
187
  return;
180
188
  checkRoot = (_c = (0, project_root_1.findProjectRoot)(resolved)) !== null && _c !== void 0 ? _c : resolved;
181
189
  }
182
- else {
183
- checkRoot = projectRoot;
184
- }
190
+ const projectRoot = crossProject.active ? cwdProjectRoot : checkRoot;
191
+ const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
192
+ // Propagate project root to worker processes
193
+ process.env.GMAX_PROJECT_ROOT = projectRoot;
194
+ // Check if project is registered
185
195
  const project = (0, project_registry_1.getProject)(checkRoot);
186
196
  if (!project) {
187
197
  console.error(`This project hasn't been added to gmax yet.\n\nRun: gmax add ${checkRoot}\n`);
@@ -72,7 +72,7 @@ exports.testFind = new commander_1.Command("test")
72
72
  (0, stale_hint_1.maybeWarnStaleEmbedding)(projectRoot, { agent: opts.agent });
73
73
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
74
74
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
75
- const { symbols, resolvedAsFile } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
75
+ const { symbols, resolvedAsFile, symbolFamilies } = yield (0, impact_1.resolveTargetSymbols)(target, vectorDb, projectRoot);
76
76
  if (symbols.length === 0) {
77
77
  console.log(resolvedAsFile
78
78
  ? `No symbols found in file: ${target}`
@@ -89,7 +89,7 @@ exports.testFind = new commander_1.Command("test")
89
89
  const queryRoot = opts.in && opts.in.length > 0
90
90
  ? scope.pathPrefix.replace(/\/$/, "")
91
91
  : projectRoot;
92
- const tests = yield (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes);
92
+ const tests = yield (0, impact_1.findTests)(symbols, vectorDb, queryRoot, depth, scope.excludePrefixes, symbolFamilies);
93
93
  if (tests.length === 0) {
94
94
  console.log(`No tests found for ${target}.`);
95
95
  return;