@colbymchenry/codegraph 0.6.6 → 0.7.2

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 (221) hide show
  1. package/README.md +180 -502
  2. package/dist/bin/codegraph.d.ts +0 -5
  3. package/dist/bin/codegraph.d.ts.map +1 -1
  4. package/dist/bin/codegraph.js +217 -263
  5. package/dist/bin/codegraph.js.map +1 -1
  6. package/dist/bin/uninstall.d.ts +0 -1
  7. package/dist/bin/uninstall.d.ts.map +1 -1
  8. package/dist/bin/uninstall.js +3 -29
  9. package/dist/bin/uninstall.js.map +1 -1
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +0 -3
  12. package/dist/config.js.map +1 -1
  13. package/dist/context/index.d.ts +3 -5
  14. package/dist/context/index.d.ts.map +1 -1
  15. package/dist/context/index.js +497 -46
  16. package/dist/context/index.js.map +1 -1
  17. package/dist/db/migrations.d.ts +1 -1
  18. package/dist/db/migrations.d.ts.map +1 -1
  19. package/dist/db/migrations.js +10 -1
  20. package/dist/db/migrations.js.map +1 -1
  21. package/dist/db/queries.d.ts +53 -0
  22. package/dist/db/queries.d.ts.map +1 -1
  23. package/dist/db/queries.js +244 -24
  24. package/dist/db/queries.js.map +1 -1
  25. package/dist/db/schema.sql +1 -16
  26. package/dist/errors.d.ts +1 -1
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +1 -7
  29. package/dist/errors.js.map +1 -1
  30. package/dist/extraction/dfm-extractor.d.ts +31 -0
  31. package/dist/extraction/dfm-extractor.d.ts.map +1 -0
  32. package/dist/extraction/dfm-extractor.js +151 -0
  33. package/dist/extraction/dfm-extractor.js.map +1 -0
  34. package/dist/extraction/grammars.d.ts +9 -1
  35. package/dist/extraction/grammars.d.ts.map +1 -1
  36. package/dist/extraction/grammars.js +34 -2
  37. package/dist/extraction/grammars.js.map +1 -1
  38. package/dist/extraction/index.d.ts +7 -1
  39. package/dist/extraction/index.d.ts.map +1 -1
  40. package/dist/extraction/index.js +373 -29
  41. package/dist/extraction/index.js.map +1 -1
  42. package/dist/extraction/languages/c-cpp.d.ts +4 -0
  43. package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
  44. package/dist/extraction/languages/c-cpp.js +126 -0
  45. package/dist/extraction/languages/c-cpp.js.map +1 -0
  46. package/dist/extraction/languages/csharp.d.ts +3 -0
  47. package/dist/extraction/languages/csharp.d.ts.map +1 -0
  48. package/dist/extraction/languages/csharp.js +72 -0
  49. package/dist/extraction/languages/csharp.js.map +1 -0
  50. package/dist/extraction/languages/dart.d.ts +3 -0
  51. package/dist/extraction/languages/dart.d.ts.map +1 -0
  52. package/dist/extraction/languages/dart.js +192 -0
  53. package/dist/extraction/languages/dart.js.map +1 -0
  54. package/dist/extraction/languages/go.d.ts +3 -0
  55. package/dist/extraction/languages/go.d.ts.map +1 -0
  56. package/dist/extraction/languages/go.js +58 -0
  57. package/dist/extraction/languages/go.js.map +1 -0
  58. package/dist/extraction/languages/index.d.ts +10 -0
  59. package/dist/extraction/languages/index.d.ts.map +1 -0
  60. package/dist/extraction/languages/index.js +43 -0
  61. package/dist/extraction/languages/index.js.map +1 -0
  62. package/dist/extraction/languages/java.d.ts +3 -0
  63. package/dist/extraction/languages/java.d.ts.map +1 -0
  64. package/dist/extraction/languages/java.js +64 -0
  65. package/dist/extraction/languages/java.js.map +1 -0
  66. package/dist/extraction/languages/javascript.d.ts +3 -0
  67. package/dist/extraction/languages/javascript.d.ts.map +1 -0
  68. package/dist/extraction/languages/javascript.js +90 -0
  69. package/dist/extraction/languages/javascript.js.map +1 -0
  70. package/dist/extraction/languages/kotlin.d.ts +3 -0
  71. package/dist/extraction/languages/kotlin.d.ts.map +1 -0
  72. package/dist/extraction/languages/kotlin.js +253 -0
  73. package/dist/extraction/languages/kotlin.js.map +1 -0
  74. package/dist/extraction/languages/pascal.d.ts +3 -0
  75. package/dist/extraction/languages/pascal.d.ts.map +1 -0
  76. package/dist/extraction/languages/pascal.js +66 -0
  77. package/dist/extraction/languages/pascal.js.map +1 -0
  78. package/dist/extraction/languages/php.d.ts +3 -0
  79. package/dist/extraction/languages/php.d.ts.map +1 -0
  80. package/dist/extraction/languages/php.js +107 -0
  81. package/dist/extraction/languages/php.js.map +1 -0
  82. package/dist/extraction/languages/python.d.ts +3 -0
  83. package/dist/extraction/languages/python.d.ts.map +1 -0
  84. package/dist/extraction/languages/python.js +56 -0
  85. package/dist/extraction/languages/python.js.map +1 -0
  86. package/dist/extraction/languages/ruby.d.ts +3 -0
  87. package/dist/extraction/languages/ruby.d.ts.map +1 -0
  88. package/dist/extraction/languages/ruby.js +114 -0
  89. package/dist/extraction/languages/ruby.js.map +1 -0
  90. package/dist/extraction/languages/rust.d.ts +3 -0
  91. package/dist/extraction/languages/rust.d.ts.map +1 -0
  92. package/dist/extraction/languages/rust.js +109 -0
  93. package/dist/extraction/languages/rust.js.map +1 -0
  94. package/dist/extraction/languages/swift.d.ts +3 -0
  95. package/dist/extraction/languages/swift.d.ts.map +1 -0
  96. package/dist/extraction/languages/swift.js +91 -0
  97. package/dist/extraction/languages/swift.js.map +1 -0
  98. package/dist/extraction/languages/typescript.d.ts +3 -0
  99. package/dist/extraction/languages/typescript.d.ts.map +1 -0
  100. package/dist/extraction/languages/typescript.js +129 -0
  101. package/dist/extraction/languages/typescript.js.map +1 -0
  102. package/dist/extraction/liquid-extractor.d.ts +52 -0
  103. package/dist/extraction/liquid-extractor.d.ts.map +1 -0
  104. package/dist/extraction/liquid-extractor.js +313 -0
  105. package/dist/extraction/liquid-extractor.js.map +1 -0
  106. package/dist/extraction/parse-worker.d.ts +8 -0
  107. package/dist/extraction/parse-worker.d.ts.map +1 -0
  108. package/dist/extraction/parse-worker.js +57 -0
  109. package/dist/extraction/parse-worker.js.map +1 -0
  110. package/dist/extraction/svelte-extractor.d.ts +47 -0
  111. package/dist/extraction/svelte-extractor.d.ts.map +1 -0
  112. package/dist/extraction/svelte-extractor.js +230 -0
  113. package/dist/extraction/svelte-extractor.js.map +1 -0
  114. package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
  115. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
  116. package/dist/extraction/tree-sitter-helpers.js +103 -0
  117. package/dist/extraction/tree-sitter-helpers.js.map +1 -0
  118. package/dist/extraction/tree-sitter-types.d.ts +179 -0
  119. package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
  120. package/dist/extraction/tree-sitter-types.js +10 -0
  121. package/dist/extraction/tree-sitter-types.js.map +1 -0
  122. package/dist/extraction/tree-sitter.d.ts +67 -125
  123. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  124. package/dist/extraction/tree-sitter.js +1052 -1860
  125. package/dist/extraction/tree-sitter.js.map +1 -1
  126. package/dist/graph/traversal.d.ts.map +1 -1
  127. package/dist/graph/traversal.js +20 -2
  128. package/dist/graph/traversal.js.map +1 -1
  129. package/dist/index.d.ts +29 -53
  130. package/dist/index.d.ts.map +1 -1
  131. package/dist/index.js +88 -117
  132. package/dist/index.js.map +1 -1
  133. package/dist/installer/claude-md-template.d.ts +1 -1
  134. package/dist/installer/claude-md-template.d.ts.map +1 -1
  135. package/dist/installer/claude-md-template.js +15 -15
  136. package/dist/installer/config-writer.d.ts +2 -13
  137. package/dist/installer/config-writer.d.ts.map +1 -1
  138. package/dist/installer/config-writer.js +4 -87
  139. package/dist/installer/config-writer.js.map +1 -1
  140. package/dist/installer/index.d.ts +3 -4
  141. package/dist/installer/index.d.ts.map +1 -1
  142. package/dist/installer/index.js +118 -127
  143. package/dist/installer/index.js.map +1 -1
  144. package/dist/mcp/index.d.ts +5 -0
  145. package/dist/mcp/index.d.ts.map +1 -1
  146. package/dist/mcp/index.js +25 -4
  147. package/dist/mcp/index.js.map +1 -1
  148. package/dist/mcp/tools.d.ts +33 -0
  149. package/dist/mcp/tools.d.ts.map +1 -1
  150. package/dist/mcp/tools.js +405 -26
  151. package/dist/mcp/tools.js.map +1 -1
  152. package/dist/mcp/transport.d.ts.map +1 -1
  153. package/dist/mcp/transport.js +0 -2
  154. package/dist/mcp/transport.js.map +1 -1
  155. package/dist/resolution/frameworks/csharp.js +29 -84
  156. package/dist/resolution/frameworks/csharp.js.map +1 -1
  157. package/dist/resolution/frameworks/express.js +44 -48
  158. package/dist/resolution/frameworks/express.js.map +1 -1
  159. package/dist/resolution/frameworks/go.js +34 -70
  160. package/dist/resolution/frameworks/go.js.map +1 -1
  161. package/dist/resolution/frameworks/java.js +29 -87
  162. package/dist/resolution/frameworks/java.js.map +1 -1
  163. package/dist/resolution/frameworks/laravel.js +6 -6
  164. package/dist/resolution/frameworks/laravel.js.map +1 -1
  165. package/dist/resolution/frameworks/python.js +33 -98
  166. package/dist/resolution/frameworks/python.js.map +1 -1
  167. package/dist/resolution/frameworks/react.js +53 -76
  168. package/dist/resolution/frameworks/react.js.map +1 -1
  169. package/dist/resolution/frameworks/ruby.js +12 -24
  170. package/dist/resolution/frameworks/ruby.js.map +1 -1
  171. package/dist/resolution/frameworks/rust.js +26 -66
  172. package/dist/resolution/frameworks/rust.js.map +1 -1
  173. package/dist/resolution/frameworks/svelte.js +11 -31
  174. package/dist/resolution/frameworks/svelte.js.map +1 -1
  175. package/dist/resolution/frameworks/swift.js +42 -160
  176. package/dist/resolution/frameworks/swift.js.map +1 -1
  177. package/dist/resolution/index.d.ts +19 -6
  178. package/dist/resolution/index.d.ts.map +1 -1
  179. package/dist/resolution/index.js +300 -144
  180. package/dist/resolution/index.js.map +1 -1
  181. package/dist/resolution/name-matcher.d.ts +5 -0
  182. package/dist/resolution/name-matcher.d.ts.map +1 -1
  183. package/dist/resolution/name-matcher.js +148 -8
  184. package/dist/resolution/name-matcher.js.map +1 -1
  185. package/dist/resolution/types.d.ts +1 -1
  186. package/dist/resolution/types.d.ts.map +1 -1
  187. package/dist/search/query-utils.d.ts +26 -1
  188. package/dist/search/query-utils.d.ts.map +1 -1
  189. package/dist/search/query-utils.js +209 -9
  190. package/dist/search/query-utils.js.map +1 -1
  191. package/dist/sync/index.d.ts +2 -4
  192. package/dist/sync/index.d.ts.map +1 -1
  193. package/dist/sync/index.js +4 -3
  194. package/dist/sync/index.js.map +1 -1
  195. package/dist/sync/watcher.d.ts +81 -0
  196. package/dist/sync/watcher.d.ts.map +1 -0
  197. package/dist/sync/watcher.js +184 -0
  198. package/dist/sync/watcher.js.map +1 -0
  199. package/dist/types.d.ts +2 -2
  200. package/dist/types.d.ts.map +1 -1
  201. package/dist/types.js +0 -1
  202. package/dist/types.js.map +1 -1
  203. package/dist/ui/shimmer-progress.d.ts +11 -0
  204. package/dist/ui/shimmer-progress.d.ts.map +1 -0
  205. package/dist/ui/shimmer-progress.js +90 -0
  206. package/dist/ui/shimmer-progress.js.map +1 -0
  207. package/dist/ui/shimmer-worker.d.ts +2 -0
  208. package/dist/ui/shimmer-worker.d.ts.map +1 -0
  209. package/dist/ui/shimmer-worker.js +112 -0
  210. package/dist/ui/shimmer-worker.js.map +1 -0
  211. package/dist/ui/types.d.ts +17 -0
  212. package/dist/ui/types.d.ts.map +1 -0
  213. package/dist/ui/types.js +3 -0
  214. package/dist/ui/types.js.map +1 -0
  215. package/dist/vectors/embedder.js +1 -1
  216. package/dist/vectors/embedder.js.map +1 -1
  217. package/dist/visualizer/server.d.ts.map +1 -1
  218. package/dist/visualizer/server.js +3 -11
  219. package/dist/visualizer/server.js.map +1 -1
  220. package/package.json +7 -12
  221. package/scripts/postinstall.js +0 -68
package/dist/mcp/tools.js CHANGED
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  })();
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.ToolHandler = exports.tools = void 0;
42
+ exports.getExploreBudget = getExploreBudget;
42
43
  const index_1 = __importStar(require("../index"));
43
44
  const crypto_1 = require("crypto");
44
45
  const fs_1 = require("fs");
@@ -47,6 +48,22 @@ const os_1 = require("os");
47
48
  const path_1 = require("path");
48
49
  /** Maximum output length to prevent context bloat (characters) */
49
50
  const MAX_OUTPUT_LENGTH = 15000;
51
+ /**
52
+ * Calculate the recommended number of codegraph_explore calls based on project size.
53
+ * Larger codebases need more exploration calls to cover their surface area,
54
+ * but smaller ones should use fewer to avoid unnecessary overhead.
55
+ */
56
+ function getExploreBudget(fileCount) {
57
+ if (fileCount < 500)
58
+ return 1;
59
+ if (fileCount < 5000)
60
+ return 2;
61
+ if (fileCount < 15000)
62
+ return 3;
63
+ if (fileCount < 25000)
64
+ return 4;
65
+ return 5;
66
+ }
50
67
  /**
51
68
  * Mark a Claude session as having consulted MCP tools.
52
69
  * This enables Grep/Glob/Bash commands that would otherwise be blocked.
@@ -207,6 +224,26 @@ exports.tools = [
207
224
  required: ['symbol'],
208
225
  },
209
226
  },
227
+ {
228
+ name: 'codegraph_explore',
229
+ description: 'Deep exploration tool — returns comprehensive context for a topic in a SINGLE call. Groups all relevant source code by file (contiguous sections, not snippets), includes a relationship map, and uses deeper graph traversal. Designed to replace multiple codegraph_node + file Read calls. Use this instead of codegraph_context when you need thorough understanding.',
230
+ inputSchema: {
231
+ type: 'object',
232
+ properties: {
233
+ query: {
234
+ type: 'string',
235
+ description: 'What you want to understand (e.g., "undo redo system", "authentication flow", "how routing works")',
236
+ },
237
+ maxFiles: {
238
+ type: 'number',
239
+ description: 'Maximum number of files to include source code from (default: 12)',
240
+ default: 12,
241
+ },
242
+ projectPath: projectPathProperty,
243
+ },
244
+ required: ['query'],
245
+ },
246
+ },
210
247
  {
211
248
  name: 'codegraph_status',
212
249
  description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
@@ -276,6 +313,31 @@ class ToolHandler {
276
313
  hasDefaultCodeGraph() {
277
314
  return this.cg !== null;
278
315
  }
316
+ /**
317
+ * Get tool definitions with dynamic descriptions based on project size.
318
+ * The codegraph_explore tool description includes a budget recommendation
319
+ * scaled to the number of indexed files.
320
+ */
321
+ getTools() {
322
+ if (!this.cg)
323
+ return exports.tools;
324
+ try {
325
+ const stats = this.cg.getStats();
326
+ const budget = getExploreBudget(stats.fileCount);
327
+ return exports.tools.map(tool => {
328
+ if (tool.name === 'codegraph_explore') {
329
+ return {
330
+ ...tool,
331
+ description: `${tool.description} Budget: make at most ${budget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).`,
332
+ };
333
+ }
334
+ return tool;
335
+ });
336
+ }
337
+ catch {
338
+ return exports.tools;
339
+ }
340
+ }
279
341
  /**
280
342
  * Get CodeGraph instance for a project
281
343
  *
@@ -350,6 +412,8 @@ class ToolHandler {
350
412
  return await this.handleCallees(args);
351
413
  case 'codegraph_impact':
352
414
  return await this.handleImpact(args);
415
+ case 'codegraph_explore':
416
+ return await this.handleExplore(args);
353
417
  case 'codegraph_node':
354
418
  return await this.handleNode(args);
355
419
  case 'codegraph_status':
@@ -361,11 +425,6 @@ class ToolHandler {
361
425
  }
362
426
  }
363
427
  catch (err) {
364
- try {
365
- const { captureException } = require('../sentry');
366
- captureException(err, { tool: toolName });
367
- }
368
- catch { }
369
428
  return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
370
429
  }
371
430
  }
@@ -457,16 +516,25 @@ class ToolHandler {
457
516
  return symbol;
458
517
  const cg = this.getCodeGraph(args.projectPath);
459
518
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
460
- const match = this.findSymbol(cg, symbol);
461
- if (!match) {
519
+ const allMatches = this.findAllSymbols(cg, symbol);
520
+ if (allMatches.nodes.length === 0) {
462
521
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
463
522
  }
464
- const callers = cg.getCallers(match.node.id);
465
- if (callers.length === 0) {
466
- return this.textResult(`No callers found for "${symbol}"${match.note}`);
523
+ // Aggregate callers across all matching symbols
524
+ const seen = new Set();
525
+ const allCallers = [];
526
+ for (const node of allMatches.nodes) {
527
+ for (const c of cg.getCallers(node.id)) {
528
+ if (!seen.has(c.node.id)) {
529
+ seen.add(c.node.id);
530
+ allCallers.push(c.node);
531
+ }
532
+ }
467
533
  }
468
- const callerNodes = callers.slice(0, limit).map(c => c.node);
469
- const formatted = this.formatNodeList(callerNodes, `Callers of ${symbol}`) + match.note;
534
+ if (allCallers.length === 0) {
535
+ return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
536
+ }
537
+ const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
470
538
  return this.textResult(this.truncateOutput(formatted));
471
539
  }
472
540
  /**
@@ -478,16 +546,25 @@ class ToolHandler {
478
546
  return symbol;
479
547
  const cg = this.getCodeGraph(args.projectPath);
480
548
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
481
- const match = this.findSymbol(cg, symbol);
482
- if (!match) {
549
+ const allMatches = this.findAllSymbols(cg, symbol);
550
+ if (allMatches.nodes.length === 0) {
483
551
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
484
552
  }
485
- const callees = cg.getCallees(match.node.id);
486
- if (callees.length === 0) {
487
- return this.textResult(`No callees found for "${symbol}"${match.note}`);
553
+ // Aggregate callees across all matching symbols
554
+ const seen = new Set();
555
+ const allCallees = [];
556
+ for (const node of allMatches.nodes) {
557
+ for (const c of cg.getCallees(node.id)) {
558
+ if (!seen.has(c.node.id)) {
559
+ seen.add(c.node.id);
560
+ allCallees.push(c.node);
561
+ }
562
+ }
488
563
  }
489
- const calleeNodes = callees.slice(0, limit).map(c => c.node);
490
- const formatted = this.formatNodeList(calleeNodes, `Callees of ${symbol}`) + match.note;
564
+ if (allCallees.length === 0) {
565
+ return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
566
+ }
567
+ const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
491
568
  return this.textResult(this.truncateOutput(formatted));
492
569
  }
493
570
  /**
@@ -499,14 +576,275 @@ class ToolHandler {
499
576
  return symbol;
500
577
  const cg = this.getCodeGraph(args.projectPath);
501
578
  const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
502
- const match = this.findSymbol(cg, symbol);
503
- if (!match) {
579
+ const allMatches = this.findAllSymbols(cg, symbol);
580
+ if (allMatches.nodes.length === 0) {
504
581
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
505
582
  }
506
- const impact = cg.getImpactRadius(match.node.id, depth);
507
- const formatted = this.formatImpact(symbol, impact) + match.note;
583
+ // Aggregate impact across all matching symbols
584
+ const mergedNodes = new Map();
585
+ const mergedEdges = [];
586
+ const seenEdges = new Set();
587
+ for (const node of allMatches.nodes) {
588
+ const impact = cg.getImpactRadius(node.id, depth);
589
+ for (const [id, n] of impact.nodes) {
590
+ mergedNodes.set(id, n);
591
+ }
592
+ for (const e of impact.edges) {
593
+ const key = `${e.source}->${e.target}:${e.kind}`;
594
+ if (!seenEdges.has(key)) {
595
+ seenEdges.add(key);
596
+ mergedEdges.push(e);
597
+ }
598
+ }
599
+ }
600
+ const mergedImpact = {
601
+ nodes: mergedNodes,
602
+ edges: mergedEdges,
603
+ roots: allMatches.nodes.map(n => n.id),
604
+ };
605
+ const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
508
606
  return this.textResult(this.truncateOutput(formatted));
509
607
  }
608
+ /** Maximum output for explore tool — sized to stay under MCP client token limits (~10k tokens) */
609
+ static EXPLORE_MAX_OUTPUT = 35000;
610
+ /**
611
+ * Handle codegraph_explore — deep exploration in a single call
612
+ *
613
+ * Strategy: find relevant symbols via graph traversal, group by file,
614
+ * then read contiguous file sections covering all symbols per file.
615
+ * This replaces multiple codegraph_node + Read calls.
616
+ */
617
+ async handleExplore(args) {
618
+ const query = this.validateString(args.query, 'query');
619
+ if (typeof query !== 'string')
620
+ return query;
621
+ const cg = this.getCodeGraph(args.projectPath);
622
+ const maxFiles = (0, utils_1.clamp)(args.maxFiles || 12, 1, 20);
623
+ const projectRoot = cg.getProjectRoot();
624
+ // Step 1: Find relevant context with generous parameters
625
+ const subgraph = await cg.findRelevantContext(query, {
626
+ searchLimit: 8,
627
+ traversalDepth: 3,
628
+ maxNodes: 80,
629
+ minScore: 0.2,
630
+ });
631
+ if (subgraph.nodes.size === 0) {
632
+ return this.textResult(`No relevant code found for "${query}"`);
633
+ }
634
+ // Step 2: Group nodes by file, score by relevance
635
+ const fileGroups = new Map();
636
+ const entryNodeIds = new Set(subgraph.roots);
637
+ // Build a set of nodes directly connected to entry points (depth 1)
638
+ const connectedToEntry = new Set();
639
+ for (const edge of subgraph.edges) {
640
+ if (entryNodeIds.has(edge.source))
641
+ connectedToEntry.add(edge.target);
642
+ if (entryNodeIds.has(edge.target))
643
+ connectedToEntry.add(edge.source);
644
+ }
645
+ for (const node of subgraph.nodes.values()) {
646
+ // Skip import/export nodes — they add noise without information
647
+ if (node.kind === 'import' || node.kind === 'export')
648
+ continue;
649
+ const group = fileGroups.get(node.filePath) || { nodes: [], score: 0 };
650
+ group.nodes.push(node);
651
+ // Score: entry point nodes worth 10, directly connected worth 3, others worth 1
652
+ if (entryNodeIds.has(node.id)) {
653
+ group.score += 10;
654
+ }
655
+ else if (connectedToEntry.has(node.id)) {
656
+ group.score += 3;
657
+ }
658
+ else {
659
+ group.score += 1;
660
+ }
661
+ fileGroups.set(node.filePath, group);
662
+ }
663
+ // Only include files that have entry points or nodes directly connected to entry points
664
+ const relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
665
+ // Extract query terms for relevance checking
666
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(t => t.length >= 3);
667
+ // Sort files: highest relevance first, deprioritize low-value files
668
+ const sortedFiles = relevantFiles.sort((a, b) => {
669
+ const aPath = a[0].toLowerCase();
670
+ const bPath = b[0].toLowerCase();
671
+ // Check if any node name or file path relates to query terms
672
+ const hasQueryRelevance = (filePath, nodes) => {
673
+ const fp = filePath.toLowerCase();
674
+ if (queryTerms.some(t => fp.includes(t)))
675
+ return true;
676
+ return nodes.some(n => queryTerms.some(t => n.name.toLowerCase().includes(t)));
677
+ };
678
+ const aRelevant = hasQueryRelevance(aPath, a[1].nodes);
679
+ const bRelevant = hasQueryRelevance(bPath, b[1].nodes);
680
+ if (aRelevant !== bRelevant)
681
+ return aRelevant ? -1 : 1;
682
+ // Deprioritize test files, icon files, and i18n files
683
+ const isLowValue = (p) => /\/(tests?|__tests?__|spec)\//i.test(p) ||
684
+ /\bicons?\b/i.test(p) ||
685
+ /\bi18n\b/i.test(p);
686
+ const aLow = isLowValue(aPath);
687
+ const bLow = isLowValue(bPath);
688
+ if (aLow !== bLow)
689
+ return aLow ? 1 : -1;
690
+ if (a[1].score !== b[1].score)
691
+ return b[1].score - a[1].score;
692
+ return b[1].nodes.length - a[1].nodes.length;
693
+ });
694
+ // Step 3: Build relationship map
695
+ const lines = [
696
+ `## Exploration: ${query}`,
697
+ '',
698
+ `Found ${subgraph.nodes.size} symbols across ${fileGroups.size} files.`,
699
+ '',
700
+ ];
701
+ // Relationship map — show how symbols connect
702
+ const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
703
+ );
704
+ if (significantEdges.length > 0) {
705
+ lines.push('### Relationships');
706
+ lines.push('');
707
+ // Group edges by kind for readability
708
+ const byKind = new Map();
709
+ for (const edge of significantEdges) {
710
+ const sourceNode = subgraph.nodes.get(edge.source);
711
+ const targetNode = subgraph.nodes.get(edge.target);
712
+ if (!sourceNode || !targetNode)
713
+ continue;
714
+ const group = byKind.get(edge.kind) || [];
715
+ group.push({ source: sourceNode.name, target: targetNode.name });
716
+ byKind.set(edge.kind, group);
717
+ }
718
+ for (const [kind, edges] of byKind) {
719
+ // Show up to 15 relationships per kind
720
+ const shown = edges.slice(0, 15);
721
+ lines.push(`**${kind}:**`);
722
+ for (const e of shown) {
723
+ lines.push(`- ${e.source} → ${e.target}`);
724
+ }
725
+ if (edges.length > 15) {
726
+ lines.push(`- ... and ${edges.length - 15} more`);
727
+ }
728
+ lines.push('');
729
+ }
730
+ }
731
+ // Step 4: Read contiguous file sections
732
+ lines.push('### Source Code');
733
+ lines.push('');
734
+ let totalChars = lines.join('\n').length;
735
+ let filesIncluded = 0;
736
+ for (const [filePath, group] of sortedFiles) {
737
+ if (filesIncluded >= maxFiles)
738
+ break;
739
+ if (totalChars > ToolHandler.EXPLORE_MAX_OUTPUT * 0.9)
740
+ break;
741
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
742
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
743
+ continue;
744
+ let fileContent;
745
+ try {
746
+ fileContent = (0, fs_1.readFileSync)(absPath, 'utf-8');
747
+ }
748
+ catch {
749
+ continue;
750
+ }
751
+ const fileLines = fileContent.split('\n');
752
+ const lang = group.nodes[0]?.language || '';
753
+ // Cluster nearby symbols to avoid reading huge gaps between distant symbols.
754
+ // Sort by start line, then merge overlapping/adjacent ranges (within 15 lines).
755
+ const ranges = group.nodes
756
+ .filter(n => n.startLine > 0 && n.endLine > 0)
757
+ .map(n => ({ start: n.startLine, end: n.endLine, name: n.name, kind: n.kind }))
758
+ .sort((a, b) => a.start - b.start);
759
+ if (ranges.length === 0)
760
+ continue;
761
+ const GAP_THRESHOLD = 15; // merge sections within 15 lines of each other
762
+ const clusters = [];
763
+ let current = { start: ranges[0].start, end: ranges[0].end, symbols: [`${ranges[0].name}(${ranges[0].kind})`] };
764
+ for (let i = 1; i < ranges.length; i++) {
765
+ const r = ranges[i];
766
+ if (r.start <= current.end + GAP_THRESHOLD) {
767
+ current.end = Math.max(current.end, r.end);
768
+ current.symbols.push(`${r.name}(${r.kind})`);
769
+ }
770
+ else {
771
+ clusters.push(current);
772
+ current = { start: r.start, end: r.end, symbols: [`${r.name}(${r.kind})`] };
773
+ }
774
+ }
775
+ clusters.push(current);
776
+ // Build file section output from clusters
777
+ const contextPadding = 3;
778
+ let fileSection = '';
779
+ const allSymbols = [];
780
+ for (const cluster of clusters) {
781
+ const startIdx = Math.max(0, cluster.start - 1 - contextPadding);
782
+ const endIdx = Math.min(fileLines.length, cluster.end + contextPadding);
783
+ const section = fileLines.slice(startIdx, endIdx).join('\n');
784
+ if (fileSection.length > 0) {
785
+ fileSection += '\n\n// ... (gap) ...\n\n';
786
+ }
787
+ fileSection += section;
788
+ allSymbols.push(...cluster.symbols);
789
+ }
790
+ // Skip if this section would blow the output limit
791
+ if (totalChars + fileSection.length + 200 > ToolHandler.EXPLORE_MAX_OUTPUT) {
792
+ const budget = ToolHandler.EXPLORE_MAX_OUTPUT - totalChars - 200;
793
+ if (budget < 500)
794
+ break;
795
+ const trimmed = fileSection.slice(0, budget) + '\n// ... trimmed ...';
796
+ lines.push(`#### ${filePath} — ${allSymbols.join(', ')}`);
797
+ lines.push('');
798
+ lines.push('```' + lang);
799
+ lines.push(trimmed);
800
+ lines.push('```');
801
+ lines.push('');
802
+ totalChars += trimmed.length + 200;
803
+ filesIncluded++;
804
+ break;
805
+ }
806
+ lines.push(`#### ${filePath} — ${allSymbols.join(', ')}`);
807
+ lines.push('');
808
+ lines.push('```' + lang);
809
+ lines.push(fileSection);
810
+ lines.push('```');
811
+ lines.push('');
812
+ totalChars += fileSection.length + 200;
813
+ filesIncluded++;
814
+ }
815
+ // Add remaining files as references (from both relevant and peripheral files)
816
+ const remainingRelevant = sortedFiles.slice(filesIncluded);
817
+ const peripheralFiles = [...fileGroups.entries()]
818
+ .filter(([, group]) => group.score < 3)
819
+ .sort((a, b) => b[1].score - a[1].score);
820
+ const remainingFiles = [...remainingRelevant, ...peripheralFiles];
821
+ if (remainingFiles.length > 0) {
822
+ lines.push('### Additional relevant files (not shown)');
823
+ lines.push('');
824
+ for (const [filePath, group] of remainingFiles.slice(0, 10)) {
825
+ const symbols = group.nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
826
+ lines.push(`- ${filePath}: ${symbols}`);
827
+ }
828
+ if (remainingFiles.length > 10) {
829
+ lines.push(`- ... and ${remainingFiles.length - 10} more files`);
830
+ }
831
+ }
832
+ // Add completeness signal so agents know they don't need to re-read these files
833
+ lines.push('');
834
+ lines.push('---');
835
+ lines.push(`> **Complete source code is included above for ${filesIncluded} files.** You do NOT need to re-read these files — the relevant sections are already shown in full. Only use Read/Grep for files listed under "Additional relevant files" if you need more detail.`);
836
+ // Add explore budget note based on project size
837
+ try {
838
+ const stats = cg.getStats();
839
+ const budget = getExploreBudget(stats.fileCount);
840
+ lines.push('');
841
+ lines.push(`> **Explore budget: ${budget} calls max for this project (${stats.fileCount.toLocaleString()} files indexed).** Stop exploring and synthesize your answer once you've used ${budget} calls — do NOT make additional explore calls beyond this budget.`);
842
+ }
843
+ catch {
844
+ // Stats unavailable — skip budget note
845
+ }
846
+ return this.textResult(lines.join('\n'));
847
+ }
510
848
  /**
511
849
  * Handle codegraph_node
512
850
  */
@@ -715,13 +1053,36 @@ class ToolHandler {
715
1053
  * Find a symbol by name, handling disambiguation when multiple matches exist.
716
1054
  * Returns the best match and a note about alternatives if any.
717
1055
  */
1056
+ /**
1057
+ * Check if a node matches a symbol query, supporting both simple names and
1058
+ * qualified "Parent.child" notation (e.g., "Session.request" matches a method
1059
+ * named "request" inside a class named "Session").
1060
+ */
1061
+ matchesSymbol(node, symbol) {
1062
+ // Simple name match
1063
+ if (node.name === symbol)
1064
+ return true;
1065
+ // File basename match (e.g., "product-card" matches "product-card.liquid")
1066
+ if (node.kind === 'file' && node.name.replace(/\.[^.]+$/, '') === symbol)
1067
+ return true;
1068
+ // Qualified name match: "Parent.child" → look for "::Parent::child" in qualified_name
1069
+ if (symbol.includes('.')) {
1070
+ const parts = symbol.split('.');
1071
+ const qualifiedSuffix = parts.join('::');
1072
+ if (node.qualifiedName.includes(qualifiedSuffix))
1073
+ return true;
1074
+ }
1075
+ return false;
1076
+ }
718
1077
  findSymbol(cg, symbol) {
719
- const results = cg.searchNodes(symbol, { limit: 10 });
1078
+ // Use higher limit for qualified lookups (e.g., "Session.request") since the
1079
+ // target may rank lower in FTS when there are many partial matches
1080
+ const limit = symbol.includes('.') ? 50 : 10;
1081
+ const results = cg.searchNodes(symbol, { limit });
720
1082
  if (results.length === 0 || !results[0]) {
721
1083
  return null;
722
1084
  }
723
- // If only one result, or first is an exact name match, use it directly
724
- const exactMatches = results.filter(r => r.node.name === symbol);
1085
+ const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
725
1086
  if (exactMatches.length === 1) {
726
1087
  return { node: exactMatches[0].node, note: '' };
727
1088
  }
@@ -735,6 +1096,24 @@ class ToolHandler {
735
1096
  // No exact match, use best fuzzy match
736
1097
  return { node: results[0].node, note: '' };
737
1098
  }
1099
+ /**
1100
+ * Find ALL symbols matching a name. Used by callers/callees/impact to aggregate
1101
+ * results across all matching symbols (e.g., multiple classes with an `execute` method).
1102
+ */
1103
+ findAllSymbols(cg, symbol) {
1104
+ const results = cg.searchNodes(symbol, { limit: 50 });
1105
+ if (results.length === 0) {
1106
+ return { nodes: [], note: '' };
1107
+ }
1108
+ const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
1109
+ if (exactMatches.length <= 1) {
1110
+ const node = exactMatches[0]?.node ?? results[0].node;
1111
+ return { nodes: [node], note: '' };
1112
+ }
1113
+ const locations = exactMatches.map(r => `${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`);
1114
+ const note = `\n\n> **Note:** Aggregated results across ${exactMatches.length} symbols named "${symbol}": ${locations.join(', ')}`;
1115
+ return { nodes: exactMatches.map(r => r.node), note };
1116
+ }
738
1117
  /**
739
1118
  * Truncate output if it exceeds the maximum length
740
1119
  */