grepmax 0.18.0 → 0.19.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.
Files changed (66) hide show
  1. package/README.md +27 -2
  2. package/dist/commands/add.js +4 -2
  3. package/dist/commands/claude-code.js +1 -1
  4. package/dist/commands/codex.js +3 -1
  5. package/dist/commands/dead.js +2 -6
  6. package/dist/commands/doctor.js +26 -7
  7. package/dist/commands/extract.js +3 -5
  8. package/dist/commands/impact.js +6 -4
  9. package/dist/commands/index.js +7 -2
  10. package/dist/commands/log.js +4 -2
  11. package/dist/commands/mcp.js +587 -715
  12. package/dist/commands/peek.js +15 -9
  13. package/dist/commands/plugin.js +1 -1
  14. package/dist/commands/project.js +15 -5
  15. package/dist/commands/related.js +8 -9
  16. package/dist/commands/review.js +1 -1
  17. package/dist/commands/search.js +1 -1
  18. package/dist/commands/similar.js +4 -4
  19. package/dist/commands/status.js +5 -3
  20. package/dist/commands/summarize.js +6 -4
  21. package/dist/commands/test-find.js +2 -2
  22. package/dist/commands/trace.js +51 -9
  23. package/dist/commands/watch.js +9 -5
  24. package/dist/eval-graph-nav.js +4 -1
  25. package/dist/eval-graph-recovery-probe.js +56 -14
  26. package/dist/eval-graph-spotcheck.js +10 -2
  27. package/dist/eval-graph-totals.js +5 -2
  28. package/dist/eval-oss.js +212 -37
  29. package/dist/eval-seed.js +13 -4
  30. package/dist/index.js +9 -9
  31. package/dist/lib/daemon/daemon.js +22 -13
  32. package/dist/lib/daemon/ipc-handler.js +18 -5
  33. package/dist/lib/daemon/mlx-server-manager.js +11 -3
  34. package/dist/lib/daemon/process-manager.js +1 -2
  35. package/dist/lib/daemon/watcher-manager.js +4 -4
  36. package/dist/lib/graph/callsites.js +151 -25
  37. package/dist/lib/help/agent-cheatsheet.js +1 -1
  38. package/dist/lib/index/batch-processor.js +6 -5
  39. package/dist/lib/index/chunker.js +2 -0
  40. package/dist/lib/index/syncer.js +7 -6
  41. package/dist/lib/llm/diff.js +56 -11
  42. package/dist/lib/llm/investigate.js +12 -5
  43. package/dist/lib/llm/review.js +21 -8
  44. package/dist/lib/llm/server.js +16 -7
  45. package/dist/lib/output/agent-search-formatter.js +25 -3
  46. package/dist/lib/output/index-state-footer.js +1 -1
  47. package/dist/lib/review/risk.js +2 -4
  48. package/dist/lib/search/searcher.js +37 -11
  49. package/dist/lib/search/seed-weight.js +4 -1
  50. package/dist/lib/skeleton/symbol-extractor.js +2 -1
  51. package/dist/lib/utils/cross-project.js +5 -1
  52. package/dist/lib/utils/daemon-client.js +5 -1
  53. package/dist/lib/utils/git.js +10 -2
  54. package/dist/lib/utils/project-registry.js +3 -2
  55. package/dist/lib/utils/scope-filter.js +3 -1
  56. package/dist/lib/utils/watcher-launcher.js +4 -1
  57. package/dist/lib/utils/watcher-store.js +2 -1
  58. package/dist/lib/workers/embeddings/mlx-client.js +7 -2
  59. package/dist/lib/workers/pool.js +9 -6
  60. package/dist/lib/workers/process-child.js +1 -1
  61. package/package.json +22 -19
  62. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  63. package/plugins/grepmax/hooks/cwd-changed.js +4 -1
  64. package/plugins/grepmax/hooks/pre-grep.js +2 -1
  65. package/plugins/grepmax/hooks/start.js +45 -10
  66. package/plugins/grepmax/hooks/subagent-start.js +1 -4
@@ -53,16 +53,18 @@ exports.mcp = void 0;
53
53
  exports.toStringArray = toStringArray;
54
54
  exports.ok = ok;
55
55
  exports.err = err;
56
+ exports.searchResultPath = searchResultPath;
57
+ exports.searchResultStartLine = searchResultStartLine;
58
+ exports.searchResultEndLine = searchResultEndLine;
56
59
  exports.filterMcpSearchResults = filterMcpSearchResults;
57
60
  exports.formatMcpPointerSearchResults = formatMcpPointerSearchResults;
58
61
  const fs = __importStar(require("node:fs"));
59
62
  const path = __importStar(require("node:path"));
60
- const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
63
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
61
64
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
62
- const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
63
65
  const commander_1 = require("commander");
66
+ const zod_1 = require("zod");
64
67
  const config_1 = require("../config");
65
- const audit_1 = require("./audit");
66
68
  const graph_builder_1 = require("../lib/graph/graph-builder");
67
69
  const index_config_1 = require("../lib/index/index-config");
68
70
  const syncer_1 = require("../lib/index/syncer");
@@ -82,482 +84,7 @@ const project_registry_1 = require("../lib/utils/project-registry");
82
84
  const project_root_1 = require("../lib/utils/project-root");
83
85
  const watcher_launcher_1 = require("../lib/utils/watcher-launcher");
84
86
  const watcher_store_1 = require("../lib/utils/watcher-store");
85
- // ---------------------------------------------------------------------------
86
- // Tool definitions
87
- // ---------------------------------------------------------------------------
88
- const TOOLS = [
89
- {
90
- name: "semantic_search",
91
- description: "Search code by meaning. Use scope:'all' for cross-project. Prefer CLI: gmax \"query\" --plain",
92
- inputSchema: {
93
- type: "object",
94
- properties: {
95
- query: {
96
- type: "string",
97
- description: "Natural language query (5+ words recommended)",
98
- },
99
- limit: {
100
- type: "number",
101
- description: "Max results (default 3, max 50)",
102
- },
103
- root: {
104
- type: "string",
105
- description: "Search a different directory (absolute path)",
106
- },
107
- path: {
108
- type: "string",
109
- description: "Path prefix filter (e.g. 'src/auth/')",
110
- },
111
- detail: {
112
- type: "string",
113
- description: "'pointer' (default), 'code', or 'full'",
114
- },
115
- min_score: { type: "number", description: "Min score 0-1 (default 0)" },
116
- max_per_file: { type: "number", description: "Max results per file" },
117
- file: {
118
- type: "string",
119
- description: "Filename filter (e.g. 'syncer.ts')",
120
- },
121
- exclude: {
122
- type: "string",
123
- description: "Exclude path prefix (e.g. 'tests/')",
124
- },
125
- language: {
126
- type: "string",
127
- description: "Extension filter (e.g. 'ts', 'py')",
128
- },
129
- role: {
130
- type: "string",
131
- description: "'ORCHESTRATION', 'DEFINITION', or 'IMPLEMENTATION'",
132
- },
133
- context_lines: {
134
- type: "number",
135
- description: "Lines before/after chunk (max 20)",
136
- },
137
- mode: {
138
- type: "string",
139
- description: "'default' or 'symbol' (appends call graph)",
140
- },
141
- include_imports: {
142
- type: "boolean",
143
- description: "Prepend file imports to results",
144
- },
145
- name_pattern: {
146
- type: "string",
147
- description: "Regex filter on symbol name",
148
- },
149
- scope: {
150
- type: "string",
151
- description: "'project' (default) or 'all' (search everything)",
152
- },
153
- projects: {
154
- type: "string",
155
- description: "Project names to include (comma-separated)",
156
- },
157
- exclude_projects: {
158
- type: "string",
159
- description: "Project names to exclude (comma-separated)",
160
- },
161
- seed_files: {
162
- type: "string",
163
- description: "Bias results toward your working context: comma-separated paths you have open (e.g. 'src/lib/llm/server.ts'). On-topic chunks in these files get lifted; off-topic ones are not.",
164
- },
165
- seed_symbols: {
166
- type: "string",
167
- description: "Bias results toward identifiers you're working with: comma-separated symbol names. Chunks defining a seeded symbol are preferred over mere callers.",
168
- },
169
- },
170
- required: ["query"],
171
- },
172
- },
173
- {
174
- name: "code_skeleton",
175
- description: "File structure with bodies collapsed (~4x fewer tokens). Accepts file, directory, or comma-separated paths.",
176
- inputSchema: {
177
- type: "object",
178
- properties: {
179
- target: {
180
- type: "string",
181
- description: "File, directory, or comma-separated paths",
182
- },
183
- limit: {
184
- type: "number",
185
- description: "Max files for directory mode (default 10)",
186
- },
187
- format: { type: "string", description: "'text' (default) or 'json'" },
188
- },
189
- required: ["target"],
190
- },
191
- },
192
- {
193
- name: "trace_calls",
194
- description: "Call graph: importers, callers (multi-hop), callees with file:line.",
195
- inputSchema: {
196
- type: "object",
197
- properties: {
198
- symbol: { type: "string", description: "Function/class name to trace" },
199
- depth: {
200
- type: "number",
201
- description: "Caller depth (default 1, max 3)",
202
- },
203
- },
204
- required: ["symbol"],
205
- },
206
- },
207
- {
208
- name: "extract_symbol",
209
- description: "Extract complete function/class body by symbol name.",
210
- inputSchema: {
211
- type: "object",
212
- properties: {
213
- symbol: { type: "string", description: "Symbol name to extract" },
214
- root: { type: "string", description: "Project root (absolute path)" },
215
- include_imports: {
216
- type: "boolean",
217
- description: "Prepend file imports",
218
- },
219
- },
220
- required: ["symbol"],
221
- },
222
- },
223
- {
224
- name: "peek_symbol",
225
- description: "Compact symbol overview: signature + callers + callees.",
226
- inputSchema: {
227
- type: "object",
228
- properties: {
229
- symbol: { type: "string", description: "Symbol name" },
230
- root: { type: "string", description: "Project root (absolute path)" },
231
- depth: {
232
- type: "number",
233
- description: "Caller depth (default 1, max 3)",
234
- },
235
- },
236
- required: ["symbol"],
237
- },
238
- },
239
- {
240
- name: "dead",
241
- description: "Report whether a symbol has zero inbound callers in the indexed call graph. Returns DEAD, PUBLIC EXPORT (exported with no internal callers), or LIVE with caller count. Hypothesis, not proof: dynamic dispatch, reflection, and string-built call sites won't show up.",
242
- inputSchema: {
243
- type: "object",
244
- properties: {
245
- symbol: { type: "string", description: "Symbol name to check" },
246
- root: { type: "string", description: "Project root (absolute path)" },
247
- },
248
- required: ["symbol"],
249
- },
250
- },
251
- {
252
- name: "audit",
253
- 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.",
254
- inputSchema: {
255
- type: "object",
256
- properties: {
257
- root: {
258
- type: "string",
259
- description: "Project root (default: current)",
260
- },
261
- top: {
262
- type: "number",
263
- description: "How many of each category to return (default 10)",
264
- },
265
- },
266
- },
267
- },
268
- {
269
- name: "get_neighbors",
270
- 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`.",
271
- inputSchema: {
272
- type: "object",
273
- properties: {
274
- symbol: { type: "string", description: "Starting symbol" },
275
- direction: {
276
- type: "string",
277
- enum: ["callers", "callees"],
278
- description: "Edge direction (default callees)",
279
- },
280
- max_hops: {
281
- type: "number",
282
- description: "Max hops (default 2, max 5)",
283
- },
284
- root: { type: "string", description: "Project root (absolute path)" },
285
- },
286
- required: ["symbol"],
287
- },
288
- },
289
- {
290
- name: "find_paths",
291
- 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.",
292
- inputSchema: {
293
- type: "object",
294
- properties: {
295
- from: { type: "string", description: "Start symbol" },
296
- to: { type: "string", description: "Target symbol" },
297
- direction: {
298
- type: "string",
299
- enum: ["callers", "callees"],
300
- description: "Search direction (default callees)",
301
- },
302
- max_hops: {
303
- type: "number",
304
- description: "Max hops (default 6, max 10)",
305
- },
306
- root: { type: "string", description: "Project root (absolute path)" },
307
- },
308
- required: ["from", "to"],
309
- },
310
- },
311
- {
312
- name: "subgraph_for_files",
313
- 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.",
314
- inputSchema: {
315
- type: "object",
316
- properties: {
317
- files: {
318
- type: "array",
319
- items: { type: "string" },
320
- description: "File paths (absolute or root-relative)",
321
- },
322
- root: { type: "string", description: "Project root (absolute path)" },
323
- },
324
- required: ["files"],
325
- },
326
- },
327
- {
328
- name: "list_symbols",
329
- description: "List indexed symbols with role and export status.",
330
- inputSchema: {
331
- type: "object",
332
- properties: {
333
- pattern: {
334
- type: "string",
335
- description: "Name filter (case-insensitive)",
336
- },
337
- limit: { type: "number", description: "Max results (default 20)" },
338
- path: { type: "string", description: "Path prefix filter" },
339
- },
340
- },
341
- },
342
- {
343
- name: "index_status",
344
- description: "Index health: chunks, files, projects, watcher status.",
345
- inputSchema: { type: "object", properties: {} },
346
- _meta: { "anthropic/alwaysLoad": true },
347
- },
348
- {
349
- name: "summarize_directory",
350
- description: "Generate LLM summaries for indexed chunks.",
351
- inputSchema: {
352
- type: "object",
353
- properties: {
354
- path: {
355
- type: "string",
356
- description: "Directory to summarize (default: project root)",
357
- },
358
- limit: {
359
- type: "number",
360
- description: "Max chunks (default 200, max 5000)",
361
- },
362
- },
363
- },
364
- },
365
- {
366
- name: "summarize_project",
367
- description: "Project overview: languages, structure, roles, key symbols, entry points.",
368
- inputSchema: {
369
- type: "object",
370
- properties: {
371
- root: {
372
- type: "string",
373
- description: "Project root (default: current)",
374
- },
375
- },
376
- },
377
- },
378
- {
379
- name: "related_files",
380
- description: "Find dependencies and dependents of a file by shared symbols.",
381
- inputSchema: {
382
- type: "object",
383
- properties: {
384
- file: {
385
- type: "string",
386
- description: "File path relative to project root",
387
- },
388
- limit: {
389
- type: "number",
390
- description: "Max results per direction (default 10)",
391
- },
392
- },
393
- required: ["file"],
394
- },
395
- },
396
- {
397
- name: "recent_changes",
398
- description: "Recently modified indexed files with timestamps.",
399
- inputSchema: {
400
- type: "object",
401
- properties: {
402
- limit: {
403
- type: "number",
404
- description: "Max files to return (default 20)",
405
- },
406
- root: {
407
- type: "string",
408
- description: "Project root (defaults to current project)",
409
- },
410
- },
411
- },
412
- },
413
- {
414
- name: "diff_changes",
415
- description: "Search code scoped to git changes. Omit ref for uncommitted changes.",
416
- inputSchema: {
417
- type: "object",
418
- properties: {
419
- ref: {
420
- type: "string",
421
- description: "Git ref to diff against (e.g. main, HEAD~5)",
422
- },
423
- query: {
424
- type: "string",
425
- description: "Semantic search within changed files",
426
- },
427
- limit: { type: "number", description: "Max results (default 10)" },
428
- role: {
429
- type: "string",
430
- description: "Filter by role: ORCHESTRATION, DEFINITION, IMPLEMENTATION",
431
- },
432
- },
433
- },
434
- },
435
- {
436
- name: "find_tests",
437
- description: "Find tests that exercise a symbol or file via reverse call graph.",
438
- inputSchema: {
439
- type: "object",
440
- properties: {
441
- target: { type: "string", description: "Symbol name or file path" },
442
- depth: {
443
- type: "number",
444
- description: "Caller traversal depth 1-3 (default 1)",
445
- },
446
- },
447
- required: ["target"],
448
- },
449
- },
450
- {
451
- name: "impact_analysis",
452
- description: "Change impact: dependents and affected tests for a symbol or file.",
453
- inputSchema: {
454
- type: "object",
455
- properties: {
456
- target: { type: "string", description: "Symbol name or file path" },
457
- depth: {
458
- type: "number",
459
- description: "Caller traversal depth 1-3 (default 1)",
460
- },
461
- },
462
- required: ["target"],
463
- },
464
- },
465
- {
466
- name: "find_similar",
467
- description: "Find semantically similar code using vector similarity.",
468
- inputSchema: {
469
- type: "object",
470
- properties: {
471
- target: { type: "string", description: "Symbol name or file path" },
472
- limit: { type: "number", description: "Max results (default 5)" },
473
- threshold: {
474
- type: "number",
475
- description: "Min similarity 0-1 (default 0)",
476
- },
477
- },
478
- required: ["target"],
479
- },
480
- },
481
- {
482
- name: "build_context",
483
- description: "Token-budgeted topic summary (search + skeleton + extract).",
484
- inputSchema: {
485
- type: "object",
486
- properties: {
487
- topic: {
488
- type: "string",
489
- description: "Natural language topic or directory path",
490
- },
491
- budget: { type: "number", description: "Max tokens (default 4000)" },
492
- limit: {
493
- type: "number",
494
- description: "Search result limit (default 10)",
495
- },
496
- },
497
- required: ["topic"],
498
- },
499
- },
500
- {
501
- name: "investigate",
502
- description: "Agentic codebase Q&A: a local LLM answers questions using search, trace, peek, impact, and related tools. Requires LLM to be enabled (gmax llm on).",
503
- inputSchema: {
504
- type: "object",
505
- properties: {
506
- question: {
507
- type: "string",
508
- description: "Natural language question about the codebase",
509
- },
510
- max_rounds: {
511
- type: "number",
512
- description: "Max tool-call rounds (default 10)",
513
- },
514
- },
515
- required: ["question"],
516
- },
517
- },
518
- {
519
- name: "review_commit",
520
- description: "Review a git commit for bugs, breaking changes, and security issues using local LLM + codebase context. Returns structured findings. Requires LLM to be enabled (gmax llm on).",
521
- inputSchema: {
522
- type: "object",
523
- properties: {
524
- commit: {
525
- type: "string",
526
- description: "Git ref to review (default: HEAD)",
527
- },
528
- },
529
- required: [],
530
- },
531
- },
532
- {
533
- name: "review_report",
534
- description: "Get the accumulated code review report for the current project. Returns findings from all reviewed commits.",
535
- inputSchema: {
536
- type: "object",
537
- properties: {
538
- json: {
539
- type: "boolean",
540
- description: "Return raw JSON instead of text (default: false)",
541
- },
542
- },
543
- required: [],
544
- },
545
- },
546
- {
547
- name: "review_risk",
548
- description: "Deterministic risk ranking of the symbols a commit's diff touches, ordered by blast radius (inbound callers) × test presence × file churn. No LLM required — fast, graph + git only. Use to triage what a change endangers before a deep review.",
549
- inputSchema: {
550
- type: "object",
551
- properties: {
552
- commit: {
553
- type: "string",
554
- description: "Git ref to analyze (default: HEAD)",
555
- },
556
- },
557
- required: [],
558
- },
559
- },
560
- ];
87
+ const audit_1 = require("./audit");
561
88
  // ---------------------------------------------------------------------------
562
89
  // Helpers
563
90
  // ---------------------------------------------------------------------------
@@ -589,6 +116,23 @@ function chunkSymbols(chunk) {
589
116
  var _a;
590
117
  return toStringArray((_a = chunk.definedSymbols) !== null && _a !== void 0 ? _a : chunk.defined_symbols);
591
118
  }
119
+ // searcher.search() returns mapped ChunkType objects: the absolute path lives in
120
+ // metadata.path and line numbers in generated_metadata, never at the top level.
121
+ // Handlers that consume search results must read through these — reading r.path /
122
+ // r.start_line directly yields undefined (and crashed build_context's rel()).
123
+ // Use nullish (not ||) so a legitimate line 0 isn't skipped.
124
+ function searchResultPath(r) {
125
+ var _a, _b, _c;
126
+ return String((_c = (_a = r === null || r === void 0 ? void 0 : r.path) !== null && _a !== void 0 ? _a : (_b = r === null || r === void 0 ? void 0 : r.metadata) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : "");
127
+ }
128
+ function searchResultStartLine(r) {
129
+ var _a, _b, _c;
130
+ return Number((_c = (_a = r === null || r === void 0 ? void 0 : r.start_line) !== null && _a !== void 0 ? _a : (_b = r === null || r === void 0 ? void 0 : r.generated_metadata) === null || _b === void 0 ? void 0 : _b.start_line) !== null && _c !== void 0 ? _c : 0);
131
+ }
132
+ function searchResultEndLine(r, fallbackStart = 0) {
133
+ var _a, _b, _c;
134
+ return Number((_c = (_a = r === null || r === void 0 ? void 0 : r.end_line) !== null && _a !== void 0 ? _a : (_b = r === null || r === void 0 ? void 0 : r.generated_metadata) === null || _b === void 0 ? void 0 : _b.end_line) !== null && _c !== void 0 ? _c : fallbackStart);
135
+ }
592
136
  function filterMcpSearchResults(data, options = {}) {
593
137
  var _a, _b;
594
138
  let results = data;
@@ -722,18 +266,34 @@ exports.mcp = new commander_1.Command("mcp")
722
266
  // --- Tool handlers ---
723
267
  function handleSemanticSearch(args_1) {
724
268
  return __awaiter(this, arguments, void 0, function* (args, isSearchAll = false) {
725
- var _a, _b;
726
269
  const query = String(args.query || "");
727
270
  if (!query)
728
271
  return err("Missing required parameter: query");
729
- const searchAll = isSearchAll || args.scope === "all";
272
+ let searchAll = isSearchAll || args.scope === "all";
730
273
  const limit = Math.min(Math.max(Number(args.limit) || 3, 1), 50);
731
274
  ensureWatcher();
275
+ // Project resolution. The server is pinned to whatever cwd it launched in
276
+ // (resolved once at startup). When that cwd ISN'T an indexed project and
277
+ // the caller hasn't pinned an explicit --root, fall back to cross-project
278
+ // search instead of dead-ending — this is what makes the server usable
279
+ // from a session that isn't sitting inside a registered repo.
280
+ let scopeNote = "";
732
281
  const proj = (0, project_registry_1.getProject)(projectRoot);
733
- if (!proj) {
282
+ if (!proj && typeof args.root !== "string") {
283
+ 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; });
284
+ if (indexed.length === 0) {
285
+ return err("No indexed projects yet. cd into a repo and run `gmax add` to index it first.");
286
+ }
287
+ searchAll = true;
288
+ scopeNote =
289
+ `Note: ${path.basename(projectRoot)}/ isn't an indexed gmax project — ` +
290
+ `searching all ${indexed.length} indexed project(s). ` +
291
+ `Call list_projects to see them, then pass projects:"name" to narrow.`;
292
+ }
293
+ else if (!proj) {
734
294
  return err("Project not added to gmax yet. Run `gmax add` to index it first.");
735
295
  }
736
- if (proj.status === "pending" || proj.chunkCount === 0) {
296
+ else if (proj.status === "pending" || proj.chunkCount === 0) {
737
297
  return err("Project not indexed yet. Run `gmax add` to index it first.");
738
298
  }
739
299
  try {
@@ -807,10 +367,19 @@ exports.mcp = new commander_1.Command("mcp")
807
367
  };
808
368
  const seedFiles = parseSeedList(args.seed_files);
809
369
  const seedSymbols = parseSeedList(args.seed_symbols);
810
- const seeds = seedFiles || seedSymbols ? { files: seedFiles, symbols: seedSymbols } : undefined;
370
+ const seeds = seedFiles || seedSymbols
371
+ ? { files: seedFiles, symbols: seedSymbols }
372
+ : undefined;
811
373
  const result = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1", seeds }, Object.keys(filters).length > 0 ? filters : undefined, pathPrefix);
374
+ // Prepend the scope-fallback note (if any) + searcher warnings to
375
+ // whatever body we return, so the agent learns it searched all projects.
376
+ const prefixNotes = (body) => {
377
+ var _a;
378
+ const notes = [scopeNote, ...((_a = result.warnings) !== null && _a !== void 0 ? _a : [])].filter(Boolean);
379
+ return notes.length ? `${notes.join("\n")}\n\n${body}` : body;
380
+ };
812
381
  if (!result.data || result.data.length === 0) {
813
- return ok("No matches found. Try broadening your query, using fewer keywords, or check `gmax status` to verify the project is indexed.");
382
+ return ok(prefixNotes("No matches found. Try broadening your query, using fewer keywords, or check `gmax status` to verify the project is indexed."));
814
383
  }
815
384
  const minScore = typeof args.min_score === "number" ? args.min_score : 0;
816
385
  const maxPerFile = typeof args.max_per_file === "number" ? args.max_per_file : 0;
@@ -837,10 +406,7 @@ exports.mcp = new commander_1.Command("mcp")
837
406
  getImportsForFile,
838
407
  query,
839
408
  });
840
- if ((_a = result.warnings) === null || _a === void 0 ? void 0 : _a.length) {
841
- return ok(`${result.warnings.join("\n")}\n\n${output}`);
842
- }
843
- return ok(output);
409
+ return ok(prefixNotes(output));
844
410
  }
845
411
  let results = result.data.map((r) => {
846
412
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
@@ -936,7 +502,7 @@ exports.mcp = new commander_1.Command("mcp")
936
502
  const regex = new RegExp(namePattern, "i");
937
503
  results = results.filter((r) => r.symbols.some((s) => regex.test(s)));
938
504
  }
939
- catch (_c) {
505
+ catch (_a) {
940
506
  // Invalid regex — skip filter
941
507
  }
942
508
  }
@@ -979,14 +545,11 @@ exports.mcp = new commander_1.Command("mcp")
979
545
  output += `\n${traceLines.join("\n")}`;
980
546
  }
981
547
  }
982
- catch (_d) {
548
+ catch (_b) {
983
549
  // Trace failed — return search results without trace
984
550
  }
985
551
  }
986
- if ((_b = result.warnings) === null || _b === void 0 ? void 0 : _b.length) {
987
- return ok(`${result.warnings.join("\n")}\n\n${output}`);
988
- }
989
- return ok(output);
552
+ return ok(prefixNotes(output));
990
553
  }
991
554
  catch (e) {
992
555
  const msg = e instanceof Error ? e.message : String(e);
@@ -1714,6 +1277,32 @@ exports.mcp = new commander_1.Command("mcp")
1714
1277
  }
1715
1278
  });
1716
1279
  }
1280
+ function handleListProjects() {
1281
+ return __awaiter(this, void 0, void 0, function* () {
1282
+ var _a;
1283
+ try {
1284
+ const projects = (0, project_registry_1.listProjects)();
1285
+ if (projects.length === 0) {
1286
+ return ok("No projects indexed yet. cd into a repo and run `gmax add` to index it.");
1287
+ }
1288
+ const currentName = (_a = (0, project_registry_1.getProject)(projectRoot)) === null || _a === void 0 ? void 0 : _a.name;
1289
+ const lines = projects.map((p) => {
1290
+ const here = p.name === currentName ? " (current)" : "";
1291
+ const chunks = typeof p.chunkCount === "number"
1292
+ ? `\t(${p.chunkCount} chunks)`
1293
+ : "";
1294
+ return `${p.name}${here}\t${p.root}\t${p.status}${chunks}`;
1295
+ });
1296
+ return ok(`${projects.length} indexed project(s). ` +
1297
+ `Pass a name to semantic_search via projects:"name", or use scope:"all".\n\n` +
1298
+ lines.join("\n"));
1299
+ }
1300
+ catch (e) {
1301
+ const msg = e instanceof Error ? e.message : String(e);
1302
+ return err(`Listing projects failed: ${msg}`);
1303
+ }
1304
+ });
1305
+ }
1717
1306
  function handleIndexStatus() {
1718
1307
  return __awaiter(this, void 0, void 0, function* () {
1719
1308
  var _a, _b, _c, _d, _e, _f;
@@ -1738,30 +1327,15 @@ exports.mcp = new commander_1.Command("mcp")
1738
1327
  watcherLine += " — search results may be incomplete";
1739
1328
  }
1740
1329
  }
1330
+ // Health overview only. The per-project listing (names, roots,
1331
+ // per-project chunk counts) lives in `list_projects` — keep this tool
1332
+ // focused on index health and avoid the N-project LanceDB scans.
1741
1333
  const lines = [
1742
1334
  `Index: ~/.gmax/lancedb (${stats.chunks} chunks, ${fileCount} files)`,
1743
1335
  `Model: ${globalConfig.embedMode === "gpu" ? ((_d = (_c = (_b = config_1.MODEL_TIERS[globalConfig.modelTier]) === null || _b === void 0 ? void 0 : _b.mlxModel) !== null && _c !== void 0 ? _c : config === null || config === void 0 ? void 0 : config.embedModel) !== null && _d !== void 0 ? _d : "unknown") : ((_e = config === null || config === void 0 ? void 0 : config.embedModel) !== null && _e !== void 0 ? _e : "unknown")} (${(_f = config === null || config === void 0 ? void 0 : config.vectorDim) !== null && _f !== void 0 ? _f : "?"}d, ${globalConfig.embedMode})`,
1744
1336
  (config === null || config === void 0 ? void 0 : config.indexedAt) ? `Last indexed: ${config.indexedAt}` : "",
1745
1337
  watcherLine,
1746
- "",
1747
- "Indexed directories:",
1748
- ...(yield Promise.all(projects.map((p) => __awaiter(this, void 0, void 0, function* () {
1749
- var _a, _b;
1750
- const prefix = p.root.endsWith("/") ? p.root : `${p.root}/`;
1751
- try {
1752
- const table = yield db.ensureTable();
1753
- const rows = yield table
1754
- .query()
1755
- .select(["id"])
1756
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
1757
- .limit(100000)
1758
- .toArray();
1759
- return ` ${p.name}\t${p.root}\t${(_a = p.lastIndexed) !== null && _a !== void 0 ? _a : "unknown"}\t(${rows.length} chunks)`;
1760
- }
1761
- catch (_c) {
1762
- return ` ${p.name}\t${p.root}\t${(_b = p.lastIndexed) !== null && _b !== void 0 ? _b : "unknown"}`;
1763
- }
1764
- })))),
1338
+ `Projects: ${projects.length} indexed (call list_projects for names + per-project chunk counts)`,
1765
1339
  ].filter(Boolean);
1766
1340
  return ok(lines.join("\n"));
1767
1341
  }
@@ -2092,7 +1666,9 @@ exports.mcp = new commander_1.Command("mcp")
2092
1666
  const searcher = getSearcher();
2093
1667
  const response = yield searcher.search(query, limit, { rerank: process.env.GMAX_RERANK === "1" }, {}, projectRoot);
2094
1668
  const changedSet = new Set(changedFiles);
2095
- let filtered = response.data.filter((r) => changedSet.has(String(r.path || "")));
1669
+ // searcher.search() returns mapped chunks (path under metadata.path);
1670
+ // changedFiles are absolute, so match on the resolved absolute path.
1671
+ let filtered = response.data.filter((r) => changedSet.has(searchResultPath(r)));
2096
1672
  if (role)
2097
1673
  filtered = filtered.filter((r) => String(r.role || "")
2098
1674
  .toUpperCase()
@@ -2100,9 +1676,9 @@ exports.mcp = new commander_1.Command("mcp")
2100
1676
  if (filtered.length === 0)
2101
1677
  return ok("No indexed results found in changed files for that query.");
2102
1678
  const lines = filtered.slice(0, limit).map((r) => {
2103
- var _a, _b, _c;
1679
+ var _a, _b;
2104
1680
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2105
- return `${rel(r.path)}:${Number((_c = r.start_line) !== null && _c !== void 0 ? _c : 0) + 1} ${sym} [${r.role || "IMPL"}]`;
1681
+ return `${rel(searchResultPath(r))}:${searchResultStartLine(r) + 1} ${sym} [${r.role || "IMPL"}]`;
2106
1682
  });
2107
1683
  return ok(lines.join("\n"));
2108
1684
  }
@@ -2276,7 +1852,7 @@ exports.mcp = new commander_1.Command("mcp")
2276
1852
  }
2277
1853
  function handleBuildContext(args) {
2278
1854
  return __awaiter(this, void 0, void 0, function* () {
2279
- var _a, _b, _c, _d;
1855
+ var _a, _b;
2280
1856
  ensureWatcher();
2281
1857
  const topic = String(args.topic || "");
2282
1858
  if (!topic)
@@ -2292,21 +1868,22 @@ exports.mcp = new commander_1.Command("mcp")
2292
1868
  const estTokens = (s) => Math.ceil(s.length / 4);
2293
1869
  let tokensUsed = 0;
2294
1870
  const sections = [];
2295
- // Entry points
1871
+ // Entry points. searcher.search() returns mapped chunks, so the path and
1872
+ // line live under metadata/generated_metadata — read via the helpers.
2296
1873
  const epLines = response.data.slice(0, 5).map((r) => {
2297
- var _a, _b, _c;
1874
+ var _a, _b;
2298
1875
  const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2299
- return `${rel(r.path)}:${Number((_c = r.start_line) !== null && _c !== void 0 ? _c : 0) + 1} ${sym} [${r.role || "IMPL"}]`;
1876
+ return `${rel(searchResultPath(r))}:${searchResultStartLine(r) + 1} ${sym} [${r.role || "IMPL"}]`;
2300
1877
  });
2301
1878
  const epSection = `## Entry Points\n${epLines.join("\n")}`;
2302
1879
  sections.push(epSection);
2303
1880
  tokensUsed += estTokens(epSection);
2304
1881
  // Key function bodies
2305
1882
  for (const r of response.data.slice(0, 3)) {
2306
- const absP = String(r.path || "");
2307
- const startLine = Number((_a = r.start_line) !== null && _a !== void 0 ? _a : 0);
2308
- const endLine = Number((_b = r.end_line) !== null && _b !== void 0 ? _b : startLine);
2309
- const sym = (_d = (_c = toStringArray(r.defined_symbols)) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : "";
1883
+ const absP = searchResultPath(r);
1884
+ const startLine = searchResultStartLine(r);
1885
+ const endLine = searchResultEndLine(r, startLine);
1886
+ const sym = (_b = (_a = toStringArray(r.defined_symbols)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : "";
2310
1887
  try {
2311
1888
  const content = fs.readFileSync(absP, "utf-8");
2312
1889
  const body = content
@@ -2319,7 +1896,7 @@ exports.mcp = new commander_1.Command("mcp")
2319
1896
  sections.push(blob);
2320
1897
  tokensUsed += estTokens(blob);
2321
1898
  }
2322
- catch (_e) {
1899
+ catch (_c) {
2323
1900
  /* skip unreadable */
2324
1901
  }
2325
1902
  }
@@ -2331,219 +1908,514 @@ exports.mcp = new commander_1.Command("mcp")
2331
1908
  }
2332
1909
  });
2333
1910
  }
2334
- // --- MCP server setup ---
2335
- const transport = new stdio_js_1.StdioServerTransport();
2336
- const server = new index_js_1.Server({
2337
- name: "gmax",
2338
- version: JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), {
2339
- encoding: "utf-8",
2340
- })).version,
2341
- }, {
2342
- capabilities: {
2343
- tools: {},
2344
- },
2345
- });
2346
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, () => __awaiter(void 0, void 0, void 0, function* () {
2347
- return { tools: TOOLS };
2348
- }));
2349
- server.setRequestHandler(types_js_1.CallToolRequestSchema, (request) => __awaiter(void 0, void 0, void 0, function* () {
2350
- var _a, _b, _c, _d, _e, _f, _g;
2351
- const { name, arguments: args } = request.params;
2352
- const toolArgs = (args !== null && args !== void 0 ? args : {});
2353
- const startMs = Date.now();
2354
- let result;
2355
- switch (name) {
2356
- case "semantic_search":
2357
- result = yield handleSemanticSearch(toolArgs, false);
2358
- break;
2359
- case "code_skeleton":
2360
- result = yield handleCodeSkeleton(toolArgs);
2361
- break;
2362
- case "trace_calls":
2363
- result = yield handleTraceCalls(toolArgs);
2364
- break;
2365
- case "extract_symbol":
2366
- result = yield handleExtractSymbol(toolArgs);
2367
- break;
2368
- case "peek_symbol":
2369
- result = yield handlePeekSymbol(toolArgs);
2370
- break;
2371
- case "dead":
2372
- result = yield handleDead(toolArgs);
2373
- break;
2374
- case "audit":
2375
- result = yield handleAudit(toolArgs);
2376
- break;
2377
- case "get_neighbors":
2378
- result = yield handleGetNeighbors(toolArgs);
2379
- break;
2380
- case "find_paths":
2381
- result = yield handleFindPaths(toolArgs);
2382
- break;
2383
- case "subgraph_for_files":
2384
- result = yield handleSubgraphForFiles(toolArgs);
2385
- break;
2386
- case "list_symbols":
2387
- result = yield handleListSymbols(toolArgs);
2388
- break;
2389
- case "index_status":
2390
- result = yield handleIndexStatus();
2391
- break;
2392
- case "summarize_directory":
2393
- result = yield handleSummarizeDirectory(toolArgs);
2394
- break;
2395
- case "summarize_project":
2396
- result = yield handleSummarizeProject(toolArgs);
2397
- break;
2398
- case "related_files":
2399
- result = yield handleRelatedFiles(toolArgs);
2400
- break;
2401
- case "recent_changes":
2402
- result = yield handleRecentChanges(toolArgs);
2403
- break;
2404
- case "diff_changes":
2405
- result = yield handleDiffChanges(toolArgs);
2406
- break;
2407
- case "find_tests":
2408
- result = yield handleFindTests(toolArgs);
2409
- break;
2410
- case "impact_analysis":
2411
- result = yield handleImpactAnalysis(toolArgs);
2412
- break;
2413
- case "find_similar":
2414
- result = yield handleFindSimilar(toolArgs);
2415
- break;
2416
- case "build_context":
2417
- result = yield handleBuildContext(toolArgs);
2418
- break;
2419
- case "investigate": {
2420
- const question = String(toolArgs.question || "");
2421
- if (!question) {
2422
- result = err("Missing required parameter: question");
2423
- break;
2424
- }
2425
- const maxRounds = Math.min(Math.max(Number(toolArgs.max_rounds) || 10, 1), 15);
2426
- try {
2427
- const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
2428
- if (yield isDaemonRunning()) {
2429
- const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
2430
- if (!llmResp.ok) {
2431
- result = err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
2432
- break;
2433
- }
2434
- }
2435
- else {
2436
- result = err("LLM server not available. Run `gmax llm on && gmax llm start`.");
2437
- break;
1911
+ // --- Review/LLM tool handlers (extracted from the old dispatch switch) ---
1912
+ function handleInvestigate(args) {
1913
+ return __awaiter(this, void 0, void 0, function* () {
1914
+ const question = String(args.question || "");
1915
+ if (!question)
1916
+ return err("Missing required parameter: question");
1917
+ const maxRounds = Math.min(Math.max(Number(args.max_rounds) || 10, 1), 15);
1918
+ try {
1919
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
1920
+ if (yield isDaemonRunning()) {
1921
+ const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
1922
+ if (!llmResp.ok) {
1923
+ return err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
2438
1924
  }
2439
- const { investigate } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/investigate")));
2440
- const inv = yield investigate({ question, projectRoot, maxRounds });
2441
- result = ok(inv.answer);
2442
1925
  }
2443
- catch (e) {
2444
- result = err(`Investigate failed: ${e instanceof Error ? e.message : String(e)}`);
1926
+ else {
1927
+ return err("LLM server not available. Run `gmax llm on && gmax llm start`.");
2445
1928
  }
2446
- break;
1929
+ const { investigate } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/investigate")));
1930
+ const inv = yield investigate({ question, projectRoot, maxRounds });
1931
+ return ok(inv.answer);
2447
1932
  }
2448
- case "review_commit": {
2449
- const commitRef = String(toolArgs.commit || "HEAD");
2450
- try {
2451
- const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
2452
- if (yield isDaemonRunning()) {
2453
- const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
2454
- if (!llmResp.ok) {
2455
- result = err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
2456
- break;
2457
- }
2458
- }
2459
- else {
2460
- result = err("LLM server not available. Run `gmax llm on && gmax llm start`.");
2461
- break;
2462
- }
2463
- const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/review")));
2464
- const rev = yield reviewCommit({ commitRef, projectRoot });
2465
- if (rev.clean) {
2466
- result = ok(`Clean commit (${rev.commit}) — no issues found in ${rev.duration}s.`);
2467
- }
2468
- else {
2469
- const { readReport } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
2470
- const report = readReport(projectRoot);
2471
- const entry = report === null || report === void 0 ? void 0 : report.reviews.find((r) => r.commit === rev.commit);
2472
- result = ok(JSON.stringify({
2473
- commit: rev.commit,
2474
- findings: (_a = entry === null || entry === void 0 ? void 0 : entry.findings) !== null && _a !== void 0 ? _a : [],
2475
- duration: rev.duration,
2476
- }, null, 2));
1933
+ catch (e) {
1934
+ return err(`Investigate failed: ${e instanceof Error ? e.message : String(e)}`);
1935
+ }
1936
+ });
1937
+ }
1938
+ function handleReviewCommit(args) {
1939
+ return __awaiter(this, void 0, void 0, function* () {
1940
+ var _a;
1941
+ const commitRef = String(args.commit || "HEAD");
1942
+ try {
1943
+ const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
1944
+ if (yield isDaemonRunning()) {
1945
+ const llmResp = yield sendDaemonCommand({ cmd: "llm-start" }, { timeoutMs: 90000 });
1946
+ if (!llmResp.ok) {
1947
+ return err(`LLM server not available: ${llmResp.error}. Run \`gmax llm on && gmax llm start\`.`);
2477
1948
  }
2478
1949
  }
2479
- catch (e) {
2480
- result = err(`Review failed: ${e instanceof Error ? e.message : String(e)}`);
2481
- }
2482
- break;
1950
+ else {
1951
+ return err("LLM server not available. Run `gmax llm on && gmax llm start`.");
1952
+ }
1953
+ const { reviewCommit } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/review")));
1954
+ const rev = yield reviewCommit({ commitRef, projectRoot });
1955
+ if (rev.clean) {
1956
+ return ok(`Clean commit (${rev.commit}) — no issues found in ${rev.duration}s.`);
1957
+ }
1958
+ const { readReport } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
1959
+ const report = readReport(projectRoot);
1960
+ const entry = report === null || report === void 0 ? void 0 : report.reviews.find((r) => r.commit === rev.commit);
1961
+ return ok(JSON.stringify({
1962
+ commit: rev.commit,
1963
+ findings: (_a = entry === null || entry === void 0 ? void 0 : entry.findings) !== null && _a !== void 0 ? _a : [],
1964
+ duration: rev.duration,
1965
+ }, null, 2));
2483
1966
  }
2484
- case "review_risk": {
2485
- ensureWatcher();
2486
- const commitRef = String(toolArgs.commit || "HEAD");
2487
- try {
2488
- const db = getVectorDb();
2489
- const builder = new graph_builder_1.GraphBuilder(db, projectRoot);
2490
- const { gatherRiskInputs, computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
2491
- const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
2492
- vectorDb: db,
2493
- graphBuilder: builder,
2494
- });
2495
- const rows = computeRiskTable(inputs);
2496
- result =
2497
- rows.length === 0
2498
- ? ok("(no changed symbols in this diff)")
2499
- : ok(formatRiskTable(rows, { agent: true }));
2500
- }
2501
- catch (e) {
2502
- result = err(`Risk ranking failed: ${e instanceof Error ? e.message : String(e)}`);
2503
- }
2504
- break;
1967
+ catch (e) {
1968
+ return err(`Review failed: ${e instanceof Error ? e.message : String(e)}`);
2505
1969
  }
2506
- case "review_report": {
2507
- try {
2508
- const { readReport, formatReportText } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
2509
- const report = readReport(projectRoot);
2510
- if (!report || report.reviews.length === 0) {
2511
- result = ok("No review findings yet.");
2512
- }
2513
- else if (toolArgs.json) {
2514
- result = ok(JSON.stringify(report, null, 2));
2515
- }
2516
- else {
2517
- result = ok(formatReportText(report));
2518
- }
1970
+ });
1971
+ }
1972
+ function handleReviewRisk(args) {
1973
+ return __awaiter(this, void 0, void 0, function* () {
1974
+ ensureWatcher();
1975
+ const commitRef = String(args.commit || "HEAD");
1976
+ try {
1977
+ const db = getVectorDb();
1978
+ const builder = new graph_builder_1.GraphBuilder(db, projectRoot);
1979
+ const { gatherRiskInputs, computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
1980
+ const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
1981
+ vectorDb: db,
1982
+ graphBuilder: builder,
1983
+ });
1984
+ const rows = computeRiskTable(inputs);
1985
+ return rows.length === 0
1986
+ ? ok("(no changed symbols in this diff)")
1987
+ : ok(formatRiskTable(rows, { agent: true }));
1988
+ }
1989
+ catch (e) {
1990
+ return err(`Risk ranking failed: ${e instanceof Error ? e.message : String(e)}`);
1991
+ }
1992
+ });
1993
+ }
1994
+ function handleReviewReport(args) {
1995
+ return __awaiter(this, void 0, void 0, function* () {
1996
+ try {
1997
+ const { readReport, formatReportText } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
1998
+ const report = readReport(projectRoot);
1999
+ if (!report || report.reviews.length === 0) {
2000
+ return ok("No review findings yet.");
2519
2001
  }
2520
- catch (e) {
2521
- result = err(`Report failed: ${e instanceof Error ? e.message : String(e)}`);
2002
+ if (args.json) {
2003
+ return ok(JSON.stringify(report, null, 2));
2522
2004
  }
2523
- break;
2005
+ return ok(formatReportText(report));
2524
2006
  }
2525
- default:
2526
- return err(`Unknown tool: ${name}`);
2527
- }
2528
- // Best-effort query logging
2007
+ catch (e) {
2008
+ return err(`Report failed: ${e instanceof Error ? e.message : String(e)}`);
2009
+ }
2010
+ });
2011
+ }
2012
+ // --- MCP server setup ---
2013
+ const transport = new stdio_js_1.StdioServerTransport();
2014
+ const server = new mcp_js_1.McpServer({
2015
+ name: "gmax",
2016
+ version: JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), {
2017
+ encoding: "utf-8",
2018
+ })).version,
2019
+ }, {
2020
+ capabilities: {
2021
+ tools: {},
2022
+ },
2023
+ instructions: "gmax is a local semantic code-search index over one or more projects. " +
2024
+ "By default tools operate on the project at the server's working directory. " +
2025
+ "If you are NOT inside a specific repo, or want a different one: call " +
2026
+ '`list_projects` to see what\'s indexed, then pass `projects:"name"` (or ' +
2027
+ '`scope:"all"`) to `semantic_search`. When the working directory isn\'t an ' +
2028
+ "indexed project, `semantic_search` automatically searches all of them and " +
2029
+ "says so. Prefer `semantic_search` (detail:'pointer') for discovery, then " +
2030
+ "`extract_symbol`/`peek_symbol`/`code_skeleton` to read, and " +
2031
+ "`trace_calls`/`impact_analysis`/`find_tests` for call-graph questions.",
2032
+ });
2033
+ // Best-effort query logging, applied uniformly to every tool exactly as the
2034
+ // old single CallToolRequestSchema dispatch did before each return.
2035
+ const logToolCall = (name, toolArgs, startMs, result) => __awaiter(void 0, void 0, void 0, function* () {
2036
+ var _a, _b, _c, _d, _e, _f;
2529
2037
  try {
2530
2038
  const { logQuery } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/query-log")));
2531
- const text = (_d = (_c = (_b = result.content) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : "";
2039
+ 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 : "";
2532
2040
  const resultLines = text.split("\n").filter((l) => l.trim()).length;
2533
2041
  logQuery({
2534
2042
  ts: new Date().toISOString(),
2535
2043
  source: "mcp",
2536
2044
  tool: name,
2537
- query: String((_g = (_f = (_e = toolArgs.query) !== null && _e !== void 0 ? _e : toolArgs.symbol) !== null && _f !== void 0 ? _f : toolArgs.target) !== null && _g !== void 0 ? _g : ""),
2045
+ query: String((_f = (_e = (_d = toolArgs.query) !== null && _d !== void 0 ? _d : toolArgs.symbol) !== null && _e !== void 0 ? _e : toolArgs.target) !== null && _f !== void 0 ? _f : ""),
2538
2046
  project: projectRoot,
2539
2047
  results: resultLines,
2540
2048
  ms: Date.now() - startMs,
2541
2049
  error: result.isError ? text.slice(0, 200) : undefined,
2542
2050
  });
2543
2051
  }
2544
- catch (_h) { }
2545
- return result;
2546
- }));
2052
+ catch (_g) { }
2053
+ });
2054
+ // Register a tool, wrapping its handler with timing + query logging. Zod raw
2055
+ // shapes give us free input validation (the SDK rejects calls that violate
2056
+ // the schema before the handler runs); handlers stay defensively coded.
2057
+ function tool(name, config, handler) {
2058
+ server.registerTool(name, config, (rawArgs) => __awaiter(this, void 0, void 0, function* () {
2059
+ const args = (rawArgs !== null && rawArgs !== void 0 ? rawArgs : {});
2060
+ const startMs = Date.now();
2061
+ const result = yield handler(args);
2062
+ yield logToolCall(name, args, startMs, result);
2063
+ return result;
2064
+ }));
2065
+ }
2066
+ tool("semantic_search", {
2067
+ description: "Search code by meaning. Use scope:'all' for cross-project. Prefer CLI: gmax \"query\" --plain",
2068
+ inputSchema: {
2069
+ query: zod_1.z
2070
+ .string()
2071
+ .describe("Natural language query (5+ words recommended)"),
2072
+ limit: zod_1.z
2073
+ .number()
2074
+ .describe("Max results (default 3, max 50)")
2075
+ .optional(),
2076
+ root: zod_1.z
2077
+ .string()
2078
+ .describe("Search a different directory (absolute path)")
2079
+ .optional(),
2080
+ path: zod_1.z
2081
+ .string()
2082
+ .describe("Path prefix filter (e.g. 'src/auth/')")
2083
+ .optional(),
2084
+ detail: zod_1.z
2085
+ .string()
2086
+ .describe("'pointer' (default), 'code', or 'full'")
2087
+ .optional(),
2088
+ min_score: zod_1.z
2089
+ .number()
2090
+ .describe("Min score 0-1 (default 0)")
2091
+ .optional(),
2092
+ max_per_file: zod_1.z.number().describe("Max results per file").optional(),
2093
+ file: zod_1.z
2094
+ .string()
2095
+ .describe("Filename filter (e.g. 'syncer.ts')")
2096
+ .optional(),
2097
+ exclude: zod_1.z
2098
+ .string()
2099
+ .describe("Exclude path prefix (e.g. 'tests/')")
2100
+ .optional(),
2101
+ language: zod_1.z
2102
+ .string()
2103
+ .describe("Extension filter (e.g. 'ts', 'py')")
2104
+ .optional(),
2105
+ role: zod_1.z
2106
+ .string()
2107
+ .describe("'ORCHESTRATION', 'DEFINITION', or 'IMPLEMENTATION'")
2108
+ .optional(),
2109
+ context_lines: zod_1.z
2110
+ .number()
2111
+ .describe("Lines before/after chunk (max 20)")
2112
+ .optional(),
2113
+ mode: zod_1.z
2114
+ .string()
2115
+ .describe("'default' or 'symbol' (appends call graph)")
2116
+ .optional(),
2117
+ include_imports: zod_1.z
2118
+ .boolean()
2119
+ .describe("Prepend file imports to results")
2120
+ .optional(),
2121
+ name_pattern: zod_1.z
2122
+ .string()
2123
+ .describe("Regex filter on symbol name")
2124
+ .optional(),
2125
+ scope: zod_1.z
2126
+ .string()
2127
+ .describe("'project' (default) or 'all' (search everything)")
2128
+ .optional(),
2129
+ projects: zod_1.z
2130
+ .string()
2131
+ .describe("Project names to include (comma-separated)")
2132
+ .optional(),
2133
+ exclude_projects: zod_1.z
2134
+ .string()
2135
+ .describe("Project names to exclude (comma-separated)")
2136
+ .optional(),
2137
+ seed_files: zod_1.z
2138
+ .string()
2139
+ .describe("Bias results toward your working context: comma-separated paths you have open (e.g. 'src/lib/llm/server.ts'). On-topic chunks in these files get lifted; off-topic ones are not.")
2140
+ .optional(),
2141
+ seed_symbols: zod_1.z
2142
+ .string()
2143
+ .describe("Bias results toward identifiers you're working with: comma-separated symbol names. Chunks defining a seeded symbol are preferred over mere callers.")
2144
+ .optional(),
2145
+ },
2146
+ }, handleSemanticSearch);
2147
+ tool("code_skeleton", {
2148
+ description: "File structure with bodies collapsed (~4x fewer tokens). Accepts file, directory, or comma-separated paths.",
2149
+ inputSchema: {
2150
+ target: zod_1.z
2151
+ .string()
2152
+ .describe("File, directory, or comma-separated paths"),
2153
+ limit: zod_1.z
2154
+ .number()
2155
+ .describe("Max files for directory mode (default 10)")
2156
+ .optional(),
2157
+ format: zod_1.z.string().describe("'text' (default) or 'json'").optional(),
2158
+ },
2159
+ }, handleCodeSkeleton);
2160
+ tool("trace_calls", {
2161
+ description: "Call graph: importers, callers (multi-hop), callees with file:line.",
2162
+ inputSchema: {
2163
+ symbol: zod_1.z.string().describe("Function/class name to trace"),
2164
+ depth: zod_1.z
2165
+ .number()
2166
+ .describe("Caller depth (default 1, max 3)")
2167
+ .optional(),
2168
+ },
2169
+ }, handleTraceCalls);
2170
+ tool("extract_symbol", {
2171
+ description: "Extract complete function/class body by symbol name.",
2172
+ inputSchema: {
2173
+ symbol: zod_1.z.string().describe("Symbol name to extract"),
2174
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2175
+ include_imports: zod_1.z
2176
+ .boolean()
2177
+ .describe("Prepend file imports")
2178
+ .optional(),
2179
+ },
2180
+ }, handleExtractSymbol);
2181
+ tool("peek_symbol", {
2182
+ description: "Compact symbol overview: signature + callers + callees.",
2183
+ inputSchema: {
2184
+ symbol: zod_1.z.string().describe("Symbol name"),
2185
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2186
+ depth: zod_1.z
2187
+ .number()
2188
+ .describe("Caller depth (default 1, max 3)")
2189
+ .optional(),
2190
+ },
2191
+ }, handlePeekSymbol);
2192
+ tool("dead", {
2193
+ description: "Report whether a symbol has zero inbound callers in the indexed call graph. Returns DEAD, PUBLIC EXPORT (exported with no internal callers), or LIVE with caller count. Hypothesis, not proof: dynamic dispatch, reflection, and string-built call sites won't show up.",
2194
+ inputSchema: {
2195
+ symbol: zod_1.z.string().describe("Symbol name to check"),
2196
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2197
+ },
2198
+ }, handleDead);
2199
+ 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.",
2201
+ inputSchema: {
2202
+ root: zod_1.z
2203
+ .string()
2204
+ .describe("Project root (default: current)")
2205
+ .optional(),
2206
+ top: zod_1.z
2207
+ .number()
2208
+ .describe("How many of each category to return (default 10)")
2209
+ .optional(),
2210
+ },
2211
+ }, handleAudit);
2212
+ tool("get_neighbors", {
2213
+ 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`.",
2214
+ inputSchema: {
2215
+ symbol: zod_1.z.string().describe("Starting symbol"),
2216
+ direction: zod_1.z
2217
+ .enum(["callers", "callees"])
2218
+ .describe("Edge direction (default callees)")
2219
+ .optional(),
2220
+ max_hops: zod_1.z
2221
+ .number()
2222
+ .describe("Max hops (default 2, max 5)")
2223
+ .optional(),
2224
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2225
+ },
2226
+ }, handleGetNeighbors);
2227
+ tool("find_paths", {
2228
+ 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.",
2229
+ inputSchema: {
2230
+ from: zod_1.z.string().describe("Start symbol"),
2231
+ to: zod_1.z.string().describe("Target symbol"),
2232
+ direction: zod_1.z
2233
+ .enum(["callers", "callees"])
2234
+ .describe("Search direction (default callees)")
2235
+ .optional(),
2236
+ max_hops: zod_1.z
2237
+ .number()
2238
+ .describe("Max hops (default 6, max 10)")
2239
+ .optional(),
2240
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2241
+ },
2242
+ }, handleFindPaths);
2243
+ tool("subgraph_for_files", {
2244
+ 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.",
2245
+ inputSchema: {
2246
+ files: zod_1.z
2247
+ .array(zod_1.z.string())
2248
+ .describe("File paths (absolute or root-relative)"),
2249
+ root: zod_1.z.string().describe("Project root (absolute path)").optional(),
2250
+ },
2251
+ }, handleSubgraphForFiles);
2252
+ tool("list_symbols", {
2253
+ description: "List indexed symbols with role and export status.",
2254
+ inputSchema: {
2255
+ pattern: zod_1.z
2256
+ .string()
2257
+ .describe("Name filter (case-insensitive)")
2258
+ .optional(),
2259
+ limit: zod_1.z.number().describe("Max results (default 20)").optional(),
2260
+ path: zod_1.z.string().describe("Path prefix filter").optional(),
2261
+ },
2262
+ }, handleListSymbols);
2263
+ tool("index_status", {
2264
+ description: "Index health: chunks, files, projects, watcher status.",
2265
+ inputSchema: {},
2266
+ _meta: { "anthropic/alwaysLoad": true },
2267
+ }, handleIndexStatus);
2268
+ tool("list_projects", {
2269
+ description: "List every gmax-indexed project (name, root, status, chunk count). Use this to pick a search scope when you're not inside a specific repo: feed a name into semantic_search's projects: param, or use scope:'all'. The working-directory project is marked (current).",
2270
+ inputSchema: {},
2271
+ _meta: { "anthropic/alwaysLoad": true },
2272
+ }, handleListProjects);
2273
+ tool("summarize_directory", {
2274
+ description: "Generate LLM summaries for indexed chunks.",
2275
+ inputSchema: {
2276
+ path: zod_1.z
2277
+ .string()
2278
+ .describe("Directory to summarize (default: project root)")
2279
+ .optional(),
2280
+ limit: zod_1.z
2281
+ .number()
2282
+ .describe("Max chunks (default 200, max 5000)")
2283
+ .optional(),
2284
+ },
2285
+ }, handleSummarizeDirectory);
2286
+ tool("summarize_project", {
2287
+ description: "Project overview: languages, structure, roles, key symbols, entry points.",
2288
+ inputSchema: {
2289
+ root: zod_1.z
2290
+ .string()
2291
+ .describe("Project root (default: current)")
2292
+ .optional(),
2293
+ },
2294
+ }, handleSummarizeProject);
2295
+ tool("related_files", {
2296
+ description: "Find dependencies and dependents of a file by shared symbols.",
2297
+ inputSchema: {
2298
+ file: zod_1.z.string().describe("File path relative to project root"),
2299
+ limit: zod_1.z
2300
+ .number()
2301
+ .describe("Max results per direction (default 10)")
2302
+ .optional(),
2303
+ },
2304
+ }, handleRelatedFiles);
2305
+ tool("recent_changes", {
2306
+ description: "Recently modified indexed files with timestamps.",
2307
+ inputSchema: {
2308
+ limit: zod_1.z
2309
+ .number()
2310
+ .describe("Max files to return (default 20)")
2311
+ .optional(),
2312
+ root: zod_1.z
2313
+ .string()
2314
+ .describe("Project root (defaults to current project)")
2315
+ .optional(),
2316
+ },
2317
+ }, handleRecentChanges);
2318
+ tool("diff_changes", {
2319
+ description: "Search code scoped to git changes. Omit ref for uncommitted changes.",
2320
+ inputSchema: {
2321
+ ref: zod_1.z
2322
+ .string()
2323
+ .describe("Git ref to diff against (e.g. main, HEAD~5)")
2324
+ .optional(),
2325
+ query: zod_1.z
2326
+ .string()
2327
+ .describe("Semantic search within changed files")
2328
+ .optional(),
2329
+ limit: zod_1.z.number().describe("Max results (default 10)").optional(),
2330
+ role: zod_1.z
2331
+ .string()
2332
+ .describe("Filter by role: ORCHESTRATION, DEFINITION, IMPLEMENTATION")
2333
+ .optional(),
2334
+ },
2335
+ }, handleDiffChanges);
2336
+ tool("find_tests", {
2337
+ description: "Find tests that exercise a symbol or file via reverse call graph.",
2338
+ inputSchema: {
2339
+ target: zod_1.z.string().describe("Symbol name or file path"),
2340
+ depth: zod_1.z
2341
+ .number()
2342
+ .describe("Caller traversal depth 1-3 (default 1)")
2343
+ .optional(),
2344
+ },
2345
+ }, handleFindTests);
2346
+ tool("impact_analysis", {
2347
+ description: "Change impact: dependents and affected tests for a symbol or file.",
2348
+ inputSchema: {
2349
+ target: zod_1.z.string().describe("Symbol name or file path"),
2350
+ depth: zod_1.z
2351
+ .number()
2352
+ .describe("Caller traversal depth 1-3 (default 1)")
2353
+ .optional(),
2354
+ },
2355
+ }, handleImpactAnalysis);
2356
+ tool("find_similar", {
2357
+ description: "Find semantically similar code using vector similarity.",
2358
+ inputSchema: {
2359
+ target: zod_1.z.string().describe("Symbol name or file path"),
2360
+ limit: zod_1.z.number().describe("Max results (default 5)").optional(),
2361
+ threshold: zod_1.z
2362
+ .number()
2363
+ .describe("Min similarity 0-1 (default 0)")
2364
+ .optional(),
2365
+ },
2366
+ }, handleFindSimilar);
2367
+ tool("build_context", {
2368
+ description: "Token-budgeted topic summary (search + skeleton + extract).",
2369
+ inputSchema: {
2370
+ topic: zod_1.z
2371
+ .string()
2372
+ .describe("Natural language topic or directory path"),
2373
+ budget: zod_1.z.number().describe("Max tokens (default 4000)").optional(),
2374
+ limit: zod_1.z
2375
+ .number()
2376
+ .describe("Search result limit (default 10)")
2377
+ .optional(),
2378
+ },
2379
+ }, handleBuildContext);
2380
+ tool("investigate", {
2381
+ description: "Agentic codebase Q&A: a local LLM answers questions using search, trace, peek, impact, and related tools. Requires LLM to be enabled (gmax llm on).",
2382
+ inputSchema: {
2383
+ question: zod_1.z
2384
+ .string()
2385
+ .describe("Natural language question about the codebase"),
2386
+ max_rounds: zod_1.z
2387
+ .number()
2388
+ .describe("Max tool-call rounds (default 10)")
2389
+ .optional(),
2390
+ },
2391
+ }, handleInvestigate);
2392
+ tool("review_commit", {
2393
+ description: "Review a git commit for bugs, breaking changes, and security issues using local LLM + codebase context. Returns structured findings. Requires LLM to be enabled (gmax llm on).",
2394
+ inputSchema: {
2395
+ commit: zod_1.z
2396
+ .string()
2397
+ .describe("Git ref to review (default: HEAD)")
2398
+ .optional(),
2399
+ },
2400
+ }, handleReviewCommit);
2401
+ tool("review_report", {
2402
+ description: "Get the accumulated code review report for the current project. Returns findings from all reviewed commits.",
2403
+ inputSchema: {
2404
+ json: zod_1.z
2405
+ .boolean()
2406
+ .describe("Return raw JSON instead of text (default: false)")
2407
+ .optional(),
2408
+ },
2409
+ }, handleReviewReport);
2410
+ tool("review_risk", {
2411
+ description: "Deterministic risk ranking of the symbols a commit's diff touches, ordered by blast radius (inbound callers) × test presence × file churn. No LLM required — fast, graph + git only. Use to triage what a change endangers before a deep review.",
2412
+ inputSchema: {
2413
+ commit: zod_1.z
2414
+ .string()
2415
+ .describe("Git ref to analyze (default: HEAD)")
2416
+ .optional(),
2417
+ },
2418
+ }, handleReviewRisk);
2547
2419
  yield server.connect(transport);
2548
2420
  // Kick off index readiness check and watcher in background
2549
2421
  ensureIndexReady().catch((e) => console.error("[MCP] Index readiness check failed:", e));