@colbymchenry/codegraph-darwin-x64 0.9.9 → 1.0.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 (296) hide show
  1. package/lib/dist/bin/codegraph.d.ts +1 -0
  2. package/lib/dist/bin/codegraph.d.ts.map +1 -1
  3. package/lib/dist/bin/codegraph.js +246 -3
  4. package/lib/dist/bin/codegraph.js.map +1 -1
  5. package/lib/dist/context/index.d.ts.map +1 -1
  6. package/lib/dist/context/index.js +7 -0
  7. package/lib/dist/context/index.js.map +1 -1
  8. package/lib/dist/db/index.d.ts.map +1 -1
  9. package/lib/dist/db/index.js +2 -1
  10. package/lib/dist/db/index.js.map +1 -1
  11. package/lib/dist/db/migrations.d.ts +1 -1
  12. package/lib/dist/db/migrations.d.ts.map +1 -1
  13. package/lib/dist/db/migrations.js +10 -1
  14. package/lib/dist/db/migrations.js.map +1 -1
  15. package/lib/dist/db/queries.d.ts +43 -0
  16. package/lib/dist/db/queries.d.ts.map +1 -1
  17. package/lib/dist/db/queries.js +103 -7
  18. package/lib/dist/db/queries.js.map +1 -1
  19. package/lib/dist/db/schema.sql +1 -0
  20. package/lib/dist/db/sqlite-adapter.d.ts +7 -0
  21. package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
  22. package/lib/dist/db/sqlite-adapter.js +3 -0
  23. package/lib/dist/db/sqlite-adapter.js.map +1 -1
  24. package/lib/dist/directory.d.ts +34 -2
  25. package/lib/dist/directory.d.ts.map +1 -1
  26. package/lib/dist/directory.js +129 -35
  27. package/lib/dist/directory.js.map +1 -1
  28. package/lib/dist/extraction/astro-extractor.d.ts +79 -0
  29. package/lib/dist/extraction/astro-extractor.d.ts.map +1 -0
  30. package/lib/dist/extraction/astro-extractor.js +320 -0
  31. package/lib/dist/extraction/astro-extractor.js.map +1 -0
  32. package/lib/dist/extraction/extraction-version.d.ts +25 -0
  33. package/lib/dist/extraction/extraction-version.d.ts.map +1 -0
  34. package/lib/dist/extraction/extraction-version.js +28 -0
  35. package/lib/dist/extraction/extraction-version.js.map +1 -0
  36. package/lib/dist/extraction/function-ref.d.ts +118 -0
  37. package/lib/dist/extraction/function-ref.d.ts.map +1 -0
  38. package/lib/dist/extraction/function-ref.js +727 -0
  39. package/lib/dist/extraction/function-ref.js.map +1 -0
  40. package/lib/dist/extraction/generated-detection.d.ts.map +1 -1
  41. package/lib/dist/extraction/generated-detection.js +3 -0
  42. package/lib/dist/extraction/generated-detection.js.map +1 -1
  43. package/lib/dist/extraction/grammars.d.ts +7 -1
  44. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  45. package/lib/dist/extraction/grammars.js +52 -4
  46. package/lib/dist/extraction/grammars.js.map +1 -1
  47. package/lib/dist/extraction/index.d.ts +34 -0
  48. package/lib/dist/extraction/index.d.ts.map +1 -1
  49. package/lib/dist/extraction/index.js +346 -62
  50. package/lib/dist/extraction/index.js.map +1 -1
  51. package/lib/dist/extraction/languages/c-cpp.d.ts +8 -0
  52. package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  53. package/lib/dist/extraction/languages/c-cpp.js +87 -28
  54. package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
  55. package/lib/dist/extraction/languages/csharp.d.ts +22 -0
  56. package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
  57. package/lib/dist/extraction/languages/csharp.js +84 -2
  58. package/lib/dist/extraction/languages/csharp.js.map +1 -1
  59. package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
  60. package/lib/dist/extraction/languages/dart.js +161 -1
  61. package/lib/dist/extraction/languages/dart.js.map +1 -1
  62. package/lib/dist/extraction/languages/go.d.ts.map +1 -1
  63. package/lib/dist/extraction/languages/go.js +43 -2
  64. package/lib/dist/extraction/languages/go.js.map +1 -1
  65. package/lib/dist/extraction/languages/index.d.ts.map +1 -1
  66. package/lib/dist/extraction/languages/index.js +2 -0
  67. package/lib/dist/extraction/languages/index.js.map +1 -1
  68. package/lib/dist/extraction/languages/java.d.ts.map +1 -1
  69. package/lib/dist/extraction/languages/java.js +42 -1
  70. package/lib/dist/extraction/languages/java.js.map +1 -1
  71. package/lib/dist/extraction/languages/javascript.d.ts.map +1 -1
  72. package/lib/dist/extraction/languages/javascript.js +16 -0
  73. package/lib/dist/extraction/languages/javascript.js.map +1 -1
  74. package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
  75. package/lib/dist/extraction/languages/kotlin.js +69 -0
  76. package/lib/dist/extraction/languages/kotlin.js.map +1 -1
  77. package/lib/dist/extraction/languages/objc.d.ts.map +1 -1
  78. package/lib/dist/extraction/languages/objc.js +42 -0
  79. package/lib/dist/extraction/languages/objc.js.map +1 -1
  80. package/lib/dist/extraction/languages/pascal.d.ts.map +1 -1
  81. package/lib/dist/extraction/languages/pascal.js +11 -0
  82. package/lib/dist/extraction/languages/pascal.js.map +1 -1
  83. package/lib/dist/extraction/languages/php.d.ts.map +1 -1
  84. package/lib/dist/extraction/languages/php.js +90 -1
  85. package/lib/dist/extraction/languages/php.js.map +1 -1
  86. package/lib/dist/extraction/languages/r.d.ts +3 -0
  87. package/lib/dist/extraction/languages/r.d.ts.map +1 -0
  88. package/lib/dist/extraction/languages/r.js +314 -0
  89. package/lib/dist/extraction/languages/r.js.map +1 -0
  90. package/lib/dist/extraction/languages/ruby.d.ts.map +1 -1
  91. package/lib/dist/extraction/languages/ruby.js +35 -0
  92. package/lib/dist/extraction/languages/ruby.js.map +1 -1
  93. package/lib/dist/extraction/languages/rust.d.ts.map +1 -1
  94. package/lib/dist/extraction/languages/rust.js +35 -2
  95. package/lib/dist/extraction/languages/rust.js.map +1 -1
  96. package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
  97. package/lib/dist/extraction/languages/scala.js +61 -1
  98. package/lib/dist/extraction/languages/scala.js.map +1 -1
  99. package/lib/dist/extraction/languages/swift.d.ts.map +1 -1
  100. package/lib/dist/extraction/languages/swift.js +61 -0
  101. package/lib/dist/extraction/languages/swift.js.map +1 -1
  102. package/lib/dist/extraction/languages/typescript.d.ts +13 -0
  103. package/lib/dist/extraction/languages/typescript.d.ts.map +1 -1
  104. package/lib/dist/extraction/languages/typescript.js +38 -0
  105. package/lib/dist/extraction/languages/typescript.js.map +1 -1
  106. package/lib/dist/extraction/liquid-extractor.d.ts +7 -0
  107. package/lib/dist/extraction/liquid-extractor.d.ts.map +1 -1
  108. package/lib/dist/extraction/liquid-extractor.js +53 -9
  109. package/lib/dist/extraction/liquid-extractor.js.map +1 -1
  110. package/lib/dist/extraction/razor-extractor.d.ts +42 -0
  111. package/lib/dist/extraction/razor-extractor.d.ts.map +1 -0
  112. package/lib/dist/extraction/razor-extractor.js +285 -0
  113. package/lib/dist/extraction/razor-extractor.js.map +1 -0
  114. package/lib/dist/extraction/svelte-extractor.d.ts.map +1 -1
  115. package/lib/dist/extraction/svelte-extractor.js +6 -3
  116. package/lib/dist/extraction/svelte-extractor.js.map +1 -1
  117. package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
  118. package/lib/dist/extraction/tree-sitter-helpers.js +59 -10
  119. package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
  120. package/lib/dist/extraction/tree-sitter-types.d.ts +33 -0
  121. package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  122. package/lib/dist/extraction/tree-sitter.d.ts +211 -0
  123. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  124. package/lib/dist/extraction/tree-sitter.js +1681 -49
  125. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  126. package/lib/dist/extraction/vue-extractor.d.ts +15 -0
  127. package/lib/dist/extraction/vue-extractor.d.ts.map +1 -1
  128. package/lib/dist/extraction/vue-extractor.js +94 -3
  129. package/lib/dist/extraction/vue-extractor.js.map +1 -1
  130. package/lib/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  131. package/lib/dist/extraction/wasm/tree-sitter-r.wasm +0 -0
  132. package/lib/dist/graph/queries.d.ts.map +1 -1
  133. package/lib/dist/graph/queries.js +13 -40
  134. package/lib/dist/graph/queries.js.map +1 -1
  135. package/lib/dist/graph/traversal.d.ts.map +1 -1
  136. package/lib/dist/graph/traversal.js +16 -4
  137. package/lib/dist/graph/traversal.js.map +1 -1
  138. package/lib/dist/index.d.ts +34 -2
  139. package/lib/dist/index.d.ts.map +1 -1
  140. package/lib/dist/index.js +90 -8
  141. package/lib/dist/index.js.map +1 -1
  142. package/lib/dist/installer/index.d.ts.map +1 -1
  143. package/lib/dist/installer/index.js +52 -2
  144. package/lib/dist/installer/index.js.map +1 -1
  145. package/lib/dist/installer/instructions-template.d.ts +34 -11
  146. package/lib/dist/installer/instructions-template.d.ts.map +1 -1
  147. package/lib/dist/installer/instructions-template.js +44 -12
  148. package/lib/dist/installer/instructions-template.js.map +1 -1
  149. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  150. package/lib/dist/installer/targets/claude.js +6 -10
  151. package/lib/dist/installer/targets/claude.js.map +1 -1
  152. package/lib/dist/installer/targets/codex.js +4 -6
  153. package/lib/dist/installer/targets/codex.js.map +1 -1
  154. package/lib/dist/installer/targets/gemini.js +4 -6
  155. package/lib/dist/installer/targets/gemini.js.map +1 -1
  156. package/lib/dist/installer/targets/opencode.d.ts +9 -1
  157. package/lib/dist/installer/targets/opencode.d.ts.map +1 -1
  158. package/lib/dist/installer/targets/opencode.js +91 -40
  159. package/lib/dist/installer/targets/opencode.js.map +1 -1
  160. package/lib/dist/installer/targets/shared.d.ts +14 -0
  161. package/lib/dist/installer/targets/shared.d.ts.map +1 -1
  162. package/lib/dist/installer/targets/shared.js +16 -0
  163. package/lib/dist/installer/targets/shared.js.map +1 -1
  164. package/lib/dist/mcp/daemon.d.ts +60 -1
  165. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  166. package/lib/dist/mcp/daemon.js +221 -8
  167. package/lib/dist/mcp/daemon.js.map +1 -1
  168. package/lib/dist/mcp/dynamic-boundaries.d.ts +41 -0
  169. package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -0
  170. package/lib/dist/mcp/dynamic-boundaries.js +359 -0
  171. package/lib/dist/mcp/dynamic-boundaries.js.map +1 -0
  172. package/lib/dist/mcp/index.d.ts.map +1 -1
  173. package/lib/dist/mcp/index.js +18 -9
  174. package/lib/dist/mcp/index.js.map +1 -1
  175. package/lib/dist/mcp/ppid-watchdog.d.ts +44 -0
  176. package/lib/dist/mcp/ppid-watchdog.d.ts.map +1 -0
  177. package/lib/dist/mcp/ppid-watchdog.js +27 -0
  178. package/lib/dist/mcp/ppid-watchdog.js.map +1 -0
  179. package/lib/dist/mcp/proxy.d.ts +6 -0
  180. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  181. package/lib/dist/mcp/proxy.js +153 -24
  182. package/lib/dist/mcp/proxy.js.map +1 -1
  183. package/lib/dist/mcp/server-instructions.d.ts +12 -1
  184. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  185. package/lib/dist/mcp/server-instructions.js +43 -16
  186. package/lib/dist/mcp/server-instructions.js.map +1 -1
  187. package/lib/dist/mcp/session.d.ts +2 -0
  188. package/lib/dist/mcp/session.d.ts.map +1 -1
  189. package/lib/dist/mcp/session.js +49 -2
  190. package/lib/dist/mcp/session.js.map +1 -1
  191. package/lib/dist/mcp/stdin-teardown.d.ts +27 -0
  192. package/lib/dist/mcp/stdin-teardown.d.ts.map +1 -0
  193. package/lib/dist/mcp/stdin-teardown.js +49 -0
  194. package/lib/dist/mcp/stdin-teardown.js.map +1 -0
  195. package/lib/dist/mcp/tools.d.ts +71 -0
  196. package/lib/dist/mcp/tools.d.ts.map +1 -1
  197. package/lib/dist/mcp/tools.js +703 -85
  198. package/lib/dist/mcp/tools.js.map +1 -1
  199. package/lib/dist/mcp/transport.d.ts.map +1 -1
  200. package/lib/dist/mcp/transport.js +18 -2
  201. package/lib/dist/mcp/transport.js.map +1 -1
  202. package/lib/dist/resolution/callback-synthesizer.d.ts +3 -3
  203. package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  204. package/lib/dist/resolution/callback-synthesizer.js +549 -21
  205. package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
  206. package/lib/dist/resolution/frameworks/astro.d.ts +9 -0
  207. package/lib/dist/resolution/frameworks/astro.d.ts.map +1 -0
  208. package/lib/dist/resolution/frameworks/astro.js +169 -0
  209. package/lib/dist/resolution/frameworks/astro.js.map +1 -0
  210. package/lib/dist/resolution/frameworks/expo-modules.d.ts.map +1 -1
  211. package/lib/dist/resolution/frameworks/expo-modules.js +6 -1
  212. package/lib/dist/resolution/frameworks/expo-modules.js.map +1 -1
  213. package/lib/dist/resolution/frameworks/index.d.ts +1 -0
  214. package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
  215. package/lib/dist/resolution/frameworks/index.js +5 -1
  216. package/lib/dist/resolution/frameworks/index.js.map +1 -1
  217. package/lib/dist/resolution/frameworks/java.js +6 -1
  218. package/lib/dist/resolution/frameworks/java.js.map +1 -1
  219. package/lib/dist/resolution/frameworks/python.d.ts.map +1 -1
  220. package/lib/dist/resolution/frameworks/python.js +7 -3
  221. package/lib/dist/resolution/frameworks/python.js.map +1 -1
  222. package/lib/dist/resolution/frameworks/react-native.d.ts.map +1 -1
  223. package/lib/dist/resolution/frameworks/react-native.js +53 -3
  224. package/lib/dist/resolution/frameworks/react-native.js.map +1 -1
  225. package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
  226. package/lib/dist/resolution/frameworks/react.js +15 -3
  227. package/lib/dist/resolution/frameworks/react.js.map +1 -1
  228. package/lib/dist/resolution/frameworks/svelte.js +5 -1
  229. package/lib/dist/resolution/frameworks/svelte.js.map +1 -1
  230. package/lib/dist/resolution/frameworks/vue.js +24 -27
  231. package/lib/dist/resolution/frameworks/vue.js.map +1 -1
  232. package/lib/dist/resolution/import-resolver.d.ts +10 -0
  233. package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
  234. package/lib/dist/resolution/import-resolver.js +564 -2
  235. package/lib/dist/resolution/import-resolver.js.map +1 -1
  236. package/lib/dist/resolution/index.d.ts +80 -0
  237. package/lib/dist/resolution/index.d.ts.map +1 -1
  238. package/lib/dist/resolution/index.js +457 -7
  239. package/lib/dist/resolution/index.js.map +1 -1
  240. package/lib/dist/resolution/name-matcher.d.ts +61 -0
  241. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  242. package/lib/dist/resolution/name-matcher.js +590 -14
  243. package/lib/dist/resolution/name-matcher.js.map +1 -1
  244. package/lib/dist/resolution/types.d.ts +27 -3
  245. package/lib/dist/resolution/types.d.ts.map +1 -1
  246. package/lib/dist/resolution/workspace-packages.d.ts +48 -0
  247. package/lib/dist/resolution/workspace-packages.d.ts.map +1 -0
  248. package/lib/dist/resolution/workspace-packages.js +208 -0
  249. package/lib/dist/resolution/workspace-packages.js.map +1 -0
  250. package/lib/dist/search/query-utils.d.ts +17 -1
  251. package/lib/dist/search/query-utils.d.ts.map +1 -1
  252. package/lib/dist/search/query-utils.js +79 -10
  253. package/lib/dist/search/query-utils.js.map +1 -1
  254. package/lib/dist/sync/watcher.d.ts +124 -32
  255. package/lib/dist/sync/watcher.d.ts.map +1 -1
  256. package/lib/dist/sync/watcher.js +326 -111
  257. package/lib/dist/sync/watcher.js.map +1 -1
  258. package/lib/dist/telemetry/index.d.ts +146 -0
  259. package/lib/dist/telemetry/index.d.ts.map +1 -0
  260. package/lib/dist/telemetry/index.js +544 -0
  261. package/lib/dist/telemetry/index.js.map +1 -0
  262. package/lib/dist/types.d.ts +17 -2
  263. package/lib/dist/types.d.ts.map +1 -1
  264. package/lib/dist/types.js +3 -0
  265. package/lib/dist/types.js.map +1 -1
  266. package/lib/dist/upgrade/index.d.ts +132 -0
  267. package/lib/dist/upgrade/index.d.ts.map +1 -0
  268. package/lib/dist/upgrade/index.js +462 -0
  269. package/lib/dist/upgrade/index.js.map +1 -0
  270. package/lib/dist/utils.d.ts +30 -24
  271. package/lib/dist/utils.d.ts.map +1 -1
  272. package/lib/dist/utils.js +64 -48
  273. package/lib/dist/utils.js.map +1 -1
  274. package/lib/node_modules/.package-lock.json +1 -29
  275. package/lib/package.json +1 -2
  276. package/package.json +1 -1
  277. package/lib/node_modules/chokidar/LICENSE +0 -21
  278. package/lib/node_modules/chokidar/README.md +0 -305
  279. package/lib/node_modules/chokidar/esm/handler.d.ts +0 -90
  280. package/lib/node_modules/chokidar/esm/handler.js +0 -629
  281. package/lib/node_modules/chokidar/esm/index.d.ts +0 -215
  282. package/lib/node_modules/chokidar/esm/index.js +0 -798
  283. package/lib/node_modules/chokidar/esm/package.json +0 -1
  284. package/lib/node_modules/chokidar/handler.d.ts +0 -90
  285. package/lib/node_modules/chokidar/handler.js +0 -635
  286. package/lib/node_modules/chokidar/index.d.ts +0 -215
  287. package/lib/node_modules/chokidar/index.js +0 -804
  288. package/lib/node_modules/chokidar/package.json +0 -69
  289. package/lib/node_modules/readdirp/LICENSE +0 -21
  290. package/lib/node_modules/readdirp/README.md +0 -120
  291. package/lib/node_modules/readdirp/esm/index.d.ts +0 -108
  292. package/lib/node_modules/readdirp/esm/index.js +0 -257
  293. package/lib/node_modules/readdirp/esm/package.json +0 -1
  294. package/lib/node_modules/readdirp/index.d.ts +0 -108
  295. package/lib/node_modules/readdirp/index.js +0 -263
  296. package/lib/node_modules/readdirp/package.json +0 -70
@@ -5,7 +5,7 @@
5
5
  * Defines the tools exposed by the CodeGraph MCP server.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.ToolHandler = exports.tools = void 0;
8
+ exports.ToolHandler = exports.tools = exports.PathRefusalError = exports.NotIndexedError = void 0;
9
9
  exports.getExploreBudget = getExploreBudget;
10
10
  exports.getExploreOutputBudget = getExploreOutputBudget;
11
11
  exports.formatStaleBanner = formatStaleBanner;
@@ -23,6 +23,28 @@ const query_utils_1 = require("../search/query-utils");
23
23
  const fs_1 = require("fs");
24
24
  const utils_1 = require("../utils");
25
25
  const generated_detection_1 = require("../extraction/generated-detection");
26
+ const dynamic_boundaries_1 = require("./dynamic-boundaries");
27
+ /**
28
+ * An expected, recoverable "codegraph can't serve this" condition — most
29
+ * importantly a project with no index. The dispatch catch converts these to
30
+ * SUCCESS-shaped responses (guidance text, NO isError): an `isError: true`
31
+ * early in a session teaches the agent the toolset is broken and it stops
32
+ * calling codegraph entirely (observed repeatedly), which is exactly wrong
33
+ * for conditions the agent can simply work around (use built-in tools for
34
+ * that codebase / pass projectPath). isError is reserved for "stop trying"
35
+ * cases: security refusals ({@link PathRefusalError}) and genuine
36
+ * malfunctions.
37
+ */
38
+ class NotIndexedError extends Error {
39
+ }
40
+ exports.NotIndexedError = NotIndexedError;
41
+ /**
42
+ * A security refusal (sensitive system path). Stays `isError: true` WITHOUT
43
+ * retry guidance — abandoning this path is the desired agent reaction.
44
+ */
45
+ class PathRefusalError extends Error {
46
+ }
47
+ exports.PathRefusalError = PathRefusalError;
26
48
  const path_1 = require("path");
27
49
  /** Maximum output length to prevent context bloat (characters) */
28
50
  const MAX_OUTPUT_LENGTH = 15000;
@@ -310,6 +332,10 @@ exports.tools = [
310
332
  type: 'string',
311
333
  description: 'Name of the function, method, or class to find callers for',
312
334
  },
335
+ file: {
336
+ type: 'string',
337
+ description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist (e.g. one UserService per app in a monorepo)',
338
+ },
313
339
  limit: {
314
340
  type: 'number',
315
341
  description: 'Maximum number of callers to return (default: 20)',
@@ -330,6 +356,10 @@ exports.tools = [
330
356
  type: 'string',
331
357
  description: 'Name of the function, method, or class to find callees for',
332
358
  },
359
+ file: {
360
+ type: 'string',
361
+ description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist',
362
+ },
333
363
  limit: {
334
364
  type: 'number',
335
365
  description: 'Maximum number of callees to return (default: 20)',
@@ -350,6 +380,10 @@ exports.tools = [
350
380
  type: 'string',
351
381
  description: 'Name of the symbol to analyze impact for',
352
382
  },
383
+ file: {
384
+ type: 'string',
385
+ description: 'Narrow to the definition in this file (path or suffix) when several same-named symbols exist',
386
+ },
353
387
  depth: {
354
388
  type: 'number',
355
389
  description: 'How many levels of dependencies to traverse (default: 2)',
@@ -362,41 +396,54 @@ exports.tools = [
362
396
  },
363
397
  {
364
398
  name: 'codegraph_node',
365
- description: 'SECONDARY (after codegraph_explore): get ONE symbol in full its location, signature, callers/callees trail, and verbatim body (includeCode=true). When the name is AMBIGUOUS (an overloaded method, or the same method name on different types), it returns EVERY matching definition\'s full body in a single call so you never need to Read a file to find the specific overload you want. For a heavily-overloaded name, pass `file` (and/or `line`) to pin the exact definition e.g. the `file:line` a trail or another tool already showed you. Reach for this when explore trimmed a body you need. Use codegraph_explore for several related symbols or the full flow.',
399
+ description: 'Two modes. (1) READ A FILE use INSTEAD of the Read tool: pass `file` (a path or basename) with no `symbol` and it returns that file\'s current on-disk source with line numbers, exactly the shape Read gives you (`<n>\\t<line>`, safe to Edit from), narrowable with `offset`/`limit` just like Read PLUS a one-line note of which files depend on it. Same bytes as Read, faster (served from the index), with the blast radius attached. Use it whenever you would Read a source file. (2) ONE SYMBOL you can name — its location, signature, verbatim source (includeCode=true) and caller/callee trail in one call, so before changing it you see what calls it and what your edit would break. For an AMBIGUOUS name it returns EVERY matching definition\'s body in one call (so you never Read a file to find the right overload); pass `file`/`line` to pin one. Use codegraph_explore for several related symbols or the full flow.',
366
400
  inputSchema: {
367
401
  type: 'object',
368
402
  properties: {
369
403
  symbol: {
370
404
  type: 'string',
371
- description: 'Name of the symbol to get details for',
405
+ description: 'Name of the symbol to read (symbol mode). Omit it and pass `file` alone to read a whole file like Read.',
372
406
  },
373
407
  includeCode: {
374
408
  type: 'boolean',
375
- description: 'Include full source code (default: false to minimize context)',
409
+ description: 'Symbol mode: include the symbol\'s full body (default: false). Ignored in file mode, which always returns source unless `symbolsOnly` is set.',
376
410
  default: false,
377
411
  },
378
412
  file: {
379
413
  type: 'string',
380
- description: 'Optional: disambiguate an overloaded name to the definition in this file (path or basename, e.g. "harness.rs").',
414
+ description: 'A file path or basename (e.g. "harness.rs", "src/auth/session.ts"). Pass it ALONE (no symbol) to READ the file like the Read tool — its full source with line numbers + which files depend on it. Or pass it WITH a symbol to disambiguate an overloaded name to the definition in this file.',
415
+ },
416
+ offset: {
417
+ type: 'number',
418
+ description: 'File mode: 1-based line to start reading from, exactly like Read\'s offset. Defaults to the start of the file.',
419
+ },
420
+ limit: {
421
+ type: 'number',
422
+ description: 'File mode: maximum number of lines to return, exactly like Read\'s limit. Defaults to the whole file (capped at 2000 lines, like Read).',
423
+ },
424
+ symbolsOnly: {
425
+ type: 'boolean',
426
+ description: 'File mode: return just the file\'s symbol map + dependents (a cheap structural overview) instead of its source.',
427
+ default: false,
381
428
  },
382
429
  line: {
383
430
  type: 'number',
384
- description: 'Optional: disambiguate to the definition at/around this line (use with the file:line a trail showed you).',
431
+ description: 'Symbol mode only: disambiguate to the definition at/around this line (use with the file:line a trail showed you).',
385
432
  },
386
433
  projectPath: projectPathProperty,
387
434
  },
388
- required: ['symbol'],
435
+ required: [],
389
436
  },
390
437
  },
391
438
  {
392
439
  name: 'codegraph_explore',
393
- description: 'PRIMARY TOOL — call FIRST for almost any question: how does X work, architecture, a bug, where/what is X, or surveying an area. Returns the verbatim source of the relevant symbols grouped by file in ONE capped call (Read-equivalent — do NOT re-open shown files). Query can be a natural-language question OR a bag of symbol/file names. Usually the ONLY call you need — answers without further search/node/Read/Grep.',
440
+ description: 'PRIMARY TOOL — call FIRST for almost any question OR before an edit: how does X work, architecture, a bug, where/what is X, surveying an area, or the symbols you are about to change. Returns the verbatim source of the relevant symbols grouped by file in ONE capped call (Read-equivalent — treat the shown source as already Read; do NOT re-open those files), plus the call path among them. Query can be a natural-language question OR a bag of symbol/file names. Usually the ONLY call you need — more accurate context, in far fewer tokens and round-trips than a search/Read/Grep loop.',
394
441
  inputSchema: {
395
442
  type: 'object',
396
443
  properties: {
397
444
  query: {
398
445
  type: 'string',
399
- description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). Use codegraph_search first to find relevant names.',
446
+ description: 'Symbol names, file names, or short code terms to explore (e.g., "AuthService loginUser session-manager", "GraphTraverser BFS impact traversal.ts"). For a flow question, name the symbols spanning the flow (e.g. "mutateElement renderScene"). A natural-language question works too — no prior codegraph_search needed.',
400
447
  },
401
448
  maxFiles: {
402
449
  type: 'number',
@@ -460,11 +507,35 @@ exports.tools = [
460
507
  */
461
508
  function getStaticTools() {
462
509
  const raw = process.env.CODEGRAPH_MCP_TOOLS;
463
- if (!raw || !raw.trim())
464
- return exports.tools;
510
+ if (!raw || !raw.trim()) {
511
+ return exports.tools.filter(t => DEFAULT_MCP_TOOLS.has(t.name.replace(/^codegraph_/, '')));
512
+ }
465
513
  const allow = new Set(raw.split(',').map(s => s.trim().replace(/^codegraph_/, '')).filter(Boolean));
466
514
  return allow.size ? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, ''))) : exports.tools;
467
515
  }
516
+ /**
517
+ * The MCP tools served by DEFAULT (short names). The other defined tools
518
+ * (callees, impact, files, status) remain fully functional — handlers stay,
519
+ * the library API and CLI are untouched, and `CODEGRAPH_MCP_TOOLS` re-enables
520
+ * any of them — they just aren't LISTED to agents anymore.
521
+ *
522
+ * Evidence for the cut (the "adapt the tool to the agent" principle —
523
+ * fewer tools = fewer mis-picks, and presence itself steers):
524
+ * - `codegraph_impact` appears in ZERO recorded eval runs ever — its
525
+ * blast-radius info already arrives inline on explore (the "Blast radius"
526
+ * section) and node (the dependents note), so agents never need the
527
+ * standalone tool.
528
+ * - `codegraph_callees` is redundant by construction: a symbol's body (which
529
+ * node returns) IS its callee list, plus the caller/callee trail.
530
+ * - `codegraph_files` / `codegraph_status`: the tiny-repo audit (see
531
+ * getTools) found they "reduce to one grep"; staleness banners already
532
+ * inline the pending-sync info on every read tool, and the CLI covers
533
+ * diagnostics.
534
+ * - `codegraph_callers` stays: exhaustive call-site enumeration (every
535
+ * caller with file:line, callback registrations labeled, one section per
536
+ * same-named definition) is the one job explore/node don't replicate.
537
+ */
538
+ const DEFAULT_MCP_TOOLS = new Set(['explore', 'node', 'search', 'callers']);
468
539
  /**
469
540
  * Tool handler that executes tools against a CodeGraph instance
470
541
  *
@@ -553,18 +624,22 @@ class ToolHandler {
553
624
  */
554
625
  getTools() {
555
626
  const allow = this.toolAllowlist();
627
+ // No explicit allowlist → the default 4-tool surface (see
628
+ // DEFAULT_MCP_TOOLS for the evidence). An allowlist replaces the
629
+ // default entirely, so any defined tool can be re-enabled.
556
630
  let visible = allow
557
631
  ? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, '')))
558
- : exports.tools;
632
+ : exports.tools.filter(t => DEFAULT_MCP_TOOLS.has(t.name.replace(/^codegraph_/, '')));
559
633
  if (!this.cg)
560
634
  return visible;
561
635
  try {
562
636
  const stats = this.cg.getStats();
563
637
  const budget = getExploreBudget(stats.fileCount);
564
638
  // Tiny-repo tool gating: on projects under TINY_REPO_FILE_THRESHOLD
565
- // files, only expose the 5 core tools (search, context, node,
566
- // explore, trace). The 5 omitted tools (callers, callees, impact,
567
- // status, files) reduce to one grep at this scale.
639
+ // files, only expose the core trio (search, node, explore) — one
640
+ // below even the 4-tool default: at this scale callers, too, reduces
641
+ // to one grep. (Historical note: the audit below ran when context and
642
+ // trace still existed; its "5 core tools" are today's trio.)
568
643
  //
569
644
  // n=2 audits ruled out cutting below 5 tools:
570
645
  // - 3-tool gate (search + context + trace): cost regressed on
@@ -619,13 +694,15 @@ class ToolHandler {
619
694
  if (!projectPath) {
620
695
  if (!this.cg) {
621
696
  const searched = this.defaultProjectHint ?? process.cwd();
622
- throw new Error('No CodeGraph project is loaded for this session.\n' +
697
+ throw new NotIndexedError('No CodeGraph project is loaded for this session.\n' +
623
698
  `Searched for a .codegraph/ directory starting from: ${searched}\n` +
624
- 'The index is likely fine this is a working-directory detection issue: ' +
699
+ 'If this project IS indexed, this is a working-directory detection issue: ' +
625
700
  "the MCP client launched the server outside your project and didn't report the " +
626
701
  'workspace root. Fix it either way:\n' +
627
702
  ' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project"\n' +
628
- ' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]');
703
+ ' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]\n' +
704
+ 'If the project simply has no index, continue with your built-in tools (Read/Grep/Glob) ' +
705
+ "and don't call codegraph again this session — the user can run 'codegraph init' to enable it.");
629
706
  }
630
707
  return this.cg;
631
708
  }
@@ -641,13 +718,16 @@ class ToolHandler {
641
718
  if ((0, fs_1.existsSync)(projectPath)) {
642
719
  const pathError = (0, utils_1.validateProjectPath)(projectPath);
643
720
  if (pathError) {
644
- throw new Error(pathError);
721
+ throw new PathRefusalError(pathError);
645
722
  }
646
723
  }
647
724
  // Walk up parent directories to find nearest .codegraph/
648
725
  const resolvedRoot = (0, directory_1.findNearestCodeGraphRoot)(projectPath);
649
726
  if (!resolvedRoot) {
650
- throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
727
+ throw new NotIndexedError(`The project at ${projectPath} isn't indexed with codegraph (no .codegraph/ directory found ` +
728
+ 'walking up from it), so codegraph cannot query it. Use your built-in tools (Read/Grep/Glob) ' +
729
+ "for that codebase instead, and don't call codegraph for it again this session. " +
730
+ "Indexing is the user's decision — they can run 'codegraph init' in that project to enable it.");
651
731
  }
652
732
  // If the path resolves to the default project, reuse the already-open
653
733
  // default instance rather than opening a SECOND connection to the same DB.
@@ -926,7 +1006,19 @@ class ToolHandler {
926
1006
  return this.withStalenessNotice(withWorktree, args.projectPath);
927
1007
  }
928
1008
  catch (err) {
929
- return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
1009
+ // Expected condition, not a malfunction: answer as a SUCCESS so the
1010
+ // agent keeps trusting the toolset for projects that ARE indexed.
1011
+ // (An isError here teaches session-long abandonment — see NotIndexedError.)
1012
+ if (err instanceof NotIndexedError) {
1013
+ return this.textResult(err.message);
1014
+ }
1015
+ // Security refusal: a clean error, no retry encouragement.
1016
+ if (err instanceof PathRefusalError) {
1017
+ return this.errorResult(err.message);
1018
+ }
1019
+ return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}. ` +
1020
+ 'This is an internal codegraph error — retry the call once; if it persists, ' +
1021
+ 'continue without codegraph for this task.');
930
1022
  }
931
1023
  }
932
1024
  /**
@@ -937,7 +1029,11 @@ class ToolHandler {
937
1029
  if (typeof query !== 'string')
938
1030
  return query;
939
1031
  const cg = this.getCodeGraph(args.projectPath);
940
- const kind = args.kind;
1032
+ const rawKind = args.kind;
1033
+ // The schema enum says 'type' (what agents naturally reach for); the
1034
+ // NodeKind is 'type_alias'. Without the mapping, kind: "type" silently
1035
+ // matched nothing — a filter value we advertise must work.
1036
+ const kind = rawKind === 'type' ? 'type_alias' : rawKind;
941
1037
  const rawLimit = Number(args.limit) || 10;
942
1038
  const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
943
1039
  const results = cg.searchNodes(query, {
@@ -958,6 +1054,43 @@ class ToolHandler {
958
1054
  const formatted = this.formatSearchResults(ranked);
959
1055
  return this.textResult(this.truncateOutput(formatted));
960
1056
  }
1057
+ /**
1058
+ * Group symbol matches into DISTINCT DEFINITIONS — one group per
1059
+ * (filePath, qualifiedName), so same-file overloads stay together while
1060
+ * unrelated same-named classes across a monorepo's apps (#764: one
1061
+ * `UserService` per NestJS app) are kept apart. Optionally narrowed by a
1062
+ * `file` path/suffix first.
1063
+ */
1064
+ groupDefinitions(nodes, fileFilter) {
1065
+ let pool = nodes;
1066
+ let filteredOut = false;
1067
+ if (fileFilter) {
1068
+ const wanted = fileFilter.replace(/^\.\//, '');
1069
+ const narrowed = pool.filter((n) => n.filePath === wanted || n.filePath.endsWith(wanted) || n.filePath.endsWith(`/${wanted}`));
1070
+ if (narrowed.length > 0) {
1071
+ pool = narrowed;
1072
+ }
1073
+ else {
1074
+ filteredOut = true;
1075
+ }
1076
+ }
1077
+ const byDef = new Map();
1078
+ for (const n of pool) {
1079
+ const key = `${n.filePath}|${n.qualifiedName}`;
1080
+ const group = byDef.get(key);
1081
+ if (group)
1082
+ group.push(n);
1083
+ else
1084
+ byDef.set(key, [n]);
1085
+ }
1086
+ return { groups: [...byDef.values()], filteredOut };
1087
+ }
1088
+ /** Section heading for one distinct definition in grouped output. */
1089
+ definitionHeading(group) {
1090
+ const head = group[0];
1091
+ const line = head.startLine ? `:${head.startLine}` : '';
1092
+ return `### ${head.qualifiedName} (${head.kind}) — ${head.filePath}${line}`;
1093
+ }
961
1094
  /**
962
1095
  * Handle codegraph_callers
963
1096
  */
@@ -967,26 +1100,64 @@ class ToolHandler {
967
1100
  return symbol;
968
1101
  const cg = this.getCodeGraph(args.projectPath);
969
1102
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1103
+ const fileFilter = typeof args.file === 'string' ? args.file : undefined;
970
1104
  const allMatches = this.findAllSymbols(cg, symbol);
971
1105
  if (allMatches.nodes.length === 0) {
972
1106
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
973
1107
  }
974
- // Aggregate callers across all matching symbols
975
- const seen = new Set();
976
- const allCallers = [];
977
- for (const node of allMatches.nodes) {
978
- for (const c of cg.getCallers(node.id)) {
979
- if (!seen.has(c.node.id)) {
980
- seen.add(c.node.id);
981
- allCallers.push(c.node);
1108
+ const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
1109
+ const filterNote = filteredOut
1110
+ ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
1111
+ : '';
1112
+ const collect = (defNodes) => {
1113
+ const seen = new Set();
1114
+ const callers = [];
1115
+ const labels = new Map();
1116
+ for (const node of defNodes) {
1117
+ for (const c of cg.getCallers(node.id)) {
1118
+ if (!seen.has(c.node.id)) {
1119
+ seen.add(c.node.id);
1120
+ callers.push(c.node);
1121
+ const label = this.edgeLabel(c.edge);
1122
+ if (label)
1123
+ labels.set(c.node.id, label);
1124
+ }
982
1125
  }
983
1126
  }
1127
+ return { callers, labels };
1128
+ };
1129
+ // Single definition (or same-file overloads): the familiar flat list.
1130
+ if (groups.length === 1) {
1131
+ const { callers, labels } = collect(groups[0]);
1132
+ if (callers.length === 0) {
1133
+ return this.textResult(`No callers found for "${symbol}"${allMatches.note}${filterNote}`);
1134
+ }
1135
+ // A successful `file` narrowing makes the multi-symbol aggregation note
1136
+ // stale — suppress it.
1137
+ const note = fileFilter && !filteredOut ? '' : allMatches.note;
1138
+ const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote;
1139
+ return this.textResult(this.truncateOutput(formatted));
1140
+ }
1141
+ // Multiple DISTINCT definitions (#764): one section per definition so an
1142
+ // agent never mistakes one app's callers for another's. Narrow with
1143
+ // `file` to focus a single definition.
1144
+ const lines = [
1145
+ `## Callers of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
1146
+ ];
1147
+ for (const group of groups) {
1148
+ const { callers, labels } = collect(group);
1149
+ lines.push('', this.definitionHeading(group));
1150
+ if (callers.length === 0) {
1151
+ lines.push('- (no callers)');
1152
+ continue;
1153
+ }
1154
+ for (const node of callers.slice(0, limit)) {
1155
+ const location = node.startLine ? `:${node.startLine}` : '';
1156
+ const label = labels.get(node.id);
1157
+ lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
1158
+ }
984
1159
  }
985
- if (allCallers.length === 0) {
986
- return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
987
- }
988
- const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
989
- return this.textResult(this.truncateOutput(formatted));
1160
+ return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
990
1161
  }
991
1162
  /**
992
1163
  * Handle codegraph_callees
@@ -997,26 +1168,61 @@ class ToolHandler {
997
1168
  return symbol;
998
1169
  const cg = this.getCodeGraph(args.projectPath);
999
1170
  const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1171
+ const fileFilter = typeof args.file === 'string' ? args.file : undefined;
1000
1172
  const allMatches = this.findAllSymbols(cg, symbol);
1001
1173
  if (allMatches.nodes.length === 0) {
1002
1174
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1003
1175
  }
1004
- // Aggregate callees across all matching symbols
1005
- const seen = new Set();
1006
- const allCallees = [];
1007
- for (const node of allMatches.nodes) {
1008
- for (const c of cg.getCallees(node.id)) {
1009
- if (!seen.has(c.node.id)) {
1010
- seen.add(c.node.id);
1011
- allCallees.push(c.node);
1176
+ const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
1177
+ const filterNote = filteredOut
1178
+ ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
1179
+ : '';
1180
+ const collect = (defNodes) => {
1181
+ const seen = new Set();
1182
+ const callees = [];
1183
+ const labels = new Map();
1184
+ for (const node of defNodes) {
1185
+ for (const c of cg.getCallees(node.id)) {
1186
+ if (!seen.has(c.node.id)) {
1187
+ seen.add(c.node.id);
1188
+ callees.push(c.node);
1189
+ const label = this.edgeLabel(c.edge);
1190
+ if (label)
1191
+ labels.set(c.node.id, label);
1192
+ }
1012
1193
  }
1013
1194
  }
1195
+ return { callees, labels };
1196
+ };
1197
+ if (groups.length === 1) {
1198
+ const { callees, labels } = collect(groups[0]);
1199
+ if (callees.length === 0) {
1200
+ return this.textResult(`No callees found for "${symbol}"${allMatches.note}${filterNote}`);
1201
+ }
1202
+ // A successful `file` narrowing makes the multi-symbol aggregation note
1203
+ // stale — suppress it.
1204
+ const note = fileFilter && !filteredOut ? '' : allMatches.note;
1205
+ const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels) + note + filterNote;
1206
+ return this.textResult(this.truncateOutput(formatted));
1207
+ }
1208
+ // Multiple DISTINCT definitions (#764): per-definition sections.
1209
+ const lines = [
1210
+ `## Callees of ${symbol} — ${groups.length} distinct definitions (narrow with \`file\`)`,
1211
+ ];
1212
+ for (const group of groups) {
1213
+ const { callees, labels } = collect(group);
1214
+ lines.push('', this.definitionHeading(group));
1215
+ if (callees.length === 0) {
1216
+ lines.push('- (no callees)');
1217
+ continue;
1218
+ }
1219
+ for (const node of callees.slice(0, limit)) {
1220
+ const location = node.startLine ? `:${node.startLine}` : '';
1221
+ const label = labels.get(node.id);
1222
+ lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
1223
+ }
1014
1224
  }
1015
- if (allCallees.length === 0) {
1016
- return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
1017
- }
1018
- const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
1019
- return this.textResult(this.truncateOutput(formatted));
1225
+ return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
1020
1226
  }
1021
1227
  /**
1022
1228
  * Handle codegraph_impact
@@ -1027,34 +1233,51 @@ class ToolHandler {
1027
1233
  return symbol;
1028
1234
  const cg = this.getCodeGraph(args.projectPath);
1029
1235
  const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
1236
+ const fileFilter = typeof args.file === 'string' ? args.file : undefined;
1030
1237
  const allMatches = this.findAllSymbols(cg, symbol);
1031
1238
  if (allMatches.nodes.length === 0) {
1032
1239
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1033
1240
  }
1034
- // Aggregate impact across all matching symbols
1035
- const mergedNodes = new Map();
1036
- const mergedEdges = [];
1037
- const seenEdges = new Set();
1038
- for (const node of allMatches.nodes) {
1039
- const impact = cg.getImpactRadius(node.id, depth);
1040
- for (const [id, n] of impact.nodes) {
1041
- mergedNodes.set(id, n);
1042
- }
1043
- for (const e of impact.edges) {
1044
- const key = `${e.source}->${e.target}:${e.kind}`;
1045
- if (!seenEdges.has(key)) {
1046
- seenEdges.add(key);
1047
- mergedEdges.push(e);
1241
+ const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter);
1242
+ const filterNote = filteredOut
1243
+ ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.`
1244
+ : '';
1245
+ const impactOf = (defNodes) => {
1246
+ const mergedNodes = new Map();
1247
+ const mergedEdges = [];
1248
+ const seenEdges = new Set();
1249
+ for (const node of defNodes) {
1250
+ const impact = cg.getImpactRadius(node.id, depth);
1251
+ for (const [id, n] of impact.nodes) {
1252
+ mergedNodes.set(id, n);
1253
+ }
1254
+ for (const e of impact.edges) {
1255
+ const key = `${e.source}->${e.target}:${e.kind}`;
1256
+ if (!seenEdges.has(key)) {
1257
+ seenEdges.add(key);
1258
+ mergedEdges.push(e);
1259
+ }
1048
1260
  }
1049
1261
  }
1050
- }
1051
- const mergedImpact = {
1052
- nodes: mergedNodes,
1053
- edges: mergedEdges,
1054
- roots: allMatches.nodes.map(n => n.id),
1262
+ return { nodes: mergedNodes, edges: mergedEdges, roots: defNodes.map((n) => n.id) };
1055
1263
  };
1056
- const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
1057
- return this.textResult(this.truncateOutput(formatted));
1264
+ // Single definition (or same-file overloads): the familiar merged report.
1265
+ if (groups.length === 1) {
1266
+ const formatted = this.formatImpact(symbol, impactOf(groups[0])) + (fileFilter && !filteredOut ? "" : allMatches.note) + filterNote;
1267
+ return this.textResult(this.truncateOutput(formatted));
1268
+ }
1269
+ // Multiple DISTINCT definitions (#764): a blast radius PER definition —
1270
+ // merging unrelated same-named classes (one UserService per monorepo app)
1271
+ // overstated impact and confused agents. Narrow with `file`.
1272
+ const sections = [
1273
+ `## Impact of ${symbol} — ${groups.length} distinct definitions (each with its own blast radius; narrow with \`file\`)`,
1274
+ ];
1275
+ for (const group of groups) {
1276
+ const head = group[0];
1277
+ const line = head.startLine ? `:${head.startLine}` : '';
1278
+ sections.push('', this.formatImpact(`${head.qualifiedName} (${head.filePath}${line})`, impactOf(group)));
1279
+ }
1280
+ return this.textResult(this.truncateOutput(sections.join('\n') + filterNote));
1058
1281
  }
1059
1282
  /**
1060
1283
  * Describe a synthesized (dynamic-dispatch) edge for human output: how the
@@ -1147,7 +1370,7 @@ class ToolHandler {
1147
1370
  // names (Class.method / Class::method) — the agent's most precise input,
1148
1371
  // resolved exactly by findAllSymbols. (The old strip mangled Class.method
1149
1372
  // into Class, throwing the method away.)
1150
- const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
1373
+ const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
1151
1374
  const tokens = [...new Set(query.split(/[\s,()[\]]+/)
1152
1375
  .map((t) => t.replace(FILE_EXT, '').trim())
1153
1376
  .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
@@ -1168,6 +1391,10 @@ class ToolHandler {
1168
1391
  // (`as_sql`, 110 defs across every Expression/Compiler subclass) is NOT here,
1169
1392
  // so naming it doesn't keep every backend variant full and flood the budget.
1170
1393
  const uniqueNamedNodeIds = new Set();
1394
+ // token → resolved node ids: drives the token-coverage check that gates
1395
+ // the dynamic-boundary scan (a token is covered when ANY of its nodes
1396
+ // lands on the main chain — overloads off the chain don't count against).
1397
+ const tokenNodes = new Map();
1171
1398
  for (const t of tokens) {
1172
1399
  const cands = this.findAllSymbols(cg, t).nodes.filter((n) => CALLABLE.has(n.kind));
1173
1400
  // A qualified or otherwise-specific name (<=3 hits) keeps all; an
@@ -1180,7 +1407,9 @@ class ToolHandler {
1180
1407
  const container = segs.length >= 2 ? segs[segs.length - 2] : '';
1181
1408
  return !!container && segPool.has(container);
1182
1409
  });
1183
- for (const n of pick.slice(0, 6)) {
1410
+ const kept = pick.slice(0, 6);
1411
+ tokenNodes.set(t, kept.map((n) => n.id));
1412
+ for (const n of kept) {
1184
1413
  named.set(n.id, n);
1185
1414
  if (specific)
1186
1415
  uniqueNamedNodeIds.add(n.id);
@@ -1188,8 +1417,19 @@ class ToolHandler {
1188
1417
  if (named.size > 40)
1189
1418
  break;
1190
1419
  }
1191
- if (named.size < 2)
1192
- return EMPTY;
1420
+ if (named.size < 2) {
1421
+ // The agent named a flow but only one side resolved (the other end is
1422
+ // anonymous / runtime-registered / not extracted). The resolved side's
1423
+ // body may still hold the dynamic-dispatch site that EXPLAINS the gap —
1424
+ // surface that instead of silently returning nothing.
1425
+ if (named.size === 0)
1426
+ return EMPTY;
1427
+ const boundaries = this.buildDynamicBoundaries(cg, [...named.values()], named);
1428
+ if (!boundaries)
1429
+ return EMPTY;
1430
+ const text = boundaries + '> Full source for these symbols is below.\n';
1431
+ return { text, pathNodeIds: new Set(), namedNodeIds: new Set(named.keys()), uniqueNamedNodeIds };
1432
+ }
1193
1433
  const MAX_HOPS = 7;
1194
1434
  let best = null;
1195
1435
  // BFS the full call graph (incl. synth edges) from each named seed, but
@@ -1237,6 +1477,43 @@ class ToolHandler {
1237
1477
  }
1238
1478
  const hasMain = !!best && best.length >= 3;
1239
1479
  const pathIds = new Set((best ?? []).map((s) => s.node.id));
1480
+ // Dynamic-boundary scan (#687) — fires ONLY when the flow the agent
1481
+ // asked about did not fully connect: some token resolved to nodes but
1482
+ // none of them sit on the main chain (or there is no chain at all). A
1483
+ // healthy flow skips this entirely. Scan order: the chain's dead end
1484
+ // first (where the partial flow stops), then the disconnected symbols,
1485
+ // agent-specific (unique-named) ones first.
1486
+ let boundaryText = '';
1487
+ {
1488
+ const uncovered = [];
1489
+ if (!hasMain) {
1490
+ // No rendered chain — but a 2-node chain still CONNECTS its two
1491
+ // endpoints (e.g. via one synthesized hop, surfaced below as a
1492
+ // dynamic-dispatch link). Only nodes off that short chain are
1493
+ // unexplained breaks worth scanning.
1494
+ for (const n of named.values())
1495
+ if (!pathIds.has(n.id))
1496
+ uncovered.push(n);
1497
+ }
1498
+ else {
1499
+ for (const ids of tokenNodes.values()) {
1500
+ if (ids.length === 0 || ids.some((id) => pathIds.has(id)))
1501
+ continue;
1502
+ for (const id of ids) {
1503
+ const n = named.get(id);
1504
+ if (n)
1505
+ uncovered.push(n);
1506
+ }
1507
+ }
1508
+ }
1509
+ if (uncovered.length > 0) {
1510
+ const scanList = [];
1511
+ if (hasMain)
1512
+ scanList.push(best[best.length - 1].node);
1513
+ scanList.push(...uncovered.sort((a, b) => (uniqueNamedNodeIds.has(b.id) ? 1 : 0) - (uniqueNamedNodeIds.has(a.id) ? 1 : 0)));
1514
+ boundaryText = this.buildDynamicBoundaries(cg, scanList, named);
1515
+ }
1516
+ }
1240
1517
  // Supplementary: dynamic-dispatch (synthesized) edges incident to a NAMED
1241
1518
  // symbol — the indirect hops an agent would otherwise grep/Read to
1242
1519
  // reconstruct ("where do the appended `validators` actually run?"). The
@@ -1255,8 +1532,13 @@ class ToolHandler {
1255
1532
  break;
1256
1533
  if (edge.provenance !== 'heuristic' || other.id === n.id)
1257
1534
  continue;
1258
- if (pathIds.has(edge.source) && pathIds.has(edge.target))
1259
- continue; // already in the main chain
1535
+ // "Already in the main chain" only applies when a chain RENDERS
1536
+ // (hasMain). A 2-node chain populates pathIds but renders nothing,
1537
+ // so a direct synthesized hop between two named symbols (custom
1538
+ // EventBus emit→handler, #687) was invisible — too short for Flow,
1539
+ // skipped here as in-chain. Surface it.
1540
+ if (hasMain && pathIds.has(edge.source) && pathIds.has(edge.target))
1541
+ continue;
1260
1542
  const src = edge.source === n.id ? n : other;
1261
1543
  const tgt = edge.source === n.id ? other : n;
1262
1544
  const key = `${src.name}>${tgt.name}`;
@@ -1267,7 +1549,7 @@ class ToolHandler {
1267
1549
  synthLines.push(`- ${src.name} → ${tgt.name} [${note ? note.compact : edge.kind}]`);
1268
1550
  }
1269
1551
  }
1270
- if (!hasMain && synthLines.length === 0)
1552
+ if (!hasMain && synthLines.length === 0 && !boundaryText)
1271
1553
  return EMPTY;
1272
1554
  const out = [];
1273
1555
  if (hasMain) {
@@ -1285,6 +1567,8 @@ class ToolHandler {
1285
1567
  if (synthLines.length) {
1286
1568
  out.push('## Dynamic-dispatch links among your symbols', '(synthesized — the indirect hops grep/Read would reconstruct; the `@file:line` is the wiring site)', '', ...synthLines, '');
1287
1569
  }
1570
+ if (boundaryText)
1571
+ out.push(boundaryText);
1288
1572
  out.push('> Full source for these symbols is below — the call flow among them, followed by their bodies.', '');
1289
1573
  // namedNodeIds = every callable the agent explicitly named (a superset of
1290
1574
  // the spine). A file holding one is something the agent asked to SEE, so it
@@ -1297,6 +1581,163 @@ class ToolHandler {
1297
1581
  return EMPTY;
1298
1582
  }
1299
1583
  }
1584
+ /**
1585
+ * Dynamic-boundary surfacing (#687): when the flow among the agent's named
1586
+ * symbols does not fully connect, scan the disconnected symbols' bodies for
1587
+ * dynamic-dispatch sites (computed member calls, getattr, reflection, typed
1588
+ * message buses, runtime-keyed emits) and ANNOUNCE the boundary — the exact
1589
+ * site, the form, and (when a key is statically visible) candidate targets —
1590
+ * instead of guessing edges. The answer to "how does A reach B" when no
1591
+ * static path exists IS the dispatch site: that's where the flow continues
1592
+ * at runtime. Query-time, deterministic, zero graph mutation; a fully
1593
+ * connected flow never reaches this method.
1594
+ */
1595
+ buildDynamicBoundaries(cg, scanList, named) {
1596
+ const MAX_NOTES = 4; // boundary bullets per explore
1597
+ const MAX_SCAN = 8; // bodies scanned
1598
+ const MAX_TOTAL_CHARS = 200_000;
1599
+ let projectRoot;
1600
+ try {
1601
+ projectRoot = cg.getProjectRoot();
1602
+ }
1603
+ catch {
1604
+ return '';
1605
+ }
1606
+ const notes = [];
1607
+ const seenNode = new Set();
1608
+ const seenSite = new Set();
1609
+ let scanned = 0, charsScanned = 0;
1610
+ for (const node of scanList) {
1611
+ if (notes.length >= MAX_NOTES || scanned >= MAX_SCAN || charsScanned > MAX_TOTAL_CHARS)
1612
+ break;
1613
+ if (seenNode.has(node.id) || !node.startLine || !node.endLine)
1614
+ continue;
1615
+ seenNode.add(node.id);
1616
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, node.filePath);
1617
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
1618
+ continue;
1619
+ let content;
1620
+ try {
1621
+ content = (0, fs_1.readFileSync)(absPath, 'utf-8');
1622
+ }
1623
+ catch {
1624
+ continue;
1625
+ }
1626
+ const body = content.split('\n').slice(node.startLine - 1, node.endLine).join('\n');
1627
+ scanned++;
1628
+ charsScanned += body.length;
1629
+ for (const m of (0, dynamic_boundaries_1.scanDynamicDispatch)(body, node.language || '', node.startLine)) {
1630
+ if (notes.length >= MAX_NOTES)
1631
+ break;
1632
+ const siteKey = `${node.filePath}:${m.line}:${m.form}`;
1633
+ if (seenSite.has(siteKey))
1634
+ continue;
1635
+ seenSite.add(siteKey);
1636
+ const more = m.moreSites ? ` (+${m.moreSites} more such site${m.moreSites > 1 ? 's' : ''} in this body)` : '';
1637
+ notes.push(`- \`${node.name}\` (${node.filePath}:${m.line}) — ${m.label}: \`${m.snippet}\`${more}`);
1638
+ if (m.key) {
1639
+ const cand = this.boundaryCandidates(cg, m.key, !!m.keyIsType, named, node.id);
1640
+ if (cand)
1641
+ notes.push(` ${cand}`);
1642
+ }
1643
+ }
1644
+ }
1645
+ if (notes.length === 0)
1646
+ return '';
1647
+ return [
1648
+ '## Dynamic boundaries (the static path ends at runtime dispatch)',
1649
+ '',
1650
+ ...notes,
1651
+ '',
1652
+ '> These sites choose their call target at runtime (registry / bus / reflection) — the site shown IS where the flow continues. To follow it, run codegraph_explore or codegraph_node on a candidate; source for the sites above is included below.',
1653
+ '',
1654
+ ].join('\n');
1655
+ }
1656
+ /**
1657
+ * Shortlist candidate runtime targets for a dispatch key surfaced by
1658
+ * {@link buildDynamicBoundaries}. Exact conventional names first (`save` →
1659
+ * `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a
1660
+ * normalized-containment post-filter (FTS camel-splitting is fuzzier than a
1661
+ * candidate list should be). Symbols the agent already named sort first and
1662
+ * are marked — that's the "you were right, here's the wiring" case.
1663
+ */
1664
+ boundaryCandidates(cg, key, keyIsType, named, selfId) {
1665
+ const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']);
1666
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
1667
+ const keyNorm = norm(key);
1668
+ if (keyNorm.length < 3)
1669
+ return '';
1670
+ const cands = new Map();
1671
+ const consider = (n) => {
1672
+ if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id))
1673
+ return;
1674
+ const nameNorm = norm(n.name || '');
1675
+ if (nameNorm.length < 3)
1676
+ return;
1677
+ if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm))
1678
+ return;
1679
+ cands.set(n.id, n);
1680
+ };
1681
+ const cap = key.charAt(0).toUpperCase() + key.slice(1);
1682
+ const probes = keyIsType
1683
+ ? [`${key}Handler`, key]
1684
+ : [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`];
1685
+ for (const p of probes) {
1686
+ try {
1687
+ for (const n of cg.getNodesByName(p))
1688
+ consider(n);
1689
+ }
1690
+ catch { /* exact probe miss is fine */ }
1691
+ }
1692
+ let raw = 0;
1693
+ try {
1694
+ const results = cg.searchNodes(key, { limit: 12 });
1695
+ raw = results.length;
1696
+ for (const r of results)
1697
+ consider(r.node);
1698
+ }
1699
+ catch { /* FTS syntax edge — exact probes already ran */ }
1700
+ if (cands.size === 0) {
1701
+ return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : '';
1702
+ }
1703
+ // A constructor candidate duplicates its class: extractors emit ctors as
1704
+ // METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class.
1705
+ const all = [...cands.values()];
1706
+ const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`));
1707
+ const namedNames = new Set([...named.values()].map((n) => n.name));
1708
+ const isNamed = (n) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class
1709
+ const list = all
1710
+ .filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`)))
1711
+ .sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0))
1712
+ .slice(0, 4)
1713
+ .map((n) => {
1714
+ // Typed-bus convention: the runtime target is the candidate class's
1715
+ // Handle/Execute/Consume method — name the exact node, not just the class.
1716
+ let display = n.qualifiedName || n.name;
1717
+ let at = `${n.filePath}:${n.startLine}`;
1718
+ if (keyIsType && n.kind === 'class') {
1719
+ try {
1720
+ const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i;
1721
+ const method = cg.getOutgoingEdges(n.id)
1722
+ .filter((e) => e.kind === 'contains')
1723
+ .map((e) => { try {
1724
+ return cg.getNode(e.target);
1725
+ }
1726
+ catch {
1727
+ return null;
1728
+ } })
1729
+ .find((c) => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name));
1730
+ if (method) {
1731
+ display = `${n.name}.${method.name}`;
1732
+ at = `${method.filePath}:${method.startLine}`;
1733
+ }
1734
+ }
1735
+ catch { /* class without resolvable members — show the class itself */ }
1736
+ }
1737
+ return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`;
1738
+ });
1739
+ return `candidates for key \`${key}\`: ${list.join(', ')}`;
1740
+ }
1300
1741
  /**
1301
1742
  * Compact "blast radius" for the entry symbols of an explore result: who
1302
1743
  * depends on each (callers) and which test files cover it — LOCATIONS ONLY,
@@ -1527,7 +1968,7 @@ class ToolHandler {
1527
1968
  // agent explicitly named is in the subgraph and its file is scored.
1528
1969
  const namedSeedIds = new Set();
1529
1970
  {
1530
- const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte)$/i;
1971
+ const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i;
1531
1972
  const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
1532
1973
  const isTestPath = (p) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p);
1533
1974
  const bodyLines = (n) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine);
@@ -1538,8 +1979,12 @@ class ToolHandler {
1538
1979
  // agent writes "DataRequest task validate", the `task`/`validate` it wants
1539
1980
  // are DataRequest's, NOT the same-named overloads in Validation.swift /
1540
1981
  // Concurrency.swift / the abstract base. Used below to bias overloaded
1541
- // names toward the file/class the query also names.
1542
- const typeTokens = tokens.filter((o) => /^[A-Z][A-Za-z0-9]{3,}/.test(o));
1982
+ // names toward the file/class the query also names. EXCLUDE the project
1983
+ // name (a PascalCase token a user naturally includes) it names the whole
1984
+ // repo, so biasing toward it just pulls overloads to whichever stack
1985
+ // embeds it, re-burying the rest (#720).
1986
+ const projectNameTokens = cg.getProjectNameTokens();
1987
+ const typeTokens = tokens.filter((o) => /^[A-Z][A-Za-z0-9]{3,}/.test(o) && !projectNameTokens.has((0, query_utils_1.normalizeNameToken)(o)));
1543
1988
  const inNamedContext = (n) => typeTokens.some((ct) => {
1544
1989
  const lc = ct.toLowerCase();
1545
1990
  return n.filePath.toLowerCase().includes(lc) || n.qualifiedName.toLowerCase().includes(lc);
@@ -1596,6 +2041,12 @@ class ToolHandler {
1596
2041
  // Skip import/export nodes — they add noise without information
1597
2042
  if (node.kind === 'import' || node.kind === 'export')
1598
2043
  continue;
2044
+ // SECURITY (#383): never render the on-disk source of a config-leaf
2045
+ // (Spring application.{yml,properties} key) — its line is `key = <secret>`,
2046
+ // so whole-file/cluster rendering here would push secrets into context
2047
+ // unbidden. The key still appears in the flow/symbol listing above.
2048
+ if ((0, utils_1.isConfigLeafNode)(node))
2049
+ continue;
1599
2050
  const group = fileGroups.get(node.filePath) || { nodes: [], score: 0 };
1600
2051
  group.nodes.push(node);
1601
2052
  // Score: a NAMED-SEED node (a symbol the agent named that FTS missed, now
@@ -2394,14 +2845,26 @@ class ToolHandler {
2394
2845
  * Handle codegraph_node
2395
2846
  */
2396
2847
  async handleNode(args) {
2397
- const symbol = this.validateString(args.symbol, 'symbol');
2398
- if (typeof symbol !== 'string')
2399
- return symbol;
2400
2848
  const cg = this.getCodeGraph(args.projectPath);
2401
2849
  // Default to false to minimize context usage
2402
2850
  const includeCode = args.includeCode === true;
2403
2851
  const fileHint = typeof args.file === 'string' && args.file.trim() ? args.file.trim() : undefined;
2404
2852
  const lineHint = typeof args.line === 'number' && args.line > 0 ? args.line : undefined;
2853
+ const offset = typeof args.offset === 'number' && args.offset > 0 ? Math.floor(args.offset) : undefined;
2854
+ const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : undefined;
2855
+ const symbolsOnly = args.symbolsOnly === true;
2856
+ const symbolRaw = typeof args.symbol === 'string' ? args.symbol.trim() : '';
2857
+ // FILE READ MODE: a `file` with no `symbol` reads that file like the Read
2858
+ // tool — its current on-disk source with line numbers, narrowable with
2859
+ // `offset`/`limit` exactly as Read does — PLUS a one-line blast-radius
2860
+ // header (which files depend on it). `symbolsOnly` returns just the
2861
+ // structural map instead. Backed by the index: same bytes Read gives you.
2862
+ if (!symbolRaw && fileHint) {
2863
+ return this.handleFileView(cg, fileHint, { offset, limit, symbolsOnly });
2864
+ }
2865
+ const symbol = this.validateString(args.symbol, 'symbol');
2866
+ if (typeof symbol !== 'string')
2867
+ return symbol;
2405
2868
  let matches = this.findSymbolMatches(cg, symbol);
2406
2869
  if (matches.length === 0) {
2407
2870
  return this.textResult(`Symbol "${symbol}" not found in the codebase`);
@@ -2487,6 +2950,140 @@ class ToolHandler {
2487
2950
  }
2488
2951
  return this.textResult(this.truncateOutput(out.join('\n')));
2489
2952
  }
2953
+ /**
2954
+ * FILE READ MODE: resolve `fileArg` (path or basename) to an indexed file and
2955
+ * read it like the Read tool — its current on-disk source with line numbers,
2956
+ * narrowable with `offset`/`limit` exactly as Read's are — preceded by a
2957
+ * one-line blast-radius header (which files depend on it). `symbolsOnly`
2958
+ * returns just the structural map (symbols + dependents) instead of source.
2959
+ *
2960
+ * Parity goal: the numbered source block is byte-for-byte the shape Read
2961
+ * returns (`<n>\t<line>`, no padding), so the agent treats it as a Read — only
2962
+ * faster (served from the index) and with the blast radius attached. Security:
2963
+ * yaml/properties files are summarized by key, never dumped (#383); reads go
2964
+ * through validatePathWithinRoot (#527).
2965
+ */
2966
+ async handleFileView(cg, fileArg, opts = {}) {
2967
+ const normalize = (p) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, '');
2968
+ const wantLower = normalize(fileArg).toLowerCase();
2969
+ const allFiles = cg.getFiles();
2970
+ if (allFiles.length === 0)
2971
+ return this.textResult('No files indexed. Run `codegraph index` first.');
2972
+ let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower);
2973
+ let candidates = [];
2974
+ if (!resolved) {
2975
+ candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower));
2976
+ if (candidates.length === 1)
2977
+ resolved = candidates[0];
2978
+ }
2979
+ if (!resolved && candidates.length === 0) {
2980
+ candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower));
2981
+ if (candidates.length === 1)
2982
+ resolved = candidates[0];
2983
+ }
2984
+ if (!resolved && candidates.length > 1) {
2985
+ return this.textResult([`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '',
2986
+ ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'));
2987
+ }
2988
+ if (!resolved) {
2989
+ return this.textResult(`No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`);
2990
+ }
2991
+ const filePath = resolved.path;
2992
+ const nodes = cg.getNodesInFile(filePath)
2993
+ .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export')
2994
+ .sort((a, b) => a.startLine - b.startLine);
2995
+ const dependents = cg.getFileDependents(filePath);
2996
+ // Compact, one-line blast radius (codegraph's value-add over a plain Read).
2997
+ const depSummary = dependents.length
2998
+ ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}`
2999
+ : 'no other indexed file depends on it';
3000
+ // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors.
3001
+ const symbolMap = (heading, limit = 200) => {
3002
+ const lines = [heading];
3003
+ for (const n of nodes.slice(0, limit)) {
3004
+ const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : '';
3005
+ lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`);
3006
+ }
3007
+ if (nodes.length > limit)
3008
+ lines.push(`- … +${nodes.length - limit} more`);
3009
+ return lines;
3010
+ };
3011
+ // symbolsOnly → the cheap structural overview, no source.
3012
+ if (opts.symbolsOnly) {
3013
+ const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, ''];
3014
+ if (nodes.length)
3015
+ out.push(...symbolMap('### Symbols'));
3016
+ else
3017
+ out.push('_No indexed symbols in this file._');
3018
+ out.push('', '> Drop `symbolsOnly` (or pass `offset`/`limit`) to read the source, like Read.');
3019
+ return this.textResult(this.truncateOutput(out.join('\n')));
3020
+ }
3021
+ // SECURITY (#383): never dump a raw config/data file — a yaml/properties
3022
+ // line is `key: <secret>`. Summarize by key and point to a real Read.
3023
+ if (utils_1.CONFIG_LEAF_LANGUAGES.has(resolved.language)) {
3024
+ const out = [`**${filePath}** — configuration/data file, ${depSummary}`, ''];
3025
+ if (nodes.length)
3026
+ out.push(...symbolMap('### Keys (values withheld for safety)'));
3027
+ out.push('', '> Values may be secrets, so codegraph indexes keys only. Read the file directly if you need a value.');
3028
+ return this.textResult(this.truncateOutput(out.join('\n')));
3029
+ }
3030
+ // Read the current bytes from disk through the security chokepoint
3031
+ // (validatePathWithinRoot: blocks `../` traversal and symlink escapes, #527).
3032
+ const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
3033
+ let content = null;
3034
+ if (abs) {
3035
+ try {
3036
+ content = (0, fs_1.readFileSync)(abs, 'utf-8');
3037
+ }
3038
+ catch {
3039
+ content = null;
3040
+ }
3041
+ }
3042
+ if (content === null) {
3043
+ const out = [`**${filePath}** — could not read from disk (it may have moved since indexing). ${depSummary}`, ''];
3044
+ if (nodes.length)
3045
+ out.push(...symbolMap('### Symbols'));
3046
+ out.push('', `> Read \`${filePath}\` directly for its current content.`);
3047
+ return this.textResult(this.truncateOutput(out.join('\n')));
3048
+ }
3049
+ // Split exactly as Read does — keep the trailing empty line a final newline
3050
+ // produces (Read numbers it too), so line numbers line up byte-for-byte.
3051
+ const fileLines = content.split('\n');
3052
+ const total = fileLines.length;
3053
+ // Read-parity windowing: `offset`/`limit` mean exactly what they do on Read
3054
+ // (1-based start line; max line count). Default: the whole file, capped like
3055
+ // Read at 2000 lines and bounded by a char budget that tracks explore's
3056
+ // proven-safe ~38k response ceiling. Overflow is stated explicitly (Read
3057
+ // paginates too) — never the silent 15k truncateOutput chop.
3058
+ const CHAR_BUDGET = 38000;
3059
+ const DEFAULT_LIMIT = 2000;
3060
+ const offset = Math.max(1, opts.offset ?? 1);
3061
+ if (offset > total) {
3062
+ return this.textResult(`**${filePath}** has ${total} line${total === 1 ? '' : 's'} — offset ${offset} is past the end. ${depSummary}`);
3063
+ }
3064
+ const maxLines = Math.max(1, opts.limit ?? DEFAULT_LIMIT);
3065
+ const start = offset - 1; // 0-based
3066
+ const header = `**${filePath}** — ${total} lines, ${nodes.length} symbol${nodes.length === 1 ? '' : 's'} · ${depSummary}`;
3067
+ // Numbered lines, byte-for-byte Read's shape: `<n>\t<line>`, no left-pad.
3068
+ const numbered = [];
3069
+ let used = header.length + 8;
3070
+ let i = start;
3071
+ for (; i < total && numbered.length < maxLines; i++) {
3072
+ const ln = `${i + 1}\t${fileLines[i]}`;
3073
+ if (used + ln.length + 1 > CHAR_BUDGET && numbered.length > 0)
3074
+ break;
3075
+ numbered.push(ln);
3076
+ used += ln.length + 1;
3077
+ }
3078
+ const shownEnd = start + numbered.length;
3079
+ const complete = offset === 1 && shownEnd >= total;
3080
+ const out = [header, '', ...numbered];
3081
+ if (!complete) {
3082
+ out.push('', `(lines ${offset}–${shownEnd} of ${total} — pass \`offset\`/\`limit\` for another range, or \`codegraph_node <symbol>\` for one symbol in full)`);
3083
+ }
3084
+ // Self-bounded to CHAR_BUDGET — do NOT route through truncateOutput (15k).
3085
+ return this.textResult(out.join('\n'));
3086
+ }
2490
3087
  /** Render one symbol: details + (optional) body/outline + its caller/callee trail. */
2491
3088
  async renderNodeSection(cg, node, includeCode) {
2492
3089
  let code = null;
@@ -2955,15 +3552,36 @@ class ToolHandler {
2955
3552
  }
2956
3553
  return lines.join('\n');
2957
3554
  }
2958
- formatNodeList(nodes, title) {
3555
+ formatNodeList(nodes, title, labels) {
2959
3556
  const lines = [`## ${title} (${nodes.length} found)`, ''];
2960
3557
  for (const node of nodes) {
2961
3558
  const location = node.startLine ? `:${node.startLine}` : '';
2962
- // Compact: just name, kind, location
2963
- lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}`);
3559
+ // Compact: just name, kind, location — plus the relationship when it
3560
+ // isn't a plain call (callback registration, instantiation, …).
3561
+ const label = labels?.get(node.id);
3562
+ lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
2964
3563
  }
2965
3564
  return lines.join('\n');
2966
3565
  }
3566
+ /**
3567
+ * Relationship label for a non-`calls` edge in callers/callees lists. A
3568
+ * function-as-value edge (#756) is the high-signal one: `callers(cb)`
3569
+ * showing "via callback registration" tells the agent this is where the
3570
+ * callback is WIRED, not where it's invoked.
3571
+ */
3572
+ edgeLabel(edge) {
3573
+ if (edge.kind === 'calls')
3574
+ return null;
3575
+ if (edge.metadata?.fnRef === true)
3576
+ return 'callback registration';
3577
+ if (edge.kind === 'instantiates')
3578
+ return 'instantiation';
3579
+ if (edge.kind === 'imports')
3580
+ return 'import';
3581
+ if (edge.kind === 'references')
3582
+ return 'reference';
3583
+ return edge.kind;
3584
+ }
2967
3585
  formatImpact(symbol, impact) {
2968
3586
  const nodeCount = impact.nodes.size;
2969
3587
  // Compact format: just list affected symbols grouped by file