@kkvcrobatz107/codegraph 0.9.6-pkm.1

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 (501) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +595 -0
  3. package/dist/bin/codegraph.d.ts +25 -0
  4. package/dist/bin/codegraph.d.ts.map +1 -0
  5. package/dist/bin/codegraph.js +1563 -0
  6. package/dist/bin/codegraph.js.map +1 -0
  7. package/dist/bin/node-version-check.d.ts +37 -0
  8. package/dist/bin/node-version-check.d.ts.map +1 -0
  9. package/dist/bin/node-version-check.js +79 -0
  10. package/dist/bin/node-version-check.js.map +1 -0
  11. package/dist/bin/uninstall.d.ts +14 -0
  12. package/dist/bin/uninstall.d.ts.map +1 -0
  13. package/dist/bin/uninstall.js +36 -0
  14. package/dist/bin/uninstall.js.map +1 -0
  15. package/dist/context/formatter.d.ts +30 -0
  16. package/dist/context/formatter.d.ts.map +1 -0
  17. package/dist/context/formatter.js +244 -0
  18. package/dist/context/formatter.js.map +1 -0
  19. package/dist/context/index.d.ts +110 -0
  20. package/dist/context/index.d.ts.map +1 -0
  21. package/dist/context/index.js +1169 -0
  22. package/dist/context/index.js.map +1 -0
  23. package/dist/db/index.d.ts +101 -0
  24. package/dist/db/index.d.ts.map +1 -0
  25. package/dist/db/index.js +251 -0
  26. package/dist/db/index.js.map +1 -0
  27. package/dist/db/migrations.d.ts +44 -0
  28. package/dist/db/migrations.d.ts.map +1 -0
  29. package/dist/db/migrations.js +131 -0
  30. package/dist/db/migrations.js.map +1 -0
  31. package/dist/db/queries.d.ts +281 -0
  32. package/dist/db/queries.d.ts.map +1 -0
  33. package/dist/db/queries.js +1300 -0
  34. package/dist/db/queries.js.map +1 -0
  35. package/dist/db/schema.sql +151 -0
  36. package/dist/db/sqlite-adapter.d.ts +46 -0
  37. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  38. package/dist/db/sqlite-adapter.js +114 -0
  39. package/dist/db/sqlite-adapter.js.map +1 -0
  40. package/dist/directory.d.ts +57 -0
  41. package/dist/directory.d.ts.map +1 -0
  42. package/dist/directory.js +264 -0
  43. package/dist/directory.js.map +1 -0
  44. package/dist/errors.d.ts +136 -0
  45. package/dist/errors.d.ts.map +1 -0
  46. package/dist/errors.js +219 -0
  47. package/dist/errors.js.map +1 -0
  48. package/dist/extraction/dfm-extractor.d.ts +31 -0
  49. package/dist/extraction/dfm-extractor.d.ts.map +1 -0
  50. package/dist/extraction/dfm-extractor.js +151 -0
  51. package/dist/extraction/dfm-extractor.js.map +1 -0
  52. package/dist/extraction/grammars.d.ts +90 -0
  53. package/dist/extraction/grammars.d.ts.map +1 -0
  54. package/dist/extraction/grammars.js +407 -0
  55. package/dist/extraction/grammars.js.map +1 -0
  56. package/dist/extraction/index.d.ts +138 -0
  57. package/dist/extraction/index.d.ts.map +1 -0
  58. package/dist/extraction/index.js +1378 -0
  59. package/dist/extraction/index.js.map +1 -0
  60. package/dist/extraction/languages/c-cpp.d.ts +4 -0
  61. package/dist/extraction/languages/c-cpp.d.ts.map +1 -0
  62. package/dist/extraction/languages/c-cpp.js +171 -0
  63. package/dist/extraction/languages/c-cpp.js.map +1 -0
  64. package/dist/extraction/languages/csharp.d.ts +3 -0
  65. package/dist/extraction/languages/csharp.d.ts.map +1 -0
  66. package/dist/extraction/languages/csharp.js +73 -0
  67. package/dist/extraction/languages/csharp.js.map +1 -0
  68. package/dist/extraction/languages/dart.d.ts +3 -0
  69. package/dist/extraction/languages/dart.d.ts.map +1 -0
  70. package/dist/extraction/languages/dart.js +192 -0
  71. package/dist/extraction/languages/dart.js.map +1 -0
  72. package/dist/extraction/languages/go.d.ts +3 -0
  73. package/dist/extraction/languages/go.d.ts.map +1 -0
  74. package/dist/extraction/languages/go.js +70 -0
  75. package/dist/extraction/languages/go.js.map +1 -0
  76. package/dist/extraction/languages/index.d.ts +10 -0
  77. package/dist/extraction/languages/index.d.ts.map +1 -0
  78. package/dist/extraction/languages/index.js +51 -0
  79. package/dist/extraction/languages/index.js.map +1 -0
  80. package/dist/extraction/languages/java.d.ts +3 -0
  81. package/dist/extraction/languages/java.d.ts.map +1 -0
  82. package/dist/extraction/languages/java.js +70 -0
  83. package/dist/extraction/languages/java.js.map +1 -0
  84. package/dist/extraction/languages/javascript.d.ts +3 -0
  85. package/dist/extraction/languages/javascript.d.ts.map +1 -0
  86. package/dist/extraction/languages/javascript.js +90 -0
  87. package/dist/extraction/languages/javascript.js.map +1 -0
  88. package/dist/extraction/languages/kotlin.d.ts +3 -0
  89. package/dist/extraction/languages/kotlin.d.ts.map +1 -0
  90. package/dist/extraction/languages/kotlin.js +259 -0
  91. package/dist/extraction/languages/kotlin.js.map +1 -0
  92. package/dist/extraction/languages/lua.d.ts +3 -0
  93. package/dist/extraction/languages/lua.d.ts.map +1 -0
  94. package/dist/extraction/languages/lua.js +150 -0
  95. package/dist/extraction/languages/lua.js.map +1 -0
  96. package/dist/extraction/languages/luau.d.ts +3 -0
  97. package/dist/extraction/languages/luau.d.ts.map +1 -0
  98. package/dist/extraction/languages/luau.js +37 -0
  99. package/dist/extraction/languages/luau.js.map +1 -0
  100. package/dist/extraction/languages/objc.d.ts +3 -0
  101. package/dist/extraction/languages/objc.d.ts.map +1 -0
  102. package/dist/extraction/languages/objc.js +133 -0
  103. package/dist/extraction/languages/objc.js.map +1 -0
  104. package/dist/extraction/languages/pascal.d.ts +3 -0
  105. package/dist/extraction/languages/pascal.d.ts.map +1 -0
  106. package/dist/extraction/languages/pascal.js +66 -0
  107. package/dist/extraction/languages/pascal.js.map +1 -0
  108. package/dist/extraction/languages/php.d.ts +3 -0
  109. package/dist/extraction/languages/php.d.ts.map +1 -0
  110. package/dist/extraction/languages/php.js +107 -0
  111. package/dist/extraction/languages/php.js.map +1 -0
  112. package/dist/extraction/languages/python.d.ts +3 -0
  113. package/dist/extraction/languages/python.d.ts.map +1 -0
  114. package/dist/extraction/languages/python.js +56 -0
  115. package/dist/extraction/languages/python.js.map +1 -0
  116. package/dist/extraction/languages/ruby.d.ts +3 -0
  117. package/dist/extraction/languages/ruby.d.ts.map +1 -0
  118. package/dist/extraction/languages/ruby.js +114 -0
  119. package/dist/extraction/languages/ruby.js.map +1 -0
  120. package/dist/extraction/languages/rust.d.ts +3 -0
  121. package/dist/extraction/languages/rust.d.ts.map +1 -0
  122. package/dist/extraction/languages/rust.js +109 -0
  123. package/dist/extraction/languages/rust.js.map +1 -0
  124. package/dist/extraction/languages/scala.d.ts +3 -0
  125. package/dist/extraction/languages/scala.d.ts.map +1 -0
  126. package/dist/extraction/languages/scala.js +139 -0
  127. package/dist/extraction/languages/scala.js.map +1 -0
  128. package/dist/extraction/languages/swift.d.ts +3 -0
  129. package/dist/extraction/languages/swift.d.ts.map +1 -0
  130. package/dist/extraction/languages/swift.js +91 -0
  131. package/dist/extraction/languages/swift.js.map +1 -0
  132. package/dist/extraction/languages/typescript.d.ts +3 -0
  133. package/dist/extraction/languages/typescript.d.ts.map +1 -0
  134. package/dist/extraction/languages/typescript.js +129 -0
  135. package/dist/extraction/languages/typescript.js.map +1 -0
  136. package/dist/extraction/liquid-extractor.d.ts +52 -0
  137. package/dist/extraction/liquid-extractor.d.ts.map +1 -0
  138. package/dist/extraction/liquid-extractor.js +313 -0
  139. package/dist/extraction/liquid-extractor.js.map +1 -0
  140. package/dist/extraction/mybatis-extractor.d.ts +48 -0
  141. package/dist/extraction/mybatis-extractor.d.ts.map +1 -0
  142. package/dist/extraction/mybatis-extractor.js +198 -0
  143. package/dist/extraction/mybatis-extractor.js.map +1 -0
  144. package/dist/extraction/parse-worker.d.ts +8 -0
  145. package/dist/extraction/parse-worker.d.ts.map +1 -0
  146. package/dist/extraction/parse-worker.js +94 -0
  147. package/dist/extraction/parse-worker.js.map +1 -0
  148. package/dist/extraction/svelte-extractor.d.ts +56 -0
  149. package/dist/extraction/svelte-extractor.d.ts.map +1 -0
  150. package/dist/extraction/svelte-extractor.js +272 -0
  151. package/dist/extraction/svelte-extractor.js.map +1 -0
  152. package/dist/extraction/tree-sitter-helpers.d.ts +28 -0
  153. package/dist/extraction/tree-sitter-helpers.d.ts.map +1 -0
  154. package/dist/extraction/tree-sitter-helpers.js +103 -0
  155. package/dist/extraction/tree-sitter-helpers.js.map +1 -0
  156. package/dist/extraction/tree-sitter-types.d.ts +193 -0
  157. package/dist/extraction/tree-sitter-types.d.ts.map +1 -0
  158. package/dist/extraction/tree-sitter-types.js +10 -0
  159. package/dist/extraction/tree-sitter-types.js.map +1 -0
  160. package/dist/extraction/tree-sitter.d.ts +291 -0
  161. package/dist/extraction/tree-sitter.d.ts.map +1 -0
  162. package/dist/extraction/tree-sitter.js +2961 -0
  163. package/dist/extraction/tree-sitter.js.map +1 -0
  164. package/dist/extraction/vue-extractor.d.ts +36 -0
  165. package/dist/extraction/vue-extractor.d.ts.map +1 -0
  166. package/dist/extraction/vue-extractor.js +163 -0
  167. package/dist/extraction/vue-extractor.js.map +1 -0
  168. package/dist/extraction/wasm/tree-sitter-lua.wasm +0 -0
  169. package/dist/extraction/wasm/tree-sitter-luau.wasm +0 -0
  170. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  171. package/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
  172. package/dist/extraction/wasm-runtime-flags.d.ts +38 -0
  173. package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -0
  174. package/dist/extraction/wasm-runtime-flags.js +105 -0
  175. package/dist/extraction/wasm-runtime-flags.js.map +1 -0
  176. package/dist/graph/index.d.ts +8 -0
  177. package/dist/graph/index.d.ts.map +1 -0
  178. package/dist/graph/index.js +13 -0
  179. package/dist/graph/index.js.map +1 -0
  180. package/dist/graph/queries.d.ts +106 -0
  181. package/dist/graph/queries.d.ts.map +1 -0
  182. package/dist/graph/queries.js +366 -0
  183. package/dist/graph/queries.js.map +1 -0
  184. package/dist/graph/traversal.d.ts +127 -0
  185. package/dist/graph/traversal.d.ts.map +1 -0
  186. package/dist/graph/traversal.js +528 -0
  187. package/dist/graph/traversal.js.map +1 -0
  188. package/dist/index.d.ts +460 -0
  189. package/dist/index.d.ts.map +1 -0
  190. package/dist/index.js +863 -0
  191. package/dist/index.js.map +1 -0
  192. package/dist/installer/claude-md-template.d.ts +14 -0
  193. package/dist/installer/claude-md-template.d.ts.map +1 -0
  194. package/dist/installer/claude-md-template.js +21 -0
  195. package/dist/installer/claude-md-template.js.map +1 -0
  196. package/dist/installer/config-writer.d.ts +29 -0
  197. package/dist/installer/config-writer.d.ts.map +1 -0
  198. package/dist/installer/config-writer.js +111 -0
  199. package/dist/installer/config-writer.js.map +1 -0
  200. package/dist/installer/index.d.ts +117 -0
  201. package/dist/installer/index.d.ts.map +1 -0
  202. package/dist/installer/index.js +528 -0
  203. package/dist/installer/index.js.map +1 -0
  204. package/dist/installer/instructions-template.d.ts +28 -0
  205. package/dist/installer/instructions-template.d.ts.map +1 -0
  206. package/dist/installer/instructions-template.js +65 -0
  207. package/dist/installer/instructions-template.js.map +1 -0
  208. package/dist/installer/targets/antigravity.d.ts +57 -0
  209. package/dist/installer/targets/antigravity.d.ts.map +1 -0
  210. package/dist/installer/targets/antigravity.js +307 -0
  211. package/dist/installer/targets/antigravity.js.map +1 -0
  212. package/dist/installer/targets/claude.d.ts +47 -0
  213. package/dist/installer/targets/claude.d.ts.map +1 -0
  214. package/dist/installer/targets/claude.js +401 -0
  215. package/dist/installer/targets/claude.js.map +1 -0
  216. package/dist/installer/targets/codex.d.ts +18 -0
  217. package/dist/installer/targets/codex.d.ts.map +1 -0
  218. package/dist/installer/targets/codex.js +185 -0
  219. package/dist/installer/targets/codex.js.map +1 -0
  220. package/dist/installer/targets/cursor.d.ts +35 -0
  221. package/dist/installer/targets/cursor.d.ts.map +1 -0
  222. package/dist/installer/targets/cursor.js +283 -0
  223. package/dist/installer/targets/cursor.js.map +1 -0
  224. package/dist/installer/targets/gemini.d.ts +26 -0
  225. package/dist/installer/targets/gemini.d.ts.map +1 -0
  226. package/dist/installer/targets/gemini.js +165 -0
  227. package/dist/installer/targets/gemini.js.map +1 -0
  228. package/dist/installer/targets/hermes.d.ts +18 -0
  229. package/dist/installer/targets/hermes.d.ts.map +1 -0
  230. package/dist/installer/targets/hermes.js +359 -0
  231. package/dist/installer/targets/hermes.js.map +1 -0
  232. package/dist/installer/targets/kiro.d.ts +27 -0
  233. package/dist/installer/targets/kiro.d.ts.map +1 -0
  234. package/dist/installer/targets/kiro.js +196 -0
  235. package/dist/installer/targets/kiro.js.map +1 -0
  236. package/dist/installer/targets/opencode.d.ts +30 -0
  237. package/dist/installer/targets/opencode.d.ts.map +1 -0
  238. package/dist/installer/targets/opencode.js +235 -0
  239. package/dist/installer/targets/opencode.js.map +1 -0
  240. package/dist/installer/targets/registry.d.ts +35 -0
  241. package/dist/installer/targets/registry.d.ts.map +1 -0
  242. package/dist/installer/targets/registry.js +91 -0
  243. package/dist/installer/targets/registry.js.map +1 -0
  244. package/dist/installer/targets/shared.d.ts +77 -0
  245. package/dist/installer/targets/shared.d.ts.map +1 -0
  246. package/dist/installer/targets/shared.js +246 -0
  247. package/dist/installer/targets/shared.js.map +1 -0
  248. package/dist/installer/targets/toml.d.ts +52 -0
  249. package/dist/installer/targets/toml.d.ts.map +1 -0
  250. package/dist/installer/targets/toml.js +147 -0
  251. package/dist/installer/targets/toml.js.map +1 -0
  252. package/dist/installer/targets/types.d.ts +116 -0
  253. package/dist/installer/targets/types.d.ts.map +1 -0
  254. package/dist/installer/targets/types.js +16 -0
  255. package/dist/installer/targets/types.js.map +1 -0
  256. package/dist/mcp/daemon-paths.d.ts +46 -0
  257. package/dist/mcp/daemon-paths.d.ts.map +1 -0
  258. package/dist/mcp/daemon-paths.js +125 -0
  259. package/dist/mcp/daemon-paths.js.map +1 -0
  260. package/dist/mcp/daemon.d.ts +161 -0
  261. package/dist/mcp/daemon.d.ts.map +1 -0
  262. package/dist/mcp/daemon.js +403 -0
  263. package/dist/mcp/daemon.js.map +1 -0
  264. package/dist/mcp/engine.d.ts +100 -0
  265. package/dist/mcp/engine.d.ts.map +1 -0
  266. package/dist/mcp/engine.js +291 -0
  267. package/dist/mcp/engine.js.map +1 -0
  268. package/dist/mcp/index.d.ts +109 -0
  269. package/dist/mcp/index.d.ts.map +1 -0
  270. package/dist/mcp/index.js +470 -0
  271. package/dist/mcp/index.js.map +1 -0
  272. package/dist/mcp/proxy.d.ts +46 -0
  273. package/dist/mcp/proxy.d.ts.map +1 -0
  274. package/dist/mcp/proxy.js +276 -0
  275. package/dist/mcp/proxy.js.map +1 -0
  276. package/dist/mcp/server-instructions.d.ts +19 -0
  277. package/dist/mcp/server-instructions.d.ts.map +1 -0
  278. package/dist/mcp/server-instructions.js +73 -0
  279. package/dist/mcp/server-instructions.js.map +1 -0
  280. package/dist/mcp/session.d.ts +67 -0
  281. package/dist/mcp/session.d.ts.map +1 -0
  282. package/dist/mcp/session.js +276 -0
  283. package/dist/mcp/session.js.map +1 -0
  284. package/dist/mcp/tools.d.ts +385 -0
  285. package/dist/mcp/tools.d.ts.map +1 -0
  286. package/dist/mcp/tools.js +2545 -0
  287. package/dist/mcp/tools.js.map +1 -0
  288. package/dist/mcp/transport.d.ts +188 -0
  289. package/dist/mcp/transport.d.ts.map +1 -0
  290. package/dist/mcp/transport.js +343 -0
  291. package/dist/mcp/transport.js.map +1 -0
  292. package/dist/mcp/version.d.ts +19 -0
  293. package/dist/mcp/version.d.ts.map +1 -0
  294. package/dist/mcp/version.js +71 -0
  295. package/dist/mcp/version.js.map +1 -0
  296. package/dist/resolution/callback-synthesizer.d.ts +10 -0
  297. package/dist/resolution/callback-synthesizer.d.ts.map +1 -0
  298. package/dist/resolution/callback-synthesizer.js +924 -0
  299. package/dist/resolution/callback-synthesizer.js.map +1 -0
  300. package/dist/resolution/frameworks/cargo-workspace.d.ts +18 -0
  301. package/dist/resolution/frameworks/cargo-workspace.d.ts.map +1 -0
  302. package/dist/resolution/frameworks/cargo-workspace.js +225 -0
  303. package/dist/resolution/frameworks/cargo-workspace.js.map +1 -0
  304. package/dist/resolution/frameworks/csharp.d.ts +8 -0
  305. package/dist/resolution/frameworks/csharp.d.ts.map +1 -0
  306. package/dist/resolution/frameworks/csharp.js +241 -0
  307. package/dist/resolution/frameworks/csharp.js.map +1 -0
  308. package/dist/resolution/frameworks/drupal.d.ts +51 -0
  309. package/dist/resolution/frameworks/drupal.d.ts.map +1 -0
  310. package/dist/resolution/frameworks/drupal.js +367 -0
  311. package/dist/resolution/frameworks/drupal.js.map +1 -0
  312. package/dist/resolution/frameworks/expo-modules.d.ts +3 -0
  313. package/dist/resolution/frameworks/expo-modules.d.ts.map +1 -0
  314. package/dist/resolution/frameworks/expo-modules.js +143 -0
  315. package/dist/resolution/frameworks/expo-modules.js.map +1 -0
  316. package/dist/resolution/frameworks/express.d.ts +8 -0
  317. package/dist/resolution/frameworks/express.d.ts.map +1 -0
  318. package/dist/resolution/frameworks/express.js +308 -0
  319. package/dist/resolution/frameworks/express.js.map +1 -0
  320. package/dist/resolution/frameworks/fabric.d.ts +3 -0
  321. package/dist/resolution/frameworks/fabric.d.ts.map +1 -0
  322. package/dist/resolution/frameworks/fabric.js +354 -0
  323. package/dist/resolution/frameworks/fabric.js.map +1 -0
  324. package/dist/resolution/frameworks/go.d.ts +8 -0
  325. package/dist/resolution/frameworks/go.d.ts.map +1 -0
  326. package/dist/resolution/frameworks/go.js +161 -0
  327. package/dist/resolution/frameworks/go.js.map +1 -0
  328. package/dist/resolution/frameworks/index.d.ts +48 -0
  329. package/dist/resolution/frameworks/index.d.ts.map +1 -0
  330. package/dist/resolution/frameworks/index.js +161 -0
  331. package/dist/resolution/frameworks/index.js.map +1 -0
  332. package/dist/resolution/frameworks/java.d.ts +8 -0
  333. package/dist/resolution/frameworks/java.d.ts.map +1 -0
  334. package/dist/resolution/frameworks/java.js +504 -0
  335. package/dist/resolution/frameworks/java.js.map +1 -0
  336. package/dist/resolution/frameworks/laravel.d.ts +13 -0
  337. package/dist/resolution/frameworks/laravel.d.ts.map +1 -0
  338. package/dist/resolution/frameworks/laravel.js +257 -0
  339. package/dist/resolution/frameworks/laravel.js.map +1 -0
  340. package/dist/resolution/frameworks/nestjs.d.ts +26 -0
  341. package/dist/resolution/frameworks/nestjs.d.ts.map +1 -0
  342. package/dist/resolution/frameworks/nestjs.js +698 -0
  343. package/dist/resolution/frameworks/nestjs.js.map +1 -0
  344. package/dist/resolution/frameworks/play.d.ts +19 -0
  345. package/dist/resolution/frameworks/play.d.ts.map +1 -0
  346. package/dist/resolution/frameworks/play.js +111 -0
  347. package/dist/resolution/frameworks/play.js.map +1 -0
  348. package/dist/resolution/frameworks/python.d.ts +10 -0
  349. package/dist/resolution/frameworks/python.d.ts.map +1 -0
  350. package/dist/resolution/frameworks/python.js +396 -0
  351. package/dist/resolution/frameworks/python.js.map +1 -0
  352. package/dist/resolution/frameworks/react-native.d.ts +3 -0
  353. package/dist/resolution/frameworks/react-native.d.ts.map +1 -0
  354. package/dist/resolution/frameworks/react-native.js +360 -0
  355. package/dist/resolution/frameworks/react-native.js.map +1 -0
  356. package/dist/resolution/frameworks/react.d.ts +8 -0
  357. package/dist/resolution/frameworks/react.d.ts.map +1 -0
  358. package/dist/resolution/frameworks/react.js +365 -0
  359. package/dist/resolution/frameworks/react.js.map +1 -0
  360. package/dist/resolution/frameworks/ruby.d.ts +8 -0
  361. package/dist/resolution/frameworks/ruby.d.ts.map +1 -0
  362. package/dist/resolution/frameworks/ruby.js +302 -0
  363. package/dist/resolution/frameworks/ruby.js.map +1 -0
  364. package/dist/resolution/frameworks/rust.d.ts +8 -0
  365. package/dist/resolution/frameworks/rust.d.ts.map +1 -0
  366. package/dist/resolution/frameworks/rust.js +304 -0
  367. package/dist/resolution/frameworks/rust.js.map +1 -0
  368. package/dist/resolution/frameworks/svelte.d.ts +9 -0
  369. package/dist/resolution/frameworks/svelte.d.ts.map +1 -0
  370. package/dist/resolution/frameworks/svelte.js +249 -0
  371. package/dist/resolution/frameworks/svelte.js.map +1 -0
  372. package/dist/resolution/frameworks/swift-objc.d.ts +37 -0
  373. package/dist/resolution/frameworks/swift-objc.d.ts.map +1 -0
  374. package/dist/resolution/frameworks/swift-objc.js +252 -0
  375. package/dist/resolution/frameworks/swift-objc.js.map +1 -0
  376. package/dist/resolution/frameworks/swift.d.ts +10 -0
  377. package/dist/resolution/frameworks/swift.d.ts.map +1 -0
  378. package/dist/resolution/frameworks/swift.js +400 -0
  379. package/dist/resolution/frameworks/swift.js.map +1 -0
  380. package/dist/resolution/frameworks/vue.d.ts +9 -0
  381. package/dist/resolution/frameworks/vue.d.ts.map +1 -0
  382. package/dist/resolution/frameworks/vue.js +306 -0
  383. package/dist/resolution/frameworks/vue.js.map +1 -0
  384. package/dist/resolution/go-module.d.ts +26 -0
  385. package/dist/resolution/go-module.d.ts.map +1 -0
  386. package/dist/resolution/go-module.js +78 -0
  387. package/dist/resolution/go-module.js.map +1 -0
  388. package/dist/resolution/import-resolver.d.ts +68 -0
  389. package/dist/resolution/import-resolver.d.ts.map +1 -0
  390. package/dist/resolution/import-resolver.js +1231 -0
  391. package/dist/resolution/import-resolver.js.map +1 -0
  392. package/dist/resolution/index.d.ts +116 -0
  393. package/dist/resolution/index.d.ts.map +1 -0
  394. package/dist/resolution/index.js +878 -0
  395. package/dist/resolution/index.js.map +1 -0
  396. package/dist/resolution/lru-cache.d.ts +24 -0
  397. package/dist/resolution/lru-cache.d.ts.map +1 -0
  398. package/dist/resolution/lru-cache.js +62 -0
  399. package/dist/resolution/lru-cache.js.map +1 -0
  400. package/dist/resolution/name-matcher.d.ts +32 -0
  401. package/dist/resolution/name-matcher.d.ts.map +1 -0
  402. package/dist/resolution/name-matcher.js +596 -0
  403. package/dist/resolution/name-matcher.js.map +1 -0
  404. package/dist/resolution/path-aliases.d.ts +68 -0
  405. package/dist/resolution/path-aliases.d.ts.map +1 -0
  406. package/dist/resolution/path-aliases.js +238 -0
  407. package/dist/resolution/path-aliases.js.map +1 -0
  408. package/dist/resolution/strip-comments.d.ts +27 -0
  409. package/dist/resolution/strip-comments.d.ts.map +1 -0
  410. package/dist/resolution/strip-comments.js +441 -0
  411. package/dist/resolution/strip-comments.js.map +1 -0
  412. package/dist/resolution/swift-objc-bridge.d.ts +134 -0
  413. package/dist/resolution/swift-objc-bridge.d.ts.map +1 -0
  414. package/dist/resolution/swift-objc-bridge.js +256 -0
  415. package/dist/resolution/swift-objc-bridge.js.map +1 -0
  416. package/dist/resolution/types.d.ts +209 -0
  417. package/dist/resolution/types.d.ts.map +1 -0
  418. package/dist/resolution/types.js +8 -0
  419. package/dist/resolution/types.js.map +1 -0
  420. package/dist/search/query-parser.d.ts +57 -0
  421. package/dist/search/query-parser.d.ts.map +1 -0
  422. package/dist/search/query-parser.js +177 -0
  423. package/dist/search/query-parser.js.map +1 -0
  424. package/dist/search/query-utils.d.ts +53 -0
  425. package/dist/search/query-utils.d.ts.map +1 -0
  426. package/dist/search/query-utils.js +350 -0
  427. package/dist/search/query-utils.js.map +1 -0
  428. package/dist/sync/git-hooks.d.ts +45 -0
  429. package/dist/sync/git-hooks.d.ts.map +1 -0
  430. package/dist/sync/git-hooks.js +223 -0
  431. package/dist/sync/git-hooks.js.map +1 -0
  432. package/dist/sync/index.d.ts +19 -0
  433. package/dist/sync/index.d.ts.map +1 -0
  434. package/dist/sync/index.js +35 -0
  435. package/dist/sync/index.js.map +1 -0
  436. package/dist/sync/watch-policy.d.ts +48 -0
  437. package/dist/sync/watch-policy.d.ts.map +1 -0
  438. package/dist/sync/watch-policy.js +124 -0
  439. package/dist/sync/watch-policy.js.map +1 -0
  440. package/dist/sync/watcher.d.ts +191 -0
  441. package/dist/sync/watcher.d.ts.map +1 -0
  442. package/dist/sync/watcher.js +398 -0
  443. package/dist/sync/watcher.js.map +1 -0
  444. package/dist/sync/worktree.d.ts +54 -0
  445. package/dist/sync/worktree.d.ts.map +1 -0
  446. package/dist/sync/worktree.js +136 -0
  447. package/dist/sync/worktree.js.map +1 -0
  448. package/dist/types.d.ts +369 -0
  449. package/dist/types.d.ts.map +1 -0
  450. package/dist/types.js +78 -0
  451. package/dist/types.js.map +1 -0
  452. package/dist/ui/glyphs.d.ts +42 -0
  453. package/dist/ui/glyphs.d.ts.map +1 -0
  454. package/dist/ui/glyphs.js +78 -0
  455. package/dist/ui/glyphs.js.map +1 -0
  456. package/dist/ui/shimmer-progress.d.ts +11 -0
  457. package/dist/ui/shimmer-progress.d.ts.map +1 -0
  458. package/dist/ui/shimmer-progress.js +90 -0
  459. package/dist/ui/shimmer-progress.js.map +1 -0
  460. package/dist/ui/shimmer-worker.d.ts +2 -0
  461. package/dist/ui/shimmer-worker.d.ts.map +1 -0
  462. package/dist/ui/shimmer-worker.js +118 -0
  463. package/dist/ui/shimmer-worker.js.map +1 -0
  464. package/dist/ui/types.d.ts +17 -0
  465. package/dist/ui/types.d.ts.map +1 -0
  466. package/dist/ui/types.js +3 -0
  467. package/dist/ui/types.js.map +1 -0
  468. package/dist/utils.d.ts +205 -0
  469. package/dist/utils.d.ts.map +1 -0
  470. package/dist/utils.js +549 -0
  471. package/dist/utils.js.map +1 -0
  472. package/package.json +59 -0
  473. package/scripts/add-lang/bench.sh +60 -0
  474. package/scripts/add-lang/check-grammar.mjs +75 -0
  475. package/scripts/add-lang/dump-ast.mjs +103 -0
  476. package/scripts/add-lang/verify-extraction.mjs +70 -0
  477. package/scripts/agent-eval/arms-F.sh +21 -0
  478. package/scripts/agent-eval/arms-matrix.sh +37 -0
  479. package/scripts/agent-eval/audit.sh +68 -0
  480. package/scripts/agent-eval/bench-readme.sh +28 -0
  481. package/scripts/agent-eval/block-read-hook.sh +19 -0
  482. package/scripts/agent-eval/hook-settings.json +15 -0
  483. package/scripts/agent-eval/itrun.sh +107 -0
  484. package/scripts/agent-eval/parse-arms.mjs +116 -0
  485. package/scripts/agent-eval/parse-bench-readme.mjs +67 -0
  486. package/scripts/agent-eval/parse-run.mjs +45 -0
  487. package/scripts/agent-eval/parse-session.mjs +93 -0
  488. package/scripts/agent-eval/probe-context.mjs +21 -0
  489. package/scripts/agent-eval/probe-explore.mjs +40 -0
  490. package/scripts/agent-eval/probe-node.mjs +20 -0
  491. package/scripts/agent-eval/probe-trace.mjs +20 -0
  492. package/scripts/agent-eval/run-agent.sh +34 -0
  493. package/scripts/agent-eval/run-all.sh +67 -0
  494. package/scripts/agent-eval/run-arms.sh +56 -0
  495. package/scripts/agent-eval/seq-matrix.mjs +137 -0
  496. package/scripts/build-bundle.sh +118 -0
  497. package/scripts/extract-release-notes.mjs +130 -0
  498. package/scripts/local-install.sh +41 -0
  499. package/scripts/npm-shim.js +246 -0
  500. package/scripts/pack-npm.sh +95 -0
  501. package/scripts/prepare-release.mjs +270 -0
@@ -0,0 +1,2545 @@
1
+ "use strict";
2
+ /**
3
+ * MCP Tool Definitions
4
+ *
5
+ * Defines the tools exposed by the CodeGraph MCP server.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.ToolHandler = exports.tools = void 0;
42
+ exports.getExploreBudget = getExploreBudget;
43
+ exports.getExploreOutputBudget = getExploreOutputBudget;
44
+ exports.formatStaleBanner = formatStaleBanner;
45
+ exports.formatStaleFooter = formatStaleFooter;
46
+ const index_1 = __importStar(require("../index"));
47
+ const worktree_1 = require("../sync/worktree");
48
+ const crypto_1 = require("crypto");
49
+ const fs_1 = require("fs");
50
+ const utils_1 = require("../utils");
51
+ const os_1 = require("os");
52
+ const path_1 = require("path");
53
+ /** Maximum output length to prevent context bloat (characters) */
54
+ const MAX_OUTPUT_LENGTH = 15000;
55
+ /**
56
+ * Maximum length for free-form string inputs (query, task, symbol).
57
+ * Bounds memory and CPU when a buggy or hostile MCP client sends a
58
+ * huge payload — without this an attacker could ship a 100MB string
59
+ * and force a full FTS5 scan / OOM the server. 10 000 characters is
60
+ * far beyond any realistic legitimate query.
61
+ */
62
+ const MAX_INPUT_LENGTH = 10_000;
63
+ /**
64
+ * Maximum length for path-like string inputs (projectPath, path
65
+ * filter, glob pattern). Paths beyond a few thousand chars are
66
+ * never legitimate and signal abuse or a bug upstream.
67
+ */
68
+ const MAX_PATH_LENGTH = 4_096;
69
+ /**
70
+ * Rust path roots that have no file-system equivalent — `crate` is the
71
+ * current crate, `super` is the parent module, `self` is the current
72
+ * module. Used by `matchesSymbol` to strip these before file-path
73
+ * matching so `crate::configurator::stage_apply::run` resolves the
74
+ * same as `configurator::stage_apply::run`.
75
+ */
76
+ const RUST_PATH_PREFIXES = new Set(['crate', 'super', 'self']);
77
+ /**
78
+ * Node kinds that contain other symbols. For these, `codegraph_node` with
79
+ * `includeCode=true` returns a structural outline (member names + signatures
80
+ * + line numbers) instead of the full body, which for a large class is a
81
+ * multi-thousand-character wall of source that bloats the agent's context.
82
+ */
83
+ const CONTAINER_NODE_KINDS = new Set([
84
+ 'class', 'struct', 'interface', 'trait', 'protocol', 'enum', 'namespace', 'module',
85
+ ]);
86
+ /** Last `::` / `.` / `/`-separated segment of a qualified symbol. */
87
+ function lastQualifierPart(symbol) {
88
+ const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
89
+ return parts[parts.length - 1] ?? symbol;
90
+ }
91
+ /**
92
+ * Calculate the recommended number of codegraph_explore calls based on project size.
93
+ * Larger codebases need more exploration calls to cover their surface area,
94
+ * but smaller ones should use fewer to avoid unnecessary overhead.
95
+ */
96
+ function getExploreBudget(fileCount) {
97
+ if (fileCount < 500)
98
+ return 1;
99
+ if (fileCount < 5000)
100
+ return 2;
101
+ if (fileCount < 15000)
102
+ return 3;
103
+ if (fileCount < 25000)
104
+ return 4;
105
+ return 5;
106
+ }
107
+ function getExploreOutputBudget(fileCount) {
108
+ if (fileCount < 500) {
109
+ return {
110
+ maxOutputChars: 18000,
111
+ defaultMaxFiles: 5,
112
+ maxCharsPerFile: 3800,
113
+ gapThreshold: 8,
114
+ maxSymbolsInFileHeader: 6,
115
+ maxEdgesPerRelationshipKind: 6,
116
+ includeRelationships: true,
117
+ includeAdditionalFiles: false,
118
+ includeCompletenessSignal: false,
119
+ includeBudgetNote: false,
120
+ };
121
+ }
122
+ if (fileCount < 5000) {
123
+ return {
124
+ // Sized so ONE explore can cover a flow that centers on a god-file (e.g.
125
+ // excalidraw's 415 KB App.tsx): the previous 2500/file returned <1% of such
126
+ // a file, forcing the agent to Read it anyway. Per-file must also stay ≥ the
127
+ // smaller <500 tier (3800) — the old 2500 was non-monotonic. Tokens are
128
+ // cheap relative to a 5–10 Read round-trip spiral; favor sufficiency.
129
+ maxOutputChars: 28000,
130
+ defaultMaxFiles: 10,
131
+ maxCharsPerFile: 6500,
132
+ gapThreshold: 12,
133
+ maxSymbolsInFileHeader: 10,
134
+ maxEdgesPerRelationshipKind: 10,
135
+ includeRelationships: true,
136
+ includeAdditionalFiles: true,
137
+ includeCompletenessSignal: true,
138
+ includeBudgetNote: true,
139
+ };
140
+ }
141
+ if (fileCount < 15000) {
142
+ return {
143
+ maxOutputChars: 35000,
144
+ defaultMaxFiles: 12,
145
+ maxCharsPerFile: 7000,
146
+ gapThreshold: 15,
147
+ maxSymbolsInFileHeader: 15,
148
+ maxEdgesPerRelationshipKind: 15,
149
+ includeRelationships: true,
150
+ includeAdditionalFiles: true,
151
+ includeCompletenessSignal: true,
152
+ includeBudgetNote: true,
153
+ };
154
+ }
155
+ return {
156
+ maxOutputChars: 38000,
157
+ defaultMaxFiles: 14,
158
+ maxCharsPerFile: 7000,
159
+ gapThreshold: 15,
160
+ maxSymbolsInFileHeader: 15,
161
+ maxEdgesPerRelationshipKind: 15,
162
+ includeRelationships: true,
163
+ includeAdditionalFiles: true,
164
+ includeCompletenessSignal: true,
165
+ includeBudgetNote: true,
166
+ };
167
+ }
168
+ /**
169
+ * Whether `codegraph_explore` should prefix source lines with their line
170
+ * numbers (cat -n style: `<num>\t<code>`).
171
+ *
172
+ * Line numbers let the agent cite `file:line` straight from the explore
173
+ * payload instead of re-Reading the file just to find a line number — the
174
+ * dominant residual cost on precise-tracing questions (#185 follow-up).
175
+ *
176
+ * Defaults ON. Set `CODEGRAPH_EXPLORE_LINENUMS=0` to disable (used by the
177
+ * A/B harness to measure the payload-cost vs. read-savings tradeoff).
178
+ */
179
+ function exploreLineNumbersEnabled() {
180
+ return process.env.CODEGRAPH_EXPLORE_LINENUMS !== '0';
181
+ }
182
+ /**
183
+ * Prefix each line of a source slice with its 1-based line number, matching
184
+ * the Read tool's `cat -n` convention (number + tab) so the agent treats it
185
+ * the same way it treats Read output.
186
+ *
187
+ * @param slice contiguous source text (already extracted from the file)
188
+ * @param firstLineNumber the 1-based line number of the slice's first line
189
+ */
190
+ function numberSourceLines(slice, firstLineNumber) {
191
+ const out = [];
192
+ const split = slice.split('\n');
193
+ for (let i = 0; i < split.length; i++) {
194
+ out.push(`${firstLineNumber + i}\t${split[i]}`);
195
+ }
196
+ return out.join('\n');
197
+ }
198
+ /**
199
+ * Mark a Claude session as having consulted MCP tools.
200
+ * This enables Grep/Glob/Bash commands that would otherwise be blocked.
201
+ *
202
+ * Why the explicit openSync + O_NOFOLLOW dance instead of plain writeFileSync:
203
+ * tmpdir() is world-writable on Linux (mode 1777), so on a shared multi-user
204
+ * machine any other local user can pre-create `codegraph-consulted-<hash>` as
205
+ * a symlink pointing at a file the victim owns. The old `writeFileSync` would
206
+ * happily follow that link and overwrite the target's contents with the ISO
207
+ * timestamp string (CWE-59). The session-id hash provides the predictability
208
+ * gate, but it's defense-in-depth: if a session id ever surfaces in logs,
209
+ * argv, or telemetry the attack becomes trivial, and the right fix is to not
210
+ * follow links from /tmp paths in the first place.
211
+ */
212
+ function markSessionConsulted(sessionId) {
213
+ try {
214
+ const hash = (0, crypto_1.createHash)('md5').update(sessionId).digest('hex').slice(0, 16);
215
+ const markerPath = (0, path_1.join)((0, os_1.tmpdir)(), `codegraph-consulted-${hash}`);
216
+ // Refuse to follow a pre-planted symlink at the marker path (CWE-59).
217
+ // O_NOFOLLOW (below) is the atomic, TOCTOU-free guard on POSIX, but it is
218
+ // `undefined` on Windows (libuv ignores it), so the bitwise-OR silently
219
+ // drops it and openSync would follow the link. This lstat check closes that
220
+ // gap cross-platform; ENOENT (path is free) falls through to create it.
221
+ try {
222
+ if ((0, fs_1.lstatSync)(markerPath).isSymbolicLink())
223
+ return;
224
+ }
225
+ catch {
226
+ // No existing entry (or stat failed) — nothing to refuse; proceed.
227
+ }
228
+ // O_NOFOLLOW makes openSync throw ELOOP if markerPath is already a symlink.
229
+ // O_CREAT + O_TRUNC keep the original "create-or-overwrite" semantics, and
230
+ // mode 0o600 prevents readback by other local users (the marker payload is
231
+ // benign, but narrowing the exposure costs nothing).
232
+ const flags = fs_1.constants.O_WRONLY | fs_1.constants.O_CREAT | fs_1.constants.O_TRUNC | fs_1.constants.O_NOFOLLOW;
233
+ const fd = (0, fs_1.openSync)(markerPath, flags, 0o600);
234
+ try {
235
+ (0, fs_1.writeSync)(fd, new Date().toISOString());
236
+ }
237
+ finally {
238
+ (0, fs_1.closeSync)(fd);
239
+ }
240
+ }
241
+ catch {
242
+ // Silently fail - don't break MCP on marker write failure. ELOOP from a
243
+ // planted symlink lands here too, which is the intended behavior: refuse
244
+ // to write rather than overwrite an attacker-chosen target.
245
+ }
246
+ }
247
+ /**
248
+ * Per-file staleness banner emitted at the top of a tool response when the
249
+ * file watcher has pending events for files referenced by the response.
250
+ * The agent uses this to fall back to Read for those specific files
251
+ * without waiting for the debounced sync (issue #403).
252
+ */
253
+ function formatStaleBanner(stale) {
254
+ const now = Date.now();
255
+ const lines = stale.map((p) => {
256
+ const ageMs = Math.max(0, now - p.lastSeenMs);
257
+ const label = p.indexing ? 'indexing in progress' : 'pending sync';
258
+ return ` - ${p.path} (edited ${ageMs}ms ago, ${label})`;
259
+ });
260
+ return ('⚠️ Some files referenced below were edited since the last index sync — ' +
261
+ 'their codegraph entries may be stale:\n' +
262
+ lines.join('\n') +
263
+ '\nFor accurate content of those specific files, Read them directly. ' +
264
+ 'The rest of this response is fresh.');
265
+ }
266
+ /**
267
+ * Compact footer listing pending files that are NOT referenced in this
268
+ * response. Gives the agent a complete project-wide freshness picture
269
+ * without bloating the main banner.
270
+ */
271
+ function formatStaleFooter(stale) {
272
+ const MAX = 5;
273
+ const now = Date.now();
274
+ const shown = stale.slice(0, MAX);
275
+ const lines = shown.map((p) => {
276
+ const ageMs = Math.max(0, now - p.lastSeenMs);
277
+ return ` - ${p.path} (edited ${ageMs}ms ago)`;
278
+ });
279
+ const more = stale.length > MAX ? `\n - …and ${stale.length - MAX} more` : '';
280
+ return (`(Note: ${stale.length} file(s) elsewhere in this project are pending index ` +
281
+ `sync but were not referenced above:\n${lines.join('\n')}${more})`);
282
+ }
283
+ /**
284
+ * Common projectPath property for cross-project queries
285
+ */
286
+ const projectPathProperty = {
287
+ type: 'string',
288
+ description: 'Path to a different project with .codegraph/ initialized. If omitted, uses current project. Use this to query other codebases.',
289
+ };
290
+ /**
291
+ * All CodeGraph MCP tools
292
+ *
293
+ * Designed for minimal context usage - use codegraph_context as the primary tool,
294
+ * and only use other tools for targeted follow-up queries.
295
+ *
296
+ * All tools support cross-project queries via the optional `projectPath` parameter.
297
+ */
298
+ exports.tools = [
299
+ {
300
+ name: 'codegraph_search',
301
+ description: 'Quick symbol search by name. Returns locations only (no code). Use codegraph_context instead for comprehensive task context.',
302
+ inputSchema: {
303
+ type: 'object',
304
+ properties: {
305
+ query: {
306
+ type: 'string',
307
+ description: 'Symbol name or partial name (e.g., "auth", "signIn", "UserService")',
308
+ },
309
+ kind: {
310
+ type: 'string',
311
+ description: 'Filter by node kind',
312
+ enum: ['function', 'method', 'class', 'interface', 'type', 'variable', 'route', 'component'],
313
+ },
314
+ limit: {
315
+ type: 'number',
316
+ description: 'Maximum results (default: 10)',
317
+ default: 10,
318
+ },
319
+ projectPath: projectPathProperty,
320
+ },
321
+ required: ['query'],
322
+ },
323
+ },
324
+ {
325
+ name: 'codegraph_context',
326
+ description: 'PRIMARY TOOL — call this FIRST for any "how does X work", architecture, feature, or bug-context question. Composes search + node + callers + callees and returns entry points, related symbols, and key code in ONE call — usually enough to answer with no further search/Read/Grep. Prefer this over chaining codegraph_search + codegraph_node, and over codegraph_explore. NOTE: provides CODE context, not product requirements; for new features still clarify UX/edge cases with the user.',
327
+ inputSchema: {
328
+ type: 'object',
329
+ properties: {
330
+ task: {
331
+ type: 'string',
332
+ description: 'Description of the task, bug, or feature to build context for',
333
+ },
334
+ maxNodes: {
335
+ type: 'number',
336
+ description: 'Maximum symbols to include (default: 20)',
337
+ default: 20,
338
+ },
339
+ includeCode: {
340
+ type: 'boolean',
341
+ description: 'Include code snippets for key symbols (default: true)',
342
+ default: true,
343
+ },
344
+ projectPath: projectPathProperty,
345
+ },
346
+ required: ['task'],
347
+ },
348
+ },
349
+ {
350
+ name: 'codegraph_callers',
351
+ description: 'Find all functions/methods that call a specific symbol. Useful for understanding usage patterns and impact of changes.',
352
+ inputSchema: {
353
+ type: 'object',
354
+ properties: {
355
+ symbol: {
356
+ type: 'string',
357
+ description: 'Name of the function, method, or class to find callers for',
358
+ },
359
+ limit: {
360
+ type: 'number',
361
+ description: 'Maximum number of callers to return (default: 20)',
362
+ default: 20,
363
+ },
364
+ projectPath: projectPathProperty,
365
+ },
366
+ required: ['symbol'],
367
+ },
368
+ },
369
+ {
370
+ name: 'codegraph_callees',
371
+ description: 'Find all functions/methods that a specific symbol calls. Useful for understanding dependencies and code flow.',
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ symbol: {
376
+ type: 'string',
377
+ description: 'Name of the function, method, or class to find callees for',
378
+ },
379
+ limit: {
380
+ type: 'number',
381
+ description: 'Maximum number of callees to return (default: 20)',
382
+ default: 20,
383
+ },
384
+ projectPath: projectPathProperty,
385
+ },
386
+ required: ['symbol'],
387
+ },
388
+ },
389
+ {
390
+ name: 'codegraph_impact',
391
+ description: 'Analyze the impact radius of changing a symbol. Shows what code could be affected by modifications.',
392
+ inputSchema: {
393
+ type: 'object',
394
+ properties: {
395
+ symbol: {
396
+ type: 'string',
397
+ description: 'Name of the symbol to analyze impact for',
398
+ },
399
+ depth: {
400
+ type: 'number',
401
+ description: 'How many levels of dependencies to traverse (default: 2)',
402
+ default: 2,
403
+ },
404
+ projectPath: projectPathProperty,
405
+ },
406
+ required: ['symbol'],
407
+ },
408
+ },
409
+ {
410
+ name: 'codegraph_node',
411
+ description: 'Get ONE symbol\'s details (location, signature, docstring) PLUS its TRAIL — what it calls and what calls it, each with file:line. Pass includeCode=true for source (functions return their body; containers return a member outline). Use this to WALK the call graph hop-by-hop — node a symbol, then node one of its trail entries — the structural, no-Read way to follow "what calls/triggers/handles X" across files. For a broad first overview of many symbols at once use codegraph_explore; use node to drill along a specific path from there. (If a trail is empty on a non-leaf, that hop is likely dynamic dispatch — read just that line.) Source returned with includeCode is the verbatim live file content — identical to Read.',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ symbol: {
416
+ type: 'string',
417
+ description: 'Name of the symbol to get details for',
418
+ },
419
+ includeCode: {
420
+ type: 'boolean',
421
+ description: 'Include full source code (default: false to minimize context)',
422
+ default: false,
423
+ },
424
+ projectPath: projectPathProperty,
425
+ },
426
+ required: ['symbol'],
427
+ },
428
+ },
429
+ {
430
+ name: 'codegraph_explore',
431
+ description: 'Returns source for SEVERAL related symbols grouped by file, plus a relationship map, in ONE capped call. This is the efficient way to inspect many related symbols at once — strongly prefer it over a series of codegraph_node or Read calls (each separate call re-reads the whole context, so 8 node calls cost far more than 1 explore). Use it after codegraph_context when you need to see the actual source of several symbols. Query with specific symbol/file/code terms, NOT natural-language sentences — run codegraph_search first to find names. Bad: "how are agent prompts loaded and passed to the CLI". Good: "renderStaticScene drawElementOnCanvas ShapeCache renderElement.ts". The code it returns is the VERBATIM live file source (byte-for-byte identical to Read), line-numbered — not a summary; treat files it shows as already Read, no need to re-open them.',
432
+ inputSchema: {
433
+ type: 'object',
434
+ properties: {
435
+ query: {
436
+ type: 'string',
437
+ 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.',
438
+ },
439
+ maxFiles: {
440
+ type: 'number',
441
+ description: 'Maximum number of files to include source code from (default: 12)',
442
+ default: 12,
443
+ },
444
+ projectPath: projectPathProperty,
445
+ },
446
+ required: ['query'],
447
+ },
448
+ },
449
+ {
450
+ name: 'codegraph_status',
451
+ description: 'Get the status of the CodeGraph index, including statistics about indexed files, nodes, and edges.',
452
+ inputSchema: {
453
+ type: 'object',
454
+ properties: {
455
+ projectPath: projectPathProperty,
456
+ },
457
+ },
458
+ },
459
+ {
460
+ name: 'codegraph_files',
461
+ description: 'REQUIRED for file/folder exploration. Get the project file structure from the CodeGraph index. Returns a tree view of all indexed files with metadata (language, symbol count). Much faster than Glob/filesystem scanning. Use this FIRST when exploring project structure, finding files, or understanding codebase organization.',
462
+ inputSchema: {
463
+ type: 'object',
464
+ properties: {
465
+ path: {
466
+ type: 'string',
467
+ description: 'Filter to files under this directory path (e.g., "src/components"). Returns all files if not specified.',
468
+ },
469
+ pattern: {
470
+ type: 'string',
471
+ description: 'Filter files matching this glob pattern (e.g., "*.tsx", "**/*.test.ts")',
472
+ },
473
+ format: {
474
+ type: 'string',
475
+ description: 'Output format: "tree" (hierarchical, default), "flat" (simple list), "grouped" (by language)',
476
+ enum: ['tree', 'flat', 'grouped'],
477
+ default: 'tree',
478
+ },
479
+ includeMetadata: {
480
+ type: 'boolean',
481
+ description: 'Include file metadata like language and symbol count (default: true)',
482
+ default: true,
483
+ },
484
+ maxDepth: {
485
+ type: 'number',
486
+ description: 'Maximum directory depth to show (default: unlimited)',
487
+ },
488
+ projectPath: projectPathProperty,
489
+ },
490
+ },
491
+ },
492
+ {
493
+ name: 'codegraph_trace',
494
+ description: 'Trace the CALL PATH between two symbols — "how does <from> reach/become <to>?" Returns the chain of functions from one to the other (each hop with file:line and its body inlined, plus the outgoing calls of the destination itself) in ONE call. This is something grep/Read structurally cannot do: there is no text pattern for "the path from A to B". Ideal for flow questions — how an update triggers a render, how a request reaches a handler, how a QuerySet becomes SQL. If no static path exists the chain likely breaks at dynamic dispatch (callbacks/descriptors/metaclasses); the tool says where and points you to codegraph_node to bridge it.',
495
+ inputSchema: {
496
+ type: 'object',
497
+ properties: {
498
+ from: {
499
+ type: 'string',
500
+ description: 'Symbol the flow starts at (e.g., "QuerySet", "handleRequest", "mutateElement")',
501
+ },
502
+ to: {
503
+ type: 'string',
504
+ description: 'Symbol the flow should reach (e.g., "execute_sql", "render", "setState")',
505
+ },
506
+ projectPath: projectPathProperty,
507
+ },
508
+ required: ['from', 'to'],
509
+ },
510
+ },
511
+ ];
512
+ /**
513
+ * Tool handler that executes tools against a CodeGraph instance
514
+ *
515
+ * Supports cross-project queries via the projectPath parameter.
516
+ * Other projects are opened on-demand and cached for performance.
517
+ */
518
+ class ToolHandler {
519
+ cg;
520
+ // Cache of opened CodeGraph instances for cross-project queries
521
+ projectCache = new Map();
522
+ // The directory the server last searched for a default project. Surfaced in
523
+ // the "not initialized" error so users can see why detection missed.
524
+ defaultProjectHint = null;
525
+ // Per-start-path cache of the git worktree/index mismatch (issue #155). The
526
+ // mismatch is a fixed property of (where the request came from → which
527
+ // .codegraph/ it resolves to), so the up-to-two `git rev-parse` spawns run
528
+ // once and every later tool call reuses the result — never shelling out to
529
+ // git on the hot path. `undefined` = not computed yet; `null` = no mismatch.
530
+ worktreeMismatchCache = new Map();
531
+ constructor(cg) {
532
+ this.cg = cg;
533
+ }
534
+ /**
535
+ * Update the default CodeGraph instance (e.g. after lazy initialization)
536
+ */
537
+ setDefaultCodeGraph(cg) {
538
+ this.cg = cg;
539
+ }
540
+ /**
541
+ * Record the directory the server tried to resolve the default project from.
542
+ * Used only to make the "no default project" error actionable.
543
+ */
544
+ setDefaultProjectHint(searchedPath) {
545
+ this.defaultProjectHint = searchedPath;
546
+ }
547
+ /**
548
+ * Whether a default CodeGraph instance is available
549
+ */
550
+ hasDefaultCodeGraph() {
551
+ return this.cg !== null;
552
+ }
553
+ /**
554
+ * Optional allowlist of exposed tools, parsed from the CODEGRAPH_MCP_TOOLS
555
+ * env var (comma-separated short names, e.g. "trace,search,node,context").
556
+ * Unset/empty → every tool is exposed. Lets an operator (or an A/B harness)
557
+ * trim the tool surface without rebuilding the client config; the ablated
558
+ * tool is then truly absent from ListTools rather than merely denied on call.
559
+ * Matching is on the short form, so "trace" and "codegraph_trace" both work.
560
+ */
561
+ toolAllowlist() {
562
+ const raw = process.env.CODEGRAPH_MCP_TOOLS;
563
+ if (!raw || !raw.trim())
564
+ return null;
565
+ const short = (s) => s.trim().replace(/^codegraph_/, '');
566
+ const set = new Set(raw.split(',').map(short).filter(Boolean));
567
+ return set.size ? set : null;
568
+ }
569
+ /** Whether a tool name passes the CODEGRAPH_MCP_TOOLS allowlist (if any). */
570
+ isToolAllowed(name) {
571
+ const allow = this.toolAllowlist();
572
+ return !allow || allow.has(name.replace(/^codegraph_/, ''));
573
+ }
574
+ /**
575
+ * Get tool definitions with dynamic descriptions based on project size.
576
+ * The codegraph_explore tool description includes a budget recommendation
577
+ * scaled to the number of indexed files. Honors the CODEGRAPH_MCP_TOOLS
578
+ * allowlist so a trimmed surface is reflected in ListTools.
579
+ */
580
+ getTools() {
581
+ const allow = this.toolAllowlist();
582
+ const visible = allow
583
+ ? exports.tools.filter(t => allow.has(t.name.replace(/^codegraph_/, '')))
584
+ : exports.tools;
585
+ if (!this.cg)
586
+ return visible;
587
+ try {
588
+ const stats = this.cg.getStats();
589
+ const budget = getExploreBudget(stats.fileCount);
590
+ return visible.map(tool => {
591
+ if (tool.name === 'codegraph_explore') {
592
+ return {
593
+ ...tool,
594
+ description: `${tool.description} Budget: make at most ${budget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).`,
595
+ };
596
+ }
597
+ return tool;
598
+ });
599
+ }
600
+ catch {
601
+ return visible;
602
+ }
603
+ }
604
+ /**
605
+ * Get CodeGraph instance for a project
606
+ *
607
+ * If projectPath is provided, opens that project's CodeGraph (cached).
608
+ * Otherwise returns the default CodeGraph instance.
609
+ *
610
+ * Walks up parent directories to find the nearest .codegraph/ folder,
611
+ * similar to how git finds .git/ directories.
612
+ */
613
+ getCodeGraph(projectPath) {
614
+ if (!projectPath) {
615
+ if (!this.cg) {
616
+ const searched = this.defaultProjectHint ?? process.cwd();
617
+ throw new Error('No CodeGraph project is loaded for this session.\n' +
618
+ `Searched for a .codegraph/ directory starting from: ${searched}\n` +
619
+ 'The index is likely fine — this is a working-directory detection issue: ' +
620
+ "the MCP client launched the server outside your project and didn't report the " +
621
+ 'workspace root. Fix it either way:\n' +
622
+ ' • Pass projectPath to the tool call, e.g. projectPath: "/absolute/path/to/your/project"\n' +
623
+ ' • Or add --path to the server\'s MCP config args: ["serve", "--mcp", "--path", "/absolute/path/to/your/project"]');
624
+ }
625
+ return this.cg;
626
+ }
627
+ // Check cache first (using original path as key)
628
+ if (this.projectCache.has(projectPath)) {
629
+ return this.projectCache.get(projectPath);
630
+ }
631
+ // Reject sensitive system directories before opening. Only validate a
632
+ // path that actually exists — a nested or not-yet-created sub-path of a
633
+ // real project must still be allowed to resolve UP to its .codegraph/
634
+ // root below (issue #238), so we don't run the existence-checking
635
+ // validator on paths that are meant to walk up.
636
+ if ((0, fs_1.existsSync)(projectPath)) {
637
+ const pathError = (0, utils_1.validateProjectPath)(projectPath);
638
+ if (pathError) {
639
+ throw new Error(pathError);
640
+ }
641
+ }
642
+ // Walk up parent directories to find nearest .codegraph/
643
+ const resolvedRoot = (0, index_1.findNearestCodeGraphRoot)(projectPath);
644
+ if (!resolvedRoot) {
645
+ throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
646
+ }
647
+ // If the path resolves to the default project, reuse the already-open
648
+ // default instance rather than opening a SECOND connection to the same DB.
649
+ // A duplicate connection serializes reads against the watcher's auto-sync
650
+ // writes; on the wasm backend (no WAL) that surfaces as intermittent
651
+ // "database is locked" on concurrent tool calls. See issue #238. Deliberately
652
+ // not cached under projectPath — the server owns and closes the default
653
+ // instance, so routing it through projectCache.closeAll() would double-close it.
654
+ if (this.cg && this.cg.getProjectRoot() === resolvedRoot) {
655
+ return this.cg;
656
+ }
657
+ // Check if we already have this resolved root cached (different path, same project)
658
+ if (this.projectCache.has(resolvedRoot)) {
659
+ const cg = this.projectCache.get(resolvedRoot);
660
+ // Cache under original path too for faster future lookups
661
+ this.projectCache.set(projectPath, cg);
662
+ return cg;
663
+ }
664
+ // Open and cache under both paths
665
+ const cg = index_1.default.openSync(resolvedRoot);
666
+ this.projectCache.set(resolvedRoot, cg);
667
+ if (projectPath !== resolvedRoot) {
668
+ this.projectCache.set(projectPath, cg);
669
+ }
670
+ return cg;
671
+ }
672
+ /**
673
+ * Close all cached project connections
674
+ */
675
+ closeAll() {
676
+ for (const cg of this.projectCache.values()) {
677
+ cg.close();
678
+ }
679
+ this.projectCache.clear();
680
+ this.worktreeMismatchCache.clear();
681
+ }
682
+ /**
683
+ * Validate that a value is a non-empty string within length bounds.
684
+ *
685
+ * The `maxLength` cap protects against MCP clients that ship huge
686
+ * payloads (10MB+ query strings either by accident or maliciously).
687
+ * Without this, a single oversized input can pin the FTS5 index or
688
+ * exhaust memory before any real work runs.
689
+ */
690
+ validateString(value, name, maxLength = MAX_INPUT_LENGTH) {
691
+ if (typeof value !== 'string' || value.length === 0) {
692
+ return this.errorResult(`${name} must be a non-empty string`);
693
+ }
694
+ if (value.length > maxLength) {
695
+ return this.errorResult(`${name} exceeds maximum length of ${maxLength} characters (got ${value.length})`);
696
+ }
697
+ return value;
698
+ }
699
+ /**
700
+ * Validate an optional path-like string input. Returns the value if
701
+ * valid (or undefined), or a ToolResult with the error.
702
+ */
703
+ validateOptionalPath(value, name) {
704
+ if (value === undefined || value === null)
705
+ return undefined;
706
+ if (typeof value !== 'string') {
707
+ return this.errorResult(`${name} must be a string`);
708
+ }
709
+ if (value.length > MAX_PATH_LENGTH) {
710
+ return this.errorResult(`${name} exceeds maximum length of ${MAX_PATH_LENGTH} characters (got ${value.length})`);
711
+ }
712
+ return value;
713
+ }
714
+ /**
715
+ * Cached git worktree/index mismatch for a tool call's effective project.
716
+ *
717
+ * The "effective project" is what the request targets: an explicit
718
+ * `projectPath` arg, else the directory the server resolved its default
719
+ * project from (`defaultProjectHint`), else cwd. Memoized per start path —
720
+ * see `worktreeMismatchCache`. Best-effort: if the project can't be resolved
721
+ * (e.g. nothing initialized yet), it reports "no mismatch" so a tool is never
722
+ * broken by this check.
723
+ */
724
+ worktreeMismatchFor(projectPath) {
725
+ const startPath = projectPath ?? this.defaultProjectHint ?? process.cwd();
726
+ const cached = this.worktreeMismatchCache.get(startPath);
727
+ if (cached !== undefined)
728
+ return cached;
729
+ let mismatch = null;
730
+ try {
731
+ mismatch = (0, worktree_1.detectWorktreeIndexMismatch)(startPath, this.getCodeGraph(projectPath).getProjectRoot());
732
+ }
733
+ catch {
734
+ // No resolvable project (or any other resolution error) → nothing to warn.
735
+ mismatch = null;
736
+ }
737
+ this.worktreeMismatchCache.set(startPath, mismatch);
738
+ return mismatch;
739
+ }
740
+ /**
741
+ * Prefix a successful read-tool result with a compact worktree-mismatch
742
+ * notice when the resolved index belongs to a different git working tree than
743
+ * the caller's (issue #155). Without this, an agent in a nested worktree
744
+ * silently trusts main-branch results. No-op on error results and when there
745
+ * is no mismatch. `codegraph_status` is excluded — it embeds its own verbose
746
+ * warning — so it stays out of this path.
747
+ */
748
+ withWorktreeNotice(result, projectPath) {
749
+ if (result.isError)
750
+ return result;
751
+ const mismatch = this.worktreeMismatchFor(projectPath);
752
+ if (!mismatch)
753
+ return result;
754
+ const notice = (0, worktree_1.worktreeMismatchNotice)(mismatch);
755
+ const [first, ...rest] = result.content;
756
+ if (first && first.type === 'text') {
757
+ return { ...result, content: [{ type: 'text', text: `${notice}\n\n${first.text}` }, ...rest] };
758
+ }
759
+ return result;
760
+ }
761
+ /**
762
+ * Annotate a successful read-tool result with per-file staleness — the
763
+ * non-blocking answer to issue #403. The file watcher tracks every event
764
+ * it sees per path; here we intersect "files referenced in this response"
765
+ * against that pending set and prepend a compact banner so the agent can
766
+ * fall back to Read for those *specific* files without waiting for the
767
+ * debounced sync to fire. Other pending files in the project (not
768
+ * referenced by this response) get a small footer so the agent has a
769
+ * complete picture without bloating the banner.
770
+ *
771
+ * Cost when nothing is pending — the common case — is one boolean check.
772
+ * No I/O, no parsing of markdown beyond a per-pending-file substring scan.
773
+ */
774
+ withStalenessNotice(result, projectPath) {
775
+ if (result.isError)
776
+ return result;
777
+ let cg;
778
+ try {
779
+ cg = this.getCodeGraph(projectPath);
780
+ }
781
+ catch {
782
+ return result; // no default project — leave as is
783
+ }
784
+ // Cross-project `projectPath` calls open a cached CodeGraph WITHOUT a
785
+ // watcher (watchers are only attached to the default session project).
786
+ // When the cross-project path happens to be the same project as the
787
+ // default cg, the cached instance is the wrong one — its pendingFiles is
788
+ // permanently empty. Detect the equal-path case and prefer the default
789
+ // cg so the staleness signal still fires when an agent passes the
790
+ // explicit projectPath form of its own project.
791
+ if (this.cg && cg !== this.cg) {
792
+ try {
793
+ const sameProject = (0, path_1.resolve)(this.cg.getProjectRoot()) === (0, path_1.resolve)(cg.getProjectRoot());
794
+ if (sameProject)
795
+ cg = this.cg;
796
+ }
797
+ catch {
798
+ /* getProjectRoot may throw on a closed instance — leave cg as is */
799
+ }
800
+ }
801
+ // Defensive: some test fakes inject a partial CodeGraph stub without the
802
+ // newer pending-files API. Treat missing/throwing as "no pending files."
803
+ let pending = [];
804
+ try {
805
+ pending = cg.getPendingFiles?.() ?? [];
806
+ }
807
+ catch {
808
+ return result;
809
+ }
810
+ if (pending.length === 0)
811
+ return result;
812
+ const [first, ...rest] = result.content;
813
+ if (!first || first.type !== 'text')
814
+ return result;
815
+ const text = first.text;
816
+ const inResponse = [];
817
+ const elsewhere = [];
818
+ for (const p of pending) {
819
+ // Substring match against the project-relative POSIX path — that's
820
+ // exactly the format both the watcher and every codegraph response
821
+ // emit, so a plain includes() is sufficient and avoids regex pitfalls.
822
+ if (text.includes(p.path))
823
+ inResponse.push(p);
824
+ else
825
+ elsewhere.push(p);
826
+ }
827
+ let banner = '';
828
+ if (inResponse.length > 0) {
829
+ banner = formatStaleBanner(inResponse);
830
+ }
831
+ let footer = '';
832
+ if (elsewhere.length > 0) {
833
+ footer = formatStaleFooter(elsewhere);
834
+ }
835
+ if (!banner && !footer)
836
+ return result;
837
+ const composed = [banner, text, footer].filter(Boolean).join('\n\n');
838
+ return { ...result, content: [{ type: 'text', text: composed }, ...rest] };
839
+ }
840
+ /**
841
+ * Execute a tool by name
842
+ */
843
+ async execute(toolName, args) {
844
+ try {
845
+ // Honor the optional tool allowlist (CODEGRAPH_MCP_TOOLS): a trimmed
846
+ // surface rejects ablated tools defensively even if a client cached them.
847
+ if (!this.isToolAllowed(toolName)) {
848
+ return this.errorResult(`Tool ${toolName} is disabled via CODEGRAPH_MCP_TOOLS`);
849
+ }
850
+ // Cross-cutting input validation. All tools accept an optional
851
+ // `projectPath` and most accept either `query`, `task`, or
852
+ // `symbol` — bound their lengths centrally so individual handlers
853
+ // can stay focused on tool-specific logic.
854
+ const pathCheck = this.validateOptionalPath(args.projectPath, 'projectPath');
855
+ if (typeof pathCheck === 'object' && pathCheck !== undefined) {
856
+ return pathCheck;
857
+ }
858
+ // The `path` and `pattern` properties used by codegraph_files are
859
+ // also path-shaped — apply the same cap.
860
+ if (args.path !== undefined) {
861
+ const check = this.validateOptionalPath(args.path, 'path');
862
+ if (typeof check === 'object' && check !== undefined)
863
+ return check;
864
+ }
865
+ if (args.pattern !== undefined) {
866
+ const check = this.validateOptionalPath(args.pattern, 'pattern');
867
+ if (typeof check === 'object' && check !== undefined)
868
+ return check;
869
+ }
870
+ // Read tools resolve through a single result variable so cross-cutting
871
+ // notices — worktree-index mismatch (issue #155) and per-file
872
+ // staleness (issue #403) — can be applied in one place. status embeds
873
+ // its own verbose worktree warning but still flows through the
874
+ // staleness wrapper so its pending-files section stays consistent
875
+ // with what the read tools surface.
876
+ let result;
877
+ switch (toolName) {
878
+ case 'codegraph_search':
879
+ result = await this.handleSearch(args);
880
+ break;
881
+ case 'codegraph_context':
882
+ result = await this.handleContext(args);
883
+ break;
884
+ case 'codegraph_callers':
885
+ result = await this.handleCallers(args);
886
+ break;
887
+ case 'codegraph_callees':
888
+ result = await this.handleCallees(args);
889
+ break;
890
+ case 'codegraph_impact':
891
+ result = await this.handleImpact(args);
892
+ break;
893
+ case 'codegraph_explore':
894
+ result = await this.handleExplore(args);
895
+ break;
896
+ case 'codegraph_node':
897
+ result = await this.handleNode(args);
898
+ break;
899
+ case 'codegraph_status':
900
+ // status embeds the pending-files list as a first-class section
901
+ // (see handleStatus), so we skip the auto-banner wrapper here to
902
+ // avoid duplicating the same info at the top of the response.
903
+ return await this.handleStatus(args);
904
+ case 'codegraph_files':
905
+ result = await this.handleFiles(args);
906
+ break;
907
+ case 'codegraph_trace':
908
+ result = await this.handleTrace(args);
909
+ break;
910
+ default:
911
+ return this.errorResult(`Unknown tool: ${toolName}`);
912
+ }
913
+ const withWorktree = this.withWorktreeNotice(result, args.projectPath);
914
+ return this.withStalenessNotice(withWorktree, args.projectPath);
915
+ }
916
+ catch (err) {
917
+ return this.errorResult(`Tool execution failed: ${err instanceof Error ? err.message : String(err)}`);
918
+ }
919
+ }
920
+ /**
921
+ * Handle codegraph_search
922
+ */
923
+ async handleSearch(args) {
924
+ const query = this.validateString(args.query, 'query');
925
+ if (typeof query !== 'string')
926
+ return query;
927
+ const cg = this.getCodeGraph(args.projectPath);
928
+ const kind = args.kind;
929
+ const rawLimit = Number(args.limit) || 10;
930
+ const limit = (0, utils_1.clamp)(rawLimit, 1, 100);
931
+ const results = cg.searchNodes(query, {
932
+ limit,
933
+ kinds: kind ? [kind] : undefined,
934
+ });
935
+ if (results.length === 0) {
936
+ return this.textResult(`No results found for "${query}"`);
937
+ }
938
+ const formatted = this.formatSearchResults(results);
939
+ return this.textResult(this.truncateOutput(formatted));
940
+ }
941
+ /**
942
+ * Handle codegraph_context
943
+ */
944
+ async handleContext(args) {
945
+ const task = this.validateString(args.task, 'task');
946
+ if (typeof task !== 'string')
947
+ return task;
948
+ // Mark session as consulted (enables Grep/Glob/Bash)
949
+ const sessionId = process.env.CLAUDE_SESSION_ID;
950
+ if (sessionId) {
951
+ markSessionConsulted(sessionId);
952
+ }
953
+ const cg = this.getCodeGraph(args.projectPath);
954
+ const maxNodes = args.maxNodes || 20;
955
+ const includeCode = args.includeCode !== false;
956
+ const context = await cg.buildContext(task, {
957
+ maxNodes,
958
+ includeCode,
959
+ format: 'markdown',
960
+ });
961
+ // Detect if this looks like a feature request (vs bug fix or exploration)
962
+ const isFeatureQuery = this.looksLikeFeatureRequest(task);
963
+ const reminder = isFeatureQuery
964
+ ? '\n\n⚠️ **Ask user:** UX preferences, edge cases, acceptance criteria'
965
+ : '';
966
+ // buildContext returns string when format is 'markdown'
967
+ if (typeof context === 'string') {
968
+ return this.textResult(this.truncateOutput(context + reminder));
969
+ }
970
+ // If it returns TaskContext, format it
971
+ return this.textResult(this.truncateOutput(this.formatTaskContext(context) + reminder));
972
+ }
973
+ /**
974
+ * Heuristic to detect if a query looks like a feature request
975
+ */
976
+ looksLikeFeatureRequest(task) {
977
+ const featureKeywords = [
978
+ 'add', 'create', 'implement', 'build', 'enable', 'allow',
979
+ 'new feature', 'support for', 'ability to', 'want to',
980
+ 'should be able', 'need to add', 'swap', 'edit', 'modify'
981
+ ];
982
+ const bugKeywords = [
983
+ 'fix', 'bug', 'error', 'broken', 'crash', 'issue', 'problem',
984
+ 'not working', 'fails', 'undefined', 'null'
985
+ ];
986
+ const explorationKeywords = [
987
+ 'how does', 'where is', 'what is', 'find', 'show me',
988
+ 'explain', 'understand', 'explore'
989
+ ];
990
+ const lowerTask = task.toLowerCase();
991
+ // If it's clearly a bug or exploration, not a feature
992
+ if (bugKeywords.some(k => lowerTask.includes(k)))
993
+ return false;
994
+ if (explorationKeywords.some(k => lowerTask.includes(k)))
995
+ return false;
996
+ // If it matches feature keywords, it's likely a feature request
997
+ return featureKeywords.some(k => lowerTask.includes(k));
998
+ }
999
+ /**
1000
+ * Handle codegraph_callers
1001
+ */
1002
+ async handleCallers(args) {
1003
+ const symbol = this.validateString(args.symbol, 'symbol');
1004
+ if (typeof symbol !== 'string')
1005
+ return symbol;
1006
+ const cg = this.getCodeGraph(args.projectPath);
1007
+ const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1008
+ const allMatches = this.findAllSymbols(cg, symbol);
1009
+ if (allMatches.nodes.length === 0) {
1010
+ return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1011
+ }
1012
+ // Aggregate callers across all matching symbols
1013
+ const seen = new Set();
1014
+ const allCallers = [];
1015
+ for (const node of allMatches.nodes) {
1016
+ for (const c of cg.getCallers(node.id)) {
1017
+ if (!seen.has(c.node.id)) {
1018
+ seen.add(c.node.id);
1019
+ allCallers.push(c.node);
1020
+ }
1021
+ }
1022
+ }
1023
+ if (allCallers.length === 0) {
1024
+ return this.textResult(`No callers found for "${symbol}"${allMatches.note}`);
1025
+ }
1026
+ const formatted = this.formatNodeList(allCallers.slice(0, limit), `Callers of ${symbol}`) + allMatches.note;
1027
+ return this.textResult(this.truncateOutput(formatted));
1028
+ }
1029
+ /**
1030
+ * Handle codegraph_callees
1031
+ */
1032
+ async handleCallees(args) {
1033
+ const symbol = this.validateString(args.symbol, 'symbol');
1034
+ if (typeof symbol !== 'string')
1035
+ return symbol;
1036
+ const cg = this.getCodeGraph(args.projectPath);
1037
+ const limit = (0, utils_1.clamp)(args.limit || 20, 1, 100);
1038
+ const allMatches = this.findAllSymbols(cg, symbol);
1039
+ if (allMatches.nodes.length === 0) {
1040
+ return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1041
+ }
1042
+ // Aggregate callees across all matching symbols
1043
+ const seen = new Set();
1044
+ const allCallees = [];
1045
+ for (const node of allMatches.nodes) {
1046
+ for (const c of cg.getCallees(node.id)) {
1047
+ if (!seen.has(c.node.id)) {
1048
+ seen.add(c.node.id);
1049
+ allCallees.push(c.node);
1050
+ }
1051
+ }
1052
+ }
1053
+ if (allCallees.length === 0) {
1054
+ return this.textResult(`No callees found for "${symbol}"${allMatches.note}`);
1055
+ }
1056
+ const formatted = this.formatNodeList(allCallees.slice(0, limit), `Callees of ${symbol}`) + allMatches.note;
1057
+ return this.textResult(this.truncateOutput(formatted));
1058
+ }
1059
+ /**
1060
+ * Handle codegraph_impact
1061
+ */
1062
+ async handleImpact(args) {
1063
+ const symbol = this.validateString(args.symbol, 'symbol');
1064
+ if (typeof symbol !== 'string')
1065
+ return symbol;
1066
+ const cg = this.getCodeGraph(args.projectPath);
1067
+ const depth = (0, utils_1.clamp)(args.depth || 2, 1, 10);
1068
+ const allMatches = this.findAllSymbols(cg, symbol);
1069
+ if (allMatches.nodes.length === 0) {
1070
+ return this.textResult(`Symbol "${symbol}" not found in the codebase`);
1071
+ }
1072
+ // Aggregate impact across all matching symbols
1073
+ const mergedNodes = new Map();
1074
+ const mergedEdges = [];
1075
+ const seenEdges = new Set();
1076
+ for (const node of allMatches.nodes) {
1077
+ const impact = cg.getImpactRadius(node.id, depth);
1078
+ for (const [id, n] of impact.nodes) {
1079
+ mergedNodes.set(id, n);
1080
+ }
1081
+ for (const e of impact.edges) {
1082
+ const key = `${e.source}->${e.target}:${e.kind}`;
1083
+ if (!seenEdges.has(key)) {
1084
+ seenEdges.add(key);
1085
+ mergedEdges.push(e);
1086
+ }
1087
+ }
1088
+ }
1089
+ const mergedImpact = {
1090
+ nodes: mergedNodes,
1091
+ edges: mergedEdges,
1092
+ roots: allMatches.nodes.map(n => n.id),
1093
+ };
1094
+ const formatted = this.formatImpact(symbol, mergedImpact) + allMatches.note;
1095
+ return this.textResult(this.truncateOutput(formatted));
1096
+ }
1097
+ /**
1098
+ * Handle codegraph_trace — shortest CALL PATH between two symbols.
1099
+ *
1100
+ * Exposes GraphTraverser.findPath: the chain of functions from `from` to `to`,
1101
+ * each hop annotated with file:line and the call-site line. This is the
1102
+ * capability grep/Read structurally cannot provide. When no static path
1103
+ * exists, the chain has almost certainly broken at dynamic dispatch
1104
+ * (callbacks, descriptors, metaclasses) — we say so and surface the start
1105
+ * symbol's outgoing calls so the agent bridges the one missing hop with
1106
+ * codegraph_node rather than blindly reading.
1107
+ */
1108
+ async handleTrace(args) {
1109
+ const from = this.validateString(args.from, 'from');
1110
+ if (typeof from !== 'string')
1111
+ return from;
1112
+ const to = this.validateString(args.to, 'to');
1113
+ if (typeof to !== 'string')
1114
+ return to;
1115
+ const cg = this.getCodeGraph(args.projectPath);
1116
+ const fromMatches = this.findAllSymbols(cg, from);
1117
+ if (fromMatches.nodes.length === 0)
1118
+ return this.textResult(`Symbol "${from}" not found in the codebase`);
1119
+ const toMatches = this.findAllSymbols(cg, to);
1120
+ if (toMatches.nodes.length === 0)
1121
+ return this.textResult(`Symbol "${to}" not found in the codebase`);
1122
+ // Trace along call edges only — a true call path. Names can map to several
1123
+ // nodes, so try a few from×to candidate pairs until a usable path turns up.
1124
+ //
1125
+ // MAX_HOPS guard: a BFS shortest path longer than this on a dense call graph
1126
+ // is almost always a spurious wander through unrelated code (django's
1127
+ // `_fetch_all → … → execute_sql` BFS detours through prefetch/filter), not
1128
+ // the real execution flow — and a confident-but-wrong 15-hop trace is worse
1129
+ // than none. Over-cap paths are rejected and reported as "no direct path"
1130
+ // (which, on real code, means the flow breaks at dynamic dispatch).
1131
+ const edgeKinds = ['calls'];
1132
+ const MAX_HOPS = 7;
1133
+ const fromTry = fromMatches.nodes.slice(0, 3);
1134
+ const toTry = toMatches.nodes.slice(0, 3);
1135
+ let path = null;
1136
+ let overCap = null;
1137
+ for (const f of fromTry) {
1138
+ for (const t of toTry) {
1139
+ const p = cg.findPath(f.id, t.id, edgeKinds);
1140
+ if (!p || p.length <= 1)
1141
+ continue;
1142
+ if (p.length <= MAX_HOPS) {
1143
+ path = p;
1144
+ break;
1145
+ }
1146
+ if (!overCap || p.length < overCap.length)
1147
+ overCap = p;
1148
+ }
1149
+ if (path)
1150
+ break;
1151
+ }
1152
+ if (!path) {
1153
+ // No static path — almost always a dynamic-dispatch break. Surface the
1154
+ // start symbol's outgoing calls so the agent can bridge the gap.
1155
+ const start = fromTry[0];
1156
+ const callees = cg.getCallees(start.id).slice(0, 10)
1157
+ .map(c => `${c.node.name} (${c.node.filePath}:${c.node.startLine})`);
1158
+ const lines = [
1159
+ `No direct call path from "${from}" to "${to}".`,
1160
+ '',
1161
+ (overCap
1162
+ ? `(Only a ${overCap.length}-hop indirect chain connects them — almost certainly a BFS wander through unrelated code, not the real flow.) `
1163
+ : '') +
1164
+ 'The direct chain most likely breaks at **dynamic dispatch** (a callback, descriptor, ' +
1165
+ 'metaclass, or attribute-as-callable) that static parsing cannot resolve into an edge. ' +
1166
+ `Inspect \`${start.name}\` (${start.filePath}:${start.startLine}) with codegraph_node ` +
1167
+ '(includeCode=true) — its body usually shows the dynamic call to follow next.',
1168
+ ];
1169
+ if (callees.length > 0) {
1170
+ lines.push('', `**${start.name} statically calls:** ${callees.join(', ')}`);
1171
+ }
1172
+ return this.textResult(lines.join('\n') + fromMatches.note + toMatches.note);
1173
+ }
1174
+ const lines = [
1175
+ `## Trace: ${from} → ${to}`,
1176
+ '',
1177
+ `Full execution path below — ${path.length} hops, each with its body, plus what the destination calls. This is the complete flow; answer from it.`,
1178
+ '',
1179
+ `${path.length} hops:`,
1180
+ '',
1181
+ ];
1182
+ // Inline what each hop needs so the agent doesn't Read/Grep to get it: the
1183
+ // call-site source line, the registration site for dynamic-dispatch hops, AND
1184
+ // the hop's own body (capped per hop so the trace stays path-scoped). Earlier
1185
+ // versions inlined only the call-site line, which left agents calling explore
1186
+ // or Read for the bodies — the exact follow-up the ablation experiment measured.
1187
+ const fileCache = new Map();
1188
+ for (let i = 0; i < path.length; i++) {
1189
+ const step = path[i];
1190
+ if (step.edge) {
1191
+ const synth = this.synthEdgeNote(step.edge);
1192
+ if (synth) {
1193
+ lines.push(` ↓ ${synth.label}`);
1194
+ if (synth.registeredAt) {
1195
+ const regSrc = this.sourceLineAt(cg, synth.registeredAt, fileCache);
1196
+ lines.push(` ↳ registered at ${synth.registeredAt}${regSrc ? ` ${regSrc}` : ''}`);
1197
+ }
1198
+ }
1199
+ else {
1200
+ // The call happens in the PREVIOUS hop's file at edge.line.
1201
+ const prev = path[i - 1];
1202
+ const ref = prev && step.edge.line ? `${prev.node.filePath}:${step.edge.line}` : undefined;
1203
+ const callSrc = this.sourceLineAt(cg, ref, fileCache);
1204
+ lines.push(` ↓ ${step.edge.kind}${step.edge.line ? `@${step.edge.line}` : ''}${callSrc ? ` ${callSrc}` : ''}`);
1205
+ }
1206
+ }
1207
+ lines.push(`${i + 1}. ${step.node.name} (${step.node.filePath}:${step.node.startLine}-${step.node.endLine})`);
1208
+ const body = this.sourceRangeAt(cg, step.node.filePath, step.node.startLine, step.node.endLine, fileCache, 60, 1800);
1209
+ if (body)
1210
+ lines.push(body);
1211
+ }
1212
+ // The "last mile": what the destination does next. Agents otherwise explore/Read
1213
+ // for exactly this (e.g. renderStaticScene → _renderStaticScene → the canvas draw),
1214
+ // so inlining the destination's callees is what actually stops the investigation —
1215
+ // sufficiency, not a "don't explore" instruction.
1216
+ const dest = path[path.length - 1].node;
1217
+ const destCallees = cg.getCallees(dest.id)
1218
+ .filter(c => !path.some(p => p.node.id === c.node.id))
1219
+ .slice(0, 6);
1220
+ if (destCallees.length > 0) {
1221
+ lines.push('', `### \`${dest.name}\` then calls (the destination's immediate work):`);
1222
+ for (const c of destCallees) {
1223
+ lines.push('', `- ${c.node.name} (${c.node.filePath}:${c.node.startLine}-${c.node.endLine})`);
1224
+ const body = this.sourceRangeAt(cg, c.node.filePath, c.node.startLine, c.node.endLine, fileCache, 16, 600);
1225
+ if (body)
1226
+ lines.push(body);
1227
+ }
1228
+ }
1229
+ lines.push('', '> Full path + every hop body + the destination\'s calls are inlined above — the complete flow. Answer from it; a Read is only needed to chase a specific local variable\'s data-flow.');
1230
+ return this.textResult(this.truncateOutput(lines.join('\n')));
1231
+ }
1232
+ /**
1233
+ * Describe a synthesized (dynamic-dispatch) edge for human output: how the
1234
+ * callback was wired up — the bridge static parsing can't see. Returns null
1235
+ * for ordinary static edges. Used by trace + the node trail so a synthesized
1236
+ * hop reads as "registered via onUpdate at App.tsx:3148", not a bare arrow.
1237
+ */
1238
+ synthEdgeNote(edge) {
1239
+ if (!edge || edge.provenance !== 'heuristic')
1240
+ return null;
1241
+ const m = edge.metadata;
1242
+ const registeredAt = typeof m?.registeredAt === 'string' ? m.registeredAt : undefined;
1243
+ const at = registeredAt ? ` @${registeredAt}` : '';
1244
+ if (m?.synthesizedBy === 'callback') {
1245
+ const via = m.via ? `\`${String(m.via)}\`` : 'a registrar';
1246
+ const field = m.field ? ` on .${String(m.field)}` : '';
1247
+ return {
1248
+ label: `callback — registered via ${via}${field} (dynamic dispatch)`,
1249
+ compact: `dynamic: callback via ${via}${at}`,
1250
+ registeredAt,
1251
+ };
1252
+ }
1253
+ if (m?.synthesizedBy === 'event-emitter') {
1254
+ const ev = m.event ? `\`${String(m.event)}\`` : 'an event';
1255
+ return {
1256
+ label: `event ${ev} — emit → handler (dynamic dispatch)`,
1257
+ compact: `dynamic: event ${ev}${at}`,
1258
+ registeredAt,
1259
+ };
1260
+ }
1261
+ if (m?.synthesizedBy === 'react-render') {
1262
+ return {
1263
+ label: `React re-render — \`setState\` re-runs render() (dynamic dispatch)`,
1264
+ compact: `dynamic: React re-render via setState${at}`,
1265
+ registeredAt,
1266
+ };
1267
+ }
1268
+ if (m?.synthesizedBy === 'jsx-render') {
1269
+ const child = m.via ? `<${String(m.via)}>` : 'a child component';
1270
+ return {
1271
+ label: `renders ${child} (JSX child — dynamic dispatch)`,
1272
+ compact: `dynamic: renders ${child}`,
1273
+ registeredAt,
1274
+ };
1275
+ }
1276
+ if (m?.synthesizedBy === 'vue-handler') {
1277
+ const ev = m.event ? `@${String(m.event)}` : 'a template event';
1278
+ return {
1279
+ label: `Vue template handler — bound to ${ev} (dynamic dispatch)`,
1280
+ compact: `dynamic: Vue ${ev} handler`,
1281
+ registeredAt,
1282
+ };
1283
+ }
1284
+ if (m?.synthesizedBy === 'interface-impl') {
1285
+ return {
1286
+ label: `interface/abstract dispatch — runs the implementation override (dynamic dispatch)`,
1287
+ compact: `dynamic: interface → impl${at}`,
1288
+ registeredAt,
1289
+ };
1290
+ }
1291
+ return null;
1292
+ }
1293
+ /**
1294
+ * Read one trimmed source line at "relpath:line" (relative to the project
1295
+ * root). `cache` holds split file contents so a multi-hop trace reads each
1296
+ * file at most once. Returns null if the file/line can't be resolved.
1297
+ */
1298
+ sourceLineAt(cg, ref, cache) {
1299
+ if (!ref)
1300
+ return null;
1301
+ const i = ref.lastIndexOf(':');
1302
+ if (i < 0)
1303
+ return null;
1304
+ const filePath = ref.slice(0, i);
1305
+ const line = parseInt(ref.slice(i + 1), 10);
1306
+ if (!Number.isFinite(line) || line < 1)
1307
+ return null;
1308
+ let fileLines = cache.get(filePath);
1309
+ if (!fileLines) {
1310
+ const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
1311
+ if (!abs || !(0, fs_1.existsSync)(abs))
1312
+ return null;
1313
+ try {
1314
+ fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
1315
+ }
1316
+ catch {
1317
+ return null;
1318
+ }
1319
+ cache.set(filePath, fileLines);
1320
+ }
1321
+ const raw = fileLines[line - 1];
1322
+ if (raw == null)
1323
+ return null;
1324
+ const t = raw.trim();
1325
+ return t.length > 160 ? t.slice(0, 157) + '…' : t;
1326
+ }
1327
+ /**
1328
+ * Read a hop's body — filePath lines [startLine..endLine] — for inlining into
1329
+ * a trace, capped (lines + chars) so the whole path stays path-scoped even on
1330
+ * a 7-hop chain. Dedents to the body's own indentation and marks truncation.
1331
+ * Shares `cache` with sourceLineAt so each file is read at most once per trace.
1332
+ */
1333
+ sourceRangeAt(cg, filePath, startLine, endLine, cache, maxLines = 28, maxChars = 1200) {
1334
+ if (!Number.isFinite(startLine) || startLine < 1)
1335
+ return null;
1336
+ let fileLines = cache.get(filePath);
1337
+ if (!fileLines) {
1338
+ const abs = (0, utils_1.validatePathWithinRoot)(cg.getProjectRoot(), filePath);
1339
+ if (!abs || !(0, fs_1.existsSync)(abs))
1340
+ return null;
1341
+ try {
1342
+ fileLines = (0, fs_1.readFileSync)(abs, 'utf-8').split('\n');
1343
+ }
1344
+ catch {
1345
+ return null;
1346
+ }
1347
+ cache.set(filePath, fileLines);
1348
+ }
1349
+ const end = Number.isFinite(endLine) && endLine >= startLine ? endLine : startLine;
1350
+ let slice = fileLines.slice(startLine - 1, end);
1351
+ if (slice.length === 0)
1352
+ return null;
1353
+ let omitted = 0;
1354
+ if (slice.length > maxLines) {
1355
+ omitted = slice.length - maxLines;
1356
+ slice = slice.slice(0, maxLines);
1357
+ }
1358
+ const nonBlank = slice.filter(l => l.trim().length > 0);
1359
+ const dedent = nonBlank.length ? Math.min(...nonBlank.map(l => l.length - l.trimStart().length)) : 0;
1360
+ let text = slice.map((l, i) => ` ${startLine + i}\t${l.slice(dedent)}`).join('\n');
1361
+ if (text.length > maxChars) {
1362
+ text = text.slice(0, maxChars).replace(/\n[^\n]*$/, '');
1363
+ omitted = Math.max(omitted, 1);
1364
+ }
1365
+ if (omitted > 0)
1366
+ text += `\n … (+${omitted} more line${omitted === 1 ? '' : 's'})`;
1367
+ return text;
1368
+ }
1369
+ /**
1370
+ * Flow-from-named-symbols: an agent's codegraph_explore query is a bag of
1371
+ * symbol names that usually spans the flow it's investigating (e.g.
1372
+ * "PmsProductController getList PmsProductService list PmsProductServiceImpl").
1373
+ * Surface the longest call chain AMONG those named symbols — scoped to what the
1374
+ * agent explicitly named, so (unlike a fuzzy relevance set) there's no
1375
+ * wrong-feature wandering. Rides synthesized edges, so controller→service-
1376
+ * interface→impl shows up. Returns '' if no chain of >=3 nodes exists.
1377
+ *
1378
+ * Ambiguous tokens (Java `list` → dozens of nodes) are disambiguated by
1379
+ * CO-NAMING: the agent names the class too, so we keep only `list` candidates
1380
+ * whose qualifiedName contains another named token (`PmsProductServiceImpl::list`),
1381
+ * dropping unrelated `OmsOrderService::list`.
1382
+ */
1383
+ buildFlowFromNamedSymbols(cg, query) {
1384
+ try {
1385
+ const CALLABLE = new Set(['method', 'function', 'component', 'constructor']);
1386
+ // Strip only a REAL file extension (Create.cs → Create); KEEP qualified
1387
+ // names (Class.method / Class::method) — the agent's most precise input,
1388
+ // resolved exactly by findAllSymbols. (The old strip mangled Class.method
1389
+ // into Class, throwing the method away.)
1390
+ 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;
1391
+ const tokens = [...new Set(query.split(/[\s,()[\]]+/)
1392
+ .map((t) => t.replace(FILE_EXT, '').trim())
1393
+ .filter((t) => t.length >= 3 && /^[A-Za-z_$][\w$]*(?:(?:::|\.)[\w$]+)*$/.test(t)))].slice(0, 16);
1394
+ if (tokens.length < 2)
1395
+ return '';
1396
+ // Pool of name SEGMENTS (Class + method from every token) used to
1397
+ // disambiguate an ambiguous SIMPLE name: keep a candidate only if its
1398
+ // CONTAINER class is itself named in the query.
1399
+ const segPool = new Set();
1400
+ for (const t of tokens)
1401
+ for (const s of t.toLowerCase().split(/::|\./))
1402
+ if (s)
1403
+ segPool.add(s);
1404
+ const named = new Map();
1405
+ for (const t of tokens) {
1406
+ const cands = this.findAllSymbols(cg, t).nodes.filter((n) => CALLABLE.has(n.kind));
1407
+ // A qualified or otherwise-specific name (<=3 hits) keeps all; an
1408
+ // ambiguous simple name keeps only candidates whose container is named.
1409
+ const pick = cands.length <= 3
1410
+ ? cands
1411
+ : cands.filter((n) => {
1412
+ const segs = (n.qualifiedName || '').toLowerCase().split(/::|\./).filter(Boolean);
1413
+ const container = segs.length >= 2 ? segs[segs.length - 2] : '';
1414
+ return !!container && segPool.has(container);
1415
+ });
1416
+ for (const n of pick.slice(0, 6))
1417
+ named.set(n.id, n);
1418
+ if (named.size > 40)
1419
+ break;
1420
+ }
1421
+ if (named.size < 2)
1422
+ return '';
1423
+ const MAX_HOPS = 7;
1424
+ let best = null;
1425
+ // BFS the full call graph (incl. synth edges) from each named seed, but
1426
+ // only ACCEPT a sink that is also named — both ends anchored to symbols the
1427
+ // agent named, so the chain stays on-topic while bridging intermediates
1428
+ // (e.g. the exact interface overload) that the token resolution missed.
1429
+ for (const seed of [...named.values()].slice(0, 8)) {
1430
+ const parent = new Map();
1431
+ parent.set(seed.id, { prev: null, edge: null, node: seed });
1432
+ const q = [{ id: seed.id, depth: 0, streak: 0 }];
1433
+ let deep = null, deepDepth = 0;
1434
+ const MAX_BRIDGE = 1; // ≤1 consecutive UNNAMED hop: bridge one missing intermediate, never wander a god-function's fan-out
1435
+ for (let h = 0; h < q.length && parent.size < 1500; h++) {
1436
+ const { id, depth, streak } = q[h];
1437
+ if (id !== seed.id && named.has(id) && depth > deepDepth) {
1438
+ deep = id;
1439
+ deepDepth = depth;
1440
+ }
1441
+ if (depth >= MAX_HOPS - 1)
1442
+ continue;
1443
+ for (const c of cg.getCallees(id)) {
1444
+ if (c.edge.kind !== 'calls' || parent.has(c.node.id))
1445
+ continue;
1446
+ const newStreak = named.has(c.node.id) ? 0 : streak + 1;
1447
+ if (newStreak > MAX_BRIDGE)
1448
+ continue;
1449
+ parent.set(c.node.id, { prev: id, edge: c.edge, node: c.node });
1450
+ q.push({ id: c.node.id, depth: depth + 1, streak: newStreak });
1451
+ }
1452
+ }
1453
+ if (!deep)
1454
+ continue;
1455
+ const chain = [];
1456
+ let cur = deep;
1457
+ while (cur) {
1458
+ const p = parent.get(cur);
1459
+ if (!p)
1460
+ break;
1461
+ chain.push({ node: p.node, edge: p.edge });
1462
+ cur = p.prev;
1463
+ }
1464
+ chain.reverse();
1465
+ if (!best || chain.length > best.length)
1466
+ best = chain;
1467
+ }
1468
+ if (!best || best.length < 3)
1469
+ return '';
1470
+ const out = ['## Flow (call path among the symbols you queried)', ''];
1471
+ for (let i = 0; i < best.length; i++) {
1472
+ const step = best[i];
1473
+ if (step.edge) {
1474
+ const sy = this.synthEdgeNote(step.edge);
1475
+ out.push(` ↓ ${sy ? sy.compact : step.edge.kind}`);
1476
+ }
1477
+ out.push(`${i + 1}. ${step.node.name} (${step.node.filePath}:${step.node.startLine})`);
1478
+ }
1479
+ out.push('', '> Full source for these symbols is below; codegraph_trace(from,to) for the exact path between two endpoints.', '');
1480
+ return out.join('\n');
1481
+ }
1482
+ catch {
1483
+ return '';
1484
+ }
1485
+ }
1486
+ /**
1487
+ * Handle codegraph_explore — deep exploration in a single call
1488
+ *
1489
+ * Strategy: find relevant symbols via graph traversal, group by file,
1490
+ * then read contiguous file sections covering all symbols per file.
1491
+ * This replaces multiple codegraph_node + Read calls.
1492
+ *
1493
+ * Output size is adaptive to project file count via
1494
+ * `getExploreOutputBudget` — see #185 for why a fixed 35k cap was a
1495
+ * tax on small projects while earning its keep on large ones.
1496
+ */
1497
+ async handleExplore(args) {
1498
+ const query = this.validateString(args.query, 'query');
1499
+ if (typeof query !== 'string')
1500
+ return query;
1501
+ const cg = this.getCodeGraph(args.projectPath);
1502
+ const projectRoot = cg.getProjectRoot();
1503
+ // Resolve adaptive output budget from project size. Falls back to the
1504
+ // largest-tier defaults if stats aren't available, which preserves
1505
+ // pre-#185 behavior for callers that hit the rare stats failure.
1506
+ let budget;
1507
+ try {
1508
+ budget = getExploreOutputBudget(cg.getStats().fileCount);
1509
+ }
1510
+ catch {
1511
+ budget = getExploreOutputBudget(Infinity);
1512
+ }
1513
+ const maxFiles = (0, utils_1.clamp)(args.maxFiles || budget.defaultMaxFiles, 1, 20);
1514
+ // Step 1: Find relevant context with generous parameters.
1515
+ // Use a large maxNodes budget — explore has its own 35k char output limit
1516
+ // that prevents context bloat, so more nodes just means better coverage
1517
+ // across entry points (especially for large files like Svelte components).
1518
+ const subgraph = await cg.findRelevantContext(query, {
1519
+ searchLimit: 8,
1520
+ traversalDepth: 3,
1521
+ maxNodes: 200,
1522
+ minScore: 0.2,
1523
+ });
1524
+ if (subgraph.nodes.size === 0) {
1525
+ return this.textResult(`No relevant code found for "${query}"`);
1526
+ }
1527
+ // Graph-aware glue: findRelevantContext builds the subgraph from name/text
1528
+ // search, so a method that BRIDGES named symbols — e.g. App.tsx's
1529
+ // triggerRender, which calls the named triggerUpdate — is never a search hit
1530
+ // and gets missed, forcing the agent to Read the file to trace it. Pull in
1531
+ // the callers/callees of the entry (root) nodes, but ONLY those that live in
1532
+ // files the subgraph already surfaces (where the agent reads to fill gaps),
1533
+ // so we add wiring without dragging in unrelated files. These get an
1534
+ // importance boost below so they survive the per-file cluster budget.
1535
+ const glueNodeIds = new Set();
1536
+ const subgraphFiles = new Set();
1537
+ for (const n of subgraph.nodes.values())
1538
+ subgraphFiles.add(n.filePath);
1539
+ const GLUE_NODE_CAP = 60;
1540
+ for (const rootId of subgraph.roots) {
1541
+ if (glueNodeIds.size >= GLUE_NODE_CAP)
1542
+ break;
1543
+ let neighbors = [];
1544
+ try {
1545
+ neighbors = [
1546
+ ...cg.getCallers(rootId).map(c => c.node),
1547
+ ...cg.getCallees(rootId).map(c => c.node),
1548
+ ];
1549
+ }
1550
+ catch {
1551
+ continue;
1552
+ }
1553
+ for (const nb of neighbors) {
1554
+ if (glueNodeIds.size >= GLUE_NODE_CAP)
1555
+ break;
1556
+ if (subgraph.nodes.has(nb.id))
1557
+ continue;
1558
+ if (!subgraphFiles.has(nb.filePath))
1559
+ continue;
1560
+ subgraph.nodes.set(nb.id, nb);
1561
+ glueNodeIds.add(nb.id);
1562
+ }
1563
+ }
1564
+ // Step 2: Group nodes by file, score by relevance
1565
+ const fileGroups = new Map();
1566
+ const entryNodeIds = new Set(subgraph.roots);
1567
+ // Build a set of nodes directly connected to entry points (depth 1)
1568
+ const connectedToEntry = new Set();
1569
+ for (const edge of subgraph.edges) {
1570
+ if (entryNodeIds.has(edge.source))
1571
+ connectedToEntry.add(edge.target);
1572
+ if (entryNodeIds.has(edge.target))
1573
+ connectedToEntry.add(edge.source);
1574
+ }
1575
+ for (const node of subgraph.nodes.values()) {
1576
+ // Skip import/export nodes — they add noise without information
1577
+ if (node.kind === 'import' || node.kind === 'export')
1578
+ continue;
1579
+ const group = fileGroups.get(node.filePath) || { nodes: [], score: 0 };
1580
+ group.nodes.push(node);
1581
+ // Score: entry point nodes worth 10, directly connected worth 3, others worth 1
1582
+ if (entryNodeIds.has(node.id)) {
1583
+ group.score += 10;
1584
+ }
1585
+ else if (connectedToEntry.has(node.id)) {
1586
+ group.score += 3;
1587
+ }
1588
+ else {
1589
+ group.score += 1;
1590
+ }
1591
+ fileGroups.set(node.filePath, group);
1592
+ }
1593
+ // Only include files that have entry points or nodes directly connected to entry points
1594
+ const relevantFiles = [...fileGroups.entries()].filter(([, group]) => group.score >= 3);
1595
+ // Extract query terms for relevance checking
1596
+ const queryTerms = query.toLowerCase().split(/\s+/).filter(t => t.length >= 3);
1597
+ // Sort files: highest relevance first, deprioritize low-value files
1598
+ const sortedFiles = relevantFiles.sort((a, b) => {
1599
+ const aPath = a[0].toLowerCase();
1600
+ const bPath = b[0].toLowerCase();
1601
+ // Check if any node name or file path relates to query terms
1602
+ const hasQueryRelevance = (filePath, nodes) => {
1603
+ const fp = filePath.toLowerCase();
1604
+ if (queryTerms.some(t => fp.includes(t)))
1605
+ return true;
1606
+ return nodes.some(n => queryTerms.some(t => n.name.toLowerCase().includes(t)));
1607
+ };
1608
+ const aRelevant = hasQueryRelevance(aPath, a[1].nodes);
1609
+ const bRelevant = hasQueryRelevance(bPath, b[1].nodes);
1610
+ if (aRelevant !== bRelevant)
1611
+ return aRelevant ? -1 : 1;
1612
+ // Deprioritize test files, icon files, and i18n files
1613
+ const isLowValue = (p) => /\/(tests?|__tests?__|spec)\//i.test(p) ||
1614
+ /\bicons?\b/i.test(p) ||
1615
+ /\bi18n\b/i.test(p);
1616
+ const aLow = isLowValue(aPath);
1617
+ const bLow = isLowValue(bPath);
1618
+ if (aLow !== bLow)
1619
+ return aLow ? 1 : -1;
1620
+ if (a[1].score !== b[1].score)
1621
+ return b[1].score - a[1].score;
1622
+ return b[1].nodes.length - a[1].nodes.length;
1623
+ });
1624
+ // Step 3: Build relationship map
1625
+ const lines = [
1626
+ `## Exploration: ${query}`,
1627
+ '',
1628
+ `Found ${subgraph.nodes.size} symbols across ${fileGroups.size} files.`,
1629
+ '',
1630
+ ];
1631
+ // Relationship map — show how symbols connect
1632
+ const significantEdges = subgraph.edges.filter(e => e.kind !== 'contains' // skip contains — it's implied by file grouping
1633
+ );
1634
+ if (budget.includeRelationships && significantEdges.length > 0) {
1635
+ lines.push('### Relationships');
1636
+ lines.push('');
1637
+ // Group edges by kind for readability
1638
+ const byKind = new Map();
1639
+ for (const edge of significantEdges) {
1640
+ const sourceNode = subgraph.nodes.get(edge.source);
1641
+ const targetNode = subgraph.nodes.get(edge.target);
1642
+ if (!sourceNode || !targetNode)
1643
+ continue;
1644
+ const group = byKind.get(edge.kind) || [];
1645
+ group.push({ source: sourceNode.name, target: targetNode.name });
1646
+ byKind.set(edge.kind, group);
1647
+ }
1648
+ for (const [kind, edges] of byKind) {
1649
+ const cap = budget.maxEdgesPerRelationshipKind;
1650
+ const shown = edges.slice(0, cap);
1651
+ lines.push(`**${kind}:**`);
1652
+ for (const e of shown) {
1653
+ lines.push(`- ${e.source} → ${e.target}`);
1654
+ }
1655
+ if (edges.length > cap) {
1656
+ lines.push(`- ... and ${edges.length - cap} more`);
1657
+ }
1658
+ lines.push('');
1659
+ }
1660
+ }
1661
+ // Step 4: Read contiguous file sections
1662
+ lines.push('### Source Code');
1663
+ lines.push('');
1664
+ lines.push('> The code below is the **verbatim, current on-disk source** of these files — re-read from disk on this call and line-numbered, byte-for-byte identical to what the Read tool returns. It is NOT a summary, outline, or stale cache. Treat each block as a Read you have already performed: do not Read a file shown here.');
1665
+ lines.push('');
1666
+ let totalChars = lines.join('\n').length;
1667
+ let filesIncluded = 0;
1668
+ let anyFileTrimmed = false;
1669
+ for (const [filePath, group] of sortedFiles) {
1670
+ if (filesIncluded >= maxFiles)
1671
+ break;
1672
+ if (totalChars > budget.maxOutputChars * 0.9)
1673
+ break;
1674
+ const absPath = (0, utils_1.validatePathWithinRoot)(projectRoot, filePath);
1675
+ if (!absPath || !(0, fs_1.existsSync)(absPath))
1676
+ continue;
1677
+ let fileContent;
1678
+ try {
1679
+ fileContent = (0, fs_1.readFileSync)(absPath, 'utf-8');
1680
+ }
1681
+ catch {
1682
+ continue;
1683
+ }
1684
+ const fileLines = fileContent.split('\n');
1685
+ const lang = group.nodes[0]?.language || '';
1686
+ // Whole-small-file rule: if a relevant file is small enough to afford,
1687
+ // return it ENTIRELY instead of clustering. Clustering exists to tame
1688
+ // god-files (App.tsx ~13k lines); on a ~134-line component a cluster is a
1689
+ // lossy subset of a file the agent will just Read in full anyway — costing
1690
+ // a round-trip and a re-read every later turn. Reserve clustering for files
1691
+ // too big to ship whole. Still bounded by the total maxOutputChars check.
1692
+ const WHOLE_FILE_MAX_LINES = 220;
1693
+ const WHOLE_FILE_MAX_CHARS = budget.maxCharsPerFile * 3;
1694
+ if (fileLines.length <= WHOLE_FILE_MAX_LINES && fileContent.length <= WHOLE_FILE_MAX_CHARS) {
1695
+ const body = fileContent.replace(/\n+$/, '');
1696
+ let wholeSection = exploreLineNumbersEnabled() ? numberSourceLines(body, 1) : body;
1697
+ const uniqSymbols = [...new Set(group.nodes
1698
+ .filter(n => n.kind !== 'import' && n.kind !== 'export')
1699
+ .map(n => `${n.name}(${n.kind})`))];
1700
+ const headerNames = uniqSymbols.slice(0, budget.maxSymbolsInFileHeader);
1701
+ const omitted = uniqSymbols.length - headerNames.length;
1702
+ const wholeHeader = `#### ${filePath} — ${omitted > 0 ? `${headerNames.join(', ')}, +${omitted} more` : headerNames.join(', ')}`;
1703
+ if (totalChars + wholeSection.length + 200 > budget.maxOutputChars) {
1704
+ const remaining = budget.maxOutputChars - totalChars - 200;
1705
+ if (remaining < 500)
1706
+ break;
1707
+ wholeSection = wholeSection.slice(0, remaining) + '\n... (trimmed) ...';
1708
+ anyFileTrimmed = true;
1709
+ }
1710
+ lines.push(wholeHeader, '', '```' + lang, wholeSection, '```', '');
1711
+ totalChars += wholeSection.length + 200;
1712
+ filesIncluded++;
1713
+ continue;
1714
+ }
1715
+ // Cluster nearby symbols to avoid reading huge gaps between distant symbols.
1716
+ // Sort by start line, then merge overlapping/adjacent ranges (within the
1717
+ // adaptive gap threshold). Include both node ranges AND edge source
1718
+ // locations so template sections with component usages/calls are
1719
+ // covered (not just script block symbols).
1720
+ //
1721
+ // Each range carries an `importance` score so we can rank clusters
1722
+ // when the per-file budget forces us to drop some: entry-point nodes
1723
+ // are worth 10, directly-connected nodes 3, peripheral nodes 1, and
1724
+ // bare edge-source lines 2 (less than a connected node but more than
1725
+ // a peripheral one — they hint at a reference but aren't a definition).
1726
+ // Container kinds whose body can span most/all of a file. When such a
1727
+ // node covers most of the file we drop it from the ranges: keeping it
1728
+ // would merge every method inside it into one giant cluster spanning
1729
+ // the whole file, which then tail-trims down to just the container's
1730
+ // opening lines (its header/declarations) and buries the methods the
1731
+ // query actually asked about (#185 follow-up — Session.swift in
1732
+ // Alamofire is the canonical case: the `Session` class spans ~1,400
1733
+ // lines). We want the granular symbols inside, not the envelope.
1734
+ const ENVELOPE_KINDS = new Set(['file', 'module', 'class', 'struct', 'interface', 'enum', 'namespace', 'protocol', 'trait', 'component']);
1735
+ const ranges = group.nodes
1736
+ .filter(n => n.startLine > 0 && n.endLine > 0)
1737
+ // Drop whole-file envelope nodes (containers covering >50% of the file).
1738
+ .filter(n => !(ENVELOPE_KINDS.has(n.kind) && (n.endLine - n.startLine + 1) > fileLines.length * 0.5))
1739
+ .map(n => {
1740
+ let importance = 1;
1741
+ if (entryNodeIds.has(n.id))
1742
+ importance = 10;
1743
+ else if (glueNodeIds.has(n.id))
1744
+ importance = 6; // bridging caller/callee of an entry
1745
+ else if (connectedToEntry.has(n.id))
1746
+ importance = 3;
1747
+ return { start: n.startLine, end: n.endLine, name: n.name, kind: n.kind, importance };
1748
+ });
1749
+ // Add edge source locations in this file — captures template references
1750
+ // (component usages, event handlers) that aren't nodes themselves.
1751
+ // Query edges directly from the DB (not just the subgraph) because BFS
1752
+ // traversal may have pruned template reference targets due to node budget.
1753
+ const edgeLines = new Set(); // dedup by "line:name"
1754
+ for (const node of group.nodes) {
1755
+ const outgoing = cg.getOutgoingEdges(node.id);
1756
+ for (const edge of outgoing) {
1757
+ if (!edge.line || edge.line <= 0 || edge.kind === 'contains')
1758
+ continue;
1759
+ const key = `${edge.line}:${edge.target}`;
1760
+ if (edgeLines.has(key))
1761
+ continue;
1762
+ edgeLines.add(key);
1763
+ // Look up target name from subgraph first, fall back to edge kind
1764
+ const targetNode = subgraph.nodes.get(edge.target);
1765
+ const targetName = targetNode?.name ?? edge.kind;
1766
+ ranges.push({ start: edge.line, end: edge.line, name: targetName, kind: edge.kind, importance: 2 });
1767
+ }
1768
+ }
1769
+ ranges.sort((a, b) => a.start - b.start);
1770
+ if (ranges.length === 0)
1771
+ continue;
1772
+ const gapThreshold = budget.gapThreshold;
1773
+ const clusters = [];
1774
+ let current = {
1775
+ start: ranges[0].start,
1776
+ end: ranges[0].end,
1777
+ symbols: [`${ranges[0].name}(${ranges[0].kind})`],
1778
+ score: ranges[0].importance,
1779
+ maxImportance: ranges[0].importance,
1780
+ };
1781
+ for (let i = 1; i < ranges.length; i++) {
1782
+ const r = ranges[i];
1783
+ if (r.start <= current.end + gapThreshold) {
1784
+ current.end = Math.max(current.end, r.end);
1785
+ current.symbols.push(`${r.name}(${r.kind})`);
1786
+ current.score += r.importance;
1787
+ current.maxImportance = Math.max(current.maxImportance, r.importance);
1788
+ }
1789
+ else {
1790
+ clusters.push(current);
1791
+ current = {
1792
+ start: r.start,
1793
+ end: r.end,
1794
+ symbols: [`${r.name}(${r.kind})`],
1795
+ score: r.importance,
1796
+ maxImportance: r.importance,
1797
+ };
1798
+ }
1799
+ }
1800
+ clusters.push(current);
1801
+ // Build file section output from clusters, capped by per-file budget.
1802
+ // The pathological case (#185): a file like Session.swift where every
1803
+ // method is adjacent collapses into one cluster spanning the whole
1804
+ // file, and dumping that into the agent's context is most of the
1805
+ // token cost on small projects. We pick clusters in priority order
1806
+ // until the per-file char cap is hit. Truly enormous single clusters
1807
+ // get tail-trimmed with a marker.
1808
+ const contextPadding = 3;
1809
+ const withLineNumbers = exploreLineNumbersEnabled();
1810
+ const buildSection = (c) => {
1811
+ const startIdx = Math.max(0, c.start - 1 - contextPadding);
1812
+ const endIdx = Math.min(fileLines.length, c.end + contextPadding);
1813
+ const slice = fileLines.slice(startIdx, endIdx).join('\n');
1814
+ // startIdx is 0-based, so the slice's first line is line startIdx + 1.
1815
+ return withLineNumbers ? numberSourceLines(slice, startIdx + 1) : slice;
1816
+ };
1817
+ // Language-neutral separator (no `//` — not a comment in Python, Ruby,
1818
+ // etc.). With line numbers on, the line-number jump also signals the gap.
1819
+ const GAP_MARKER = '\n\n... (gap) ...\n\n';
1820
+ // Rank clusters for inclusion under the per-file cap. Entry-point
1821
+ // clusters come first: a cluster containing a query entry point
1822
+ // (importance 10) must outrank a dense block of mere declarations,
1823
+ // otherwise on a large file like Session.swift the top-of-file class
1824
+ // header + property list (many adjacent low-importance nodes, high
1825
+ // density) wins the budget and buries the actual methods the query
1826
+ // asked about (perform/didCreateURLRequest/task live deep in the
1827
+ // file). Within the same importance tier, prefer density (score per
1828
+ // line) so we still favor focused clusters over sprawling ones, then
1829
+ // smaller span as a cheap-to-include tiebreak.
1830
+ const rankedClusters = clusters
1831
+ .map((c, i) => ({ idx: i, span: c.end - c.start + 1, c }))
1832
+ .sort((a, b) => {
1833
+ if (b.c.maxImportance !== a.c.maxImportance)
1834
+ return b.c.maxImportance - a.c.maxImportance;
1835
+ const densityA = a.c.score / a.span;
1836
+ const densityB = b.c.score / b.span;
1837
+ if (densityB !== densityA)
1838
+ return densityB - densityA;
1839
+ if (b.c.score !== a.c.score)
1840
+ return b.c.score - a.c.score;
1841
+ return a.span - b.span;
1842
+ });
1843
+ const chosenIndices = new Set();
1844
+ let projectedChars = 0;
1845
+ for (const rc of rankedClusters) {
1846
+ const sectionLen = buildSection(rc.c).length + (chosenIndices.size > 0 ? GAP_MARKER.length : 0);
1847
+ // Always take the top-ranked cluster, even if oversize, so we don't
1848
+ // return an empty file section (agent would then re-Read the file,
1849
+ // negating the savings).
1850
+ if (chosenIndices.size === 0) {
1851
+ chosenIndices.add(rc.idx);
1852
+ projectedChars += sectionLen;
1853
+ continue;
1854
+ }
1855
+ if (projectedChars + sectionLen > budget.maxCharsPerFile)
1856
+ continue;
1857
+ chosenIndices.add(rc.idx);
1858
+ projectedChars += sectionLen;
1859
+ }
1860
+ // Emit chosen clusters in source order so the file reads top-to-bottom.
1861
+ let fileSection = '';
1862
+ const allSymbols = [];
1863
+ let fileTrimmed = false;
1864
+ for (let i = 0; i < clusters.length; i++) {
1865
+ if (!chosenIndices.has(i))
1866
+ continue;
1867
+ const cluster = clusters[i];
1868
+ const section = buildSection(cluster);
1869
+ if (fileSection.length > 0)
1870
+ fileSection += GAP_MARKER;
1871
+ fileSection += section;
1872
+ allSymbols.push(...cluster.symbols);
1873
+ }
1874
+ // If a single chosen cluster is still oversize (long monolithic
1875
+ // function), tail-trim it. Better one trimmed view than nothing.
1876
+ if (fileSection.length > budget.maxCharsPerFile) {
1877
+ fileSection = fileSection.slice(0, budget.maxCharsPerFile) + '\n... (trimmed) ...';
1878
+ fileTrimmed = true;
1879
+ }
1880
+ if (chosenIndices.size < clusters.length || fileTrimmed) {
1881
+ anyFileTrimmed = true;
1882
+ }
1883
+ // Dedupe + cap the symbols list shown in the per-file header. Some
1884
+ // files (Session.swift in Alamofire) produced 3.4KB symbol lists
1885
+ // from cluster scoring + edge-source lines, dwarfing the per-file
1886
+ // body cap. Show top names by frequency, with a "+N more" tail.
1887
+ const symbolCounts = new Map();
1888
+ for (const s of allSymbols) {
1889
+ symbolCounts.set(s, (symbolCounts.get(s) ?? 0) + 1);
1890
+ }
1891
+ const sortedSymbols = [...symbolCounts.entries()]
1892
+ .sort((a, b) => b[1] - a[1])
1893
+ .map(([name]) => name);
1894
+ const headerCap = budget.maxSymbolsInFileHeader;
1895
+ const headerSymbols = sortedSymbols.slice(0, headerCap);
1896
+ const omittedCount = sortedSymbols.length - headerSymbols.length;
1897
+ const headerSuffix = omittedCount > 0
1898
+ ? `${headerSymbols.join(', ')}, +${omittedCount} more`
1899
+ : headerSymbols.join(', ');
1900
+ const fileHeader = `#### ${filePath} — ${headerSuffix}`;
1901
+ // Respect the total output cap on a file-by-file basis.
1902
+ if (totalChars + fileSection.length + 200 > budget.maxOutputChars) {
1903
+ const remaining = budget.maxOutputChars - totalChars - 200;
1904
+ if (remaining < 500)
1905
+ break;
1906
+ const trimmed = fileSection.slice(0, remaining) + '\n... (trimmed) ...';
1907
+ lines.push(fileHeader);
1908
+ lines.push('');
1909
+ lines.push('```' + lang);
1910
+ lines.push(trimmed);
1911
+ lines.push('```');
1912
+ lines.push('');
1913
+ totalChars += trimmed.length + 200;
1914
+ filesIncluded++;
1915
+ anyFileTrimmed = true;
1916
+ break;
1917
+ }
1918
+ lines.push(fileHeader);
1919
+ lines.push('');
1920
+ lines.push('```' + lang);
1921
+ lines.push(fileSection);
1922
+ lines.push('```');
1923
+ lines.push('');
1924
+ totalChars += fileSection.length + 200;
1925
+ filesIncluded++;
1926
+ }
1927
+ // Add remaining files as references (from both relevant and peripheral files).
1928
+ // Small projects (per budget) skip this — the relevant story already fits
1929
+ // in the source section, and a trailing pointer list is pure overhead.
1930
+ if (budget.includeAdditionalFiles) {
1931
+ const remainingRelevant = sortedFiles.slice(filesIncluded);
1932
+ const peripheralFiles = [...fileGroups.entries()]
1933
+ .filter(([, group]) => group.score < 3)
1934
+ .sort((a, b) => b[1].score - a[1].score);
1935
+ const remainingFiles = [...remainingRelevant, ...peripheralFiles];
1936
+ if (remainingFiles.length > 0) {
1937
+ lines.push('### Not shown above — explore these names for their source');
1938
+ lines.push('');
1939
+ for (const [filePath, group] of remainingFiles.slice(0, 10)) {
1940
+ const symbols = group.nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
1941
+ lines.push(`- ${filePath}: ${symbols}`);
1942
+ }
1943
+ if (remainingFiles.length > 10) {
1944
+ lines.push(`- ... and ${remainingFiles.length - 10} more files`);
1945
+ }
1946
+ }
1947
+ }
1948
+ // Add completeness signal so agents know they don't need to re-read these files.
1949
+ // On small projects the budget gates this off — but if we actually had to
1950
+ // trim or drop clusters, surface a brief note so the agent knows it can
1951
+ // still Read for more detail.
1952
+ if (budget.includeCompletenessSignal) {
1953
+ lines.push('');
1954
+ lines.push('---');
1955
+ lines.push(`> **Complete source for ${filesIncluded} files is included above — do NOT re-read them.** If your question also needs files/symbols listed under "Not shown above" (or any area this call didn't cover), make ANOTHER codegraph_explore targeting those names — it returns the same source with line numbers and is cheaper and more complete than reading. Reserve Read for a single specific line range explore can't surface.`);
1956
+ }
1957
+ else if (anyFileTrimmed) {
1958
+ lines.push('');
1959
+ lines.push(`> Some file sections were trimmed for size. For a specific symbol you still need, run another \`codegraph_explore\` (or \`codegraph_node\`) with its exact name — line-numbered source, cheaper and more complete than Read.`);
1960
+ }
1961
+ // Add explore budget note based on project size
1962
+ if (budget.includeBudgetNote) {
1963
+ try {
1964
+ const stats = cg.getStats();
1965
+ const callBudget = getExploreBudget(stats.fileCount);
1966
+ lines.push('');
1967
+ lines.push(`> **Explore budget: ${callBudget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).** Each call covers ~6 files; if your question spans more, spend your remaining calls on the uncovered area BEFORE falling back to Read — another explore is cheaper and more complete than reading those files. Synthesize once you've used ${callBudget}.`);
1968
+ }
1969
+ catch {
1970
+ // Stats unavailable — skip budget note
1971
+ }
1972
+ }
1973
+ // Hard-cap to the adaptive budget. The per-file loop bounds the source
1974
+ // sections, but the relationship map, additional-files list, and
1975
+ // completeness/budget notes can still push the assembled output past
1976
+ // maxOutputChars (observed 30k against a 28k tier cap). A fat explore
1977
+ // payload persists in the agent's context and is re-read as cache-input
1978
+ // on every subsequent turn, so the overrun is paid many times over.
1979
+ const output = this.buildFlowFromNamedSymbols(cg, query) + lines.join('\n');
1980
+ if (output.length > budget.maxOutputChars) {
1981
+ const cut = output.slice(0, budget.maxOutputChars);
1982
+ const lastNewline = cut.lastIndexOf('\n');
1983
+ const safe = lastNewline > budget.maxOutputChars * 0.8 ? cut.slice(0, lastNewline) : cut;
1984
+ return this.textResult(safe + '\n\n... (output truncated to budget; the source above is complete and verbatim — treat it as already Read. For any area not covered, run another codegraph_explore with the specific names — do NOT Read these files.)');
1985
+ }
1986
+ return this.textResult(output);
1987
+ }
1988
+ /**
1989
+ * Handle codegraph_node
1990
+ */
1991
+ async handleNode(args) {
1992
+ const symbol = this.validateString(args.symbol, 'symbol');
1993
+ if (typeof symbol !== 'string')
1994
+ return symbol;
1995
+ const cg = this.getCodeGraph(args.projectPath);
1996
+ // Default to false to minimize context usage
1997
+ const includeCode = args.includeCode === true;
1998
+ const match = this.findSymbol(cg, symbol);
1999
+ if (!match) {
2000
+ return this.textResult(`Symbol "${symbol}" not found in the codebase`);
2001
+ }
2002
+ let code = null;
2003
+ let outline = null;
2004
+ if (includeCode) {
2005
+ // For container symbols (class/interface/struct/…), the full body is the
2006
+ // sum of every method body — a wall of source (e.g. a 10k-char class)
2007
+ // that bloats context and is rarely needed in full. Return a structural
2008
+ // outline (members + signatures + line numbers) instead; the agent can
2009
+ // Read or codegraph_node a specific method for its body. Leaf symbols
2010
+ // (function/method/etc.) return their full body as before.
2011
+ if (CONTAINER_NODE_KINDS.has(match.node.kind)) {
2012
+ outline = this.buildContainerOutline(cg, match.node);
2013
+ }
2014
+ if (!outline) {
2015
+ code = await cg.getCode(match.node.id);
2016
+ }
2017
+ }
2018
+ const trail = this.formatTrail(cg, match.node);
2019
+ const formatted = this.formatNodeDetails(match.node, code, outline) + trail + match.note;
2020
+ return this.textResult(this.truncateOutput(formatted));
2021
+ }
2022
+ /**
2023
+ * Build the "trail" for a symbol: its direct callees (what it calls) and
2024
+ * callers (what calls it), each with file:line — so codegraph_node doubles as
2025
+ * the structural Grep→Read→expand primitive: a spot PLUS where to go next.
2026
+ * Capped to stay cheap. Walk the graph by calling codegraph_node on a trail
2027
+ * entry; no Read needed for covered hops. Empty edges on a non-leaf often mean
2028
+ * dynamic dispatch the static graph couldn't resolve — that absence is itself
2029
+ * a signal (read that one hop) rather than a dead end.
2030
+ */
2031
+ formatTrail(cg, node) {
2032
+ const TRAIL_CAP = 12;
2033
+ const fmt = (e) => {
2034
+ const base = `${e.node.name} (${e.node.filePath}:${e.node.startLine})`;
2035
+ const synth = this.synthEdgeNote(e.edge);
2036
+ return synth ? `${base} [${synth.compact}]` : base;
2037
+ };
2038
+ const collect = (edges) => {
2039
+ const seen = new Set([node.id]);
2040
+ const out = [];
2041
+ for (const e of edges) {
2042
+ if (seen.has(e.node.id))
2043
+ continue;
2044
+ seen.add(e.node.id);
2045
+ out.push(e);
2046
+ }
2047
+ return out;
2048
+ };
2049
+ const callees = collect(cg.getCallees(node.id));
2050
+ const callers = collect(cg.getCallers(node.id));
2051
+ if (callees.length === 0 && callers.length === 0)
2052
+ return '';
2053
+ const lines = ['', '### Trail — codegraph_node any of these to follow it (no Read needed)'];
2054
+ if (callees.length > 0) {
2055
+ lines.push(`**Calls →** ${callees.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callees.length > TRAIL_CAP ? `, +${callees.length - TRAIL_CAP} more` : ''}`);
2056
+ }
2057
+ if (callers.length > 0) {
2058
+ lines.push(`**Called by ←** ${callers.slice(0, TRAIL_CAP).map(fmt).join(', ')}${callers.length > TRAIL_CAP ? `, +${callers.length - TRAIL_CAP} more` : ''}`);
2059
+ }
2060
+ return lines.join('\n');
2061
+ }
2062
+ /**
2063
+ * Handle codegraph_status
2064
+ */
2065
+ async handleStatus(args) {
2066
+ let cg = this.getCodeGraph(args.projectPath);
2067
+ // Same trick as withStalenessNotice — when an explicit projectPath
2068
+ // resolves to the same project as the default session cg, prefer the
2069
+ // default so getPendingFiles() (only populated by the default's watcher)
2070
+ // is non-empty when there are pending edits.
2071
+ if (this.cg && cg !== this.cg) {
2072
+ try {
2073
+ if ((0, path_1.resolve)(this.cg.getProjectRoot()) === (0, path_1.resolve)(cg.getProjectRoot())) {
2074
+ cg = this.cg;
2075
+ }
2076
+ }
2077
+ catch { /* closed instance — leave as is */ }
2078
+ }
2079
+ const stats = cg.getStats();
2080
+ // Warn when this index actually belongs to a different git working tree
2081
+ // (e.g. the server resolved up from a nested worktree to the main checkout).
2082
+ // Queries then reflect that tree's branch, not the worktree being edited.
2083
+ // status shows the verbose, multi-line form; the read tools get the compact
2084
+ // one-liner via withWorktreeNotice. Both share the cached detection.
2085
+ const mismatch = this.worktreeMismatchFor(args.projectPath);
2086
+ const lines = [
2087
+ '## CodeGraph Status',
2088
+ '',
2089
+ ];
2090
+ if (mismatch) {
2091
+ lines.push(`> ⚠ ${(0, worktree_1.worktreeMismatchWarning)(mismatch).replace(/\n/g, '\n> ')}`, '');
2092
+ }
2093
+ lines.push(`**Files indexed:** ${stats.fileCount}`, `**Total nodes:** ${stats.nodeCount}`, `**Total edges:** ${stats.edgeCount}`, `**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
2094
+ // Surface the active SQLite backend (node:sqlite, Node's built-in real
2095
+ // SQLite — full WAL + FTS5, no native build).
2096
+ lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
2097
+ // Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
2098
+ // anything else ⇒ they can ("database is locked"). node:sqlite supports WAL
2099
+ // everywhere, so a non-wal mode means the filesystem can't (network/
2100
+ // virtualized mounts, WSL2 /mnt). See issue #238.
2101
+ const journalMode = cg.getJournalMode();
2102
+ if (journalMode === 'wal') {
2103
+ lines.push(`**Journal mode:** wal (concurrent reads safe)`);
2104
+ }
2105
+ else {
2106
+ lines.push(`**Journal mode:** ⚠ ${journalMode || 'unknown'} — WAL not active, so reads ` +
2107
+ `can block on a concurrent write (WAL appears unsupported on this filesystem)`);
2108
+ }
2109
+ lines.push('', '### Nodes by Kind:');
2110
+ for (const [kind, count] of Object.entries(stats.nodesByKind)) {
2111
+ if (count > 0) {
2112
+ lines.push(`- ${kind}: ${count}`);
2113
+ }
2114
+ }
2115
+ lines.push('', '### Languages:');
2116
+ for (const [lang, count] of Object.entries(stats.filesByLanguage)) {
2117
+ if (count > 0) {
2118
+ lines.push(`- ${lang}: ${count}`);
2119
+ }
2120
+ }
2121
+ // Per-file freshness — the inverse of the auto-prepended staleness banner
2122
+ // (issue #403). Surfacing it inside `status` gives the agent a single
2123
+ // place to ask "is the index caught up?" rather than inferring from
2124
+ // banners on other tool calls.
2125
+ const pending = cg.getPendingFiles();
2126
+ if (pending.length > 0) {
2127
+ lines.push('', '### Pending sync:');
2128
+ const now = Date.now();
2129
+ for (const p of pending) {
2130
+ const ageMs = Math.max(0, now - p.lastSeenMs);
2131
+ const label = p.indexing ? 'indexing in progress' : 'pending sync';
2132
+ lines.push(`- ${p.path} (edited ${ageMs}ms ago, ${label})`);
2133
+ }
2134
+ }
2135
+ return this.textResult(lines.join('\n'));
2136
+ }
2137
+ /**
2138
+ * Handle codegraph_files - get project file structure from the index
2139
+ */
2140
+ async handleFiles(args) {
2141
+ const cg = this.getCodeGraph(args.projectPath);
2142
+ const pathFilter = args.path;
2143
+ const pattern = args.pattern;
2144
+ const format = args.format || 'tree';
2145
+ const includeMetadata = args.includeMetadata !== false;
2146
+ const maxDepth = args.maxDepth != null ? (0, utils_1.clamp)(args.maxDepth, 1, 20) : undefined;
2147
+ // Get all files from the index
2148
+ const allFiles = cg.getFiles();
2149
+ if (allFiles.length === 0) {
2150
+ return this.textResult('No files indexed. Run `codegraph index` first.');
2151
+ }
2152
+ // Filter by path prefix. Stored paths are project-relative POSIX (e.g.
2153
+ // "src/foo.ts"), but agents commonly pass project-root variants like "/",
2154
+ // ".", "./", "" or Windows-style "src\foo" — and prefixes with leading
2155
+ // "/", "./" or "\". Normalize all of those before matching so the agent
2156
+ // gets results instead of falling back to Read/Glob (see #426).
2157
+ const normalizedFilter = pathFilter
2158
+ ? pathFilter
2159
+ .replace(/\\/g, '/')
2160
+ .replace(/^(?:\.?\/+)+/, '')
2161
+ .replace(/^\.$/, '')
2162
+ .replace(/\/+$/, '')
2163
+ : '';
2164
+ let files = normalizedFilter
2165
+ ? allFiles.filter(f => f.path === normalizedFilter || f.path.startsWith(normalizedFilter + '/'))
2166
+ : allFiles;
2167
+ // Filter by glob pattern
2168
+ if (pattern) {
2169
+ const regex = this.globToRegex(pattern);
2170
+ files = files.filter(f => regex.test(f.path));
2171
+ }
2172
+ if (files.length === 0) {
2173
+ return this.textResult(`No files found matching the criteria.`);
2174
+ }
2175
+ // Format output
2176
+ let output;
2177
+ switch (format) {
2178
+ case 'flat':
2179
+ output = this.formatFilesFlat(files, includeMetadata);
2180
+ break;
2181
+ case 'grouped':
2182
+ output = this.formatFilesGrouped(files, includeMetadata);
2183
+ break;
2184
+ case 'tree':
2185
+ default:
2186
+ output = this.formatFilesTree(files, includeMetadata, maxDepth);
2187
+ break;
2188
+ }
2189
+ return this.textResult(this.truncateOutput(output));
2190
+ }
2191
+ /**
2192
+ * Convert glob pattern to regex
2193
+ */
2194
+ globToRegex(pattern) {
2195
+ const escaped = pattern
2196
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars except * and ?
2197
+ .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
2198
+ .replace(/\*/g, '[^/]*') // * matches anything except /
2199
+ .replace(/\?/g, '[^/]') // ? matches single char except /
2200
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*'); // ** matches anything including /
2201
+ return new RegExp(escaped);
2202
+ }
2203
+ /**
2204
+ * Format files as a flat list
2205
+ */
2206
+ formatFilesFlat(files, includeMetadata) {
2207
+ const lines = [`## Files (${files.length})`, ''];
2208
+ for (const file of files.sort((a, b) => a.path.localeCompare(b.path))) {
2209
+ if (includeMetadata) {
2210
+ lines.push(`- ${file.path} (${file.language}, ${file.nodeCount} symbols)`);
2211
+ }
2212
+ else {
2213
+ lines.push(`- ${file.path}`);
2214
+ }
2215
+ }
2216
+ return lines.join('\n');
2217
+ }
2218
+ /**
2219
+ * Format files grouped by language
2220
+ */
2221
+ formatFilesGrouped(files, includeMetadata) {
2222
+ const byLang = new Map();
2223
+ for (const file of files) {
2224
+ const existing = byLang.get(file.language) || [];
2225
+ existing.push(file);
2226
+ byLang.set(file.language, existing);
2227
+ }
2228
+ const lines = [`## Files by Language (${files.length} total)`, ''];
2229
+ // Sort languages by file count (descending)
2230
+ const sortedLangs = [...byLang.entries()].sort((a, b) => b[1].length - a[1].length);
2231
+ for (const [lang, langFiles] of sortedLangs) {
2232
+ lines.push(`### ${lang} (${langFiles.length})`);
2233
+ for (const file of langFiles.sort((a, b) => a.path.localeCompare(b.path))) {
2234
+ if (includeMetadata) {
2235
+ lines.push(`- ${file.path} (${file.nodeCount} symbols)`);
2236
+ }
2237
+ else {
2238
+ lines.push(`- ${file.path}`);
2239
+ }
2240
+ }
2241
+ lines.push('');
2242
+ }
2243
+ return lines.join('\n');
2244
+ }
2245
+ /**
2246
+ * Format files as a tree structure
2247
+ */
2248
+ formatFilesTree(files, includeMetadata, maxDepth) {
2249
+ const root = { name: '', children: new Map() };
2250
+ for (const file of files) {
2251
+ const parts = file.path.split('/');
2252
+ let current = root;
2253
+ for (let i = 0; i < parts.length; i++) {
2254
+ const part = parts[i];
2255
+ if (!part)
2256
+ continue;
2257
+ if (!current.children.has(part)) {
2258
+ current.children.set(part, { name: part, children: new Map() });
2259
+ }
2260
+ current = current.children.get(part);
2261
+ // If this is the last part, it's a file
2262
+ if (i === parts.length - 1) {
2263
+ current.file = { language: file.language, nodeCount: file.nodeCount };
2264
+ }
2265
+ }
2266
+ }
2267
+ // Render tree
2268
+ const lines = [`## Project Structure (${files.length} files)`, ''];
2269
+ const renderNode = (node, prefix, isLast, depth) => {
2270
+ if (maxDepth !== undefined && depth > maxDepth)
2271
+ return;
2272
+ const connector = isLast ? '└── ' : '├── ';
2273
+ const childPrefix = isLast ? ' ' : '│ ';
2274
+ if (node.name) {
2275
+ let line = prefix + connector + node.name;
2276
+ if (node.file && includeMetadata) {
2277
+ line += ` (${node.file.language}, ${node.file.nodeCount} symbols)`;
2278
+ }
2279
+ lines.push(line);
2280
+ }
2281
+ const children = [...node.children.values()];
2282
+ // Sort: directories first, then files, both alphabetically
2283
+ children.sort((a, b) => {
2284
+ const aIsDir = a.children.size > 0 && !a.file;
2285
+ const bIsDir = b.children.size > 0 && !b.file;
2286
+ if (aIsDir !== bIsDir)
2287
+ return aIsDir ? -1 : 1;
2288
+ return a.name.localeCompare(b.name);
2289
+ });
2290
+ for (let i = 0; i < children.length; i++) {
2291
+ const child = children[i];
2292
+ const nextPrefix = node.name ? prefix + childPrefix : prefix;
2293
+ renderNode(child, nextPrefix, i === children.length - 1, depth + 1);
2294
+ }
2295
+ };
2296
+ renderNode(root, '', true, 0);
2297
+ return lines.join('\n');
2298
+ }
2299
+ // =========================================================================
2300
+ // Symbol resolution helpers
2301
+ // =========================================================================
2302
+ /**
2303
+ * Find a symbol by name, handling disambiguation when multiple matches exist.
2304
+ * Returns the best match and a note about alternatives if any.
2305
+ */
2306
+ /**
2307
+ * Check if a node matches a symbol query.
2308
+ *
2309
+ * Accepts simple names (`run`) and three flavors of qualifier:
2310
+ * - dotted `Session.request` (TS/JS/Python)
2311
+ * - colon-pair `stage_apply::run` (Rust, C++, Ruby)
2312
+ * - slash `configurator/stage_apply` (path-ish)
2313
+ *
2314
+ * Multi-level qualifiers compose: `crate::configurator::stage_apply::run`
2315
+ * works. Rust path prefixes (`crate`, `super`, `self`) are stripped so
2316
+ * the canonical `crate::module::symbol` form resolves.
2317
+ *
2318
+ * Resolution order, last part must always equal `node.name`:
2319
+ * 1. Suffix-match against `qualifiedName` (handles class-scoped methods
2320
+ * where the extractor builds the qualified name from the AST stack)
2321
+ * 2. File-path containment (handles file-derived modules in Rust/
2322
+ * Python — `stage_apply::run` matches a `run` in `stage_apply.rs`)
2323
+ */
2324
+ matchesSymbol(node, symbol) {
2325
+ // Simple name match
2326
+ if (node.name === symbol)
2327
+ return true;
2328
+ // File basename match (e.g., "product-card" matches "product-card.liquid")
2329
+ if (node.kind === 'file' && node.name.replace(/\.[^.]+$/, '') === symbol)
2330
+ return true;
2331
+ // Qualified-name lookups: split on any supported separator. `\w` keeps
2332
+ // identifier chars (incl. `_`) intact; everything else is treated as
2333
+ // a separator we tolerate.
2334
+ if (!/[.\/]|::/.test(symbol))
2335
+ return false;
2336
+ const parts = symbol.split(/::|[./]/).filter((p) => p.length > 0);
2337
+ if (parts.length < 2)
2338
+ return false;
2339
+ const lastPart = parts[parts.length - 1];
2340
+ if (node.name !== lastPart)
2341
+ return false;
2342
+ // Stage 1: qualified-name suffix match. The extractor joins the
2343
+ // semantic hierarchy with `::`, so `Session.request` and
2344
+ // `Session::request` both become `Session::request` here.
2345
+ const colonSuffix = parts.join('::');
2346
+ if (node.qualifiedName.includes(colonSuffix))
2347
+ return true;
2348
+ // Stage 2: file-path containment. Rust modules and Python packages
2349
+ // are not in `qualifiedName` — they're encoded in the file path. So
2350
+ // `stage_apply::run` matches a `run` in any file whose path
2351
+ // contains a `stage_apply` segment (with or without an extension).
2352
+ //
2353
+ // Filter out Rust path prefixes that have no file-system equivalent.
2354
+ const containerHints = parts.slice(0, -1).filter((p) => !RUST_PATH_PREFIXES.has(p));
2355
+ if (containerHints.length === 0)
2356
+ return false;
2357
+ const segments = node.filePath.split('/').filter((s) => s.length > 0);
2358
+ return containerHints.every((hint) => segments.some((seg) => seg === hint || seg.replace(/\.[^.]+$/, '') === hint));
2359
+ }
2360
+ findSymbol(cg, symbol) {
2361
+ // Use higher limit for qualified lookups (e.g., "Session.request",
2362
+ // "stage_apply::run") since the target may rank lower in FTS when
2363
+ // there are many partial matches across the qualifier parts.
2364
+ const isQualified = /[.\/]|::/.test(symbol);
2365
+ const limit = isQualified ? 50 : 10;
2366
+ let results = cg.searchNodes(symbol, { limit });
2367
+ // FTS strips colons as a special char, so `stage_apply::run` searches
2368
+ // for the literal `stage_applyrun` and finds nothing. Re-search by
2369
+ // the bare last part and let `matchesSymbol` filter by qualifier.
2370
+ if (isQualified && results.length === 0) {
2371
+ const tail = lastQualifierPart(symbol);
2372
+ if (tail && tail !== symbol)
2373
+ results = cg.searchNodes(tail, { limit });
2374
+ }
2375
+ if (results.length === 0 || !results[0]) {
2376
+ return null;
2377
+ }
2378
+ const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
2379
+ if (exactMatches.length === 1) {
2380
+ return { node: exactMatches[0].node, note: '' };
2381
+ }
2382
+ if (exactMatches.length > 1) {
2383
+ // Multiple exact matches - pick first, note the others
2384
+ const picked = exactMatches[0].node;
2385
+ const others = exactMatches.slice(1).map(r => `${r.node.name} (${r.node.kind}) at ${r.node.filePath}:${r.node.startLine}`);
2386
+ const note = `\n\n> **Note:** ${exactMatches.length} symbols named "${symbol}". Showing results for \`${picked.filePath}:${picked.startLine}\`. Others: ${others.join(', ')}`;
2387
+ return { node: picked, note };
2388
+ }
2389
+ // No exact match. For qualified lookups, don't silently fall back
2390
+ // to a fuzzy result — the user typed a specific qualifier, and
2391
+ // resolving `stage_apply::nonexistent_fn` to the unrelated
2392
+ // `stage_apply.rs` file would be actively misleading (#173).
2393
+ if (isQualified)
2394
+ return null;
2395
+ return { node: results[0].node, note: '' };
2396
+ }
2397
+ /**
2398
+ * Find ALL symbols matching a name. Used by callers/callees/impact to aggregate
2399
+ * results across all matching symbols (e.g., multiple classes with an `execute` method).
2400
+ */
2401
+ findAllSymbols(cg, symbol) {
2402
+ let results = cg.searchNodes(symbol, { limit: 50 });
2403
+ // Mirror the fallback in `findSymbol` for qualified queries — FTS
2404
+ // strips colons, so a module-qualified lookup needs a second pass
2405
+ // by the bare last part.
2406
+ if (results.length === 0 && /[.\/]|::/.test(symbol)) {
2407
+ const tail = lastQualifierPart(symbol);
2408
+ if (tail && tail !== symbol)
2409
+ results = cg.searchNodes(tail, { limit: 50 });
2410
+ }
2411
+ if (results.length === 0) {
2412
+ return { nodes: [], note: '' };
2413
+ }
2414
+ const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
2415
+ if (exactMatches.length <= 1) {
2416
+ const node = exactMatches[0]?.node ?? results[0].node;
2417
+ return { nodes: [node], note: '' };
2418
+ }
2419
+ const locations = exactMatches.map(r => `${r.node.kind} at ${r.node.filePath}:${r.node.startLine}`);
2420
+ const note = `\n\n> **Note:** Aggregated results across ${exactMatches.length} symbols named "${symbol}": ${locations.join(', ')}`;
2421
+ return { nodes: exactMatches.map(r => r.node), note };
2422
+ }
2423
+ /**
2424
+ * Truncate output if it exceeds the maximum length
2425
+ */
2426
+ truncateOutput(text) {
2427
+ if (text.length <= MAX_OUTPUT_LENGTH)
2428
+ return text;
2429
+ const truncated = text.slice(0, MAX_OUTPUT_LENGTH);
2430
+ const lastNewline = truncated.lastIndexOf('\n');
2431
+ const cutPoint = lastNewline > MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : MAX_OUTPUT_LENGTH;
2432
+ return truncated.slice(0, cutPoint) + '\n\n... (output truncated)';
2433
+ }
2434
+ // =========================================================================
2435
+ // Formatting helpers (compact by default to reduce context usage)
2436
+ // =========================================================================
2437
+ formatSearchResults(results) {
2438
+ const lines = [`## Search Results (${results.length} found)`, ''];
2439
+ for (const result of results) {
2440
+ const { node } = result;
2441
+ const location = node.startLine ? `:${node.startLine}` : '';
2442
+ // Compact format: one line per result with key info
2443
+ lines.push(`### ${node.name} (${node.kind})`);
2444
+ lines.push(`${node.filePath}${location}`);
2445
+ if (node.signature)
2446
+ lines.push(`\`${node.signature}\``);
2447
+ lines.push('');
2448
+ }
2449
+ return lines.join('\n');
2450
+ }
2451
+ formatNodeList(nodes, title) {
2452
+ const lines = [`## ${title} (${nodes.length} found)`, ''];
2453
+ for (const node of nodes) {
2454
+ const location = node.startLine ? `:${node.startLine}` : '';
2455
+ // Compact: just name, kind, location
2456
+ lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}`);
2457
+ }
2458
+ return lines.join('\n');
2459
+ }
2460
+ formatImpact(symbol, impact) {
2461
+ const nodeCount = impact.nodes.size;
2462
+ // Compact format: just list affected symbols grouped by file
2463
+ const lines = [
2464
+ `## Impact: "${symbol}" affects ${nodeCount} symbols`,
2465
+ '',
2466
+ ];
2467
+ // Group by file
2468
+ const byFile = new Map();
2469
+ for (const node of impact.nodes.values()) {
2470
+ const existing = byFile.get(node.filePath) || [];
2471
+ existing.push(node);
2472
+ byFile.set(node.filePath, existing);
2473
+ }
2474
+ for (const [file, nodes] of byFile) {
2475
+ lines.push(`**${file}:**`);
2476
+ // Compact: inline list
2477
+ const nodeList = nodes.map(n => `${n.name}:${n.startLine}`).join(', ');
2478
+ lines.push(nodeList);
2479
+ lines.push('');
2480
+ }
2481
+ return lines.join('\n');
2482
+ }
2483
+ /**
2484
+ * Build a compact structural outline of a container symbol from its
2485
+ * indexed children (methods, fields, properties, …) — name, kind,
2486
+ * line number, and signature — so the agent gets the shape of a class
2487
+ * without the full source of every method. Returns '' when the container
2488
+ * has no indexed children, so the caller can fall back to full source.
2489
+ */
2490
+ buildContainerOutline(cg, node) {
2491
+ const children = cg.getChildren(node.id)
2492
+ .filter(c => c.kind !== 'import' && c.kind !== 'export')
2493
+ .sort((a, b) => (a.startLine ?? 0) - (b.startLine ?? 0));
2494
+ if (children.length === 0)
2495
+ return '';
2496
+ const lines = [`**Members (${children.length}):**`, ''];
2497
+ for (const c of children) {
2498
+ const loc = c.startLine ? `:${c.startLine}` : '';
2499
+ const sig = c.signature ? ` — \`${c.signature}\`` : '';
2500
+ lines.push(`- ${c.name} (${c.kind})${loc}${sig}`);
2501
+ }
2502
+ return lines.join('\n');
2503
+ }
2504
+ formatNodeDetails(node, code, outline) {
2505
+ const location = node.startLine ? `:${node.startLine}` : '';
2506
+ const lines = [
2507
+ `## ${node.name} (${node.kind})`,
2508
+ '',
2509
+ `**Location:** ${node.filePath}${location}`,
2510
+ ];
2511
+ if (node.signature) {
2512
+ lines.push(`**Signature:** \`${node.signature}\``);
2513
+ }
2514
+ // Only include docstring if it's short and useful
2515
+ if (node.docstring && node.docstring.length < 200) {
2516
+ lines.push('', node.docstring);
2517
+ }
2518
+ if (outline) {
2519
+ lines.push('', outline, '', `> Structural outline only. Read \`${node.filePath}\` or call codegraph_node on a specific member for its body.`);
2520
+ }
2521
+ else if (code) {
2522
+ // Line-numbered (cat -n style, like codegraph_explore and Read) so the
2523
+ // agent can cite/edit exact lines without re-Reading the file for them.
2524
+ const numbered = node.startLine ? numberSourceLines(code, node.startLine) : code;
2525
+ lines.push('', '```' + node.language, numbered, '```');
2526
+ }
2527
+ return lines.join('\n');
2528
+ }
2529
+ formatTaskContext(context) {
2530
+ return context.summary || 'No context found';
2531
+ }
2532
+ textResult(text) {
2533
+ return {
2534
+ content: [{ type: 'text', text }],
2535
+ };
2536
+ }
2537
+ errorResult(message) {
2538
+ return {
2539
+ content: [{ type: 'text', text: `Error: ${message}` }],
2540
+ isError: true,
2541
+ };
2542
+ }
2543
+ }
2544
+ exports.ToolHandler = ToolHandler;
2545
+ //# sourceMappingURL=tools.js.map